'use client' import { useEffect, useState, type CSSProperties } from 'react' import { PageBlockView, IntentBadge } from '@/components/interactive-page/page-blocks' import { PageMd } from '@/components/interactive-page/page-md' import { PageStickyNav } from '@/components/interactive-page/page-sticky-nav' import type { PageSpecV1 } from '@/lib/interactive-page' import { cn } from '@/lib/utils' export type PageViewProps = { page: PageSpecV1 /** interactive = hydrate demo players; static = final-state only (SSR/noscript) */ demoMode?: 'interactive' | 'static' className?: string paper?: boolean } function usePrefersReducedMotion(): boolean { const [reduced, setReduced] = useState(false) useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)') setReduced(mq.matches) const onChange = () => setReduced(mq.matches) mq.addEventListener('change', onChange) return () => mq.removeEventListener('change', onChange) }, []) return reduced } function useScrollReveal(enabled: boolean) { useEffect(() => { if (!enabled) { document .querySelectorAll('[data-scroll-init]') .forEach((el) => el.setAttribute('data-scroll-visible', 'true')) return } const nodes = Array.from( document.querySelectorAll('[data-scroll-init]') ) const obs = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) { e.target.setAttribute('data-scroll-visible', 'true') obs.unobserve(e.target) } } }, { rootMargin: '0px 0px -8% 0px', threshold: 0.12 } ) nodes.forEach((n) => obs.observe(n)) return () => obs.disconnect() }, [enabled]) } /** * PageView — Kimi/AttnRes-style explainer page. * Design tokens replicated from the reference page (paper, plum accent, * mono labels, formula left-bar, stat cards) with dark-mode adaptation. */ export function PageView({ page, demoMode = 'interactive', className, paper = true, }: PageViewProps) { const reducedMotion = usePrefersReducedMotion() useScrollReveal(!reducedMotion) const paperStyle = { '--page-paper': paper ? 'var(--pp-paper)' : 'var(--background)', } as CSSProperties return (
{/* Without JS the scroll-reveal never fires — show everything */} {/* ── Hero (kicker / 800 title / ink subtitle / mono meta) ── */}

{page.hero.kicker}

{page.hero.title}

{page.hero.subtitle ? (

{page.hero.subtitle}

) : null} {page.hero.meta ? (

{page.hero.meta}

) : null}
{/* ── One-minute overview ── */} {page.overview ? (

{page.lang.startsWith('fr') ? 'L’essentiel en une minute' : 'One-minute overview'}

{page.overview.cards.map((card) => (

{card.title}

))}
) : null} {/* ── Sections ── */}
{page.sections.map((section, i) => (

{String(i + 1).padStart(2, '0')} {section.title}

{section.blocks.map((block, bi) => { const narrow = block.type === 'prose' || block.type === 'formula' || block.type === 'callout' return (
) })}
))}
{page.footer ? ( ) : null}
) }