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>
This commit is contained in:
104
memento-note/components/interactive-page/sim-block.tsx
Normal file
104
memento-note/components/interactive-page/sim-block.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user