@classytic/stage 0.2.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 (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -1
  3. package/dist/assets/index.mjs +0 -1
  4. package/dist/builder/Palette.mjs +50 -89
  5. package/dist/builder/SceneBuilder.mjs +15 -73
  6. package/dist/core/index.d.mts +2 -1
  7. package/dist/core/index.mjs +2 -1
  8. package/dist/core/math.d.mts +26 -0
  9. package/dist/core/math.mjs +37 -0
  10. package/dist/finance/bizsim.d.mts +93 -0
  11. package/dist/finance/bizsim.mjs +117 -0
  12. package/dist/finance/index.d.mts +118 -0
  13. package/dist/finance/index.mjs +203 -0
  14. package/dist/index.d.mts +4 -3
  15. package/dist/index.mjs +4 -4
  16. package/dist/interaction/MovableDot.mjs +19 -0
  17. package/dist/interaction/useDraggable.mjs +24 -4
  18. package/dist/math/calculus.d.mts +22 -1
  19. package/dist/math/calculus.mjs +156 -2
  20. package/dist/math/index.d.mts +2 -2
  21. package/dist/math/index.mjs +2 -2
  22. package/dist/math/latex.mjs +8 -0
  23. package/dist/primitives/Dot.d.mts +2 -15
  24. package/dist/primitives/Dot.mjs +6 -4
  25. package/dist/primitives/Grid.d.mts +33 -17
  26. package/dist/primitives/Grid.mjs +89 -17
  27. package/dist/primitives/Label.d.mts +1 -14
  28. package/dist/primitives/Label.mjs +3 -2
  29. package/dist/primitives/Lines.d.mts +4 -32
  30. package/dist/primitives/Lines.mjs +10 -8
  31. package/dist/primitives/Shapes.d.mts +5 -43
  32. package/dist/primitives/Shapes.mjs +12 -10
  33. package/dist/primitives/index.d.mts +2 -2
  34. package/dist/primitives/index.mjs +2 -2
  35. package/dist/primitives/props.mjs +31 -0
  36. package/dist/scene/Scene.d.mts +6 -1
  37. package/dist/scene/Scene.mjs +15 -40
  38. package/dist/view/Stage.mjs +4 -11
  39. package/package.json +30 -22
  40. package/styles.css +125 -8
  41. 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
@@ -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
  |---|---|
@@ -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 };
@@ -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
@@ -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,39 +232,29 @@ 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
260
  children: ", "
@@ -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
@@ -5,4 +5,5 @@ import { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragO
5
5
  import { ClockProvider, ClockProviderProps, FrameDriver, FrameInfo, useFrameLoop } from "./clock.mjs";
6
6
  import { Learner, LearnerProvider, LearnerResult, PriorAttempts, useLearner } from "./learner.mjs";
7
7
  import { EaseFn, EaseName, Keyframe, OscillateOpts, SpringOpts, WaveShape, ease, lerp, oscillate, prefersReducedMotion, spring, timeline } from "./motion.mjs";
8
- export { ClockProvider, type ClockProviderProps, type ControlMap, type ControlSpec, type ControlSurface, type CoordinateSystem, CoordsContext, type CoordsOptions, DragOverlayContext, type EaseFn, type EaseName, type FrameDriver, type FrameInfo, type Keyframe, type Learner, LearnerProvider, type LearnerResult, type Matrix, type OscillateOpts, type PriorAttempts, type SpringOpts, StageRefContext, type Vec2, type ViewBox, type WaveShape, createCoords, ease, fmt, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, spring, timeline, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
8
+ import { approxEq, clamp, clamp01, gcd, remap, round, snapTo, toDeg, toRad } from "./math.mjs";
9
+ export { ClockProvider, type ClockProviderProps, type ControlMap, type ControlSpec, type ControlSurface, type CoordinateSystem, CoordsContext, type CoordsOptions, DragOverlayContext, type EaseFn, type EaseName, type FrameDriver, type FrameInfo, type Keyframe, type Learner, LearnerProvider, type LearnerResult, type Matrix, type OscillateOpts, type PriorAttempts, type SpringOpts, StageRefContext, type Vec2, type ViewBox, type WaveShape, approxEq, clamp, clamp01, createCoords, ease, fmt, gcd, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, remap, round, snapTo, spring, timeline, toDeg, toRad, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
@@ -4,6 +4,7 @@ import { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragO
4
4
  import { ClockProvider, useFrameLoop } from "./clock.mjs";
5
5
  import { getControlSurface, listControlSurfaces, onControlChange, useControlSurface } from "./control.mjs";
6
6
  import { LearnerProvider, useLearner } from "./learner.mjs";
7
+ import { approxEq, clamp, clamp01, gcd, remap, round, snapTo, toDeg, toRad } from "./math.mjs";
7
8
  import { ease, lerp, oscillate, prefersReducedMotion, spring, timeline } from "./motion.mjs";
8
9
 
9
- export { ClockProvider, CoordsContext, DragOverlayContext, LearnerProvider, StageRefContext, createCoords, ease, fmt, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, spring, timeline, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
10
+ export { ClockProvider, CoordsContext, DragOverlayContext, LearnerProvider, StageRefContext, approxEq, clamp, clamp01, createCoords, ease, fmt, gcd, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, remap, round, snapTo, spring, timeline, toDeg, toRad, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
@@ -0,0 +1,26 @@
1
+ //#region src/core/math.d.ts
2
+ /**
3
+ * Small numeric kit shared by every consumer (labs re-exports these). One
4
+ * source of truth: labs presets used to hand-roll `clamp` 8+ different times.
5
+ * Pure, SSR-deterministic (no transcendental output is emitted directly into
6
+ * SVG — callers still `fmt()` pixel values).
7
+ */
8
+ declare const clamp: (v: number, lo: number, hi: number) => number;
9
+ /** Clamp to the unit interval [0, 1] — the ubiquitous "fraction / progress" guard. */
10
+ declare const clamp01: (v: number) => number;
11
+ /** Round to `dp` decimal places (default 2), as a number. */
12
+ declare const round: (n: number, dp?: number) => number;
13
+ /** Greatest common divisor (Euclid), 0→1 so a ratio/fraction can always reduce. */
14
+ declare const gcd: (a: number, b: number) => number;
15
+ /** Degrees → radians. */
16
+ declare const toRad: (deg: number) => number;
17
+ /** Radians → degrees. */
18
+ declare const toDeg: (rad: number) => number;
19
+ /** Are two numbers equal within a tolerance (default 1e-6)? */
20
+ declare const approxEq: (a: number, b: number, eps?: number) => boolean;
21
+ /** Map x from [inMin,inMax] to [outMin,outMax], optionally clamped. */
22
+ declare function remap(x: number, inMin: number, inMax: number, outMin: number, outMax: number, doClamp?: boolean): number;
23
+ /** Snap `v` to the nearest multiple of `step` (step <= 0 → identity). */
24
+ declare const snapTo: (v: number, step: number) => number;
25
+ //#endregion
26
+ export { approxEq, clamp, clamp01, gcd, remap, round, snapTo, toDeg, toRad };
@@ -0,0 +1,37 @@
1
+ //#region src/core/math.ts
2
+ /**
3
+ * Small numeric kit shared by every consumer (labs re-exports these). One
4
+ * source of truth: labs presets used to hand-roll `clamp` 8+ different times.
5
+ * Pure, SSR-deterministic (no transcendental output is emitted directly into
6
+ * SVG — callers still `fmt()` pixel values).
7
+ */
8
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
9
+ /** Clamp to the unit interval [0, 1] — the ubiquitous "fraction / progress" guard. */
10
+ const clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
11
+ /** Round to `dp` decimal places (default 2), as a number. */
12
+ const round = (n, dp = 2) => Math.round(n * 10 ** dp) / 10 ** dp;
13
+ /** Greatest common divisor (Euclid), 0→1 so a ratio/fraction can always reduce. */
14
+ const gcd = (a, b) => {
15
+ a = Math.abs(a);
16
+ b = Math.abs(b);
17
+ while (b) [a, b] = [b, a % b];
18
+ return a || 1;
19
+ };
20
+ const DEG = Math.PI / 180;
21
+ /** Degrees → radians. */
22
+ const toRad = (deg) => deg * DEG;
23
+ /** Radians → degrees. */
24
+ const toDeg = (rad) => rad / DEG;
25
+ /** Are two numbers equal within a tolerance (default 1e-6)? */
26
+ const approxEq = (a, b, eps = 1e-6) => Math.abs(a - b) <= eps;
27
+ /** Map x from [inMin,inMax] to [outMin,outMax], optionally clamped. */
28
+ function remap(x, inMin, inMax, outMin, outMax, doClamp = false) {
29
+ const t = inMax === inMin ? 0 : (x - inMin) / (inMax - inMin);
30
+ const v = outMin + (outMax - outMin) * t;
31
+ return doClamp ? clamp(v, Math.min(outMin, outMax), Math.max(outMin, outMax)) : v;
32
+ }
33
+ /** Snap `v` to the nearest multiple of `step` (step <= 0 → identity). */
34
+ const snapTo = (v, step) => step > 0 ? Math.round(v / step) * step : v;
35
+
36
+ //#endregion
37
+ export { approxEq, clamp, clamp01, gcd, remap, round, snapTo, toDeg, toRad };
@@ -0,0 +1,93 @@
1
+ //#region src/finance/bizsim.d.ts
2
+ /**
3
+ * bizsim — a tiny, always-balanced business-simulation engine (part of `finance`).
4
+ *
5
+ * This is the PRIMITIVE a "run a business" story lesson is built on: a teacher/creator
6
+ * authors a scenario as a sequence of business ACTIONS (invest capital, take a loan,
7
+ * buy equipment, buy stock, sell, pay an expense, depreciate, close the period). Each
8
+ * action is a pure, DOUBLE-ENTRY-BALANCED transform of the business state, so the
9
+ * derived income statement and balance sheet are always correct and the balance sheet
10
+ * always balances — no accounting knowledge required to author, and it scales all the
11
+ * way to financial analysis (ratios) at the end.
12
+ *
13
+ * The engine is content-neutral: it knows nothing about samosas, apps or coffee — the
14
+ * creator supplies the story and the numbers.
15
+ */
16
+ interface BizState {
17
+ cash: number;
18
+ /** Stock on hand: quantity and its value at cost. */
19
+ inventoryQty: number;
20
+ inventoryValue: number;
21
+ /** Fixed assets at book value. */
22
+ equipment: number;
23
+ /** Liabilities (loans). */
24
+ loan: number;
25
+ /** Owner capital contributed. */
26
+ capital: number;
27
+ /** Accumulated profit from closed periods. */
28
+ retained: number;
29
+ /** Current open-period tallies (become net profit at closePeriod). */
30
+ revenue: number;
31
+ cogs: number;
32
+ expenses: number;
33
+ }
34
+ declare const initialBiz: (over?: Partial<BizState>) => BizState;
35
+ type BizAction = {
36
+ type: 'invest';
37
+ amount: number;
38
+ } | {
39
+ type: 'loan';
40
+ amount: number;
41
+ } | {
42
+ type: 'repay';
43
+ amount: number;
44
+ } | {
45
+ type: 'buyEquipment';
46
+ amount: number;
47
+ } | {
48
+ type: 'depreciate';
49
+ amount: number;
50
+ } | {
51
+ type: 'buyStock';
52
+ qty: number;
53
+ unitCost: number;
54
+ } | {
55
+ type: 'sell';
56
+ qty: number;
57
+ price: number;
58
+ } | {
59
+ type: 'expense';
60
+ amount: number;
61
+ } | {
62
+ type: 'closePeriod';
63
+ };
64
+ /** Apply one action; pure and balance-preserving (assets = liabilities + equity). */
65
+ declare function applyBiz(s: BizState, a: BizAction): BizState;
66
+ /** Run a whole authored scenario from an (optional) starting state. */
67
+ declare function runBiz(actions: BizAction[], init?: Partial<BizState>): BizState;
68
+ interface IncomeStatement {
69
+ revenue: number;
70
+ cogs: number;
71
+ grossProfit: number;
72
+ expenses: number;
73
+ netProfit: number;
74
+ }
75
+ /** Income statement for the current open period. */
76
+ declare function incomeStatement(s: BizState): IncomeStatement;
77
+ interface BalanceSheet {
78
+ cash: number;
79
+ inventory: number;
80
+ equipment: number;
81
+ totalAssets: number;
82
+ loan: number;
83
+ capital: number;
84
+ retained: number;
85
+ openProfit: number;
86
+ totalEquity: number;
87
+ totalLiabilitiesEquity: number;
88
+ balances: boolean;
89
+ }
90
+ /** Balance sheet; `balances` is always true by construction (a self-check). */
91
+ declare function balanceSheet(s: BizState): BalanceSheet;
92
+ //#endregion
93
+ export { BalanceSheet, BizAction, BizState, IncomeStatement, applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz };
@@ -0,0 +1,117 @@
1
+ //#region src/finance/bizsim.ts
2
+ const initialBiz = (over = {}) => ({
3
+ cash: 0,
4
+ inventoryQty: 0,
5
+ inventoryValue: 0,
6
+ equipment: 0,
7
+ loan: 0,
8
+ capital: 0,
9
+ retained: 0,
10
+ revenue: 0,
11
+ cogs: 0,
12
+ expenses: 0,
13
+ ...over
14
+ });
15
+ /** Apply one action; pure and balance-preserving (assets = liabilities + equity). */
16
+ function applyBiz(s, a) {
17
+ switch (a.type) {
18
+ case "invest": return {
19
+ ...s,
20
+ cash: s.cash + a.amount,
21
+ capital: s.capital + a.amount
22
+ };
23
+ case "loan": return {
24
+ ...s,
25
+ cash: s.cash + a.amount,
26
+ loan: s.loan + a.amount
27
+ };
28
+ case "repay": return {
29
+ ...s,
30
+ cash: s.cash - a.amount,
31
+ loan: s.loan - a.amount
32
+ };
33
+ case "buyEquipment": return {
34
+ ...s,
35
+ cash: s.cash - a.amount,
36
+ equipment: s.equipment + a.amount
37
+ };
38
+ case "depreciate": return {
39
+ ...s,
40
+ equipment: s.equipment - a.amount,
41
+ expenses: s.expenses + a.amount
42
+ };
43
+ case "buyStock": return {
44
+ ...s,
45
+ cash: s.cash - a.qty * a.unitCost,
46
+ inventoryQty: s.inventoryQty + a.qty,
47
+ inventoryValue: s.inventoryValue + a.qty * a.unitCost
48
+ };
49
+ case "sell": {
50
+ const avg = s.inventoryQty > 0 ? s.inventoryValue / s.inventoryQty : 0;
51
+ const qty = Math.min(a.qty, s.inventoryQty);
52
+ const cost = qty * avg, sales = qty * a.price;
53
+ return {
54
+ ...s,
55
+ cash: s.cash + sales,
56
+ revenue: s.revenue + sales,
57
+ cogs: s.cogs + cost,
58
+ inventoryQty: s.inventoryQty - qty,
59
+ inventoryValue: s.inventoryValue - cost
60
+ };
61
+ }
62
+ case "expense": return {
63
+ ...s,
64
+ cash: s.cash - a.amount,
65
+ expenses: s.expenses + a.amount
66
+ };
67
+ case "closePeriod": {
68
+ const net = s.revenue - s.cogs - s.expenses;
69
+ return {
70
+ ...s,
71
+ retained: s.retained + net,
72
+ revenue: 0,
73
+ cogs: 0,
74
+ expenses: 0
75
+ };
76
+ }
77
+ default: return s;
78
+ }
79
+ }
80
+ /** Run a whole authored scenario from an (optional) starting state. */
81
+ function runBiz(actions, init = {}) {
82
+ return actions.reduce(applyBiz, initialBiz(init));
83
+ }
84
+ /** Income statement for the current open period. */
85
+ function incomeStatement(s) {
86
+ const grossProfit = s.revenue - s.cogs;
87
+ return {
88
+ revenue: s.revenue,
89
+ cogs: s.cogs,
90
+ grossProfit,
91
+ expenses: s.expenses,
92
+ netProfit: grossProfit - s.expenses
93
+ };
94
+ }
95
+ /** Balance sheet; `balances` is always true by construction (a self-check). */
96
+ function balanceSheet(s) {
97
+ const totalAssets = s.cash + s.inventoryValue + s.equipment;
98
+ const openProfit = s.revenue - s.cogs - s.expenses;
99
+ const totalEquity = s.capital + s.retained + openProfit;
100
+ const totalLiabilitiesEquity = s.loan + totalEquity;
101
+ return {
102
+ cash: s.cash,
103
+ inventory: s.inventoryValue,
104
+ equipment: s.equipment,
105
+ totalAssets,
106
+ loan: s.loan,
107
+ capital: s.capital,
108
+ retained: s.retained,
109
+ openProfit,
110
+ totalEquity,
111
+ totalLiabilitiesEquity,
112
+ balances: Math.abs(totalAssets - totalLiabilitiesEquity) < 1e-6
113
+ };
114
+ }
115
+
116
+ //#endregion
117
+ export { applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz };