Files
Momento/memento-note/components/interactive-page/steps-block.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

135 lines
4.4 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import katex from 'katex'
import { AnimPlayerShell } from '@/components/simulators/anim-player-shell'
import type { StepsBlock } from '@/lib/interactive-page'
import { cn } from '@/lib/utils'
function renderTex(tex: string, displayMode: boolean): string {
try {
return katex.renderToString(tex, { displayMode, throwOnError: false })
} catch {
return tex
}
}
/**
* Step-by-step derivation (Symbolab/Khan style): equation states revealed
* line by line, current line highlighted, transformation rule in the margin.
* No boxes, no LLM drawing — pure KaTeX + deterministic chrome.
*/
export function StepsBlockView({
block,
lang,
}: {
block: StepsBlock
lang: string
}) {
const fr = lang.startsWith('fr')
const steps = block.steps
const beats = useMemo(
() =>
steps.map((s, i) => ({
id: `st${i + 1}`,
speak: {
fr: s.speak || s.rule || (fr ? `Étape ${i + 1}` : `Step ${i + 1}`),
en: s.speak || s.rule || `Step ${i + 1}`,
},
})),
[steps, fr]
)
const rendered = useMemo(() => steps.map((s) => renderTex(s.tex, true)), [steps])
return (
<figure
className="my-8 rounded-2xl border p-4 md:p-5"
style={{ background: 'var(--pp-card)', borderColor: 'var(--pp-line)' }}
>
{block.title ? (
<div className="mb-4 flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold tracking-tight">{block.title}</h3>
<span
className="text-[10px] font-semibold uppercase tracking-[0.16em]"
style={{ color: 'var(--pp-muted)' }}
>
{fr ? 'Dérivation pas à pas' : 'Step-by-step derivation'}
</span>
</div>
) : null}
<AnimPlayerShell beats={beats} lang={lang}>
{(stepIndex) => (
<div className="space-y-1.5 p-3 md:p-4" style={{ background: 'var(--pp-paper)' }}>
{steps.slice(0, stepIndex + 1).map((s, i) => {
const current = i === stepIndex
return (
<div
key={i}
className={cn(
'flex items-start gap-3 rounded-lg border-l-4 px-3 py-2',
current ? 'shadow-sm' : 'border-transparent'
)}
style={{
borderLeftColor: current ? 'var(--pp-plum)' : 'transparent',
background: current ? 'var(--pp-card)' : 'transparent',
opacity: current ? 1 : 0.72,
transition: 'opacity 400ms ease, background 400ms ease',
}}
>
<span
className="mt-1 shrink-0 text-[10px] font-semibold tabular-nums"
style={{ color: 'var(--pp-muted)', fontFamily: 'var(--pp-mono)' }}
>
{String(i + 1).padStart(2, '0')}
</span>
<div
className="min-w-0 flex-1 overflow-x-auto text-[1.02em] [&_.katex-display]:my-1"
dangerouslySetInnerHTML={{ __html: rendered[i] }}
/>
{s.rule ? (
<span
className="mt-0.5 shrink-0 rounded-md px-2 py-1 text-[11px] leading-snug"
style={{
fontFamily: 'var(--pp-mono)',
color: 'var(--pp-plum)',
background: 'color-mix(in oklab, var(--pp-plum) 10%, transparent)',
maxWidth: '38%',
}}
>
{s.rule}
</span>
) : null}
</div>
)
})}
</div>
)}
</AnimPlayerShell>
{block.caption ? (
<figcaption
className="mt-3 text-center text-sm"
style={{ color: 'var(--pp-muted)' }}
>
{block.caption}
</figcaption>
) : null}
{/* Without JS: full derivation visible + rules listed */}
<noscript>
<ol className="mt-3 list-inside list-decimal space-y-2 text-sm">
{steps.map((s, i) => (
<li key={i}>
<span dangerouslySetInnerHTML={{ __html: rendered[i] }} />
{s.rule ? <em className="ml-2 text-muted-foreground">({s.rule})</em> : null}
</li>
))}
</ol>
</noscript>
</figure>
)
}