'use client' import { useEffect, useMemo, useState } from 'react' import type { PageSpecV1 } from '@/lib/interactive-page' import { intentColor } from '@/lib/interactive-demo/intent-colors' import type { IntentId } from '@/lib/interactive-demo/types' import { useDarkMode } from '@/components/interactive-demo/demo-speak' import { cn } from '@/lib/utils' function collectIntents(page: PageSpecV1): IntentId[] { const set = new Set() for (const card of page.overview?.cards ?? []) { if (card.intent) set.add(card.intent) } for (const section of page.sections) { for (const block of section.blocks) { if (block.type === 'stats') { for (const item of block.items) { if (item.intent) set.add(item.intent) } } if (block.type === 'chart') { for (const s of block.payload.series) { if (s.intent) set.add(s.intent) } } if (block.type === 'demo') { const panels = [ ...block.demo.scene.panels, ...block.demo.acts.flatMap((a) => a.scene?.panels ?? []), ] for (const panel of panels) { if (panel.type === 'svg-scene') { for (const n of panel.payload.nodes) if (n.intent) set.add(n.intent) for (const e of panel.payload.edges ?? []) if (e.intent) set.add(e.intent) } if (panel.type === 'chart') { for (const s of panel.payload.series) if (s.intent) set.add(s.intent) } } } } } return [...set] } const INTENT_LABELS_FR: Record = { highlight: 'Focus', flow: 'Flux', cache: 'Mémoire', compute: 'Calcul', output: 'Résultat', warning: 'Attention', } const INTENT_LABELS_EN: Record = { highlight: 'Focus', flow: 'Flow', cache: 'Memory', compute: 'Compute', output: 'Result', warning: 'Warning', } export function PageStickyNav({ page }: { page: PageSpecV1 }) { const [active, setActive] = useState(page.sections[0]?.id ?? '') const dark = useDarkMode() const intents = useMemo(() => collectIntents(page), [page]) useEffect(() => { const nodes = page.sections .map((s) => document.getElementById(s.id)) .filter(Boolean) as HTMLElement[] if (!nodes.length) return const obs = new IntersectionObserver( (entries) => { const visible = entries .filter((e) => e.isIntersecting) .sort((a, b) => b.intersectionRatio - a.intersectionRatio) const top = visible[0]?.target?.id if (top) setActive(top) }, { rootMargin: '-20% 0px -55% 0px', threshold: [0.1, 0.25, 0.5] } ) nodes.forEach((n) => obs.observe(n)) return () => obs.disconnect() }, [page.sections]) return (
{intents.length > 0 ? (
{page.lang.startsWith('fr') ? 'Légende' : 'Legend'} {intents.map((id) => { const color = intentColor(id, dark) return ( {page.lang.startsWith('fr') ? INTENT_LABELS_FR[id] : INTENT_LABELS_EN[id]} ) })}
) : null}
) }