Files
Momento/memento-note/components/interactive-page/sim-block.tsx
Antigravity 69c99e4f4f feat: page interactive, démos Play/Step et simulateur Carnot
Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 17:51:43 +00:00

105 lines
3.2 KiB
TypeScript

'use client'
import { GENERIC_SIM_ID, getPlugin } from '@/lib/simulators'
import { ANIM_VIEWS, SIMULATOR_VIEWS } from '@/components/simulators'
import { AnimPlayerShell } from '@/components/simulators/anim-player-shell'
import { GenericFormulaView } from '@/components/simulators/generic-formula-view'
import type { SimBlock } from '@/lib/interactive-page'
/** Renders a `sim` block: catalog plugin (bespoke view) or generic formula. */
export function SimBlockView({
block,
lang,
}: {
block: SimBlock
lang: string
}) {
const sim = block.sim
const fr = lang.startsWith('fr')
const plugin = sim.simId === GENERIC_SIM_ID ? null : getPlugin(sim.simId)
const kindLabel =
plugin?.family === 'anim'
? fr
? 'Animation interactive'
: 'Interactive animation'
: fr
? 'Simulation interactive'
: 'Interactive simulation'
let title: string | undefined
let body: React.ReactNode = null
if (sim.simId === GENERIC_SIM_ID) {
title = sim.title
body = (
<GenericFormulaView
sim={sim as Extract<typeof sim, { simId: 'generic-formula' }>}
lang={lang}
/>
)
} else {
if (!plugin) {
return (
<div className="my-6 rounded-xl border border-dashed border-border p-4 text-sm text-muted-foreground">
{fr ? 'Simulateur indisponible' : 'Simulator unavailable'} ({sim.simId})
</div>
)
}
title = sim.title || (fr ? plugin.title.fr : plugin.title.en)
if (plugin.family === 'anim') {
const AnimScene = ANIM_VIEWS[sim.simId]
if (!AnimScene) {
return (
<div className="my-6 rounded-xl border border-dashed border-border p-4 text-sm text-muted-foreground">
{fr ? 'Animation indisponible' : 'Animation unavailable'} ({sim.simId})
</div>
)
}
body = (
<AnimPlayerShell beats={plugin.beats} lang={lang} disclaimer={plugin.disclaimer}>
{(stepIndex) => <AnimScene step={stepIndex} lang={lang} />}
</AnimPlayerShell>
)
} else {
const View = SIMULATOR_VIEWS[sim.simId]
if (!View) {
return (
<div className="my-6 rounded-xl border border-dashed border-border p-4 text-sm text-muted-foreground">
{fr ? 'Simulateur indisponible' : 'Simulator unavailable'} ({sim.simId})
</div>
)
}
const preset = (sim as { preset?: Record<string, number> }).preset
body = (
<View
preset={preset}
title={title}
disclaimer={sim.disclaimer}
lang={lang}
/>
)
}
}
return (
<figure
className="my-8 rounded-2xl border p-4 md:p-5"
style={{ background: 'var(--pp-card)', borderColor: 'var(--pp-line)' }}
>
<div className="mb-4 flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold tracking-tight">{title}</h3>
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{kindLabel}
</span>
</div>
{body}
{block.caption ? (
<figcaption className="mt-3 text-center text-sm text-muted-foreground">
{block.caption}
</figcaption>
) : null}
</figure>
)
}