diff --git a/src/components/ChatWidget.tsx b/src/components/ChatWidget.tsx index ba3f322..2fa8503 100644 --- a/src/components/ChatWidget.tsx +++ b/src/components/ChatWidget.tsx @@ -12,8 +12,7 @@ import { import { buildPaletteData } from '@/lib/search' import type { PaletteItem, PaletteAction } from '@/lib/search' import { iconByType, iconColorStyles } from '@/lib/palette-icons' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches +import { prefersReducedMotion } from '@/lib/utils' const MAX_HISTORY = 10 diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 5bade87..697f6d3 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -9,8 +9,7 @@ import type { PaletteItem, PaletteAction } from '@/lib/search' import { iconByType, iconColorStyles } from '@/lib/palette-icons' import { isModelReady, embedQuery } from '@/lib/embedding-model' import { semanticSearch, loadEmbeddings } from '@/lib/semantic-search' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches +import { prefersReducedMotion } from '@/lib/utils' interface CommandPaletteProps { isOpen: boolean diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index f3de6b4..86b7b1d 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -17,8 +17,7 @@ import { useDetailPanel } from '@/contexts/DetailPanelContext' import { timelineConsultations } from '@/data/timeline' import { skills } from '@/data/skills' import type { PaletteAction } from '@/lib/search' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches +import { hexToRgba, prefersReducedMotion } from '@/lib/utils' const sidebarVariants = { hidden: prefersReducedMotion ? { x: 0, opacity: 1 } : { x: -272, opacity: 0 }, @@ -41,13 +40,6 @@ const contentVariants = { }, } -function hexToRgba(hex: string, opacity: number): string { - const r = parseInt(hex.slice(1, 3), 16) - const g = parseInt(hex.slice(3, 5), 16) - const b = parseInt(hex.slice(5, 7), 16) - return `rgba(${r},${g},${b},${opacity})` -} - interface LastConsultationSubsectionProps { highlightedRoleId?: string | null } diff --git a/src/components/TimelineInterventionsSubsection.tsx b/src/components/TimelineInterventionsSubsection.tsx index dadf631..e9dfacc 100644 --- a/src/components/TimelineInterventionsSubsection.tsx +++ b/src/components/TimelineInterventionsSubsection.tsx @@ -5,15 +5,7 @@ import { useDetailPanel } from '@/contexts/DetailPanelContext' import { timelineEntities, timelineConsultations } from '@/data/timeline' import { getExperienceEducationUICopy } from '@/lib/profile-content' import type { TimelineEntity } from '@/types/pmr' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches - -function hexToRgba(hex: string, opacity: number): string { - const r = parseInt(hex.slice(1, 3), 16) - const g = parseInt(hex.slice(3, 5), 16) - const b = parseInt(hex.slice(5, 7), 16) - return `rgba(${r},${g},${b},${opacity})` -} +import { hexToRgba, prefersReducedMotion } from '@/lib/utils' interface TimelineInterventionItemProps { entity: TimelineEntity diff --git a/src/components/WorkExperienceSubsection.tsx b/src/components/WorkExperienceSubsection.tsx index 3569fb1..43afe3f 100644 --- a/src/components/WorkExperienceSubsection.tsx +++ b/src/components/WorkExperienceSubsection.tsx @@ -4,15 +4,7 @@ import { ChevronRight } from 'lucide-react' import { CardHeader } from './Card' import { timelineConsultations } from '@/data/timeline' import { useDetailPanel } from '@/contexts/DetailPanelContext' - -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches - -function hexToRgba(hex: string, opacity: number): string { - const r = parseInt(hex.slice(1, 3), 16) - const g = parseInt(hex.slice(3, 5), 16) - const b = parseInt(hex.slice(5, 7), 16) - return `rgba(${r},${g},${b},${opacity})` -} +import { hexToRgba, prefersReducedMotion } from '@/lib/utils' interface RoleItemProps { consultation: typeof timelineConsultations[0] diff --git a/src/components/constellation/constants.ts b/src/components/constellation/constants.ts index f32adf2..e60a9b9 100644 --- a/src/components/constellation/constants.ts +++ b/src/components/constellation/constants.ts @@ -86,5 +86,5 @@ export const HIDDEN_ENTITY_IDS = new Set([ ]) // Media queries (evaluated once at module level) -export const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches +export { prefersReducedMotion } from '@/lib/utils' export const supportsCoarsePointer = window.matchMedia('(pointer: coarse)').matches diff --git a/src/data/constellation.ts b/src/data/constellation.ts index 3913f97..39f7684 100644 --- a/src/data/constellation.ts +++ b/src/data/constellation.ts @@ -1,3 +1,5 @@ +// Module-level cache: buildConstellationData() is expensive (D3 graph construction). +// 5 consumers import from here instead of calling the builder independently. import type { ConstellationLink, ConstellationNode, RoleSkillMapping } from '@/types/pmr' import { buildConstellationData } from '@/data/timeline' diff --git a/src/data/tags.ts b/src/data/tags.ts index 1835765..d47f7d8 100644 --- a/src/data/tags.ts +++ b/src/data/tags.ts @@ -1,3 +1,5 @@ +// Derives sidebar tags from timeline skills with color assignment. +// Separated from Sidebar.tsx to keep data derivation out of UI components. import type { Tag } from '@/types/pmr' import { getTopTimelineSkills } from '@/data/timeline' diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ab165f4..5b95965 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,3 +6,12 @@ export function calculateSkillOffset(level: number, radius: number): number { export function formatBootLine(text: string): string { return text } + +export function hexToRgba(hex: string, opacity: number): string { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return `rgba(${r},${g},${b},${opacity})` +} + +export const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches