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>
135 lines
4.4 KiB
TypeScript
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>
|
|
)
|
|
}
|