Files
Momento/memento-note/components/interactive-page/page-view.tsx
Antigravity 80ccc1f6de
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m14s
CI / Deploy production (on server) (push) Successful in 1m25s
feat: dashboard Second Brain, essai 7 jours et vérification e-mail
Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 07:19:36 +00:00

283 lines
9.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<HTMLElement>('[data-scroll-init]')
.forEach((el) => el.setAttribute('data-scroll-visible', 'true'))
return
}
const nodes = Array.from(
document.querySelectorAll<HTMLElement>('[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 (
<article
className={cn('interactive-page min-h-screen', paper && 'page-paper', className)}
data-page-id={page.id}
style={paperStyle}
>
<style>{`
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
.interactive-page {
--pp-paper: #F4F0E8;
--pp-paper-deep: #EAE4D9;
--pp-card: #FFFDF8;
--pp-ink: #242422;
--pp-muted: #686762;
--pp-line: #D6D0C6;
--pp-plum: #9F3F70;
--pp-plum-soft: #F2DCE8;
--pp-blue: #3F6F9F;
--pp-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
color: var(--pp-ink);
}
.dark .interactive-page {
--pp-paper: #17140F;
--pp-paper-deep: #201C15;
--pp-card: #221E17;
--pp-ink: #EDE7DB;
--pp-muted: #A39C8D;
--pp-line: #3B352A;
--pp-plum: #D07AA6;
--pp-plum-soft: #3A2530;
--pp-blue: #7FA8CC;
}
.interactive-page.page-paper {
background-color: var(--pp-paper);
background-image: radial-gradient(
color-mix(in oklab, var(--pp-ink) 7%, transparent) 0.7px,
transparent 0.7px
);
background-size: 18px 18px;
}
.pp-card {
background: var(--pp-card);
border: 1px solid var(--pp-line);
border-radius: 14px;
}
.pp-mono {
font-family: var(--pp-mono);
}
.interactive-page [data-scroll-init]:not([data-scroll-visible]) {
opacity: 0;
transform: translateY(12px);
}
.interactive-page [data-scroll-init] {
transition: opacity 420ms ease, transform 420ms ease;
will-change: opacity, transform;
}
.interactive-page [data-scroll-visible] {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.interactive-page [data-scroll-init] {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
}
`}</style>
{/* Without JS the scroll-reveal never fires — show everything */}
<noscript>
<style>{`
.interactive-page [data-scroll-init] {
opacity: 1 !important;
transform: none !important;
}
`}</style>
</noscript>
{/* ── Hero (kicker / 800 title / ink subtitle / mono meta) ── */}
<header className="mx-auto max-w-[1100px] px-5 pb-8 pt-12 md:pt-16">
<p
className="pp-mono mb-2.5 text-xs uppercase"
style={{ letterSpacing: '0.14em', color: 'var(--pp-plum)' }}
>
{page.hero.kicker}
</p>
<h1
className="max-w-[22ch] font-extrabold leading-[1.14] tracking-[-0.015em]"
style={{ fontSize: 'clamp(26px, 3.4vw, 44px)' }}
>
{page.hero.title}
</h1>
{page.hero.subtitle ? (
<p
className="mt-2.5 font-semibold"
style={{ fontSize: 'clamp(16px, 1.6vw, 21px)' }}
>
{page.hero.subtitle}
</p>
) : null}
{page.hero.meta ? (
<p
className="pp-mono mt-3 leading-relaxed"
style={{ fontSize: '12.5px', color: 'var(--pp-muted)' }}
>
{page.hero.meta}
</p>
) : null}
</header>
<PageStickyNav page={page} />
<div className="mx-auto max-w-[1100px] px-5 pb-24 pt-10">
{/* ── One-minute overview ── */}
{page.overview ? (
<section className="pp-card mb-20 p-6 shadow-sm md:p-8" data-scroll-init>
<p
className="pp-mono mb-3 text-[11px] uppercase"
style={{ letterSpacing: '0.14em', color: 'var(--pp-muted)' }}
>
{page.lang.startsWith('fr')
? 'Lessentiel en une minute'
: 'One-minute overview'}
</p>
<PageMd
md={page.overview.lead}
className="max-w-[75ch] text-lg leading-relaxed [&_p]:my-0"
/>
<div className="mt-5 grid gap-3.5 sm:grid-cols-2 lg:grid-cols-3">
{page.overview.cards.map((card) => (
<div
key={card.badge + card.title}
className="rounded-xl border p-4"
style={{
background: 'var(--pp-paper)',
borderColor: 'var(--pp-line)',
}}
>
<IntentBadge label={card.badge} intent={card.intent} />
<h3 className="mt-3 text-base font-semibold tracking-tight">
{card.title}
</h3>
<PageMd
md={card.body}
className="mt-1.5 text-sm text-muted-foreground [&_p]:my-0"
/>
</div>
))}
</div>
</section>
) : null}
{/* ── Sections ── */}
<div className="space-y-20">
{page.sections.map((section, i) => (
<section
key={section.id}
id={section.id}
className="scroll-mt-28"
data-scroll-init
>
<h2 className="mb-6 flex items-baseline gap-3 text-2xl font-extrabold tracking-tight md:text-[28px]">
<span
className="pp-mono text-sm font-semibold"
style={{ color: 'var(--pp-plum)' }}
>
{String(i + 1).padStart(2, '0')}
</span>
{section.title}
</h2>
<div>
{section.blocks.map((block, bi) => {
const narrow =
block.type === 'prose' ||
block.type === 'formula' ||
block.type === 'callout'
return (
<div
key={`${section.id}-${bi}`}
className={narrow ? 'max-w-[75ch]' : 'max-w-[880px]'}
>
<PageBlockView
block={block}
demoMode={demoMode}
lang={page.lang}
/>
</div>
)
})}
</div>
</section>
))}
</div>
{page.footer ? (
<footer
className="pp-mono mt-16 border-t pt-5 text-xs italic"
style={{ borderColor: 'var(--pp-line)', color: 'var(--pp-muted)' }}
>
{page.footer}
</footer>
) : null}
</div>
</article>
)
}