@mathscan/math-exercise-engine 1.2.9 → 1.2.10

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.
package/README.md CHANGED
@@ -314,6 +314,30 @@ All layout items include unique `id` fields for React/JSX rendering:
314
314
  }
315
315
  ```
316
316
 
317
+ ### Migrating to grouped-bar charts
318
+
319
+ `ChartItem` (the `chart` layout item) is now a discriminated union on `chartType`, and each variant carries only its own fields:
320
+
321
+ | `chartType` | Fields |
322
+ |-------------|--------|
323
+ | `bar` | `bars` |
324
+ | `pictograph` | `pictos` |
325
+ | `grouped-bar` | `series`, `groups`, `legend` |
326
+
327
+ Older stored JSON still parses: an empty `pictos: []` on a bar chart or `bars: []` on a pictograph is accepted and dropped. Compiled output changes too: it now contains only the fields of its variant, so a bar chart no longer emits `pictos: []` and a pictograph no longer emits `bars: []`. Code that read `item.bars` or `item.pictos` on any chart must narrow first:
328
+
329
+ ```ts
330
+ if (item.chartType === 'bar') {
331
+ item.bars.forEach(/* … */);
332
+ }
333
+ ```
334
+
335
+ Other chart changes in the same release:
336
+
337
+ - Compiled interactive bars now also carry `answer`, next to the existing `interactive: true` and `widgetId`. Bar JSON with only `answer` is accepted too.
338
+ - **Hosts must not strip `answer` from chart items.** On a `PICTO` and a grouped-bar `SERIES_BAR`, `answer` is what makes the item draggable: remove it and the item renders as display-only. A `BAR` stays draggable through `interactive: true` + `widgetId`, but keep its `answer` too for consistency. The answers are also in `validation.answers`, so hiding them from students takes more than stripping one of the two.
339
+ - Interactive chart item ids (`BAR`, `PICTO`, `SERIES_BAR` with an `answer`) share the widget namespace. They must not clash with any input name, including `VISUAL_FRACTION` and interactive `CLOCK` names. A clash is now a compile error ("Duplicate name") instead of silently overwriting the widget or answer.
340
+
317
341
  ## ✅ Testing
318
342
 
319
343
  ```bash
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const react = require("@mathscan/geometry-engine/react");
6
6
  const validation = require("@mathscan/geometry-engine/validation");
7
- const index = require("./index-D-QbiBn5.js");
7
+ const index = require("./index-tEgJE6m2.js");
8
8
  const GeometryWidget = ({
9
9
  widgetId,
10
10
  widget,
@@ -47,4 +47,4 @@ const GeometryWidget = ({
47
47
  ) });
48
48
  };
49
49
  exports.GeometryWidget = GeometryWidget;
50
- //# sourceMappingURL=GeometryWidget-D2QyGzW-.js.map
50
+ //# sourceMappingURL=GeometryWidget-DgPfqyQv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"GeometryWidget-D2QyGzW-.js","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["useExerciseLocked","useMemo","readGeometryAttempt","useEffect","logRejectedGeometryAttempt","jsx","StudentGeometryExercise","serializeGeometryState","issues"],"mappings":";;;;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiBA,MAAAA,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAWC,MAAAA,QAAQ,MAAMC,MAAAA,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7EC,QAAAA,UAAU,MAAM;AACd,QAAI,OAAQC,OAAAA,2BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACEC,2BAAAA,IAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAAA,2BAAAA;AAAAA,IAACC,MAAAA;AAAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAUC,WAAAA,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACC,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;;"}
1
+ {"version":3,"file":"GeometryWidget-DgPfqyQv.js","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["useExerciseLocked","useMemo","readGeometryAttempt","useEffect","logRejectedGeometryAttempt","jsx","StudentGeometryExercise","serializeGeometryState","issues"],"mappings":";;;;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiBA,MAAAA,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAWC,MAAAA,QAAQ,MAAMC,MAAAA,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7EC,QAAAA,UAAU,MAAM;AACd,QAAI,OAAQC,OAAAA,2BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACEC,2BAAAA,IAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAAA,2BAAAA;AAAAA,IAACC,MAAAA;AAAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAUC,WAAAA,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACC,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;;"}
@@ -2,7 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { useMemo, useEffect } from "react";
3
3
  import { StudentGeometryExercise } from "@mathscan/geometry-engine/react";
4
4
  import { serializeGeometryState } from "@mathscan/geometry-engine/validation";
5
- import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-XAInSHXa.mjs";
5
+ import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-DBteHZkS.mjs";
6
6
  const GeometryWidget = ({
7
7
  widgetId,
8
8
  widget,
@@ -47,4 +47,4 @@ const GeometryWidget = ({
47
47
  export {
48
48
  GeometryWidget
49
49
  };
50
- //# sourceMappingURL=GeometryWidget-BRV6LLcW.mjs.map
50
+ //# sourceMappingURL=GeometryWidget-gtAP_-_S.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"GeometryWidget-BRV6LLcW.mjs","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["issues"],"mappings":";;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAW,QAAQ,MAAM,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7E,YAAU,MAAM;AACd,QAAI,OAAQ,4BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACE,oBAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAU,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACA,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"GeometryWidget-gtAP_-_S.mjs","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["issues"],"mappings":";;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAW,QAAQ,MAAM,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7E,YAAU,MAAM;AACd,QAAI,OAAQ,4BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACE,oBAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAU,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACA,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}