@mathscan/math-exercise-engine 1.0.15 → 1.0.16

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/dist/index.mjs CHANGED
@@ -52,6 +52,8 @@ const AST_TYPES = {
52
52
  MATCHER_ROW: "MatcherRow",
53
53
  /** Item within a matcher row */
54
54
  MATCHER_ITEM: "MatcherItem",
55
+ /** Visual fraction inline widget */
56
+ VISUAL_FRACTION: "VisualFraction",
55
57
  // ═══════════════════════════════════════════════════════════════════════════
56
58
  // Tools
57
59
  // ═══════════════════════════════════════════════════════════════════════════
@@ -132,7 +134,9 @@ const JSON_TYPES = {
132
134
  /** Numeric keypad widget */
133
135
  NUMERIC_KEYPAD: "numeric-keypad",
134
136
  /** Inline fraction widget */
135
- FRACTION: "fraction"
137
+ FRACTION: "fraction",
138
+ /** Visual fraction widget */
139
+ VISUAL_FRACTION: "visual-fraction"
136
140
  };
137
141
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
138
142
  const VALIDATION_MODES = {
@@ -266,6 +270,10 @@ const HIGHLIGHT = createToken({
266
270
  name: "HIGHLIGHT",
267
271
  pattern: /HIGHLIGHT(?!_TEXT)/
268
272
  });
273
+ const VISUAL_FRACTION = createToken({
274
+ name: "VISUAL_FRACTION",
275
+ pattern: /VISUAL_FRACTION/
276
+ });
269
277
  const FRACTION = createToken({
270
278
  name: "FRACTION",
271
279
  pattern: /FRACTION/
@@ -395,6 +403,7 @@ const allTokens = [
395
403
  CELL,
396
404
  COUNTER,
397
405
  HIGHLIGHT,
406
+ VISUAL_FRACTION,
398
407
  FRACTION,
399
408
  MATCHER,
400
409
  ITEM,
@@ -860,6 +869,8 @@ class MathExerciseParser extends CstParser {
860
869
  { ALT: () => this.SUBRULE(this.fraction) },
861
870
  // IMAGE (inline image element)
862
871
  { ALT: () => this.SUBRULE(this.image) },
872
+ // VISUAL_FRACTION — always allowed; display-only when answer=[] or absence of answer parameter, interactive when answer=[...] with values.
873
+ { ALT: () => this.SUBRULE(this.visualFraction) },
863
874
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
864
875
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
865
876
  ]);
@@ -897,6 +908,41 @@ class MathExerciseParser extends CstParser {
897
908
  this.SUBRULE(this.parameterList);
898
909
  this.CONSUME(RBracket);
899
910
  });
911
+ this.visualFraction = this.RULE("visualFraction", () => {
912
+ this.CONSUME(VISUAL_FRACTION);
913
+ this.CONSUME(LBracket);
914
+ this.SUBRULE(this.parameterList);
915
+ this.CONSUME(RBracket);
916
+ });
917
+ this.numberArray = this.RULE("numberArray", () => {
918
+ this.CONSUME(LBracket);
919
+ this.MANY(() => {
920
+ this.CONSUME(Space);
921
+ });
922
+ this.OPTION(() => {
923
+ this.CONSUME(NumberLiteral, { LABEL: "items" });
924
+ this.MANY2(() => {
925
+ this.MANY3(() => {
926
+ this.CONSUME2(Space);
927
+ });
928
+ this.CONSUME(Comma);
929
+ this.MANY4(() => {
930
+ this.CONSUME3(Space);
931
+ });
932
+ this.CONSUME2(NumberLiteral, { LABEL: "items" });
933
+ });
934
+ this.OPTION2(() => {
935
+ this.MANY5(() => {
936
+ this.CONSUME4(Space);
937
+ });
938
+ this.CONSUME2(Comma);
939
+ });
940
+ });
941
+ this.MANY6(() => {
942
+ this.CONSUME5(Space);
943
+ });
944
+ this.CONSUME(RBracket);
945
+ });
900
946
  this.inlineInput = this.RULE("inlineInput", () => {
901
947
  this.OR([
902
948
  { ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
@@ -984,7 +1030,9 @@ class MathExerciseParser extends CstParser {
984
1030
  // Tuple value: (start, end)
985
1031
  { ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
986
1032
  // Curly-brace string list: {"a", "b", "c"}
987
- { ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) }
1033
+ { ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) },
1034
+ // Square-bracket number array: [1, 2, 3] — used by VISUAL_FRACTION filled/answer
1035
+ { ALT: () => this.SUBRULE(this.numberArray, { LABEL: "value" }) }
988
1036
  ]);
989
1037
  });
990
1038
  this.tuple = this.RULE("tuple", () => {
@@ -1040,6 +1088,25 @@ class MathExerciseParser extends CstParser {
1040
1088
  }
1041
1089
  }
1042
1090
  const parser = new MathExerciseParser();
1091
+ function parseOrThrow(schema, data, label) {
1092
+ const result = schema.safeParse(data);
1093
+ if (!result.success) {
1094
+ const issue = result.error.issues[0];
1095
+ if (!issue) throw new Error(`${label}: validation failed`);
1096
+ const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
1097
+ throw new Error(`${label}: ${path} ${issue.message}`);
1098
+ }
1099
+ return result.data;
1100
+ }
1101
+ const VISUAL_FRACTION_SHAPES = ["bar", "circle", "triangle", "grid"];
1102
+ const VISUAL_FRACTION_SHAPE = {
1103
+ BAR: "bar",
1104
+ CIRCLE: "circle",
1105
+ TRIANGLE: "triangle",
1106
+ GRID: "grid"
1107
+ };
1108
+ const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
1109
+ const VISUAL_FRACTION_MAX_DIVISIONS = 100;
1043
1110
  const FreeTextInlineSchema = z.object({
1044
1111
  type: z.literal(AST_TYPES.FREE_TEXT),
1045
1112
  value: z.string()
@@ -1069,6 +1136,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1069
1136
  name: z.string(),
1070
1137
  inputType: z.literal(INPUT_TYPES.STRING),
1071
1138
  length: z.number().int().positive(),
1139
+ placeholder: z.string().max(32).optional(),
1072
1140
  answer: z.union([z.string(), z.number()]).optional()
1073
1141
  }),
1074
1142
  z.object({
@@ -1076,6 +1144,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1076
1144
  name: z.string(),
1077
1145
  inputType: z.literal(INPUT_TYPES.NUMERIC),
1078
1146
  digits: z.number().int().positive(),
1147
+ placeholder: z.string().max(32).optional(),
1079
1148
  answer: z.union([z.string(), z.number()]).optional()
1080
1149
  })
1081
1150
  ]);
@@ -1098,6 +1167,60 @@ const ImageInlineSchema = z.object({
1098
1167
  width: z.number().positive().optional(),
1099
1168
  height: z.number().positive().optional()
1100
1169
  });
1170
+ const VisualFractionInlineSchema = z.object({
1171
+ type: z.literal(AST_TYPES.VISUAL_FRACTION),
1172
+ name: z.string(),
1173
+ shape: z.enum(VISUAL_FRACTION_SHAPES).default("bar"),
1174
+ divisions: z.number().int().positive().max(VISUAL_FRACTION_MAX_DIVISIONS, {
1175
+ message: `divisions cannot exceed ${VISUAL_FRACTION_MAX_DIVISIONS}`
1176
+ }),
1177
+ filled: z.array(z.number().int().nonnegative()).default([]),
1178
+ answer: z.array(z.number().int().nonnegative()).default([]),
1179
+ orientation: z.enum(["horizontal", "vertical"]).optional(),
1180
+ label: z.string().optional(),
1181
+ /** Unified author size.
1182
+ * - bar / grid: overrides `--visual-fraction-part-size` (cell size).
1183
+ * - circle / triangle: maps to `--visual-fraction-size` with the renderer scale factor.
1184
+ * Omit to use CSS defaults. */
1185
+ size: z.number().positive().optional()
1186
+ }).superRefine(validateVisualFractionCapacity);
1187
+ function validateVisualFractionCapacity(data, ctx) {
1188
+ const filledTotal = data.filled.reduce((sum, count) => sum + count, 0);
1189
+ const answerTotal = data.answer.reduce((sum, count) => sum + count, 0);
1190
+ if (filledTotal > data.divisions) {
1191
+ ctx.addIssue({
1192
+ code: z.ZodIssueCode.custom,
1193
+ message: `filled total (${filledTotal}) cannot exceed divisions (${data.divisions})`,
1194
+ path: ["filled"]
1195
+ });
1196
+ }
1197
+ if (filledTotal + answerTotal > data.divisions) {
1198
+ ctx.addIssue({
1199
+ code: z.ZodIssueCode.custom,
1200
+ message: `filled + answer total (${filledTotal + answerTotal}) cannot exceed divisions (${data.divisions})`,
1201
+ path: ["answer"]
1202
+ });
1203
+ }
1204
+ const shape = data.shape;
1205
+ if (shape === "triangle" && !isPerfectSquare(data.divisions)) {
1206
+ ctx.addIssue({
1207
+ code: z.ZodIssueCode.custom,
1208
+ message: `triangle divisions (${data.divisions}) must be a perfect square to render equal triangle parts`,
1209
+ path: ["divisions"]
1210
+ });
1211
+ }
1212
+ if (shape === "grid" && !isPerfectSquare(data.divisions)) {
1213
+ ctx.addIssue({
1214
+ code: z.ZodIssueCode.custom,
1215
+ message: `grid divisions (${data.divisions}) must be a perfect square to render equal rows and columns`,
1216
+ path: ["divisions"]
1217
+ });
1218
+ }
1219
+ }
1220
+ function isPerfectSquare(value) {
1221
+ const root = Math.sqrt(value);
1222
+ return Number.isInteger(root);
1223
+ }
1101
1224
  const InlineNodeSchema = z.union([
1102
1225
  FreeTextInlineSchema,
1103
1226
  SpaceInlineSchema,
@@ -1106,7 +1229,8 @@ const InlineNodeSchema = z.union([
1106
1229
  BoldTextInlineSchema,
1107
1230
  InlineInputWidgetSchema,
1108
1231
  FractionInlineSchema,
1109
- ImageInlineSchema
1232
+ ImageInlineSchema,
1233
+ VisualFractionInlineSchema
1110
1234
  ]);
1111
1235
  function createFreeText(value) {
1112
1236
  return FreeTextInlineSchema.parse({
@@ -1141,23 +1265,25 @@ function createBoldText(value, highlight) {
1141
1265
  highlight
1142
1266
  });
1143
1267
  }
1144
- function createStringInput(name, length, answer) {
1145
- return InlineInputWidgetSchema.parse({
1268
+ function createStringInput(name, length, answer, placeholder) {
1269
+ return parseOrThrow(InlineInputWidgetSchema, {
1146
1270
  type: AST_TYPES.INLINE_INPUT,
1147
1271
  name,
1148
1272
  inputType: INPUT_TYPES.STRING,
1149
1273
  length,
1274
+ placeholder,
1150
1275
  answer
1151
- });
1276
+ }, "STRING_INPUT");
1152
1277
  }
1153
- function createNumericInput(name, digits, answer) {
1154
- return InlineInputWidgetSchema.parse({
1278
+ function createNumericInput(name, digits, answer, placeholder) {
1279
+ return parseOrThrow(InlineInputWidgetSchema, {
1155
1280
  type: AST_TYPES.INLINE_INPUT,
1156
1281
  name,
1157
1282
  inputType: INPUT_TYPES.NUMERIC,
1158
1283
  digits,
1284
+ placeholder,
1159
1285
  answer
1160
- });
1286
+ }, "NUMERIC_INPUT");
1161
1287
  }
1162
1288
  function createImage(name, src, alt, options = {}) {
1163
1289
  return ImageInlineSchema.parse({
@@ -1175,6 +1301,14 @@ function createFraction(name, options = {}) {
1175
1301
  ...options
1176
1302
  });
1177
1303
  }
1304
+ function createVisualFraction(name, divisions, options = {}) {
1305
+ return VisualFractionInlineSchema.parse({
1306
+ type: AST_TYPES.VISUAL_FRACTION,
1307
+ name,
1308
+ divisions,
1309
+ ...options
1310
+ });
1311
+ }
1178
1312
  function isFreeText(value) {
1179
1313
  return FreeTextInlineSchema.safeParse(value).success;
1180
1314
  }
@@ -1193,6 +1327,9 @@ function isImage(value) {
1193
1327
  function isInlineNode(value) {
1194
1328
  return InlineNodeSchema.safeParse(value).success;
1195
1329
  }
1330
+ function isVisualFractionInline(value) {
1331
+ return VisualFractionInlineSchema.safeParse(value).success;
1332
+ }
1196
1333
  const TextBlockSchema = z.object({
1197
1334
  type: z.literal(AST_TYPES.TEXT_BLOCK),
1198
1335
  value: z.string()
@@ -1388,6 +1525,10 @@ const DisplayFractionInlineSchema = FractionInlineSchema.refine(
1388
1525
  (f) => f.numerator !== void 0 && f.denominator !== void 0,
1389
1526
  { message: "FRACTION inside MATCHER ITEM must be in display mode (both numerator and denominator required)" }
1390
1527
  );
1528
+ const DisplayVisualFractionInlineSchema = VisualFractionInlineSchema.refine(
1529
+ (vf) => vf.answer.length === 0,
1530
+ { message: "VISUAL_FRACTION inside MATCHER ITEM must be display-only (answer must be empty: answer=[])" }
1531
+ );
1391
1532
  const DisplayInlineNodeSchema = z.union([
1392
1533
  FreeTextInlineSchema,
1393
1534
  SpaceInlineSchema,
@@ -1395,7 +1536,8 @@ const DisplayInlineNodeSchema = z.union([
1395
1536
  ColorTextInlineSchema,
1396
1537
  BoldTextInlineSchema,
1397
1538
  DisplayFractionInlineSchema,
1398
- ImageInlineSchema
1539
+ ImageInlineSchema,
1540
+ DisplayVisualFractionInlineSchema
1399
1541
  ]);
1400
1542
  const MatcherItemSchema = z.object({
1401
1543
  type: z.literal(AST_TYPES.MATCHER_ITEM),
@@ -1491,7 +1633,8 @@ const DragOptionsToolSchema = z.object({
1491
1633
  const NumericKeypadToolSchema = z.object({
1492
1634
  type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
1493
1635
  includeOperators: z.boolean().optional(),
1494
- includeDecimal: z.boolean().optional()
1636
+ includeDecimal: z.boolean().optional(),
1637
+ style: z.enum(["diamond", "classic"]).optional()
1495
1638
  });
1496
1639
  const ToolNodeSchema = z.discriminatedUnion("type", [
1497
1640
  DragOptionsToolSchema,
@@ -1503,11 +1646,12 @@ function createDragOptions(options) {
1503
1646
  options
1504
1647
  });
1505
1648
  }
1506
- function createNumericKeypad(includeOperators, includeDecimal) {
1649
+ function createNumericKeypad(includeOperators, includeDecimal, style) {
1507
1650
  return NumericKeypadToolSchema.parse({
1508
1651
  type: AST_TYPES.NUMERIC_KEYPAD,
1509
1652
  includeOperators,
1510
- includeDecimal
1653
+ includeDecimal,
1654
+ style
1511
1655
  });
1512
1656
  }
1513
1657
  function isDragOptions(value) {
@@ -1861,6 +2005,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1861
2005
  if (ctx.image) {
1862
2006
  return this.visit(ctx.image);
1863
2007
  }
2008
+ if (ctx.visualFraction) {
2009
+ return this.visit(ctx.visualFraction);
2010
+ }
1864
2011
  if (ctx.inlineInput) {
1865
2012
  return this.visit(ctx.inlineInput);
1866
2013
  }
@@ -1912,16 +2059,32 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1912
2059
  const height = params.height;
1913
2060
  return createImage(name, src, alt, { width, height });
1914
2061
  }
2062
+ visualFraction(ctx) {
2063
+ const params = this.visit(ctx.parameterList);
2064
+ const name = params.name;
2065
+ const divisions = params.divisions;
2066
+ const shape = params.shape;
2067
+ const filled = params.filled ?? [];
2068
+ const answer = params.answer ?? [];
2069
+ const orientation = params.orientation;
2070
+ const label = params.label !== void 0 ? params.label : void 0;
2071
+ const size = params.size !== void 0 ? params.size : void 0;
2072
+ return createVisualFraction(name, divisions, { shape, filled, answer, orientation, label, size });
2073
+ }
2074
+ numberArray(ctx) {
2075
+ return ctx.items ? ctx.items.map((t) => parseFloat(t.image)) : [];
2076
+ }
1915
2077
  inlineInput(ctx) {
1916
2078
  const params = this.visit(ctx.parameterList);
1917
2079
  const name = params.name;
1918
2080
  const answer = params.answer;
2081
+ const placeholder = params.placeholder;
1919
2082
  if (ctx.stringInput) {
1920
2083
  const length = params.length;
1921
- return createStringInput(name, length, answer);
2084
+ return createStringInput(name, length, answer, placeholder);
1922
2085
  }
1923
2086
  const digits = params.digits;
1924
- return createNumericInput(name, digits, answer);
2087
+ return createNumericInput(name, digits, answer, placeholder);
1925
2088
  }
1926
2089
  // ═══════════════════════════════════════════════════════════════════════════
1927
2090
  // Tools
@@ -1943,7 +2106,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1943
2106
  const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
1944
2107
  const includeOperators = params.includeOperators === true || params.includeOperators === "true";
1945
2108
  const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
1946
- return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0);
2109
+ const style = params.style;
2110
+ return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0, style);
1947
2111
  }
1948
2112
  // ═══════════════════════════════════════════════════════════════════════════
1949
2113
  // Validation
@@ -2186,12 +2350,14 @@ const MatcherRowItemSchema = z.object({
2186
2350
  const StringInputJSONSchema = z.object({
2187
2351
  type: z.literal(JSON_TYPES.INLINE_INPUT),
2188
2352
  inputType: z.literal(INPUT_TYPES.STRING),
2189
- length: z.number().int().positive()
2353
+ length: z.number().int().positive(),
2354
+ placeholder: z.string().max(32).optional()
2190
2355
  });
2191
2356
  const NumericInputJSONSchema = z.object({
2192
2357
  type: z.literal(JSON_TYPES.INLINE_INPUT),
2193
2358
  inputType: z.literal(INPUT_TYPES.NUMERIC),
2194
- digits: z.number().int().positive()
2359
+ digits: z.number().int().positive(),
2360
+ placeholder: z.string().max(32).optional()
2195
2361
  });
2196
2362
  const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
2197
2363
  StringInputJSONSchema,
@@ -2208,7 +2374,8 @@ const RadioOptionsJSONSchema = z.object({
2208
2374
  const NumericKeypadJSONSchema = z.object({
2209
2375
  type: z.literal(JSON_TYPES.NUMERIC_KEYPAD),
2210
2376
  includeOperators: z.boolean().optional(),
2211
- includeDecimal: z.boolean().optional()
2377
+ includeDecimal: z.boolean().optional(),
2378
+ style: z.enum(["diamond", "classic"]).optional()
2212
2379
  });
2213
2380
  const FractionJSONSchema = z.object({
2214
2381
  type: z.literal(JSON_TYPES.FRACTION),
@@ -2224,24 +2391,44 @@ const MatcherWidgetJSONSchema = z.object({
2224
2391
  /** Number of answer groups — equals answers.length, stored here to avoid prop-drilling. */
2225
2392
  groupCount: z.number().int().min(1)
2226
2393
  });
2394
+ const VisualFractionJSONSchema = z.object({
2395
+ type: z.literal(JSON_TYPES.VISUAL_FRACTION),
2396
+ shape: z.enum(["bar", "circle", "triangle", "grid"]).default("bar"),
2397
+ divisions: z.number().int().positive().max(VISUAL_FRACTION_MAX_DIVISIONS, {
2398
+ message: `divisions cannot exceed ${VISUAL_FRACTION_MAX_DIVISIONS}`
2399
+ }),
2400
+ filled: z.array(z.number().int().nonnegative()),
2401
+ answer: z.array(z.number().int().nonnegative()),
2402
+ orientation: z.enum(["horizontal", "vertical"]).optional(),
2403
+ /** Optional label text rendered near the shape — replaces the old showLabel boolean */
2404
+ label: z.string().optional(),
2405
+ /** Overall shape size in unified author units.
2406
+ * - bar / grid: overrides `--visual-fraction-part-size` CSS custom property (cell size in rem).
2407
+ * - circle / triangle: overrides `--visual-fraction-size` through the renderer scale factor.
2408
+ */
2409
+ size: z.number().positive().optional()
2410
+ }).superRefine(validateVisualFractionCapacity);
2227
2411
  const WidgetJSONSchema = z.union([
2228
2412
  InlineInputJSONSchema,
2229
2413
  DragOptionsJSONSchema,
2230
2414
  RadioOptionsJSONSchema,
2231
2415
  NumericKeypadJSONSchema,
2232
2416
  FractionJSONSchema,
2233
- MatcherWidgetJSONSchema
2417
+ MatcherWidgetJSONSchema,
2418
+ VisualFractionJSONSchema
2234
2419
  ]);
2235
2420
  const ValidationJSONSchema = z.object({
2236
2421
  mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
2237
2422
  // Simple answers: string|number per widget id.
2238
2423
  // Matcher answers: string[][] keyed by matcher name. Outer/inner min(1)
2239
2424
  // mirrors MatcherBlockSchema invariants in src/ast/matcher.ts.
2425
+ // VisualFraction answers: number[] — one count per color group.
2240
2426
  answers: z.record(
2241
2427
  z.union([
2242
2428
  z.string(),
2243
2429
  z.number(),
2244
- z.array(z.array(z.string().min(1)).min(1)).min(1)
2430
+ z.array(z.array(z.string().min(1)).min(1)).min(1),
2431
+ z.array(z.number().int().nonnegative())
2245
2432
  ])
2246
2433
  ).optional(),
2247
2434
  expression: z.string().optional(),
@@ -2392,13 +2579,15 @@ function toJSON(ast) {
2392
2579
  widgets[inline.name] = {
2393
2580
  type: JSON_TYPES.INLINE_INPUT,
2394
2581
  inputType: INPUT_TYPES.STRING,
2395
- length: inline.length
2582
+ length: inline.length,
2583
+ placeholder: inline.placeholder
2396
2584
  };
2397
2585
  } else {
2398
2586
  widgets[inline.name] = {
2399
2587
  type: JSON_TYPES.INLINE_INPUT,
2400
2588
  inputType: INPUT_TYPES.NUMERIC,
2401
- digits: inline.digits
2589
+ digits: inline.digits,
2590
+ placeholder: inline.placeholder
2402
2591
  };
2403
2592
  }
2404
2593
  if (inline.answer !== void 0) {
@@ -2416,6 +2605,22 @@ function toJSON(ast) {
2416
2605
  answers[inline.name] = inline.answer;
2417
2606
  }
2418
2607
  }
2608
+ if (inline.type === AST_TYPES.VISUAL_FRACTION) {
2609
+ const answerArr = inline.answer ?? [];
2610
+ widgets[inline.name] = {
2611
+ type: JSON_TYPES.VISUAL_FRACTION,
2612
+ shape: inline.shape,
2613
+ divisions: inline.divisions,
2614
+ filled: inline.filled ?? [],
2615
+ answer: answerArr,
2616
+ ...inline.orientation !== void 0 && { orientation: inline.orientation },
2617
+ ...inline.label !== void 0 && { label: inline.label },
2618
+ ...inline.size !== void 0 && { size: inline.size }
2619
+ };
2620
+ if (answerArr.length > 0) {
2621
+ answers[inline.name] = answerArr;
2622
+ }
2623
+ }
2419
2624
  }
2420
2625
  for (const block of ast.lines) {
2421
2626
  if (block.type === AST_TYPES.RADIO_OPTIONS) {
@@ -2442,7 +2647,8 @@ function toJSON(ast) {
2442
2647
  widgets["_numeric_keypad"] = {
2443
2648
  type: JSON_TYPES.NUMERIC_KEYPAD,
2444
2649
  includeOperators: tool.includeOperators,
2445
- includeDecimal: tool.includeDecimal
2650
+ includeDecimal: tool.includeDecimal,
2651
+ style: tool.style
2446
2652
  };
2447
2653
  }
2448
2654
  }
@@ -2656,6 +2862,12 @@ function transformInline(inline, idGenerator) {
2656
2862
  ...inline.width !== void 0 && { width: inline.width },
2657
2863
  ...inline.height !== void 0 && { height: inline.height }
2658
2864
  };
2865
+ case AST_TYPES.VISUAL_FRACTION:
2866
+ return {
2867
+ id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
2868
+ type: JSON_TYPES.WIDGET_REF,
2869
+ widgetId: inline.name
2870
+ };
2659
2871
  default:
2660
2872
  throw new Error(`Unknown inline type: ${inline.type}`);
2661
2873
  }
@@ -8198,6 +8410,11 @@ function areMatcherEqual(expected, result) {
8198
8410
  const normalize2 = (groups) => lodashExports.sortBy(groups.map((g) => lodashExports.sortBy(g)), (g) => g.join("|"));
8199
8411
  return lodashExports.isEqual(normalize2(expected), normalize2(result));
8200
8412
  }
8413
+ function areVisualFractionCountsEqual(expected, result) {
8414
+ if (!Array.isArray(result)) return false;
8415
+ if (expected.length !== result.length) return false;
8416
+ return lodashExports.isEqual(lodashExports.sortBy(expected), lodashExports.sortBy(result));
8417
+ }
8201
8418
  const math$1 = create(all);
8202
8419
  function collectNumberLines(items) {
8203
8420
  const result = [];
@@ -8246,6 +8463,15 @@ function validateSimple(exercise, userInputs) {
8246
8463
  else allCorrect = false;
8247
8464
  continue;
8248
8465
  }
8466
+ if (widget?.type === JSON_TYPES.VISUAL_FRACTION) {
8467
+ const userValue2 = userInputs[widgetId];
8468
+ const isCorrect2 = areVisualFractionCountsEqual(expected, userValue2);
8469
+ details[widgetId] = { widgetId, userValue: userValue2, isCorrect: isCorrect2 };
8470
+ totalCount++;
8471
+ if (isCorrect2) correctCount++;
8472
+ else allCorrect = false;
8473
+ continue;
8474
+ }
8249
8475
  totalCount++;
8250
8476
  const userValue = userInputs[widgetId];
8251
8477
  const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
@@ -8752,10 +8978,10 @@ function getOwnerDocument(target) {
8752
8978
  }
8753
8979
  return document;
8754
8980
  }
8755
- const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;
8981
+ const useIsomorphicLayoutEffect$1 = canUseDOM ? useLayoutEffect : useEffect;
8756
8982
  function useEvent(handler) {
8757
8983
  const handlerRef = useRef(handler);
8758
- useIsomorphicLayoutEffect(() => {
8984
+ useIsomorphicLayoutEffect$1(() => {
8759
8985
  handlerRef.current = handler;
8760
8986
  });
8761
8987
  return useCallback(function() {
@@ -8783,7 +9009,7 @@ function useLatestValue(value, dependencies) {
8783
9009
  dependencies = [value];
8784
9010
  }
8785
9011
  const valueRef = useRef(value);
8786
- useIsomorphicLayoutEffect(() => {
9012
+ useIsomorphicLayoutEffect$1(() => {
8787
9013
  if (valueRef.current !== value) {
8788
9014
  valueRef.current = value;
8789
9015
  }
@@ -10651,9 +10877,9 @@ function useResizeObserver(_ref) {
10651
10877
  return void 0;
10652
10878
  }
10653
10879
  const {
10654
- ResizeObserver
10880
+ ResizeObserver: ResizeObserver2
10655
10881
  } = window;
10656
- return new ResizeObserver(handleResize);
10882
+ return new ResizeObserver2(handleResize);
10657
10883
  },
10658
10884
  // eslint-disable-next-line react-hooks/exhaustive-deps
10659
10885
  [disabled]
@@ -10707,7 +10933,7 @@ function useRect(element, measure, fallbackRect) {
10707
10933
  const resizeObserver = useResizeObserver({
10708
10934
  callback: measureRect
10709
10935
  });
10710
- useIsomorphicLayoutEffect(() => {
10936
+ useIsomorphicLayoutEffect$1(() => {
10711
10937
  measureRect();
10712
10938
  if (element) {
10713
10939
  resizeObserver == null ? void 0 : resizeObserver.observe(element);
@@ -10881,7 +11107,7 @@ function useRects(elements, measure) {
10881
11107
  const resizeObserver = useResizeObserver({
10882
11108
  callback: measureRects
10883
11109
  });
10884
- useIsomorphicLayoutEffect(() => {
11110
+ useIsomorphicLayoutEffect$1(() => {
10885
11111
  resizeObserver == null ? void 0 : resizeObserver.disconnect();
10886
11112
  measureRects();
10887
11113
  elements.forEach((element) => resizeObserver == null ? void 0 : resizeObserver.observe(element));
@@ -11243,7 +11469,7 @@ function useLayoutShiftScrollCompensation(_ref) {
11243
11469
  x: config,
11244
11470
  y: config
11245
11471
  } : config;
11246
- useIsomorphicLayoutEffect(() => {
11472
+ useIsomorphicLayoutEffect$1(() => {
11247
11473
  const disabled = !x && !y;
11248
11474
  if (disabled || !activeNode) {
11249
11475
  initialized.current = false;
@@ -11604,7 +11830,7 @@ const DndContext = /* @__PURE__ */ memo(function DndContext2(_ref) {
11604
11830
  }, [draggableNodes, instantiateSensor]);
11605
11831
  const activators = useCombineActivators(sensors, bindActivatorToSensorInstantiator);
11606
11832
  useSensorSetup(sensors);
11607
- useIsomorphicLayoutEffect(() => {
11833
+ useIsomorphicLayoutEffect$1(() => {
11608
11834
  if (activeNodeRect && status === Status.Initializing) {
11609
11835
  setStatus(Status.Initialized);
11610
11836
  }
@@ -11688,7 +11914,7 @@ const DndContext = /* @__PURE__ */ memo(function DndContext2(_ref) {
11688
11914
  // eslint-disable-next-line react-hooks/exhaustive-deps
11689
11915
  [overId]
11690
11916
  );
11691
- useIsomorphicLayoutEffect(() => {
11917
+ useIsomorphicLayoutEffect$1(() => {
11692
11918
  sensorContext.current = {
11693
11919
  activatorEvent,
11694
11920
  active,
@@ -11815,7 +12041,7 @@ function useDraggable(_ref) {
11815
12041
  const [activatorNode, setActivatorNodeRef] = useNodeRef();
11816
12042
  const listeners = useSyntheticListeners(activators, id);
11817
12043
  const dataRef = useLatestValue(data);
11818
- useIsomorphicLayoutEffect(
12044
+ useIsomorphicLayoutEffect$1(
11819
12045
  () => {
11820
12046
  draggableNodes.set(id, {
11821
12047
  id,
@@ -11988,7 +12214,7 @@ function AnimationManager(_ref) {
11988
12214
  if (!children && !clonedChildren && previousChildren) {
11989
12215
  setClonedChildren(previousChildren);
11990
12216
  }
11991
- useIsomorphicLayoutEffect(() => {
12217
+ useIsomorphicLayoutEffect$1(() => {
11992
12218
  if (!element) {
11993
12219
  return;
11994
12220
  }
@@ -12670,6 +12896,7 @@ const InlineInput = ({
12670
12896
  inputType,
12671
12897
  length,
12672
12898
  digits,
12899
+ placeholder,
12673
12900
  onChange,
12674
12901
  onFocus
12675
12902
  }) => {
@@ -12715,19 +12942,88 @@ const InlineInput = ({
12715
12942
  onChange: (e) => handleChange(e.target.value, false),
12716
12943
  onFocus: () => onFocus?.(widgetId),
12717
12944
  maxLength: expectedLength,
12718
- style: { width: dynamicWidth }
12945
+ style: { width: dynamicWidth },
12946
+ placeholder
12719
12947
  }
12720
12948
  )
12721
12949
  )
12722
12950
  }
12723
12951
  );
12724
12952
  };
12953
+ const PillBackground = ({
12954
+ width = "100%",
12955
+ minHeight = "72px",
12956
+ fill = "#FFE7B4",
12957
+ children,
12958
+ className = "",
12959
+ style,
12960
+ ...rest
12961
+ }) => {
12962
+ return /* @__PURE__ */ jsxs(
12963
+ "div",
12964
+ {
12965
+ className: `pill-bg-wrapper ${className}`.trim(),
12966
+ style: {
12967
+ position: "relative",
12968
+ display: "inline-flex",
12969
+ alignItems: "center",
12970
+ justifyContent: "center",
12971
+ width,
12972
+ minHeight,
12973
+ ...style
12974
+ },
12975
+ ...rest,
12976
+ children: [
12977
+ /* @__PURE__ */ jsx(
12978
+ "svg",
12979
+ {
12980
+ viewBox: "0 0 510 108",
12981
+ preserveAspectRatio: "none",
12982
+ xmlns: "http://www.w3.org/2000/svg",
12983
+ "aria-hidden": "true",
12984
+ style: {
12985
+ position: "absolute",
12986
+ inset: 0,
12987
+ width: "100%",
12988
+ height: "100%",
12989
+ display: "block"
12990
+ },
12991
+ children: /* @__PURE__ */ jsx(
12992
+ "path",
12993
+ {
12994
+ d: "M439.707 0C457.071 0 472.52 8.196 482.398 20.9297C498.608 26.3308 510 39.1028 510 54C510 68.8966 498.608 81.667 482.399 87.0684C472.521 99.803 457.072 108 439.707 108H70.8975C53.653 108 38.2956 99.9172 28.4092 87.333C11.7719 82.0918 0.00025108 69.1432 0 54C0 38.8562 11.7721 25.906 28.4102 20.665C38.2966 8.08186 53.6537 0 70.8975 0H439.707Z",
12995
+ fill
12996
+ }
12997
+ )
12998
+ }
12999
+ ),
13000
+ /* @__PURE__ */ jsx(
13001
+ "div",
13002
+ {
13003
+ style: {
13004
+ position: "relative",
13005
+ zIndex: 1,
13006
+ display: "flex",
13007
+ alignItems: "center",
13008
+ justifyContent: "center",
13009
+ width: "100%",
13010
+ height: "100%",
13011
+ padding: "0 2rem",
13012
+ boxSizing: "border-box"
13013
+ },
13014
+ children
13015
+ }
13016
+ )
13017
+ ]
13018
+ }
13019
+ );
13020
+ };
12725
13021
  const DragOptions = ({
12726
13022
  options,
12727
13023
  onOptionClick
12728
13024
  }) => {
12729
13025
  const { usedOptionIds } = useExerciseDnd();
12730
- return /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
13026
+ return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
12731
13027
  const optionId = `drag-option-${idx}`;
12732
13028
  const optionValue = String(option);
12733
13029
  const isUsed = usedOptionIds.has(optionId);
@@ -12754,7 +13050,7 @@ const DragOptions = ({
12754
13050
  }
12755
13051
  )
12756
13052
  ) }, optionId);
12757
- }) });
13053
+ }) }) });
12758
13054
  };
12759
13055
  const NumericKeypad = ({
12760
13056
  value,
@@ -12850,28 +13146,6 @@ const FractionInput = ({
12850
13146
  /* @__PURE__ */ jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator") })
12851
13147
  ] });
12852
13148
  };
12853
- const RadioOptions = ({
12854
- name,
12855
- options,
12856
- value,
12857
- validationStatus = null,
12858
- onChange
12859
- }) => {
12860
- return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
12861
- const isSelected = value === option;
12862
- const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
12863
- return /* @__PURE__ */ jsx(
12864
- "button",
12865
- {
12866
- className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
12867
- onClick: () => onChange(name, option),
12868
- type: "button",
12869
- children: option
12870
- },
12871
- `${name}-radio-${idx}`
12872
- );
12873
- }) });
12874
- };
12875
13149
  function isLightColor(hex) {
12876
13150
  const clean = hex.replace("#", "");
12877
13151
  const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
@@ -12886,7 +13160,8 @@ function isLightColor(hex) {
12886
13160
  return 1.05 / (L + 0.05) < 4.5;
12887
13161
  }
12888
13162
  function getTextColorForBackground(color) {
12889
- if (color.startsWith("hsl(")) return "#1a1a1a";
13163
+ const hslLightness = parseHslLightness(color);
13164
+ if (hslLightness !== null) return hslLightness >= 60 ? "#1a1a1a" : "#ffffff";
12890
13165
  return isLightColor(color) ? "#1a1a1a" : "#ffffff";
12891
13166
  }
12892
13167
  function generateHslPalette(n) {
@@ -12895,7 +13170,7 @@ function generateHslPalette(n) {
12895
13170
  return `hsl(${hue}, 60%, 72%)`;
12896
13171
  });
12897
13172
  }
12898
- function resolveMatcherPalette(userColors, groupCount) {
13173
+ function resolveGroupPalette(userColors, groupCount) {
12899
13174
  if (groupCount <= 0) return [];
12900
13175
  if (!userColors || userColors.length === 0) {
12901
13176
  return generateHslPalette(groupCount);
@@ -12907,6 +13182,478 @@ function resolveMatcherPalette(userColors, groupCount) {
12907
13182
  const extras = generateHslPalette(needed);
12908
13183
  return [...userColors, ...extras];
12909
13184
  }
13185
+ function resolveDistinctGroupPalette(groupCount, options = {}) {
13186
+ if (groupCount <= 0) return [];
13187
+ const used = new Set((options.excludeColors ?? []).map(normalizeColorToken));
13188
+ const palette = [];
13189
+ for (const color of options.seedColors ?? []) {
13190
+ const token = normalizeColorToken(color);
13191
+ if (used.has(token)) continue;
13192
+ palette.push(color);
13193
+ used.add(token);
13194
+ if (palette.length === groupCount) return palette;
13195
+ }
13196
+ const addCandidates = (candidates) => {
13197
+ for (const color of candidates) {
13198
+ const token = normalizeColorToken(color);
13199
+ if (used.has(token)) continue;
13200
+ palette.push(color);
13201
+ used.add(token);
13202
+ if (palette.length === groupCount) return;
13203
+ }
13204
+ };
13205
+ addCandidates(generateHslPalette(groupCount));
13206
+ let candidateCount = groupCount + 1;
13207
+ while (palette.length < groupCount) {
13208
+ addCandidates(generateHslPalette(candidateCount++));
13209
+ }
13210
+ return palette;
13211
+ }
13212
+ function getGroupClassName(groupIndex) {
13213
+ return `widget-group-${groupIndex + 1}`;
13214
+ }
13215
+ function normalizeColorToken(color) {
13216
+ return color.replace(/\s+/g, "").toLowerCase();
13217
+ }
13218
+ function parseHslLightness(color) {
13219
+ const match = color.match(/^hsl\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*([\d.]+)%\s*\)$/i);
13220
+ return match ? Number(match[1]) : null;
13221
+ }
13222
+ function sumCounts(counts) {
13223
+ return counts.reduce((sum, count) => sum + count, 0);
13224
+ }
13225
+ function countsToPartGroups(counts) {
13226
+ return counts.flatMap(
13227
+ (count, groupIndex) => Array.from({ length: count }, () => groupIndex)
13228
+ );
13229
+ }
13230
+ function normalizeCounts(value, expected) {
13231
+ if (!Array.isArray(value)) return expected.map(() => 0);
13232
+ return expected.map((max, i) => Math.max(0, Math.min(max, value[i] ?? 0)));
13233
+ }
13234
+ function serializeCounts(value) {
13235
+ return Array.isArray(value) ? value.join("|") : null;
13236
+ }
13237
+ function countsToAssignments(counts, divisions, filledCount) {
13238
+ const assignments = Array.from({ length: divisions }, () => null);
13239
+ let index = filledCount;
13240
+ counts.forEach((count, groupIndex) => {
13241
+ for (let i = 0; i < count && index < divisions; i++) {
13242
+ assignments[index] = groupIndex;
13243
+ index++;
13244
+ }
13245
+ });
13246
+ return assignments;
13247
+ }
13248
+ function getNextAssignments(assignments, partIndex, groupCount) {
13249
+ if (groupCount === 0) return [...assignments];
13250
+ const current = assignments[partIndex] ?? null;
13251
+ const next = current === null ? 0 : current + 1 >= groupCount ? null : current + 1;
13252
+ const updated = [...assignments];
13253
+ updated[partIndex] = next;
13254
+ return updated;
13255
+ }
13256
+ function assignmentsToCounts(assignments, groupCount) {
13257
+ const counts = Array.from({ length: groupCount }, () => 0);
13258
+ for (const groupIndex of assignments) {
13259
+ if (groupIndex !== null && groupIndex >= 0 && groupIndex < groupCount) {
13260
+ counts[groupIndex] = (counts[groupIndex] ?? 0) + 1;
13261
+ }
13262
+ }
13263
+ return counts;
13264
+ }
13265
+ function useVisualFractionState({
13266
+ widgetId,
13267
+ divisions,
13268
+ filledCount,
13269
+ answer,
13270
+ value,
13271
+ onChange
13272
+ }) {
13273
+ const [partAssignments, setPartAssignments] = useState(
13274
+ () => countsToAssignments(normalizeCounts(value, answer), divisions, filledCount)
13275
+ );
13276
+ const lastInternalValueRef = useRef(null);
13277
+ const valueKey = serializeCounts(value);
13278
+ useEffect(() => {
13279
+ if (valueKey !== null && valueKey === lastInternalValueRef.current) return;
13280
+ lastInternalValueRef.current = null;
13281
+ setPartAssignments(
13282
+ countsToAssignments(normalizeCounts(value, answer), divisions, filledCount)
13283
+ );
13284
+ }, [answer, divisions, filledCount, valueKey]);
13285
+ const handlePartClick = (partIndex) => {
13286
+ if (partIndex < filledCount) return;
13287
+ const updated = getNextAssignments(partAssignments, partIndex, answer.length);
13288
+ const nextCounts = assignmentsToCounts(updated, answer.length);
13289
+ lastInternalValueRef.current = serializeCounts(nextCounts);
13290
+ setPartAssignments(updated);
13291
+ onChange(widgetId, nextCounts);
13292
+ };
13293
+ return { partAssignments, handlePartClick };
13294
+ }
13295
+ const VISUAL_FRACTION_DISPLAY_COLOR = "#AE9CFF";
13296
+ const VISUAL_FRACTION_BORDER_COLOR = "#705DC0";
13297
+ const VISUAL_FRACTION_SVG_SIZE_SCALE = 8;
13298
+ function getCircleSectorProps(index, divisions) {
13299
+ if (divisions <= 1) return { kind: "circle" };
13300
+ const angle = 360 / divisions;
13301
+ const startAngle = -90 + index * angle;
13302
+ const endAngle = startAngle + angle;
13303
+ const start = polarToCartesian(50, 50, 48, endAngle);
13304
+ const end = polarToCartesian(50, 50, 48, startAngle);
13305
+ const largeArcFlag = angle > 180 ? 1 : 0;
13306
+ return {
13307
+ kind: "path",
13308
+ d: [
13309
+ "M",
13310
+ 50,
13311
+ 50,
13312
+ "L",
13313
+ start.x,
13314
+ start.y,
13315
+ "A",
13316
+ 48,
13317
+ 48,
13318
+ 0,
13319
+ largeArcFlag,
13320
+ 0,
13321
+ end.x,
13322
+ end.y,
13323
+ "Z"
13324
+ ].join(" ")
13325
+ };
13326
+ }
13327
+ function polarToCartesian(cx, cy, r, angleInDegrees) {
13328
+ const rad = angleInDegrees * Math.PI / 180;
13329
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
13330
+ }
13331
+ function getEqualTrianglePartPath(index, divisions) {
13332
+ const side = Math.sqrt(divisions);
13333
+ if (!Number.isInteger(side)) {
13334
+ throw new Error(`getEqualTrianglePartPath: divisions (${divisions}) must be a perfect square.`);
13335
+ }
13336
+ const smallSide = 96 / side;
13337
+ const smallHeight = smallSide * Math.sqrt(3) / 2;
13338
+ const totalHeight = side * smallHeight;
13339
+ const offsetY = (100 - totalHeight) / 2;
13340
+ let cursor = 0;
13341
+ for (let row = 0; row < side; row++) {
13342
+ const y = offsetY + row * smallHeight;
13343
+ const rowLeft = 50 - (row + 1) * smallSide / 2;
13344
+ for (let col = 0; col <= row; col++) {
13345
+ if (cursor === index) {
13346
+ const x = rowLeft + col * smallSide;
13347
+ return ["M", x + smallSide / 2, y, "L", x + smallSide, y + smallHeight, "L", x, y + smallHeight, "Z"].join(" ");
13348
+ }
13349
+ cursor++;
13350
+ }
13351
+ for (let col = 0; col < row; col++) {
13352
+ if (cursor === index) {
13353
+ const x = rowLeft + col * smallSide;
13354
+ return ["M", x + smallSide, y + smallHeight, "L", x + smallSide / 2, y, "L", x + smallSide * 1.5, y, "Z"].join(" ");
13355
+ }
13356
+ cursor++;
13357
+ }
13358
+ }
13359
+ return "";
13360
+ }
13361
+ function getGridColumnCount(divisions) {
13362
+ return Math.ceil(Math.sqrt(divisions));
13363
+ }
13364
+ function getInteractiveProps(part, onPartClick) {
13365
+ if (!part.isInteractiveSlot) return {};
13366
+ return {
13367
+ onClick: () => onPartClick(part.index),
13368
+ role: "button",
13369
+ "aria-pressed": part.isSelected,
13370
+ "aria-label": getPartAriaLabel(part),
13371
+ tabIndex: 0,
13372
+ onKeyDown: (event) => {
13373
+ if (event.key === "Enter" || event.key === " ") {
13374
+ event.preventDefault();
13375
+ onPartClick(part.index);
13376
+ }
13377
+ }
13378
+ };
13379
+ }
13380
+ function getPartAriaLabel(part) {
13381
+ return part.groupIndex === null ? `Part ${part.index + 1}, empty` : `Part ${part.index + 1}, group ${part.groupIndex + 1}`;
13382
+ }
13383
+ const BarShape = ({
13384
+ parts,
13385
+ divisions,
13386
+ orientation,
13387
+ onPartClick
13388
+ }) => {
13389
+ const renderedParts = orientation === "vertical" ? [...parts].reverse() : parts;
13390
+ return /* @__PURE__ */ jsx(
13391
+ "div",
13392
+ {
13393
+ className: `visual-fraction-widget__parts${orientation === "vertical" ? " visual-fraction-widget__parts--vertical" : " visual-fraction-widget__parts--horizontal"}`,
13394
+ style: { "--visual-fraction-divisions": divisions },
13395
+ children: renderedParts.map((part) => /* @__PURE__ */ jsx(
13396
+ "div",
13397
+ {
13398
+ className: part.className,
13399
+ style: part.style,
13400
+ ...getInteractiveProps(part, onPartClick)
13401
+ },
13402
+ part.index
13403
+ ))
13404
+ }
13405
+ );
13406
+ };
13407
+ BarShape.displayName = "BarShape";
13408
+ const GridShape = ({
13409
+ parts,
13410
+ divisions,
13411
+ onPartClick
13412
+ }) => /* @__PURE__ */ jsx(
13413
+ "div",
13414
+ {
13415
+ className: "visual-fraction-widget__parts visual-fraction-widget__parts--horizontal",
13416
+ style: {
13417
+ "--visual-fraction-divisions": divisions,
13418
+ "--visual-fraction-grid-cols": getGridColumnCount(divisions)
13419
+ },
13420
+ children: parts.map((part) => /* @__PURE__ */ jsx(
13421
+ "div",
13422
+ {
13423
+ className: part.className,
13424
+ style: part.style,
13425
+ ...getInteractiveProps(part, onPartClick)
13426
+ },
13427
+ part.index
13428
+ ))
13429
+ }
13430
+ );
13431
+ GridShape.displayName = "GridShape";
13432
+ const CircleShape = ({
13433
+ parts,
13434
+ divisions,
13435
+ onPartClick
13436
+ }) => /* @__PURE__ */ jsx(
13437
+ "svg",
13438
+ {
13439
+ className: "visual-fraction-widget__shape visual-fraction-widget__circle",
13440
+ viewBox: "0 0 100 100",
13441
+ children: parts.map((part) => {
13442
+ const sectorProps = getCircleSectorProps(part.index, divisions);
13443
+ const className = `${part.className} visual-fraction-widget__sector`;
13444
+ const interactiveProps = getInteractiveProps(part, onPartClick);
13445
+ return sectorProps.kind === "circle" ? /* @__PURE__ */ jsx(
13446
+ "circle",
13447
+ {
13448
+ className,
13449
+ style: part.style,
13450
+ cx: "50",
13451
+ cy: "50",
13452
+ r: "48",
13453
+ ...interactiveProps
13454
+ },
13455
+ part.index
13456
+ ) : /* @__PURE__ */ jsx(
13457
+ "path",
13458
+ {
13459
+ className,
13460
+ style: part.style,
13461
+ d: sectorProps.d,
13462
+ ...interactiveProps
13463
+ },
13464
+ part.index
13465
+ );
13466
+ })
13467
+ }
13468
+ );
13469
+ CircleShape.displayName = "CircleShape";
13470
+ const TriangleShape = ({
13471
+ parts,
13472
+ divisions,
13473
+ onPartClick
13474
+ }) => /* @__PURE__ */ jsx(
13475
+ "svg",
13476
+ {
13477
+ className: "visual-fraction-widget__shape visual-fraction-widget__triangle",
13478
+ viewBox: "0 0 100 100",
13479
+ children: parts.map((part) => /* @__PURE__ */ jsx(
13480
+ "path",
13481
+ {
13482
+ className: `${part.className} visual-fraction-widget__sector`,
13483
+ style: part.style,
13484
+ d: getEqualTrianglePartPath(part.index, divisions),
13485
+ ...getInteractiveProps(part, onPartClick)
13486
+ },
13487
+ part.index
13488
+ ))
13489
+ }
13490
+ );
13491
+ TriangleShape.displayName = "TriangleShape";
13492
+ const ShapeBar = ({ parts, divisions, orientation, onPartClick }) => /* @__PURE__ */ jsx(BarShape, { parts, divisions, orientation, onPartClick });
13493
+ const ShapeCircle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(CircleShape, { parts, divisions, onPartClick });
13494
+ const ShapeTriangle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(TriangleShape, { parts, divisions, onPartClick });
13495
+ const ShapeGrid = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(GridShape, { parts, divisions, onPartClick });
13496
+ const SHAPE_COMPONENTS = {
13497
+ [VISUAL_FRACTION_SHAPE.BAR]: ShapeBar,
13498
+ [VISUAL_FRACTION_SHAPE.CIRCLE]: ShapeCircle,
13499
+ [VISUAL_FRACTION_SHAPE.TRIANGLE]: ShapeTriangle,
13500
+ [VISUAL_FRACTION_SHAPE.GRID]: ShapeGrid
13501
+ };
13502
+ const VisualFraction = ({
13503
+ widgetId,
13504
+ widget,
13505
+ value,
13506
+ validationStatus = null,
13507
+ onChange
13508
+ }) => {
13509
+ const { shape, divisions, filled, answer, orientation, label } = widget;
13510
+ const filledCount = sumCounts(filled);
13511
+ const filledPartGroups = useMemo(() => countsToPartGroups(filled), [filled]);
13512
+ const filledPalette = useMemo(() => {
13513
+ if (filled.length === 0) return [];
13514
+ return resolveDistinctGroupPalette(filled.length, {
13515
+ seedColors: [VISUAL_FRACTION_DISPLAY_COLOR],
13516
+ excludeColors: [VISUAL_FRACTION_BORDER_COLOR]
13517
+ });
13518
+ }, [filled]);
13519
+ const answerPalette = useMemo(() => {
13520
+ if (answer.length === 0) return [];
13521
+ return resolveDistinctGroupPalette(answer.length, {
13522
+ excludeColors: [VISUAL_FRACTION_BORDER_COLOR, ...filledPalette]
13523
+ });
13524
+ }, [answer, filledPalette]);
13525
+ const { partAssignments, handlePartClick } = useVisualFractionState({
13526
+ widgetId,
13527
+ divisions,
13528
+ filledCount,
13529
+ answer,
13530
+ value,
13531
+ onChange
13532
+ });
13533
+ const parts = useMemo(
13534
+ () => Array.from({ length: divisions }, (_, i) => {
13535
+ let partClass = "visual-fraction-widget__part visual-fraction-widget__part--empty";
13536
+ let groupIdx = null;
13537
+ if (i < filledCount) {
13538
+ groupIdx = filledPartGroups[i] ?? 0;
13539
+ partClass = "visual-fraction-widget__part visual-fraction-widget__part--filled";
13540
+ } else if (partAssignments[i] != null) {
13541
+ groupIdx = partAssignments[i];
13542
+ partClass = "visual-fraction-widget__part visual-fraction-widget__part--answer";
13543
+ }
13544
+ if (groupIdx !== null) {
13545
+ partClass += ` visual-fraction-widget__part--group-${groupIdx + 1} ${getGroupClassName(groupIdx)}`;
13546
+ }
13547
+ const colorKind = i < filledCount ? "filled" : "answer";
13548
+ const palette = colorKind === "filled" ? filledPalette : answerPalette;
13549
+ const fallbackColor = palette[groupIdx ?? 0];
13550
+ const style = groupIdx !== null ? {
13551
+ "--visual-fraction-part-color": `var(--visual-fraction-${colorKind}-group-${groupIdx + 1}-color, ${fallbackColor})`,
13552
+ "--visual-fraction-part-text-color": `var(--visual-fraction-${colorKind}-group-${groupIdx + 1}-text-color, ${getTextColorForBackground(fallbackColor)})`
13553
+ } : void 0;
13554
+ return {
13555
+ index: i,
13556
+ className: partClass,
13557
+ style,
13558
+ // A slot is interactive when there are answer groups AND it falls in the non-filled range.
13559
+ // answer.length counts groups (not total slots); the state hook handles per-slot cycling.
13560
+ isInteractiveSlot: answer.length > 0 && i >= filledCount,
13561
+ isSelected: groupIdx !== null,
13562
+ groupIndex: groupIdx
13563
+ };
13564
+ }),
13565
+ [divisions, filledCount, filledPartGroups, partAssignments, answer, filledPalette, answerPalette]
13566
+ );
13567
+ const statusClass = validationStatus ? ` visual-fraction-widget--${validationStatus} widget--${validationStatus}` : "";
13568
+ const rootStyle = useMemo(() => getSizeStyle(widget.size, shape), [widget.size, shape]);
13569
+ const ShapeComponent = SHAPE_COMPONENTS[shape];
13570
+ return /* @__PURE__ */ jsxs(
13571
+ "div",
13572
+ {
13573
+ className: `visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
13574
+ style: rootStyle,
13575
+ role: "group",
13576
+ "aria-label": `${divisions} part ${shape} fraction`,
13577
+ children: [
13578
+ /* @__PURE__ */ jsx(
13579
+ ShapeComponent,
13580
+ {
13581
+ parts,
13582
+ divisions,
13583
+ orientation: orientation ?? "horizontal",
13584
+ onPartClick: handlePartClick
13585
+ }
13586
+ ),
13587
+ label && /* @__PURE__ */ jsx("span", { className: "visual-fraction-widget__label", children: label })
13588
+ ]
13589
+ }
13590
+ );
13591
+ };
13592
+ VisualFraction.displayName = "VisualFraction";
13593
+ function getSizeStyle(size, shape) {
13594
+ if (size === void 0) return void 0;
13595
+ if (shape === "circle" || shape === "triangle") {
13596
+ return {
13597
+ "--visual-fraction-size": `${size * VISUAL_FRACTION_SVG_SIZE_SCALE}rem`
13598
+ };
13599
+ }
13600
+ return {
13601
+ "--visual-fraction-part-size": `${size}rem`
13602
+ };
13603
+ }
13604
+ const RadioOptions = ({
13605
+ name,
13606
+ options,
13607
+ value,
13608
+ validationStatus = null,
13609
+ onChange
13610
+ }) => {
13611
+ return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
13612
+ const isSelected = value === option;
13613
+ const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
13614
+ return /* @__PURE__ */ jsx(
13615
+ "button",
13616
+ {
13617
+ className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
13618
+ onClick: () => onChange(name, option),
13619
+ type: "button",
13620
+ children: option
13621
+ },
13622
+ `${name}-radio-${idx}`
13623
+ );
13624
+ }) });
13625
+ };
13626
+ const Btn = ({
13627
+ label,
13628
+ extraClass = "keypad-btn-digit",
13629
+ onClick
13630
+ }) => /* @__PURE__ */ jsx("button", { className: `keypad-btn ${extraClass}`, onClick, children: /* @__PURE__ */ jsx("span", { children: label }) });
13631
+ const DiamondKeypad = ({ value, onChange }) => {
13632
+ const append = (ch) => onChange(value + ch);
13633
+ const backspace = () => onChange(value.slice(0, -1));
13634
+ return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", children: [
13635
+ /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
13636
+ /* @__PURE__ */ jsx(Btn, { label: "1", onClick: () => append("1") }),
13637
+ /* @__PURE__ */ jsx(Btn, { label: "2", onClick: () => append("2") }),
13638
+ /* @__PURE__ */ jsx(Btn, { label: "3", onClick: () => append("3") })
13639
+ ] }),
13640
+ /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
13641
+ /* @__PURE__ */ jsx(Btn, { label: "4", onClick: () => append("4") }),
13642
+ /* @__PURE__ */ jsx(Btn, { label: "5", onClick: () => append("5") }),
13643
+ /* @__PURE__ */ jsx(Btn, { label: "6", onClick: () => append("6") }),
13644
+ /* @__PURE__ */ jsx(Btn, { label: "7", onClick: () => append("7") })
13645
+ ] }),
13646
+ /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
13647
+ /* @__PURE__ */ jsx(Btn, { label: "8", onClick: () => append("8") }),
13648
+ /* @__PURE__ */ jsx(Btn, { label: "9", onClick: () => append("9") }),
13649
+ /* @__PURE__ */ jsx(Btn, { label: "0", onClick: () => append("0") })
13650
+ ] }),
13651
+ /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
13652
+ /* @__PURE__ */ jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
13653
+ /* @__PURE__ */ jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
13654
+ ] })
13655
+ ] });
13656
+ };
12910
13657
  function collectItemIds(rows) {
12911
13658
  return rows.flatMap((row) => row.items.map((item) => item.itemId));
12912
13659
  }
@@ -12948,7 +13695,7 @@ const MatcherWidget = ({
12948
13695
  const { rows, colors: userColors, groupCount } = widget;
12949
13696
  const widgetsMap = widgets ?? EMPTY_WIDGETS;
12950
13697
  const palette = useMemo(
12951
- () => resolveMatcherPalette(userColors, groupCount),
13698
+ () => resolveGroupPalette(userColors, groupCount),
12952
13699
  [userColors, groupCount]
12953
13700
  );
12954
13701
  const textColors = useMemo(
@@ -12964,13 +13711,21 @@ const MatcherWidget = ({
12964
13711
  setAssignments((prev) => {
12965
13712
  const current = prev[itemId] ?? null;
12966
13713
  const next = current === null ? 0 : current + 1 >= groupCount ? null : current + 1;
12967
- const updated = { ...prev, [itemId]: next };
12968
- onChange(widgetId, buildGroups(updated));
12969
- return updated;
13714
+ return { ...prev, [itemId]: next };
12970
13715
  });
12971
13716
  },
12972
- [groupCount, onChange, widgetId]
13717
+ [groupCount]
12973
13718
  );
13719
+ const onChangeRef = useRef(onChange);
13720
+ onChangeRef.current = onChange;
13721
+ const isMounted = useRef(false);
13722
+ useEffect(() => {
13723
+ if (!isMounted.current) {
13724
+ isMounted.current = true;
13725
+ return;
13726
+ }
13727
+ onChangeRef.current(widgetId, buildGroups(assignments));
13728
+ }, [assignments, widgetId]);
12974
13729
  const maxCols = useMemo(
12975
13730
  () => rows.length === 0 ? 1 : Math.max(...rows.map((r) => r.items.length)),
12976
13731
  [rows]
@@ -12991,9 +13746,9 @@ const MatcherWidget = ({
12991
13746
  {
12992
13747
  type: "button",
12993
13748
  className: `matcher-card${isSelected ? " matcher-card--selected" : ""}`,
12994
- style: color ? {
12995
- "--matcher-card-color": color,
12996
- "--matcher-card-text-color": textColors[colorIdx]
13749
+ style: colorIdx !== null && color ? {
13750
+ "--matcher-card-color": `var(--matcher-group-${colorIdx + 1}-color, ${color})`,
13751
+ "--matcher-card-text-color": `var(--matcher-group-${colorIdx + 1}-text-color, ${textColors[colorIdx]})`
12997
13752
  } : void 0,
12998
13753
  onClick: () => handleCardClick(item.itemId),
12999
13754
  "aria-pressed": isSelected,
@@ -13035,6 +13790,9 @@ function isFractionWidget(widget) {
13035
13790
  function isMatcherWidget(widget) {
13036
13791
  return widget.type === "matcher";
13037
13792
  }
13793
+ function isVisualFractionWidget(widget) {
13794
+ return widget.type === "visual-fraction";
13795
+ }
13038
13796
  const WidgetRenderer = ({
13039
13797
  widget,
13040
13798
  widgetId,
@@ -13060,6 +13818,7 @@ const WidgetRenderer = ({
13060
13818
  inputType: widget.inputType,
13061
13819
  length: widget.inputType === "string" ? widget.length : void 0,
13062
13820
  digits: widget.inputType === "numeric" ? widget.digits : void 0,
13821
+ placeholder: widget.placeholder,
13063
13822
  onChange,
13064
13823
  onFocus: onInputFocus
13065
13824
  }
@@ -13088,14 +13847,24 @@ const WidgetRenderer = ({
13088
13847
  );
13089
13848
  }
13090
13849
  if (isNumericKeypadWidget(widget)) {
13850
+ const keypadOnChange = onKeypadChange || (() => {
13851
+ });
13852
+ if (widget.style === "classic") {
13853
+ return /* @__PURE__ */ jsx(
13854
+ NumericKeypad,
13855
+ {
13856
+ value: strValue,
13857
+ includeOperators: widget.includeOperators,
13858
+ includeDecimal: widget.includeDecimal,
13859
+ onChange: keypadOnChange
13860
+ }
13861
+ );
13862
+ }
13091
13863
  return /* @__PURE__ */ jsx(
13092
- NumericKeypad,
13864
+ DiamondKeypad,
13093
13865
  {
13094
13866
  value: strValue,
13095
- includeOperators: widget.includeOperators,
13096
- includeDecimal: widget.includeDecimal,
13097
- onChange: onKeypadChange || (() => {
13098
- })
13867
+ onChange: keypadOnChange
13099
13868
  }
13100
13869
  );
13101
13870
  }
@@ -13127,6 +13896,18 @@ const WidgetRenderer = ({
13127
13896
  }
13128
13897
  );
13129
13898
  }
13899
+ if (isVisualFractionWidget(widget)) {
13900
+ return /* @__PURE__ */ jsx(
13901
+ VisualFraction,
13902
+ {
13903
+ widgetId,
13904
+ widget,
13905
+ value: Array.isArray(value) && value.every((v) => typeof v === "number") ? value : void 0,
13906
+ validationStatus,
13907
+ onChange: (id, nextValue) => onChange(id, nextValue)
13908
+ }
13909
+ );
13910
+ }
13130
13911
  return null;
13131
13912
  };
13132
13913
  const EPSILON = 10 ** -INTERNAL_PRECISION;
@@ -13629,6 +14410,102 @@ const CounterControls = ({
13629
14410
  ] }),
13630
14411
  counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
13631
14412
  ] });
14413
+ const CONTENT_ROW_EXTRA_SPACE = 8;
14414
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
14415
+ function useMeasuredPositionContentHeight(positionIds) {
14416
+ const [heightPx, setHeightPx] = useState(null);
14417
+ const elementsRef = useRef(/* @__PURE__ */ new Map());
14418
+ const refCallbacksRef = useRef(/* @__PURE__ */ new Map());
14419
+ const observerRef = useRef(null);
14420
+ const measureFrameRef = useRef(null);
14421
+ const idsKey = positionIds.join("|");
14422
+ const runMeasure = useCallback(() => {
14423
+ let maxHeight = 0;
14424
+ for (const element of elementsRef.current.values()) {
14425
+ const rectHeight = element.getBoundingClientRect().height;
14426
+ const measuredHeight = Math.max(rectHeight, element.offsetHeight);
14427
+ maxHeight = Math.max(maxHeight, measuredHeight);
14428
+ }
14429
+ const nextHeight = maxHeight > 0 ? Math.ceil(maxHeight + CONTENT_ROW_EXTRA_SPACE) : null;
14430
+ setHeightPx((current) => current === nextHeight ? current : nextHeight);
14431
+ }, []);
14432
+ const measure = useCallback(() => {
14433
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
14434
+ runMeasure();
14435
+ return;
14436
+ }
14437
+ if (measureFrameRef.current !== null) return;
14438
+ measureFrameRef.current = window.requestAnimationFrame(() => {
14439
+ measureFrameRef.current = null;
14440
+ runMeasure();
14441
+ });
14442
+ }, [runMeasure]);
14443
+ useEffect(
14444
+ () => () => {
14445
+ if (measureFrameRef.current !== null && typeof window !== "undefined") {
14446
+ window.cancelAnimationFrame(measureFrameRef.current);
14447
+ measureFrameRef.current = null;
14448
+ }
14449
+ },
14450
+ []
14451
+ );
14452
+ const getPositionContentRef = useCallback(
14453
+ (id) => {
14454
+ const cachedRef = refCallbacksRef.current.get(id);
14455
+ if (cachedRef) return cachedRef;
14456
+ const refCallback = (node) => {
14457
+ const current = elementsRef.current.get(id);
14458
+ if (current && observerRef.current) {
14459
+ observerRef.current.unobserve(current);
14460
+ }
14461
+ if (node) {
14462
+ elementsRef.current.set(id, node);
14463
+ observerRef.current?.observe(node);
14464
+ } else {
14465
+ elementsRef.current.delete(id);
14466
+ }
14467
+ measure();
14468
+ };
14469
+ refCallbacksRef.current.set(id, refCallback);
14470
+ return refCallback;
14471
+ },
14472
+ [measure]
14473
+ );
14474
+ useEffect(() => {
14475
+ const liveIds = new Set(positionIds);
14476
+ for (const id of elementsRef.current.keys()) {
14477
+ if (!liveIds.has(id)) {
14478
+ elementsRef.current.delete(id);
14479
+ }
14480
+ }
14481
+ for (const id of refCallbacksRef.current.keys()) {
14482
+ if (!liveIds.has(id)) {
14483
+ refCallbacksRef.current.delete(id);
14484
+ }
14485
+ }
14486
+ measure();
14487
+ }, [idsKey, measure]);
14488
+ useIsomorphicLayoutEffect(() => {
14489
+ if (typeof ResizeObserver === "undefined") {
14490
+ measure();
14491
+ return void 0;
14492
+ }
14493
+ const observer = new ResizeObserver(measure);
14494
+ observerRef.current = observer;
14495
+ for (const element of elementsRef.current.values()) {
14496
+ observer.observe(element);
14497
+ }
14498
+ measure();
14499
+ return () => {
14500
+ observer.disconnect();
14501
+ observerRef.current = null;
14502
+ };
14503
+ }, [idsKey, measure]);
14504
+ return {
14505
+ positionContentHeight: heightPx === null ? void 0 : `${heightPx}px`,
14506
+ getPositionContentRef
14507
+ };
14508
+ }
13632
14509
  const NumberLineRenderer = ({
13633
14510
  item,
13634
14511
  widgets,
@@ -13683,17 +14560,26 @@ const NumberLineRenderer = ({
13683
14560
  () => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
13684
14561
  [isDragMode, positions]
13685
14562
  );
14563
+ const visiblePositionIds = useMemo(
14564
+ () => visiblePositions.map((pos) => pos.id),
14565
+ [visiblePositions]
14566
+ );
14567
+ const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
13686
14568
  return /* @__PURE__ */ jsxs(
13687
14569
  "div",
13688
14570
  {
13689
14571
  className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
13690
- style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
14572
+ style: {
14573
+ [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
14574
+ ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
14575
+ },
13691
14576
  role: "group",
13692
14577
  "aria-label": `Number line from ${min} to ${max}`,
13693
14578
  children: [
13694
14579
  visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
13695
14580
  "div",
13696
14581
  {
14582
+ ref: getPositionContentRef(pos.id),
13697
14583
  className: "number-line-position-content",
13698
14584
  style: { left: `${toPercent(pos.value, min, max)}%` },
13699
14585
  children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
@@ -13896,7 +14782,7 @@ const LayoutRenderer = ({
13896
14782
  if (!widget) return null;
13897
14783
  const isCorrect = validationDetails?.[item.widgetId];
13898
14784
  const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
13899
- const widgetKey = widget.type === JSON_TYPES.MATCHER ? `${item.widgetId}-${resetKey}` : item.widgetId;
14785
+ const widgetKey = widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.VISUAL_FRACTION ? `${item.widgetId}-${resetKey}` : item.widgetId;
13900
14786
  return /* @__PURE__ */ jsx(
13901
14787
  WidgetRenderer,
13902
14788
  {
@@ -14301,9 +15187,14 @@ export {
14301
15187
  TextItemSchema,
14302
15188
  ToolNodeSchema,
14303
15189
  TransformError,
15190
+ VISUAL_FRACTION_DEFAULT_SIZE,
15191
+ VISUAL_FRACTION_MAX_DIVISIONS,
15192
+ VISUAL_FRACTION_SHAPE,
15193
+ VISUAL_FRACTION_SHAPES,
14304
15194
  ValidationError,
14305
15195
  ValidationJSONSchema,
14306
15196
  ValidationNodeSchema,
15197
+ VisualFractionInlineSchema,
14307
15198
  WidgetJSONSchema,
14308
15199
  WidgetRefSchema,
14309
15200
  WidgetRenderer,
@@ -14328,6 +15219,7 @@ export {
14328
15219
  createSpace,
14329
15220
  createStringInput,
14330
15221
  createTextBlock,
15222
+ createVisualFraction,
14331
15223
  evaluate,
14332
15224
  extractVariables as extractExpressionVariables,
14333
15225
  findValidCombination,
@@ -14371,6 +15263,7 @@ export {
14371
15263
  isToolNode,
14372
15264
  isValidExpression,
14373
15265
  isValidationNode,
15266
+ isVisualFractionInline,
14374
15267
  isWidgetRef,
14375
15268
  parse,
14376
15269
  parseFractionKey,
@@ -14387,6 +15280,7 @@ export {
14387
15280
  validateExpression,
14388
15281
  validateInputFormats,
14389
15282
  validateSimple,
15283
+ validateVisualFractionCapacity,
14390
15284
  validateWidgetJSON,
14391
15285
  validateWidgetJSONSafe
14392
15286
  };