@classytic/stage 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -3
  3. package/dist/assets/index.mjs +0 -1
  4. package/dist/assets/kit/glyphs.d.mts +5 -5
  5. package/dist/assets/kit/glyphs.mjs +4 -4
  6. package/dist/builder/Palette.mjs +50 -89
  7. package/dist/builder/SceneBuilder.mjs +18 -76
  8. package/dist/builder/editor.d.mts +2 -1
  9. package/dist/builder/editor.mjs +14 -4
  10. package/dist/builder/tools.d.mts +1 -1
  11. package/dist/builder/tools.mjs +1 -1
  12. package/dist/chem/index.d.mts +5 -5
  13. package/dist/chem/index.mjs +4 -4
  14. package/dist/circuit/index.d.mts +92 -0
  15. package/dist/circuit/index.mjs +333 -0
  16. package/dist/core/clock.d.mts +1 -1
  17. package/dist/core/clock.mjs +3 -3
  18. package/dist/core/control.d.mts +1 -1
  19. package/dist/core/control.mjs +2 -2
  20. package/dist/core/coords.d.mts +4 -4
  21. package/dist/core/coords.mjs +1 -1
  22. package/dist/core/index.d.mts +2 -1
  23. package/dist/core/index.mjs +2 -1
  24. package/dist/core/learner.d.mts +1 -1
  25. package/dist/core/learner.mjs +1 -1
  26. package/dist/core/math.d.mts +26 -0
  27. package/dist/core/math.mjs +37 -0
  28. package/dist/core/motion.d.mts +6 -6
  29. package/dist/core/motion.mjs +6 -6
  30. package/dist/core/richText.d.mts +2 -2
  31. package/dist/core/vec.d.mts +1 -1
  32. package/dist/field/index.d.mts +11 -3
  33. package/dist/field/index.mjs +20 -3
  34. package/dist/finance/bizsim.d.mts +93 -0
  35. package/dist/finance/bizsim.mjs +117 -0
  36. package/dist/finance/index.d.mts +118 -0
  37. package/dist/finance/index.mjs +203 -0
  38. package/dist/index.d.mts +6 -3
  39. package/dist/index.mjs +6 -4
  40. package/dist/interaction/MovableDot.d.mts +17 -1
  41. package/dist/interaction/MovableDot.mjs +160 -43
  42. package/dist/interaction/index.d.mts +2 -1
  43. package/dist/interaction/index.mjs +2 -1
  44. package/dist/interaction/useDraggable.d.mts +1 -1
  45. package/dist/interaction/useDraggable.mjs +55 -5
  46. package/dist/interaction/usePressSpring.d.mts +24 -0
  47. package/dist/interaction/usePressSpring.mjs +67 -0
  48. package/dist/logic/ast.d.mts +1 -1
  49. package/dist/logic/index.mjs +8 -3
  50. package/dist/logic/minimize.d.mts +2 -2
  51. package/dist/logic/minimize.mjs +56 -18
  52. package/dist/logic/table.mjs +1 -1
  53. package/dist/math/ast.d.mts +1 -1
  54. package/dist/math/ast.mjs +1 -1
  55. package/dist/math/calculus.d.mts +22 -1
  56. package/dist/math/calculus.mjs +224 -11
  57. package/dist/math/compile.d.mts +8 -0
  58. package/dist/math/compile.mjs +40 -0
  59. package/dist/math/defs.mjs +1 -1
  60. package/dist/math/index.d.mts +5 -4
  61. package/dist/math/index.mjs +5 -4
  62. package/dist/math/latex.mjs +8 -0
  63. package/dist/math/parse.mjs +2 -2
  64. package/dist/math/tokenize.mjs +1 -1
  65. package/dist/primitives/CanvasLayer.mjs +3 -3
  66. package/dist/primitives/Dot.d.mts +2 -15
  67. package/dist/primitives/Dot.mjs +6 -4
  68. package/dist/primitives/Grid.d.mts +41 -14
  69. package/dist/primitives/Grid.mjs +100 -22
  70. package/dist/primitives/Label.d.mts +2 -15
  71. package/dist/primitives/Label.mjs +4 -3
  72. package/dist/primitives/Lines.d.mts +4 -32
  73. package/dist/primitives/Lines.mjs +10 -8
  74. package/dist/primitives/Shapes.d.mts +5 -43
  75. package/dist/primitives/Shapes.mjs +12 -10
  76. package/dist/primitives/Tex.mjs +2 -2
  77. package/dist/primitives/index.d.mts +2 -2
  78. package/dist/primitives/index.mjs +2 -2
  79. package/dist/primitives/props.mjs +31 -0
  80. package/dist/scene/Scene.d.mts +6 -1
  81. package/dist/scene/Scene.mjs +16 -41
  82. package/dist/scene/assets.d.mts +2 -2
  83. package/dist/scene/assets.mjs +2 -2
  84. package/dist/scene/commands.d.mts +10 -1
  85. package/dist/scene/commands.mjs +42 -2
  86. package/dist/scene/evaluators.mjs +2 -2
  87. package/dist/scene/index.d.mts +2 -2
  88. package/dist/scene/index.mjs +2 -2
  89. package/dist/scene/migrate.mjs +1 -1
  90. package/dist/scene/sims.mjs +1 -1
  91. package/dist/scene/types.d.mts +4 -4
  92. package/dist/sim/particles.d.mts +2 -2
  93. package/dist/sim/rate.d.mts +4 -4
  94. package/dist/sim/rate.mjs +1 -1
  95. package/dist/sim/registry.d.mts +1 -1
  96. package/dist/sim/sampler.d.mts +5 -5
  97. package/dist/sim/thermal.d.mts +3 -3
  98. package/dist/sim/thermal.mjs +2 -2
  99. package/dist/sim/types.d.mts +2 -2
  100. package/dist/sim/wave.d.mts +2 -2
  101. package/dist/sim/wave.mjs +2 -2
  102. package/dist/steps/index.d.mts +1 -1
  103. package/dist/steps/index.mjs +3 -3
  104. package/dist/thermo/index.d.mts +1 -1
  105. package/dist/thermo/index.mjs +1 -1
  106. package/dist/view/Stage.mjs +6 -13
  107. package/dist/view/useInView.mjs +1 -1
  108. package/package.json +37 -25
  109. package/styles.css +141 -1
  110. package/dist/assets/kit/index.mjs +0 -4
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Classytic
3
+ Copyright (c) 2026 Classytic LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Domain‑neutral visual primitive engine for the web.** Declarative SVG diagram/math
4
4
  primitives, draggable handles, a math↔pixel coordinate system, theming, a parametric
5
5
  asset/glyph kit, a serializable `SceneDoc` graph (resolver + command/undo), and a
6
- `LessonDoc` sequence engine. Reactive, web‑native, SSR‑safe, and agent‑drivable — the
6
+ stepped‑lesson sequence engine (`StepSpec`). Reactive, web‑native, SSR‑safe, and agent‑drivable — the
7
7
  render foundation under [`@classytic/labs`](https://github.com/classytic/labs).
8
8
 
9
9
  ```bash
@@ -14,7 +14,7 @@ npm i @classytic/stage react react-dom
14
14
 
15
15
  Everything is also re‑exported from the root for convenience, but prefer subpaths so
16
16
  your bundle pulls only what it uses (ESM, per‑file emit; `sideEffects` are scoped to
17
- `**/*.css` + `**/assets/**`, so JS treeshakes while styles/assets are preserved):
17
+ `**/*.css`, so JavaScript tree-shakes while the shared stylesheet is preserved):
18
18
 
19
19
  | Subpath | What's in it |
20
20
  |---|---|
@@ -27,7 +27,7 @@ your bundle pulls only what it uses (ESM, per‑file emit; `sideEffects` are sco
27
27
  | `@classytic/stage/builder` | `Editor`, `SceneBuilder`, `Palette`, construction `*_TOOLS` |
28
28
  | `@classytic/stage/assets` | `StageAssetDefs` + the glyph kit (`XBlockGlyph`, `WeightGlyph`, `BucketGlyph`, `HangerHook`) |
29
29
  | `@classytic/stage/math` | dependency‑free expression engine (`compileExpr`, `differentiate`, `toLatex`…) |
30
- | `@classytic/stage/lesson` | `LessonProvider`, `useLessonProgress`, `LessonStepScope` (sequence + gating + progress) |
30
+ | `@classytic/stage/steps` | `StepProgressProvider`, `useStepProgress`, `StepScope` (step sequence + gating + progress) |
31
31
  | `@classytic/stage/styles.css` | theme tokens + control‑kit CSS (import once in global CSS) |
32
32
 
33
33
  ## Quick start
@@ -63,3 +63,7 @@ export function Demo() {
63
63
  ## License
64
64
 
65
65
  MIT
66
+
67
+ ## Trademark
68
+
69
+ MIT-licensed code. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see [TRADEMARK.md](TRADEMARK.md).
@@ -1,5 +1,4 @@
1
1
  import { StageAssetDefs } from "./kit/svg-defs.mjs";
2
2
  import { BucketGlyph, HangerHook, WeightGlyph, XBlockGlyph } from "./kit/glyphs.mjs";
3
- import "./kit/index.mjs";
4
3
 
5
4
  export { BucketGlyph, HangerHook, StageAssetDefs, WeightGlyph, XBlockGlyph };
@@ -8,7 +8,7 @@ interface GlyphBox {
8
8
  hpx: number;
9
9
  label: string;
10
10
  }
11
- /** The UNKNOWN a crisp rounded tile with an italic label (e.g. "2x"). */
11
+ /** The UNKNOWN, a crisp rounded tile with an italic label (e.g. "2x"). */
12
12
  declare function XBlockGlyph({
13
13
  cx,
14
14
  top,
@@ -16,7 +16,7 @@ declare function XBlockGlyph({
16
16
  hpx,
17
17
  label
18
18
  }: GlyphBox): ReactNode;
19
- /** A container of UNKNOWN amount a pail, brand-colored (it IS an unknown, like
19
+ /** A container of UNKNOWN amount, a pail, brand-colored (it IS an unknown, like
20
20
  * the tile), labelled "?" until revealed. Built with real form: one light
21
21
  * direction (top-left), 2-tone shading for volume (gradient body + a soft
22
22
  * right-side shadow + a left specular streak), a rim with visible thickness
@@ -33,13 +33,13 @@ interface HangerHookProps {
33
33
  /** Attach point on the beam/arm (pixels). */
34
34
  topX: number;
35
35
  topY: number;
36
- /** Attach point on the load the top-centre of the glyph it carries (pixels). */
36
+ /** Attach point on the load, the top-centre of the glyph it carries (pixels). */
37
37
  botX: number;
38
38
  botY: number;
39
39
  }
40
40
  /**
41
41
  * A suspension: a thin eye on the beam, a taut thread, and an S-hook that grips
42
- * the load the realistic way a weight hangs from an arm (replaces the old
42
+ * the load, the realistic way a weight hangs from an arm (replaces the old
43
43
  * bare "line + dot"). Pure pixel glyph: callers project math→px first. Layered
44
44
  * edge/metal/sheen strokes match the beam's metallic read.
45
45
  */
@@ -49,7 +49,7 @@ declare function HangerHook({
49
49
  botX,
50
50
  botY
51
51
  }: HangerHookProps): ReactNode;
52
- /** A KNOWN value a clean flat coin with the number. */
52
+ /** A KNOWN value, a clean flat coin with the number. */
53
53
  declare function WeightGlyph({
54
54
  cx,
55
55
  top,
@@ -3,7 +3,7 @@
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
 
5
5
  //#region src/assets/kit/glyphs.tsx
6
- /** The UNKNOWN a crisp rounded tile with an italic label (e.g. "2x"). */
6
+ /** The UNKNOWN, a crisp rounded tile with an italic label (e.g. "2x"). */
7
7
  function XBlockGlyph({ cx, top, wpx, hpx, label }) {
8
8
  const s = Math.min(wpx, hpx);
9
9
  const x0 = cx - s / 2;
@@ -50,7 +50,7 @@ function XBlockGlyph({ cx, top, wpx, hpx, label }) {
50
50
  })
51
51
  ] });
52
52
  }
53
- /** A container of UNKNOWN amount a pail, brand-colored (it IS an unknown, like
53
+ /** A container of UNKNOWN amount, a pail, brand-colored (it IS an unknown, like
54
54
  * the tile), labelled "?" until revealed. Built with real form: one light
55
55
  * direction (top-left), 2-tone shading for volume (gradient body + a soft
56
56
  * right-side shadow + a left specular streak), a rim with visible thickness
@@ -156,7 +156,7 @@ function BucketGlyph({ cx, top, wpx, hpx, label }) {
156
156
  }
157
157
  /**
158
158
  * A suspension: a thin eye on the beam, a taut thread, and an S-hook that grips
159
- * the load the realistic way a weight hangs from an arm (replaces the old
159
+ * the load, the realistic way a weight hangs from an arm (replaces the old
160
160
  * bare "line + dot"). Pure pixel glyph: callers project math→px first. Layered
161
161
  * edge/metal/sheen strokes match the beam's metallic read.
162
162
  */
@@ -210,7 +210,7 @@ function HangerHook({ topX, topY, botX, botY }) {
210
210
  })
211
211
  ] });
212
212
  }
213
- /** A KNOWN value a clean flat coin with the number. */
213
+ /** A KNOWN value, a clean flat coin with the number. */
214
214
  function WeightGlyph({ cx, top, wpx, hpx, label }) {
215
215
  const r = Math.min(wpx, hpx) / 2;
216
216
  const cy = top + r;
@@ -4,96 +4,57 @@ import { ToolIcon } from "./icons.mjs";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
 
6
6
  //#region src/builder/Palette.tsx
7
- const btn = (active) => ({
8
- display: "inline-flex",
9
- alignItems: "center",
10
- gap: 6,
11
- padding: "6px 10px",
12
- borderRadius: 8,
13
- fontSize: 13,
14
- fontWeight: 600,
15
- cursor: "pointer",
16
- color: active ? "var(--stage-bg)" : "var(--stage-fg)",
17
- background: active ? "var(--stage-accent)" : "transparent",
18
- border: `1px solid ${active ? "var(--stage-accent)" : "var(--stage-muted)"}`
19
- });
20
- const iconBtn = (disabled) => ({
21
- display: "inline-flex",
22
- alignItems: "center",
23
- gap: 6,
24
- padding: "6px 10px",
25
- borderRadius: 8,
26
- fontSize: 13,
27
- fontWeight: 600,
28
- cursor: disabled ? "not-allowed" : "pointer",
29
- opacity: disabled ? .4 : 1,
30
- color: "var(--stage-fg)",
31
- background: "transparent",
32
- border: "1px solid var(--stage-muted)"
33
- });
34
7
  function Palette({ tools, activeTool, onSelect, onUndo, onRedo, canUndo, canRedo, onClear, onSave, hint }) {
35
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
36
- role: "toolbar",
37
- "aria-label": "construction tools",
38
- style: {
39
- display: "flex",
40
- flexWrap: "wrap",
41
- gap: 6,
42
- alignItems: "center"
43
- },
44
- children: [
45
- tools.map((t) => /* @__PURE__ */ jsxs("button", {
46
- type: "button",
47
- "aria-pressed": activeTool === t.id,
48
- onClick: () => onSelect(t.id),
49
- style: btn(activeTool === t.id),
50
- children: [/* @__PURE__ */ jsx(ToolIcon, { name: t.icon }), t.label]
51
- }, t.id)),
52
- /* @__PURE__ */ jsx("span", { style: {
53
- width: 1,
54
- alignSelf: "stretch",
55
- background: "var(--stage-muted)",
56
- margin: "0 4px"
57
- } }),
58
- /* @__PURE__ */ jsxs("button", {
59
- type: "button",
60
- onClick: onUndo,
61
- disabled: !canUndo,
62
- style: iconBtn(!canUndo),
63
- children: [/* @__PURE__ */ jsx(ToolIcon, { name: "undo" }), "Undo"]
64
- }),
65
- /* @__PURE__ */ jsxs("button", {
66
- type: "button",
67
- onClick: onRedo,
68
- disabled: !canRedo,
69
- style: iconBtn(!canRedo),
70
- children: [/* @__PURE__ */ jsx(ToolIcon, { name: "redo" }), "Redo"]
71
- }),
72
- /* @__PURE__ */ jsx("button", {
73
- type: "button",
74
- onClick: onClear,
75
- style: iconBtn(false),
76
- children: "Clear"
77
- }),
78
- onSave && /* @__PURE__ */ jsx("button", {
79
- type: "button",
80
- onClick: onSave,
81
- style: {
82
- ...iconBtn(false),
83
- color: "var(--stage-good)",
84
- borderColor: "var(--stage-good)"
85
- },
86
- children: "Save"
87
- })
88
- ]
89
- }), /* @__PURE__ */ jsx("p", {
90
- style: {
91
- fontSize: 12,
92
- opacity: .7,
93
- margin: "6px 2px 0"
94
- },
95
- children: hint
96
- })] });
8
+ return /* @__PURE__ */ jsxs("div", {
9
+ className: "stage-palette",
10
+ children: [/* @__PURE__ */ jsxs("div", {
11
+ role: "toolbar",
12
+ "aria-label": "construction tools",
13
+ className: "stage-palette__toolbar",
14
+ children: [
15
+ tools.map((t) => /* @__PURE__ */ jsxs("button", {
16
+ type: "button",
17
+ "aria-pressed": activeTool === t.id,
18
+ onClick: () => onSelect(t.id),
19
+ className: "stage-tool-button",
20
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: t.icon }), t.label]
21
+ }, t.id)),
22
+ /* @__PURE__ */ jsx("span", {
23
+ className: "stage-palette__divider",
24
+ "aria-hidden": "true"
25
+ }),
26
+ /* @__PURE__ */ jsxs("button", {
27
+ type: "button",
28
+ onClick: onUndo,
29
+ disabled: !canUndo,
30
+ className: "stage-tool-button",
31
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: "undo" }), "Undo"]
32
+ }),
33
+ /* @__PURE__ */ jsxs("button", {
34
+ type: "button",
35
+ onClick: onRedo,
36
+ disabled: !canRedo,
37
+ className: "stage-tool-button",
38
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: "redo" }), "Redo"]
39
+ }),
40
+ /* @__PURE__ */ jsx("button", {
41
+ type: "button",
42
+ onClick: onClear,
43
+ className: "stage-tool-button",
44
+ children: "Clear"
45
+ }),
46
+ onSave && /* @__PURE__ */ jsx("button", {
47
+ type: "button",
48
+ onClick: onSave,
49
+ className: "stage-tool-button stage-tool-button--save",
50
+ children: "Save"
51
+ })
52
+ ]
53
+ }), /* @__PURE__ */ jsx("p", {
54
+ className: "stage-palette__hint",
55
+ children: hint
56
+ })]
57
+ });
97
58
  }
98
59
 
99
60
  //#endregion
@@ -14,9 +14,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
14
14
 
15
15
  //#region src/builder/SceneBuilder.tsx
16
16
  /**
17
- * <SceneBuilder> the click-to-construct authoring surface. Pick a tool, click
17
+ * <SceneBuilder>, the click-to-construct authoring surface. Pick a tool, click
18
18
  * the canvas: every action funnels through the Editor command stack (undo/redo +
19
- * the agent API). Output is a SceneDoc (onSave) the same JSON the player
19
+ * the agent API). Output is a SceneDoc (onSave), the same JSON the player
20
20
  * renders and the DB stores. Free points drag only in Select mode, so clicks in
21
21
  * other tools go to the tool, not a drag.
22
22
  */
@@ -149,12 +149,7 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
149
149
  }, `pend-${id}`) : null;
150
150
  }), [pending, resolved]);
151
151
  return /* @__PURE__ */ jsxs("div", {
152
- className: "not-prose",
153
- style: {
154
- display: "flex",
155
- flexDirection: "column",
156
- gap: 10
157
- },
152
+ className: "not-prose stage-builder",
158
153
  children: [
159
154
  /* @__PURE__ */ jsx(Palette, {
160
155
  tools: toolSet,
@@ -184,21 +179,11 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
184
179
  ]
185
180
  }),
186
181
  authoring && scalars.length > 0 && /* @__PURE__ */ jsx("div", {
187
- style: {
188
- display: "flex",
189
- flexWrap: "wrap",
190
- gap: 14,
191
- padding: "2px",
192
- fontSize: 13
193
- },
182
+ className: "stage-builder__scalars",
194
183
  children: scalars.map((el) => {
195
184
  const f = el.free;
196
185
  return /* @__PURE__ */ jsxs("label", {
197
- style: {
198
- display: "inline-flex",
199
- alignItems: "center",
200
- gap: 6
201
- },
186
+ className: "stage-builder__scalar",
202
187
  children: [
203
188
  /* @__PURE__ */ jsx("strong", { children: el.label ?? el.id }),
204
189
  /* @__PURE__ */ jsx("input", {
@@ -214,10 +199,7 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
214
199
  })
215
200
  }),
216
201
  /* @__PURE__ */ jsx("span", {
217
- style: {
218
- fontVariantNumeric: "tabular-nums",
219
- minWidth: 24
220
- },
202
+ className: "stage-builder__scalar-value",
221
203
  children: f.value
222
204
  })
223
205
  ]
@@ -225,30 +207,12 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
225
207
  })
226
208
  }),
227
209
  authoring && /* @__PURE__ */ jsxs("details", {
228
- style: {
229
- fontSize: 13,
230
- border: "1px solid var(--stage-muted)",
231
- borderRadius: 8,
232
- padding: "6px 10px"
233
- },
234
- children: [/* @__PURE__ */ jsx("summary", {
235
- style: {
236
- cursor: "pointer",
237
- fontWeight: 600
238
- },
239
- children: "Pedagogy — objectives · hints · difficulty"
240
- }), /* @__PURE__ */ jsxs("div", {
241
- style: {
242
- display: "grid",
243
- gap: 8,
244
- marginTop: 8
245
- },
210
+ className: "stage-builder__pedagogy",
211
+ children: [/* @__PURE__ */ jsx("summary", { children: "Pedagogy, objectives · hints · difficulty" }), /* @__PURE__ */ jsxs("div", {
212
+ className: "stage-builder__pedagogy-fields",
246
213
  children: [
247
214
  /* @__PURE__ */ jsxs("label", {
248
- style: {
249
- display: "grid",
250
- gap: 3
251
- },
215
+ className: "stage-builder__field-label",
252
216
  children: ["Objectives (one per line)", /* @__PURE__ */ jsx("textarea", {
253
217
  rows: 2,
254
218
  value: (pedagogy.objectives ?? []).join("\n"),
@@ -256,14 +220,11 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
256
220
  ...p,
257
221
  objectives: splitLines(e.currentTarget.value)
258
222
  })),
259
- style: FIELD
223
+ className: "stage-builder__field"
260
224
  })]
261
225
  }),
262
226
  /* @__PURE__ */ jsxs("label", {
263
- style: {
264
- display: "grid",
265
- gap: 3
266
- },
227
+ className: "stage-builder__field-label",
267
228
  children: ["Hints (one per line)", /* @__PURE__ */ jsx("textarea", {
268
229
  rows: 2,
269
230
  value: (pedagogy.hints ?? []).join("\n"),
@@ -271,42 +232,32 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
271
232
  ...p,
272
233
  hints: splitLines(e.currentTarget.value)
273
234
  })),
274
- style: FIELD
235
+ className: "stage-builder__field"
275
236
  })]
276
237
  }),
277
238
  /* @__PURE__ */ jsxs("label", {
278
- style: {
279
- display: "grid",
280
- gap: 3
281
- },
239
+ className: "stage-builder__field-label",
282
240
  children: ["Success criteria", /* @__PURE__ */ jsx("input", {
283
241
  value: pedagogy.successCriteria ?? "",
284
242
  onChange: (e) => setPedagogy((p) => ({
285
243
  ...p,
286
244
  successCriteria: e.currentTarget.value || void 0
287
245
  })),
288
- style: FIELD
246
+ className: "stage-builder__field"
289
247
  })]
290
248
  }),
291
249
  /* @__PURE__ */ jsxs("label", {
292
- style: {
293
- display: "inline-flex",
294
- alignItems: "center",
295
- gap: 8
296
- },
250
+ className: "stage-builder__difficulty",
297
251
  children: ["Difficulty", /* @__PURE__ */ jsxs("select", {
298
252
  value: pedagogy.difficulty ?? "",
299
253
  onChange: (e) => setPedagogy((p) => ({
300
254
  ...p,
301
255
  difficulty: e.currentTarget.value ? Number(e.currentTarget.value) : void 0
302
256
  })),
303
- style: {
304
- ...FIELD,
305
- width: "auto"
306
- },
257
+ className: "stage-builder__field stage-builder__field--select",
307
258
  children: [/* @__PURE__ */ jsx("option", {
308
259
  value: "",
309
- children: ""
260
+ children: ", "
310
261
  }), [
311
262
  1,
312
263
  2,
@@ -325,15 +276,6 @@ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring =
325
276
  ]
326
277
  });
327
278
  }
328
- const FIELD = {
329
- width: "100%",
330
- border: "1px solid var(--stage-muted)",
331
- borderRadius: 6,
332
- padding: "4px 8px",
333
- background: "transparent",
334
- color: "inherit",
335
- font: "inherit"
336
- };
337
279
  const splitLines = (s) => s.split("\n").map((x) => x.trim()).filter(Boolean);
338
280
 
339
281
  //#endregion
@@ -21,7 +21,8 @@ declare class Editor {
21
21
  getResolved(): Resolved;
22
22
  subscribe(fn: () => void): () => void;
23
23
  private emit;
24
- /** Apply a command. The ONLY mutation entry point. */
24
+ /** Apply a command. The ONLY mutation entry point. Validates first: an invalid command (bad
25
+ * id, dangling binding, missing target) is REJECTED with a reason and the doc is untouched. */
25
26
  dispatch(cmd: Command, opts?: DispatchOpts): CommandResult;
26
27
  /** Group boundary marker (label-only in this impl; batch commands group atomically). */
27
28
  mark(_label?: string): void;
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { resolve } from "../scene/resolve.mjs";
4
- import { applyCommand, inverse } from "../scene/commands.mjs";
4
+ import { applyCommand, createdIds, inverse, validateCommand } from "../scene/commands.mjs";
5
5
  import { useEffect, useReducer, useRef, useState } from "react";
6
6
 
7
7
  //#region src/builder/editor.ts
8
8
  /**
9
- * Editor the single funnel for ALL scene mutations (builder tools, agents,
9
+ * Editor, the single funnel for ALL scene mutations (builder tools, agents,
10
10
  * undo/redo, the control-surface bridge). Commands ARE the agent action API:
11
11
  * dispatch(cmd) is exactly what a human tool, a voice agent, and an undo all call.
12
12
  * Drag is an ephemeral overlay layer; updateDrag writes no command (no undo, no
@@ -48,8 +48,14 @@ var Editor = class {
48
48
  emit() {
49
49
  this.listeners.forEach((l) => l());
50
50
  }
51
- /** Apply a command. The ONLY mutation entry point. */
51
+ /** Apply a command. The ONLY mutation entry point. Validates first: an invalid command (bad
52
+ * id, dangling binding, missing target) is REJECTED with a reason and the doc is untouched. */
52
53
  dispatch(cmd, opts = {}) {
54
+ const error = validateCommand(this.doc, cmd);
55
+ if (error) return {
56
+ ok: false,
57
+ error
58
+ };
53
59
  const inv = inverse(cmd, this.doc);
54
60
  this.doc = applyCommand(this.doc, cmd);
55
61
  if (opts.history !== "ignore") {
@@ -60,7 +66,11 @@ var Editor = class {
60
66
  this.redoStack = [];
61
67
  }
62
68
  this.emit();
63
- return { ok: true };
69
+ const ids = createdIds(cmd);
70
+ return ids.length ? {
71
+ ok: true,
72
+ createdIds: ids
73
+ } : { ok: true };
64
74
  }
65
75
  /** Group boundary marker (label-only in this impl; batch commands group atomically). */
66
76
  mark(_label) {}
@@ -36,7 +36,7 @@ declare const MIDPOINT: Tool;
36
36
  declare const INTERSECT: Tool;
37
37
  declare const MEASURE: Tool;
38
38
  declare const DELETE: Tool;
39
- /** A free scalar parameter appears as a live slider in the authoring strip. */
39
+ /** A free scalar parameter, appears as a live slider in the authoring strip. */
40
40
  declare const SCALAR: Tool;
41
41
  /** A free text note placed in the scene. */
42
42
  declare const NOTE: Tool;
@@ -224,7 +224,7 @@ const DELETE = {
224
224
  });
225
225
  }
226
226
  };
227
- /** A free scalar parameter appears as a live slider in the authoring strip. */
227
+ /** A free scalar parameter, appears as a live slider in the authoring strip. */
228
228
  const SCALAR = {
229
229
  id: "scalar",
230
230
  icon: "scalar",
@@ -1,6 +1,6 @@
1
1
  //#region src/chem/index.d.ts
2
2
  /**
3
- * `chem` acid–base / pH AND electrochemistry / Nernst, as a PURE kernel (no
3
+ * `chem`, acid–base / pH AND electrochemistry / Nernst, as a PURE kernel (no
4
4
  * React, no pixels). The engine behind titration curves, pH problems, and galvanic
5
5
  * cells.
6
6
  *
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * where n_a is the acid initially present, n_b the strong base added, V the total
13
13
  * volume. A strong acid is just the K_a → ∞ limit (fully dissociated). Solving the
14
- * balance (monotonic in h → bisection) is robust everywhere initial point, buffer
15
- * region, equivalence, and past it with no piecewise approximations to get wrong.
14
+ * balance (monotonic in h → bisection) is robust everywhere, initial point, buffer
15
+ * region, equivalence, and past it, with no piecewise approximations to get wrong.
16
16
  *
17
17
  * Concentrations in mol/L, volumes in litres. Currently models an ACID analyte
18
18
  * titrated by a STRONG BASE (covers strong-acid and weak-acid curves).
@@ -81,7 +81,7 @@ interface CellResult {
81
81
  declare function galvanicCell(x: HalfCell, y: HalfCell, T?: number): CellResult;
82
82
  /** Arrhenius rate constant k = A·e^(−Ea/RT). */
83
83
  declare function arrhenius(A: number, Ea: number, T: number): number;
84
- /** Ratio k(T)/k(Tref) at fixed Ea the stable way to scale a rate without a raw A. */
84
+ /** Ratio k(T)/k(Tref) at fixed Ea, the stable way to scale a rate without a raw A. */
85
85
  declare function arrheniusRatio(Ea: number, T: number, Tref?: number): number;
86
86
  /** Fraction of molecules with energy ≥ Ea (Boltzmann) ≈ e^(−Ea/RT). */
87
87
  declare function fractionAboveEa(Ea: number, T: number): number;
@@ -94,7 +94,7 @@ interface StoichSpecies {
94
94
  name: string;
95
95
  /** Stoichiometric coefficient in the balanced equation. */
96
96
  coeff: number;
97
- /** Molar mass, g/mol (optional enables grams in the result). */
97
+ /** Molar mass, g/mol (optional, enables grams in the result). */
98
98
  molarMass?: number;
99
99
  }
100
100
  interface StoichResult {
@@ -1,6 +1,6 @@
1
1
  //#region src/chem/index.ts
2
2
  /**
3
- * `chem` acid–base / pH AND electrochemistry / Nernst, as a PURE kernel (no
3
+ * `chem`, acid–base / pH AND electrochemistry / Nernst, as a PURE kernel (no
4
4
  * React, no pixels). The engine behind titration curves, pH problems, and galvanic
5
5
  * cells.
6
6
  *
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * where n_a is the acid initially present, n_b the strong base added, V the total
13
13
  * volume. A strong acid is just the K_a → ∞ limit (fully dissociated). Solving the
14
- * balance (monotonic in h → bisection) is robust everywhere initial point, buffer
15
- * region, equivalence, and past it with no piecewise approximations to get wrong.
14
+ * balance (monotonic in h → bisection) is robust everywhere, initial point, buffer
15
+ * region, equivalence, and past it, with no piecewise approximations to get wrong.
16
16
  *
17
17
  * Concentrations in mol/L, volumes in litres. Currently models an ACID analyte
18
18
  * titrated by a STRONG BASE (covers strong-acid and weak-acid curves).
@@ -90,7 +90,7 @@ function galvanicCell(x, y, T = T_STD) {
90
90
  function arrhenius(A, Ea, T) {
91
91
  return A * Math.exp(-Ea / (R * T));
92
92
  }
93
- /** Ratio k(T)/k(Tref) at fixed Ea the stable way to scale a rate without a raw A. */
93
+ /** Ratio k(T)/k(Tref) at fixed Ea, the stable way to scale a rate without a raw A. */
94
94
  function arrheniusRatio(Ea, T, Tref = T_STD) {
95
95
  return Math.exp(-(Ea / R) * (1 / T - 1 / Tref));
96
96
  }