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:
Antigravity
2026-07-24 17:51:43 +00:00
parent f385d43d5d
commit 69c99e4f4f
67 changed files with 12005 additions and 33 deletions

View File

@@ -0,0 +1,62 @@
import type { IntentId } from '@/lib/interactive-demo/types'
/**
* Desaturated palette — one set for light, one for dark (P11).
* No free hex in demo JSON; app maps intents here.
*/
export const INTENT_PALETTE_LIGHT: Record<IntentId | 'neutral', string> = {
neutral: '#5c5c5c',
highlight: '#5a7a9a', // prune / slate-blue
flow: '#4a7d68',
cache: '#7a6a8a',
compute: '#8a6b4a',
output: '#4a7a8a',
warning: '#9a6548',
}
export const INTENT_PALETTE_DARK: Record<IntentId | 'neutral', string> = {
neutral: '#a8a8a8',
highlight: '#8aabca',
flow: '#7ab89a',
cache: '#a898b8',
compute: '#c0a080',
output: '#7ab0c0',
warning: '#d09070',
}
/** Light: ~35% readable on white; dark: ~20% as spec. */
export const DIM_OPACITY_LIGHT = 0.35
export const DIM_OPACITY_DARK = 0.2
export function intentColor(
intent: IntentId | null | undefined,
dark: boolean
): string {
const palette = dark ? INTENT_PALETTE_DARK : INTENT_PALETTE_LIGHT
if (!intent) return palette.neutral
return palette[intent] ?? palette.neutral
}
export function spotlightColor(dark: boolean): string {
return intentColor('highlight', dark)
}
export function dimOpacity(dark: boolean): number {
return dark ? DIM_OPACITY_DARK : DIM_OPACITY_LIGHT
}
export const CIRCLED_NUMBERS = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'] as const
export function badgeGlyph(index: number): string {
if (index >= 1 && index <= CIRCLED_NUMBERS.length) {
return CIRCLED_NUMBERS[index - 1]!
}
return String(index)
}
/** Writing rule for generation prompts (not enforced in renderer). */
export const SPEAK_WRITING_GUIDE = {
maxWordsApprox: 25,
sentences: '12',
note: 'Demo tempo is decided at writing time, not at render.',
} as const