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:
136
memento-note/lib/interactive-demo/schema.ts
Normal file
136
memento-note/lib/interactive-demo/schema.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
ANNOTATION_KINDS,
|
||||
CHART_TYPES,
|
||||
INTENT_IDS,
|
||||
INTERACTIVE_DEMO_CAPS,
|
||||
INTERACTIVE_DEMO_SCHEMA_VERSION,
|
||||
PANEL_TYPES,
|
||||
PATTERN_IDS,
|
||||
SCOPE_IDS,
|
||||
TRANSITIONS,
|
||||
} from './constants'
|
||||
|
||||
const intentSchema = z.enum(INTENT_IDS).optional()
|
||||
const patternSchema = z.enum(PATTERN_IDS)
|
||||
const scopeSchema = z.enum(SCOPE_IDS)
|
||||
|
||||
const revealSchema = z.object({
|
||||
ids: z.array(z.string().min(1)).min(1),
|
||||
scope: scopeSchema,
|
||||
})
|
||||
|
||||
const annotationSchema = z.object({
|
||||
kind: z.enum(ANNOTATION_KINDS),
|
||||
targetIds: z.array(z.string().min(1)).min(1),
|
||||
scope: scopeSchema,
|
||||
text: z.string().optional(),
|
||||
intent: intentSchema,
|
||||
})
|
||||
|
||||
const stepSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
speak: z.string().min(1),
|
||||
pattern: patternSchema.optional(),
|
||||
pointTo: z.array(z.string().min(1)).optional(),
|
||||
reveal: z.array(revealSchema).optional(),
|
||||
annotate: z
|
||||
.array(annotationSchema)
|
||||
.max(INTERACTIVE_DEMO_CAPS.maxAnnotationsPerStep)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const svgNodeSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().optional(),
|
||||
intent: intentSchema,
|
||||
})
|
||||
|
||||
const svgEdgeSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
from: z.string().min(1),
|
||||
to: z.string().min(1),
|
||||
style: z.enum(['solid', 'dashed']).optional(),
|
||||
weight: z.number().optional(),
|
||||
intent: intentSchema,
|
||||
})
|
||||
|
||||
const svgScenePayloadSchema = z.object({
|
||||
nodes: z.array(svgNodeSchema).min(1),
|
||||
edges: z.array(svgEdgeSchema).optional(),
|
||||
})
|
||||
|
||||
const chartSeriesSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().optional(),
|
||||
values: z.array(z.number()),
|
||||
intent: intentSchema,
|
||||
})
|
||||
|
||||
const chartPayloadSchema = z.object({
|
||||
chartType: z.enum(CHART_TYPES),
|
||||
series: z.array(chartSeriesSchema).min(1),
|
||||
})
|
||||
|
||||
const heatmapPayloadSchema = z.object({
|
||||
rows: z.number().int().positive(),
|
||||
cols: z.number().int().positive(),
|
||||
values: z.array(z.array(z.number())),
|
||||
triangular: z.enum(['lower', 'upper', 'none']).optional(),
|
||||
rowLabels: z.array(z.string()).optional(),
|
||||
colLabels: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
const panelSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.literal('svg-scene'),
|
||||
payload: svgScenePayloadSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.literal('chart'),
|
||||
payload: chartPayloadSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.literal('heatmap-matrix'),
|
||||
payload: heatmapPayloadSchema,
|
||||
}),
|
||||
])
|
||||
|
||||
const sceneSchema = z.object({
|
||||
id: z.string().min(1).optional(),
|
||||
panels: z
|
||||
.array(panelSchema)
|
||||
.min(1)
|
||||
.max(INTERACTIVE_DEMO_CAPS.maxPanelsPerScene),
|
||||
})
|
||||
|
||||
const actSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
scene: sceneSchema.optional(),
|
||||
transition: z.enum(TRANSITIONS).optional(),
|
||||
pattern: patternSchema.optional(),
|
||||
steps: z.array(stepSchema).min(1).max(INTERACTIVE_DEMO_CAPS.maxStepsPerAct),
|
||||
})
|
||||
|
||||
/** Loose BCP-47: primary tag + optional subtags (fr, en, zh-Hans, pt-BR). */
|
||||
const langSchema = z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/, 'Invalid BCP-47 language tag')
|
||||
|
||||
export const interactiveDemoV1Schema = z.object({
|
||||
schemaVersion: z.literal(INTERACTIVE_DEMO_SCHEMA_VERSION),
|
||||
id: z.string().min(1),
|
||||
lang: langSchema,
|
||||
disclaimer: z.string().optional(),
|
||||
scene: sceneSchema,
|
||||
acts: z.array(actSchema).min(1).max(INTERACTIVE_DEMO_CAPS.maxActs),
|
||||
})
|
||||
|
||||
export type InteractiveDemoV1Parsed = z.infer<typeof interactiveDemoV1Schema>
|
||||
|
||||
/** Re-export allowlists for callers that need them at runtime. */
|
||||
export { PANEL_TYPES, PATTERN_IDS }
|
||||
Reference in New Issue
Block a user