feat: add global focus mode with cross-component dimming on hover
When hovering a constellation node, skill pill, or timeline item, non-related UI elements across all components dim to 0.25 opacity, creating a focused visual relationship view. The constellation axis and year labels also dim via CSS class. Respects reduced-motion.
This commit is contained in:
@@ -12,8 +12,9 @@ import { LastConsultationCard } from './LastConsultationCard'
|
|||||||
import { ChatWidget } from './ChatWidget'
|
import { ChatWidget } from './ChatWidget'
|
||||||
import { useActiveSection } from '@/hooks/useActiveSection'
|
import { useActiveSection } from '@/hooks/useActiveSection'
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||||
import { timelineConsultations } from '@/data/timeline'
|
import { timelineConsultations, timelineEntities } from '@/data/timeline'
|
||||||
import { skills } from '@/data/skills'
|
import { skills } from '@/data/skills'
|
||||||
|
import { constellationNodes } from '@/data/constellation'
|
||||||
import type { PaletteAction } from '@/lib/search'
|
import type { PaletteAction } from '@/lib/search'
|
||||||
import { prefersReducedMotion, motionSafeTransition } from '@/lib/utils'
|
import { prefersReducedMotion, motionSafeTransition } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -49,6 +50,47 @@ export function DashboardLayout() {
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Global focus mode: tracks which entity (skill or role) is being hovered across all components
|
||||||
|
const [globalFocusId, setGlobalFocusId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Build lookup maps for resolving relationships between skills and roles
|
||||||
|
const nodeTypeById = useMemo(
|
||||||
|
() => new Map(constellationNodes.map(n => [n.id, n.type])),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const skillToRoles = useMemo(() => {
|
||||||
|
const map = new Map<string, Set<string>>()
|
||||||
|
for (const entity of timelineEntities) {
|
||||||
|
for (const skillId of entity.skills) {
|
||||||
|
if (!map.has(skillId)) map.set(skillId, new Set())
|
||||||
|
map.get(skillId)!.add(entity.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [])
|
||||||
|
const roleToSkills = useMemo(
|
||||||
|
() => new Map(timelineEntities.map(e => [e.id, new Set(e.skills)])),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Derive the set of all IDs related to the focused entity
|
||||||
|
const focusRelatedIds = useMemo(() => {
|
||||||
|
if (!globalFocusId) return null
|
||||||
|
const related = new Set<string>()
|
||||||
|
related.add(globalFocusId)
|
||||||
|
const nodeType = nodeTypeById.get(globalFocusId)
|
||||||
|
if (nodeType === 'skill') {
|
||||||
|
// Skill focused: related roles are those containing this skill
|
||||||
|
const roles = skillToRoles.get(globalFocusId)
|
||||||
|
if (roles) roles.forEach(r => related.add(r))
|
||||||
|
} else {
|
||||||
|
// Role/education focused: related skills are that entity's skills
|
||||||
|
const entitySkills = roleToSkills.get(globalFocusId)
|
||||||
|
if (entitySkills) entitySkills.forEach(s => related.add(s))
|
||||||
|
}
|
||||||
|
return related
|
||||||
|
}, [globalFocusId, nodeTypeById, skillToRoles, roleToSkills])
|
||||||
|
|
||||||
// Signal constellation animation readiness when patient summary scrolls out of view
|
// Signal constellation animation readiness when patient summary scrolls out of view
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = patientSummaryRef.current
|
const el = patientSummaryRef.current
|
||||||
@@ -115,11 +157,14 @@ export function DashboardLayout() {
|
|||||||
|
|
||||||
const handleNodeHighlight = useCallback((id: string | null) => {
|
const handleNodeHighlight = useCallback((id: string | null) => {
|
||||||
setHighlightedNodeId(id)
|
setHighlightedNodeId(id)
|
||||||
|
setGlobalFocusId(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleNodeHover = useCallback((id: string | null) => {
|
const handleNodeHover = useCallback((id: string | null) => {
|
||||||
setHighlightedRoleId(id)
|
const nodeType = id ? nodeTypeById.get(id) : null
|
||||||
}, [])
|
setHighlightedRoleId(nodeType !== 'skill' ? id : null)
|
||||||
|
setGlobalFocusId(id)
|
||||||
|
}, [nodeTypeById])
|
||||||
|
|
||||||
// Global Ctrl+K listener to open command palette
|
// Global Ctrl+K listener to open command palette
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -243,11 +288,11 @@ export function DashboardLayout() {
|
|||||||
|
|
||||||
|
|
||||||
<div className="chronology-item">
|
<div className="chronology-item">
|
||||||
<LastConsultationCard highlightedRoleId={highlightedRoleId} />
|
<LastConsultationCard highlightedRoleId={highlightedRoleId} focusRelatedIds={focusRelatedIds} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="chronology-item">
|
<div className="chronology-item">
|
||||||
<TimelineInterventionsSubsection onNodeHighlight={handleNodeHighlight} highlightedRoleId={highlightedRoleId} />
|
<TimelineInterventionsSubsection onNodeHighlight={handleNodeHighlight} highlightedRoleId={highlightedRoleId} focusRelatedIds={focusRelatedIds} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pathway-graph-sticky">
|
<div className="pathway-graph-sticky">
|
||||||
@@ -258,6 +303,7 @@ export function DashboardLayout() {
|
|||||||
highlightedNodeId={highlightedNodeId}
|
highlightedNodeId={highlightedNodeId}
|
||||||
containerHeight={chronologyHeight}
|
containerHeight={chronologyHeight}
|
||||||
animationReady={constellationReady}
|
animationReady={constellationReady}
|
||||||
|
globalFocusActive={globalFocusId !== null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -265,7 +311,7 @@ export function DashboardLayout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
||||||
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} />
|
<RepeatMedicationsSubsection onNodeHighlight={handleNodeHighlight} focusRelatedIds={focusRelatedIds} />
|
||||||
</div>
|
</div>
|
||||||
</ParentSection>
|
</ParentSection>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { hexToRgba, motionSafeTransition } from '@/lib/utils'
|
|||||||
interface ExpandableCardShellProps {
|
interface ExpandableCardShellProps {
|
||||||
isExpanded: boolean
|
isExpanded: boolean
|
||||||
isHighlighted: boolean
|
isHighlighted: boolean
|
||||||
|
isDimmedByFocus?: boolean
|
||||||
accentColor: string
|
accentColor: string
|
||||||
onToggle: () => void
|
onToggle: () => void
|
||||||
ariaLabel: string
|
ariaLabel: string
|
||||||
@@ -21,6 +22,7 @@ interface ExpandableCardShellProps {
|
|||||||
export function ExpandableCardShell({
|
export function ExpandableCardShell({
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
|
isDimmedByFocus = false,
|
||||||
accentColor,
|
accentColor,
|
||||||
onToggle,
|
onToggle,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
@@ -52,6 +54,10 @@ export function ExpandableCardShell({
|
|||||||
className={className}
|
className={className}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
style={{
|
||||||
|
opacity: isDimmedByFocus ? 0.25 : 1,
|
||||||
|
transition: 'opacity 150ms ease-out',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ import { DEFAULT_ORG_COLOR } from '@/lib/theme-colors'
|
|||||||
|
|
||||||
interface LastConsultationCardProps {
|
interface LastConsultationCardProps {
|
||||||
highlightedRoleId?: string | null
|
highlightedRoleId?: string | null
|
||||||
|
focusRelatedIds?: Set<string> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LastConsultationCard({ highlightedRoleId }: LastConsultationCardProps) {
|
export function LastConsultationCard({ highlightedRoleId, focusRelatedIds }: LastConsultationCardProps) {
|
||||||
const { openPanel } = useDetailPanel()
|
const { openPanel } = useDetailPanel()
|
||||||
const consultation = timelineConsultations.find(c => c.isCurrent) ?? timelineConsultations[0]
|
const consultation = timelineConsultations.find(c => c.isCurrent) ?? timelineConsultations[0]
|
||||||
if (!consultation) {
|
if (!consultation) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const isHighlighted = highlightedRoleId === consultation.id
|
const isHighlighted = highlightedRoleId === consultation.id
|
||||||
|
const isDimmed = focusRelatedIds != null && !focusRelatedIds.has(consultation.id)
|
||||||
|
|
||||||
const handleOpenPanel = () => {
|
const handleOpenPanel = () => {
|
||||||
openPanel({ type: 'consultation', consultation })
|
openPanel({ type: 'consultation', consultation })
|
||||||
@@ -67,9 +69,10 @@ export function LastConsultationCard({ highlightedRoleId }: LastConsultationCard
|
|||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: isHighlighted ? hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.2) : 'transparent',
|
borderColor: isHighlighted ? hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.2) : 'transparent',
|
||||||
background: isHighlighted ? hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.03) : 'transparent',
|
background: isHighlighted ? hexToRgba(consultation.orgColor ?? DEFAULT_ORG_COLOR, 0.03) : 'transparent',
|
||||||
transition: 'border-color 150ms ease-out, background-color 150ms ease-out',
|
transition: 'border-color 150ms ease-out, background-color 150ms ease-out, opacity 150ms ease-out',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
margin: '-8px',
|
margin: '-8px',
|
||||||
|
opacity: isDimmed ? 0.25 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardHeader dotColor="green" title="LAST CONSULTATION" rightText="Current role" />
|
<CardHeader dotColor="green" title="LAST CONSULTATION" rightText="Current role" />
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ interface SkillRowProps {
|
|||||||
yearsSuffix: string
|
yearsSuffix: string
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
onHighlight?: (id: string | null) => void
|
onHighlight?: (id: string | null) => void
|
||||||
|
isDimmedByFocus?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function SkillRow({ skill, yearsSuffix, onClick, onHighlight }: SkillRowProps) {
|
function SkillRow({ skill, yearsSuffix, onClick, onHighlight, isDimmedByFocus = false }: SkillRowProps) {
|
||||||
const IconComponent = iconMap[skill.icon]
|
const IconComponent = iconMap[skill.icon]
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
@@ -55,7 +56,8 @@ function SkillRow({ skill, yearsSuffix, onClick, onHighlight }: SkillRowProps) {
|
|||||||
borderRadius: 'var(--radius-sm)',
|
borderRadius: 'var(--radius-sm)',
|
||||||
border: '1px solid var(--border-light)',
|
border: '1px solid var(--border-light)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
transition: 'border-color 0.15s, box-shadow 0.15s, opacity 150ms ease-out',
|
||||||
|
opacity: isDimmedByFocus ? 0.25 : 1,
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
||||||
@@ -134,6 +136,7 @@ interface CategorySectionProps {
|
|||||||
onSkillClick: (skill: SkillMedication) => void
|
onSkillClick: (skill: SkillMedication) => void
|
||||||
isFirst: boolean
|
isFirst: boolean
|
||||||
onNodeHighlight?: (id: string | null) => void
|
onNodeHighlight?: (id: string | null) => void
|
||||||
|
focusRelatedIds?: Set<string> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategorySection({
|
function CategorySection({
|
||||||
@@ -144,6 +147,7 @@ function CategorySection({
|
|||||||
onSkillClick,
|
onSkillClick,
|
||||||
isFirst,
|
isFirst,
|
||||||
onNodeHighlight,
|
onNodeHighlight,
|
||||||
|
focusRelatedIds,
|
||||||
}: CategorySectionProps) {
|
}: CategorySectionProps) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: isFirst ? 0 : '16px' }}>
|
<div style={{ marginTop: isFirst ? 0 : '16px' }}>
|
||||||
@@ -193,6 +197,7 @@ function CategorySection({
|
|||||||
yearsSuffix={yearsSuffix}
|
yearsSuffix={yearsSuffix}
|
||||||
onClick={() => onSkillClick(skill)}
|
onClick={() => onSkillClick(skill)}
|
||||||
onHighlight={onNodeHighlight}
|
onHighlight={onNodeHighlight}
|
||||||
|
isDimmedByFocus={focusRelatedIds != null && !focusRelatedIds.has(skill.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -202,9 +207,10 @@ function CategorySection({
|
|||||||
|
|
||||||
interface RepeatMedicationsSubsectionProps {
|
interface RepeatMedicationsSubsectionProps {
|
||||||
onNodeHighlight?: (id: string | null) => void
|
onNodeHighlight?: (id: string | null) => void
|
||||||
|
focusRelatedIds?: Set<string> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepeatMedicationsSubsection({ onNodeHighlight }: RepeatMedicationsSubsectionProps) {
|
export function RepeatMedicationsSubsection({ onNodeHighlight, focusRelatedIds }: RepeatMedicationsSubsectionProps) {
|
||||||
const { openPanel } = useDetailPanel()
|
const { openPanel } = useDetailPanel()
|
||||||
const skillsCopy = getSkillsUICopy()
|
const skillsCopy = getSkillsUICopy()
|
||||||
|
|
||||||
@@ -238,6 +244,7 @@ export function RepeatMedicationsSubsection({ onNodeHighlight }: RepeatMedicatio
|
|||||||
onSkillClick={handleSkillClick}
|
onSkillClick={handleSkillClick}
|
||||||
isFirst
|
isFirst
|
||||||
onNodeHighlight={onNodeHighlight}
|
onNodeHighlight={onNodeHighlight}
|
||||||
|
focusRelatedIds={focusRelatedIds}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface TimelineInterventionItemProps {
|
|||||||
entity: TimelineEntity
|
entity: TimelineEntity
|
||||||
isExpanded: boolean
|
isExpanded: boolean
|
||||||
isHighlightedFromGraph: boolean
|
isHighlightedFromGraph: boolean
|
||||||
|
isDimmedByFocus: boolean
|
||||||
isEducationAnchor: boolean
|
isEducationAnchor: boolean
|
||||||
onToggle: () => void
|
onToggle: () => void
|
||||||
onViewFull: () => void
|
onViewFull: () => void
|
||||||
@@ -21,6 +22,7 @@ function TimelineInterventionItem({
|
|||||||
entity,
|
entity,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isHighlightedFromGraph,
|
isHighlightedFromGraph,
|
||||||
|
isDimmedByFocus,
|
||||||
isEducationAnchor,
|
isEducationAnchor,
|
||||||
onToggle,
|
onToggle,
|
||||||
onViewFull,
|
onViewFull,
|
||||||
@@ -34,6 +36,7 @@ function TimelineInterventionItem({
|
|||||||
<ExpandableCardShell
|
<ExpandableCardShell
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
isHighlighted={isHighlightedFromGraph}
|
isHighlighted={isHighlightedFromGraph}
|
||||||
|
isDimmedByFocus={isDimmedByFocus}
|
||||||
accentColor={entity.orgColor}
|
accentColor={entity.orgColor}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
ariaLabel={`${entity.title} at ${entity.organization}, ${entity.dateRange.display}. Click to ${isExpanded ? 'collapse' : 'expand'} details.`}
|
ariaLabel={`${entity.title} at ${entity.organization}, ${entity.dateRange.display}. Click to ${isExpanded ? 'collapse' : 'expand'} details.`}
|
||||||
@@ -254,9 +257,10 @@ function TimelineInterventionItem({
|
|||||||
interface TimelineInterventionsSubsectionProps {
|
interface TimelineInterventionsSubsectionProps {
|
||||||
onNodeHighlight?: (id: string | null) => void
|
onNodeHighlight?: (id: string | null) => void
|
||||||
highlightedRoleId?: string | null
|
highlightedRoleId?: string | null
|
||||||
|
focusRelatedIds?: Set<string> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimelineInterventionsSubsection({ onNodeHighlight, highlightedRoleId }: TimelineInterventionsSubsectionProps) {
|
export function TimelineInterventionsSubsection({ onNodeHighlight, highlightedRoleId, focusRelatedIds }: TimelineInterventionsSubsectionProps) {
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||||
const { openPanel } = useDetailPanel()
|
const { openPanel } = useDetailPanel()
|
||||||
|
|
||||||
@@ -288,6 +292,7 @@ export function TimelineInterventionsSubsection({ onNodeHighlight, highlightedRo
|
|||||||
entity={entity}
|
entity={entity}
|
||||||
isExpanded={expandedId === entity.id}
|
isExpanded={expandedId === entity.id}
|
||||||
isHighlightedFromGraph={highlightedRoleId === entity.id}
|
isHighlightedFromGraph={highlightedRoleId === entity.id}
|
||||||
|
isDimmedByFocus={focusRelatedIds !== null && focusRelatedIds !== undefined && !focusRelatedIds.has(entity.id)}
|
||||||
isEducationAnchor={entity.id === firstEducationId}
|
isEducationAnchor={entity.id === firstEducationId}
|
||||||
onToggle={() => handleToggle(entity.id)}
|
onToggle={() => handleToggle(entity.id)}
|
||||||
onViewFull={() => handleViewFull(entity)}
|
onViewFull={() => handleViewFull(entity)}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface CareerConstellationProps {
|
|||||||
highlightedNodeId?: string | null
|
highlightedNodeId?: string | null
|
||||||
containerHeight?: number | null
|
containerHeight?: number | null
|
||||||
animationReady?: boolean
|
animationReady?: boolean
|
||||||
|
globalFocusActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeById = new Map(constellationNodes.map(node => [node.id, node]))
|
const nodeById = new Map(constellationNodes.map(node => [node.id, node]))
|
||||||
@@ -39,6 +40,7 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
highlightedNodeId,
|
highlightedNodeId,
|
||||||
containerHeight,
|
containerHeight,
|
||||||
animationReady = false,
|
animationReady = false,
|
||||||
|
globalFocusActive = false,
|
||||||
}) => {
|
}) => {
|
||||||
const svgRef = useRef<SVGSVGElement>(null)
|
const svgRef = useRef<SVGSVGElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -301,6 +303,7 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label="Clinical pathway constellation showing career roles and skills in reverse-chronological order along a vertical timeline"
|
aria-label="Clinical pathway constellation showing career roles and skills in reverse-chronological order along a vertical timeline"
|
||||||
|
className={globalFocusActive || highlightedNodeId || pinnedNodeId ? 'constellation-focus-active' : ''}
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ export function useConstellationInteraction(deps: {
|
|||||||
if (supportsCoarsePointer) return
|
if (supportsCoarsePointer) return
|
||||||
deps.pauseForInteraction?.()
|
deps.pauseForInteraction?.()
|
||||||
deps.highlightGraphRef.current?.(d.id)
|
deps.highlightGraphRef.current?.(d.id)
|
||||||
if (d.type !== 'skill') {
|
deps.callbacksRef.current.onNodeHover?.(d.id)
|
||||||
deps.callbacksRef.current.onNodeHover?.(d.id)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
nodeSelection.on('mouseleave.interaction', function() {
|
nodeSelection.on('mouseleave.interaction', function() {
|
||||||
|
|||||||
@@ -494,6 +494,27 @@ html {
|
|||||||
to { transform: scale(1); opacity: 1; }
|
to { transform: scale(1); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== CONSTELLATION FOCUS MODE — axis/background dimming ===== */
|
||||||
|
svg.constellation-focus-active .axis-line,
|
||||||
|
svg.constellation-focus-active .year-tick {
|
||||||
|
stroke-opacity: 0.25;
|
||||||
|
transition: stroke-opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.constellation-focus-active .year-label {
|
||||||
|
opacity: 0.25;
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:not(.constellation-focus-active) .axis-line,
|
||||||
|
svg:not(.constellation-focus-active) .year-tick {
|
||||||
|
transition: stroke-opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg:not(.constellation-focus-active) .year-label {
|
||||||
|
transition: opacity 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== FOCUS VISIBLE STYLES (WCAG Compliance) ===== */
|
/* ===== FOCUS VISIBLE STYLES (WCAG Compliance) ===== */
|
||||||
/* Default focus ring for all focusable elements */
|
/* Default focus ring for all focusable elements */
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
@@ -593,6 +614,16 @@ textarea:focus-visible {
|
|||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No transition for constellation focus mode axis dimming */
|
||||||
|
svg.constellation-focus-active .axis-line,
|
||||||
|
svg.constellation-focus-active .year-tick,
|
||||||
|
svg.constellation-focus-active .year-label,
|
||||||
|
svg:not(.constellation-focus-active) .axis-line,
|
||||||
|
svg:not(.constellation-focus-active) .year-tick,
|
||||||
|
svg:not(.constellation-focus-active) .year-label {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Instant constellation fullscreen */
|
/* Instant constellation fullscreen */
|
||||||
@keyframes constellation-fullscreen-in {
|
@keyframes constellation-fullscreen-in {
|
||||||
from { transform: none; opacity: 1; }
|
from { transform: none; opacity: 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user