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>
105 lines
3.2 KiB
TypeScript
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>
|
|
)
|
|
}
|