@mathscan/math-exercise-engine 1.2.13 → 1.2.15

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.
@@ -24,6 +24,8 @@ const AST_TYPES = {
24
24
  BOLD_TEXT: "BoldText",
25
25
  /** Inline input widget (string or numeric) */
26
26
  INLINE_INPUT: "InlineInput",
27
+ /** Drop-only inline input; stores the selected option name */
28
+ DROP_INPUT: "DropInput",
27
29
  /** Single inline draggable option (appears in-flow with text) */
28
30
  INLINE_DRAG_OPTION: "InlineDragOption",
29
31
  /** Inline fraction widget (numerator/denominator with optional inputs) */
@@ -111,6 +113,8 @@ const AST_TYPES = {
111
113
  // ═══════════════════════════════════════════════════════════════════════════
112
114
  /** Drag-and-drop options tool */
113
115
  DRAG_OPTIONS: "DragOptions",
116
+ /** Rich draggable options tool */
117
+ DRAGGABLE_OPTIONS: "DraggableOptions",
114
118
  /** Radio button options tool */
115
119
  RADIO_OPTIONS: "RadioOptions",
116
120
  /** Numeric keypad input tool */
@@ -216,8 +220,12 @@ const JSON_TYPES = {
216
220
  // ═══════════════════════════════════════════════════════════════════════════
217
221
  /** Inline input widget (string or numeric) */
218
222
  INLINE_INPUT: "inline-input",
223
+ /** Drop-only inline input; stores the selected option name */
224
+ DROP_INPUT: "drop-input",
219
225
  /** Drag-and-drop options widget */
220
226
  DRAG_OPTIONS: "drag-options",
227
+ /** Rich draggable options widget */
228
+ DRAGGABLE_OPTIONS: "draggable-options",
221
229
  /** Single inline draggable option (appears in-flow with text) */
222
230
  INLINE_DRAG_OPTION: "inline-drag-option",
223
231
  /** Radio button options widget */
@@ -240,6 +248,7 @@ const JSON_TYPES = {
240
248
  GEOMETRY: "geometry"
241
249
  };
242
250
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
251
+ const DRAGGABLE_OPTIONS_WIDGET_ID = "_draggable_options";
243
252
  const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
244
253
  const RULER_WIDGET_ID = "_ruler";
245
254
  const PROTRACTOR_WIDGET_ID = "_protractor";
@@ -255,6 +264,7 @@ const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
255
264
  const GEOMETRY_WIDGET_PREFIX = "_geometry_";
256
265
  const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
257
266
  JSON_TYPES.INLINE_INPUT,
267
+ JSON_TYPES.DROP_INPUT,
258
268
  JSON_TYPES.INLINE_DRAG_OPTION,
259
269
  JSON_TYPES.FRACTION,
260
270
  JSON_TYPES.VISUAL_FRACTION
@@ -354,6 +364,10 @@ const STRING_INPUT = createToken({
354
364
  name: "STRING_INPUT",
355
365
  pattern: /STRING_INPUT/
356
366
  });
367
+ const DROP_INPUT = createToken({
368
+ name: "DROP_INPUT",
369
+ pattern: /DROP_INPUT/
370
+ });
357
371
  const INLINE_DRAG_OPTION = createToken({
358
372
  name: "INLINE_DRAG_OPTION",
359
373
  pattern: /INLINE_DRAG_OPTION/
@@ -362,6 +376,18 @@ const DRAGABLE_BUTTONS_OPTIONS = createToken({
362
376
  name: "DRAGABLE_BUTTONS_OPTIONS",
363
377
  pattern: /DRAGABLE_BUTTONS_OPTIONS/
364
378
  });
379
+ const DRAGGABLE_OPTIONS = createToken({
380
+ name: "DRAGGABLE_OPTIONS",
381
+ pattern: /DRAGGABLE_OPTIONS/
382
+ });
383
+ const OPTION = createToken({
384
+ name: "OPTION",
385
+ // Only a keyword when immediately followed by `[` (which the grammar always
386
+ // requires: `OPTION[name=...]`). Without the lookahead, the bare common word
387
+ // "OPTION"/"OPTIONS" in exercise prose would tokenize as a keyword and break
388
+ // parsing.
389
+ pattern: /OPTION(?=\[)/
390
+ });
365
391
  const RADIO_BUTTONS_OPTIONS = createToken({
366
392
  name: "RADIO_BUTTONS_OPTIONS",
367
393
  pattern: /RADIO_BUTTONS_OPTIONS/
@@ -672,8 +698,11 @@ const allTokens = [
672
698
  HIGHLIGHT_TEXT,
673
699
  W_S_I,
674
700
  STRING_INPUT,
701
+ DROP_INPUT,
675
702
  INLINE_DRAG_OPTION,
703
+ DRAGGABLE_OPTIONS,
676
704
  DRAGABLE_BUTTONS_OPTIONS,
705
+ OPTION,
677
706
  RADIO_BUTTONS_OPTIONS,
678
707
  NUMERIC_KEYPAD,
679
708
  WIDGET_ANSWER,
@@ -1445,6 +1474,8 @@ class MathExerciseParser extends CstParser {
1445
1474
  { ALT: () => this.SUBRULE(this.visualFraction) },
1446
1475
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
1447
1476
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
1477
+ // DROP_INPUT (drop-only inline answer widget) — excluded in display-only contexts
1478
+ { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.dropInput) },
1448
1479
  // INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
1449
1480
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) },
1450
1481
  // CLOCK (inline clock widget)
@@ -1536,6 +1567,12 @@ class MathExerciseParser extends CstParser {
1536
1567
  this.SUBRULE(this.parameterList);
1537
1568
  this.CONSUME(RBracket);
1538
1569
  });
1570
+ this.dropInput = this.RULE("dropInput", () => {
1571
+ this.CONSUME(DROP_INPUT);
1572
+ this.CONSUME(LBracket);
1573
+ this.SUBRULE(this.parameterList);
1574
+ this.CONSUME(RBracket);
1575
+ });
1539
1576
  this.inlineDragOption = this.RULE("inlineDragOption", () => {
1540
1577
  this.CONSUME(INLINE_DRAG_OPTION);
1541
1578
  this.CONSUME(LBracket);
@@ -1544,6 +1581,7 @@ class MathExerciseParser extends CstParser {
1544
1581
  });
1545
1582
  this.tool = this.RULE("tool", () => {
1546
1583
  this.OR([
1584
+ { ALT: () => this.SUBRULE(this.draggableOptions) },
1547
1585
  { ALT: () => this.SUBRULE(this.dragableButtons) },
1548
1586
  { ALT: () => this.SUBRULE(this.numericKeypad) },
1549
1587
  { ALT: () => this.SUBRULE(this.rulerTool) },
@@ -1551,6 +1589,44 @@ class MathExerciseParser extends CstParser {
1551
1589
  { ALT: () => this.SUBRULE(this.setSquareTool) }
1552
1590
  ]);
1553
1591
  });
1592
+ this.draggableOptions = this.RULE("draggableOptions", () => {
1593
+ this.CONSUME(DRAGGABLE_OPTIONS);
1594
+ this.CONSUME(LBracket);
1595
+ this.OPTION(() => this.SUBRULE(this.parameterList));
1596
+ this.CONSUME(RBracket);
1597
+ this.MANY(() => {
1598
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1599
+ });
1600
+ this.CONSUME(LBrace);
1601
+ this.AT_LEAST_ONE(() => this.SUBRULE(this.draggableOption));
1602
+ this.CONSUME(RBrace);
1603
+ this.MANY2(() => {
1604
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1605
+ });
1606
+ });
1607
+ this.draggableOption = this.RULE("draggableOption", () => {
1608
+ this.MANY(() => {
1609
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1610
+ });
1611
+ this.CONSUME(OPTION);
1612
+ this.CONSUME(LBracket);
1613
+ this.SUBRULE(this.parameterList);
1614
+ this.CONSUME(RBracket);
1615
+ this.MANY2(() => {
1616
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1617
+ });
1618
+ this.CONSUME(LBrace);
1619
+ this.MANY3(() => {
1620
+ this.OR3([
1621
+ { ALT: () => this.SUBRULE(this.inlineElement, { ARGS: [true] }) },
1622
+ { ALT: () => this.CONSUME3(Newline) }
1623
+ ]);
1624
+ });
1625
+ this.CONSUME(RBrace);
1626
+ this.MANY4(() => {
1627
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
1628
+ });
1629
+ });
1554
1630
  this.dragableButtons = this.RULE("dragableButtons", () => {
1555
1631
  this.CONSUME(DRAGABLE_BUTTONS_OPTIONS);
1556
1632
  this.CONSUME(LBracket);
@@ -2091,6 +2167,8 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2091
2167
  width: InlineInputWidthSchema,
2092
2168
  placeholder: z.string().max(32).optional(),
2093
2169
  disabled: z.boolean().optional(),
2170
+ /** Starting value the student can edit (shown read-only when disabled). */
2171
+ prefill: z.string().optional(),
2094
2172
  answer: z.union([z.string(), z.number()]).optional()
2095
2173
  }),
2096
2174
  z.object({
@@ -2101,9 +2179,19 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2101
2179
  width: InlineInputWidthSchema,
2102
2180
  placeholder: z.string().max(32).optional(),
2103
2181
  disabled: z.boolean().optional(),
2182
+ /** Starting value the student can edit (shown read-only when disabled). */
2183
+ prefill: z.string().optional(),
2104
2184
  answer: z.union([z.string(), z.number()]).optional()
2105
2185
  })
2106
2186
  ]);
2187
+ const DropInputSchema = z.object({
2188
+ type: z.literal(AST_TYPES.DROP_INPUT),
2189
+ name: z.string().min(1),
2190
+ answer: z.string().min(1),
2191
+ /** Box width in characters: the answer is an option name, so nothing else implies a size. */
2192
+ width: InlineInputWidthSchema,
2193
+ placeholder: z.string().max(32).optional()
2194
+ });
2107
2195
  const FractionInlineSchema = z.object({
2108
2196
  type: z.literal(AST_TYPES.FRACTION),
2109
2197
  name: z.string(),
@@ -2217,6 +2305,7 @@ const InlineNodeSchema = z.union([
2217
2305
  ColorTextInlineSchema,
2218
2306
  BoldTextInlineSchema,
2219
2307
  InlineInputWidgetSchema,
2308
+ DropInputSchema,
2220
2309
  FractionInlineSchema,
2221
2310
  ImageInlineSchema,
2222
2311
  VisualFractionInlineSchema,
@@ -2256,7 +2345,7 @@ function createBoldText(value, highlight) {
2256
2345
  highlight
2257
2346
  });
2258
2347
  }
2259
- function createStringInput(name, length, answer, placeholder, disabled, width) {
2348
+ function createStringInput(name, length, answer, placeholder, disabled, width, prefill) {
2260
2349
  return parseOrThrow(
2261
2350
  InlineInputWidgetSchema,
2262
2351
  {
@@ -2267,13 +2356,14 @@ function createStringInput(name, length, answer, placeholder, disabled, width) {
2267
2356
  width,
2268
2357
  placeholder,
2269
2358
  disabled,
2359
+ prefill,
2270
2360
  // A disabled input is never graded, so its answer is dropped.
2271
2361
  answer: disabled ? void 0 : answer
2272
2362
  },
2273
2363
  "STRING_INPUT"
2274
2364
  );
2275
2365
  }
2276
- function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2366
+ function createNumericInput(name, digits, answer, placeholder, disabled, width, prefill) {
2277
2367
  return parseOrThrow(
2278
2368
  InlineInputWidgetSchema,
2279
2369
  {
@@ -2284,12 +2374,26 @@ function createNumericInput(name, digits, answer, placeholder, disabled, width)
2284
2374
  width,
2285
2375
  placeholder,
2286
2376
  disabled,
2377
+ prefill,
2287
2378
  // A disabled input is never graded, so its answer is dropped.
2288
2379
  answer: disabled ? void 0 : answer
2289
2380
  },
2290
2381
  "NUMERIC_INPUT"
2291
2382
  );
2292
2383
  }
2384
+ function createDropInput(name, answer, placeholder, width) {
2385
+ return parseOrThrow(
2386
+ DropInputSchema,
2387
+ {
2388
+ type: AST_TYPES.DROP_INPUT,
2389
+ name,
2390
+ answer,
2391
+ placeholder,
2392
+ width
2393
+ },
2394
+ "DROP_INPUT"
2395
+ );
2396
+ }
2293
2397
  function createImage(name, src, alt, options = {}) {
2294
2398
  return ImageInlineSchema.parse({
2295
2399
  type: AST_TYPES.IMAGE,
@@ -2344,6 +2448,9 @@ function isSpace(value) {
2344
2448
  function isInlineInput(value) {
2345
2449
  return InlineInputWidgetSchema.safeParse(value).success;
2346
2450
  }
2451
+ function isDropInput(value) {
2452
+ return DropInputSchema.safeParse(value).success;
2453
+ }
2347
2454
  function isFraction(value) {
2348
2455
  return FractionInlineSchema.safeParse(value).success;
2349
2456
  }
@@ -2398,7 +2505,7 @@ function createNumberLinePosition(value, inlines) {
2398
2505
  if (!hasRealContent) {
2399
2506
  throw new Error(`POSITION[value=${value}]: content cannot be empty or whitespace only`);
2400
2507
  }
2401
- const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT);
2508
+ const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT || i.type === AST_TYPES.DROP_INPUT);
2402
2509
  if (inputs.length > 1) {
2403
2510
  throw new Error(
2404
2511
  `POSITION[value=${value}]: only one input widget is allowed, got ${inputs.length}`
@@ -3111,6 +3218,45 @@ const DragOptionsToolSchema = z.object({
3111
3218
  type: z.literal(AST_TYPES.DRAG_OPTIONS),
3112
3219
  options: z.array(z.string()).min(1)
3113
3220
  });
3221
+ function hasOptionContent(content, spaceType = AST_TYPES.SPACE) {
3222
+ return content.some((node) => node.type !== spaceType && !(typeof node.value === "string" && node.value.trim() === ""));
3223
+ }
3224
+ function isAnsweredFraction(node) {
3225
+ return node.type === AST_TYPES.FRACTION && node.answer !== void 0;
3226
+ }
3227
+ function isOptionContentNode(node) {
3228
+ const parsed = DisplayInlineNodeSchema.safeParse(node);
3229
+ return parsed.success && !isAnsweredFraction(parsed.data);
3230
+ }
3231
+ const DraggableOptionSchema = z.object({
3232
+ name: z.string().min(1),
3233
+ content: z.array(DisplayInlineNodeSchema).min(1).refine((content) => hasOptionContent(content), { message: "OPTION content cannot be empty or whitespace only" }).refine(
3234
+ (content) => !content.some(isAnsweredFraction),
3235
+ { message: "FRACTION inside OPTION must be display-only (no answer)" }
3236
+ )
3237
+ });
3238
+ function addDuplicateOptionNameIssues(options, ctx, pathPrefix = []) {
3239
+ const seen = /* @__PURE__ */ new Set();
3240
+ options.forEach((option, index) => {
3241
+ const key2 = normalize(option.name);
3242
+ if (seen.has(key2)) {
3243
+ ctx.addIssue({
3244
+ code: z.ZodIssueCode.custom,
3245
+ message: `Duplicate option name "${option.name}" (names are compared ignoring case and number format)`,
3246
+ path: [...pathPrefix, "options", index, "name"]
3247
+ });
3248
+ }
3249
+ seen.add(key2);
3250
+ });
3251
+ }
3252
+ const DraggableOptionsToolObjectSchema = z.object({
3253
+ type: z.literal(AST_TYPES.DRAGGABLE_OPTIONS),
3254
+ reusable: z.boolean().default(false),
3255
+ options: z.array(DraggableOptionSchema).min(1)
3256
+ });
3257
+ const DraggableOptionsToolSchema = DraggableOptionsToolObjectSchema.superRefine((tool, ctx) => {
3258
+ addDuplicateOptionNameIssues(tool.options, ctx);
3259
+ });
3114
3260
  const NumericKeypadToolSchema = z.object({
3115
3261
  type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
3116
3262
  includeOperators: z.boolean().optional(),
@@ -3131,6 +3277,7 @@ const SetSquareToolSchema = z.object({
3131
3277
  });
3132
3278
  const ToolNodeSchema = z.discriminatedUnion("type", [
3133
3279
  DragOptionsToolSchema,
3280
+ DraggableOptionsToolObjectSchema,
3134
3281
  NumericKeypadToolSchema,
3135
3282
  RulerToolSchema,
3136
3283
  ProtractorToolSchema,
@@ -3142,6 +3289,19 @@ function createDragOptions(options) {
3142
3289
  options
3143
3290
  });
3144
3291
  }
3292
+ function createDraggableOptions(options, reusable = false) {
3293
+ const result = DraggableOptionsToolSchema.safeParse({
3294
+ type: AST_TYPES.DRAGGABLE_OPTIONS,
3295
+ reusable,
3296
+ options
3297
+ });
3298
+ if (result.success) return result.data;
3299
+ const issue = result.error.issues[0];
3300
+ throw new Error(`DRAGGABLE_OPTIONS: ${issue ? issue.message : "validation failed"}`);
3301
+ }
3302
+ function createDraggableOption(name, content) {
3303
+ return DraggableOptionSchema.parse({ name, content });
3304
+ }
3145
3305
  function createNumericKeypad(includeOperators, includeDecimal, style) {
3146
3306
  return NumericKeypadToolSchema.parse({
3147
3307
  type: AST_TYPES.NUMERIC_KEYPAD,
@@ -3162,6 +3322,9 @@ function createSetSquareTool(size) {
3162
3322
  function isDragOptions(value) {
3163
3323
  return DragOptionsToolSchema.safeParse(value).success;
3164
3324
  }
3325
+ function isDraggableOptions(value) {
3326
+ return DraggableOptionsToolSchema.safeParse(value).success;
3327
+ }
3165
3328
  function isNumericKeypad(value) {
3166
3329
  return NumericKeypadToolSchema.safeParse(value).success;
3167
3330
  }
@@ -3720,9 +3883,13 @@ const ExerciseDocumentSchema = z.object({
3720
3883
  seen.add(name);
3721
3884
  }
3722
3885
  };
3886
+ const dropInputs = [];
3723
3887
  const visitInlines = (inlines, basePath) => {
3724
3888
  inlines.forEach((inline, i) => {
3725
- if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE || inline.type === AST_TYPES.VISUAL_FRACTION) {
3889
+ if (inline.type === AST_TYPES.DROP_INPUT) {
3890
+ dropInputs.push({ name: inline.name, answer: inline.answer, path: [...basePath, i] });
3891
+ }
3892
+ if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.DROP_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE || inline.type === AST_TYPES.VISUAL_FRACTION) {
3726
3893
  record(inline.name, [...basePath, i, "name"]);
3727
3894
  } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
3728
3895
  record(inline.name, [...basePath, i, "name"]);
@@ -3739,9 +3906,13 @@ const ExerciseDocumentSchema = z.object({
3739
3906
  });
3740
3907
  });
3741
3908
  } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
3742
- block.positions.forEach((pos, pi) => {
3909
+ const numberLine = block;
3910
+ numberLine.positions.forEach((pos, pi) => {
3743
3911
  visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
3744
3912
  });
3913
+ numberLine.arrows?.forEach((arrow, ai) => {
3914
+ visitInlines(arrow.inlines, ["lines", bi, "arrows", ai, "inlines"]);
3915
+ });
3745
3916
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
3746
3917
  const m = block;
3747
3918
  record(m.name, ["lines", bi, "name"]);
@@ -3784,15 +3955,78 @@ const ExerciseDocumentSchema = z.object({
3784
3955
  record(block.name, ["lines", bi, "name"]);
3785
3956
  }
3786
3957
  });
3958
+ const richBanks = (doc.tools ?? []).map((tool, toolIndex) => ({ tool, toolIndex })).filter((entry) => entry.tool.type === AST_TYPES.DRAGGABLE_OPTIONS);
3959
+ richBanks.forEach(({ tool: bank, toolIndex }) => {
3960
+ if (bank.type !== AST_TYPES.DRAGGABLE_OPTIONS) return;
3961
+ bank.options.forEach((option, optionIndex) => {
3962
+ visitInlines(option.content, ["tools", toolIndex, "options", optionIndex, "content"]);
3963
+ });
3964
+ addDuplicateOptionNameIssues(bank.options, ctx, ["tools", toolIndex]);
3965
+ if (dropInputs.length === 0) {
3966
+ ctx.addIssue({
3967
+ code: z.ZodIssueCode.custom,
3968
+ message: "DRAGGABLE_OPTIONS needs at least one DROP_INPUT: its cards can only be dropped there",
3969
+ path: ["tools", toolIndex]
3970
+ });
3971
+ }
3972
+ });
3973
+ if (richBanks.length > 1) {
3974
+ ctx.addIssue({
3975
+ code: z.ZodIssueCode.custom,
3976
+ message: "Only one DRAGGABLE_OPTIONS tool is allowed",
3977
+ path: ["tools"]
3978
+ });
3979
+ }
3980
+ const optionNames = new Set(
3981
+ richBanks.flatMap(({ tool }) => (tool.type === AST_TYPES.DRAGGABLE_OPTIONS ? tool.options : []).map((o) => o.name))
3982
+ );
3983
+ dropInputs.forEach(({ name, answer, path }) => {
3984
+ if (richBanks.length === 0) {
3985
+ ctx.addIssue({
3986
+ code: z.ZodIssueCode.custom,
3987
+ message: `DROP_INPUT "${name}" needs a DRAGGABLE_OPTIONS bank to draw from`,
3988
+ path
3989
+ });
3990
+ } else if (!optionNames.has(answer)) {
3991
+ ctx.addIssue({
3992
+ code: z.ZodIssueCode.custom,
3993
+ message: `DROP_INPUT "${name}": answer "${answer}" does not match any OPTION name`,
3994
+ path: [...path, "answer"]
3995
+ });
3996
+ }
3997
+ });
3998
+ const isSingleUseBank = richBanks.some(({ tool }) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS && !tool.reusable);
3999
+ if (isSingleUseBank) {
4000
+ const firstInputByAnswer = /* @__PURE__ */ new Map();
4001
+ dropInputs.forEach(({ name, answer, path }) => {
4002
+ if (!optionNames.has(answer)) return;
4003
+ const firstInput = firstInputByAnswer.get(answer);
4004
+ if (firstInput === void 0) {
4005
+ firstInputByAnswer.set(answer, name);
4006
+ return;
4007
+ }
4008
+ ctx.addIssue({
4009
+ code: z.ZodIssueCode.custom,
4010
+ message: `DROP_INPUT "${name}": answer "${answer}" is also the answer of "${firstInput}", but its card can only be placed once. Add a second option or use DRAGGABLE_OPTIONS[reusable=true]`,
4011
+ path: [...path, "answer"]
4012
+ });
4013
+ });
4014
+ }
3787
4015
  });
3788
4016
  function createExerciseDocument(question, lines, validation, tools) {
3789
- return ExerciseDocumentSchema.parse({
4017
+ const result = ExerciseDocumentSchema.safeParse({
3790
4018
  type: AST_TYPES.EXERCISE_DOCUMENT,
3791
4019
  question,
3792
4020
  lines,
3793
4021
  tools,
3794
4022
  validation
3795
4023
  });
4024
+ if (result.success) return result.data;
4025
+ const { issues } = result.error;
4026
+ if (issues.every((issue) => issue.code === z.ZodIssueCode.custom)) {
4027
+ throw new Error(issues.map((issue) => issue.message).join("\n"));
4028
+ }
4029
+ throw result.error;
3796
4030
  }
3797
4031
  function isExerciseDocument(value) {
3798
4032
  return ExerciseDocumentSchema.safeParse(value).success;
@@ -4192,6 +4426,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4192
4426
  if (ctx.inlineInput) {
4193
4427
  return this.visit(ctx.inlineInput);
4194
4428
  }
4429
+ if (ctx.dropInput) {
4430
+ return this.visit(ctx.dropInput);
4431
+ }
4195
4432
  if (ctx.inlineDragOption) {
4196
4433
  return this.visit(ctx.inlineDragOption);
4197
4434
  }
@@ -4323,6 +4560,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4323
4560
  const name = params.name;
4324
4561
  const answer = params.answer;
4325
4562
  const placeholder = params.placeholder;
4563
+ const prefill = params.prefill === void 0 ? void 0 : String(params.prefill);
4326
4564
  let disabled;
4327
4565
  if (params.disabled === true || params.disabled === "true") disabled = true;
4328
4566
  else if (params.disabled === false || params.disabled === "false") disabled = false;
@@ -4340,10 +4578,22 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4340
4578
  }
4341
4579
  if (ctx.stringInput) {
4342
4580
  const length = params.length;
4343
- return createStringInput(name, length, answer, placeholder, disabled, width);
4581
+ return createStringInput(name, length, answer, placeholder, disabled, width, prefill);
4344
4582
  }
4345
4583
  const digits = params.digits;
4346
- return createNumericInput(name, digits, answer, placeholder, disabled, width);
4584
+ return createNumericInput(name, digits, answer, placeholder, disabled, width, prefill);
4585
+ }
4586
+ dropInput(ctx) {
4587
+ const params = this.visit(ctx.parameterList);
4588
+ if (typeof params.name === "string" && params.name.startsWith("_")) {
4589
+ throw new Error(`DROP_INPUT: name "${params.name}" is reserved (names cannot start with "_")`);
4590
+ }
4591
+ return createDropInput(
4592
+ params.name,
4593
+ params.answer,
4594
+ params.placeholder,
4595
+ params.width
4596
+ );
4347
4597
  }
4348
4598
  inlineDragOption(ctx) {
4349
4599
  const valueToken = ctx.StringLiteral[0];
@@ -4483,6 +4733,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4483
4733
  // Tools
4484
4734
  // ═══════════════════════════════════════════════════════════════════════════
4485
4735
  tool(ctx) {
4736
+ if (ctx.draggableOptions) {
4737
+ return this.visit(ctx.draggableOptions);
4738
+ }
4486
4739
  if (ctx.dragableButtons) {
4487
4740
  return this.visit(ctx.dragableButtons);
4488
4741
  }
@@ -4500,6 +4753,33 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4500
4753
  }
4501
4754
  throw new Error("Unknown tool");
4502
4755
  }
4756
+ draggableOptions(ctx) {
4757
+ const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
4758
+ if (params.reusable !== void 0 && !["true", "false"].includes(String(params.reusable))) {
4759
+ throw new Error("DRAGGABLE_OPTIONS: reusable must be true or false");
4760
+ }
4761
+ const reusable = params.reusable === true || params.reusable === "true";
4762
+ const options = ctx.draggableOption.map((option) => this.visit(option));
4763
+ return createDraggableOptions(options, reusable);
4764
+ }
4765
+ draggableOption(ctx) {
4766
+ const params = this.visit(ctx.parameterList);
4767
+ const rawContent = ctx.inlineElement ? ctx.inlineElement.map((element) => this.visit(element)) : [];
4768
+ const name = params.name;
4769
+ if (typeof name !== "string" || name === "") {
4770
+ throw new Error("DRAGGABLE_OPTIONS: OPTION requires a non-empty string name");
4771
+ }
4772
+ if (!hasOptionContent(rawContent)) {
4773
+ throw new Error(`DRAGGABLE_OPTIONS: OPTION "${name}" content cannot be empty or whitespace only`);
4774
+ }
4775
+ const invalid = rawContent.find((node) => !isOptionContentNode(node));
4776
+ if (invalid) {
4777
+ throw new Error(
4778
+ `DRAGGABLE_OPTIONS: OPTION "${name}" cannot contain ${invalid.type} here. Content must be display-only: text, styled text, images, and fractions or visual fractions without an answer`
4779
+ );
4780
+ }
4781
+ return createDraggableOption(name, rawContent);
4782
+ }
4503
4783
  dragableButtons(ctx) {
4504
4784
  const strings = this.visit(ctx.stringList);
4505
4785
  return createDragOptions(strings);
@@ -5087,7 +5367,9 @@ const StringInputJSONSchema = z.object({
5087
5367
  width: InlineInputWidthSchema,
5088
5368
  placeholder: z.string().max(32).optional(),
5089
5369
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5090
- disabled: z.boolean().optional()
5370
+ disabled: z.boolean().optional(),
5371
+ /** Starting value the student can edit (shown read-only when disabled). */
5372
+ prefill: z.string().optional()
5091
5373
  });
5092
5374
  const NumericInputJSONSchema = z.object({
5093
5375
  type: z.literal(JSON_TYPES.INLINE_INPUT),
@@ -5097,16 +5379,38 @@ const NumericInputJSONSchema = z.object({
5097
5379
  width: InlineInputWidthSchema,
5098
5380
  placeholder: z.string().max(32).optional(),
5099
5381
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5100
- disabled: z.boolean().optional()
5382
+ disabled: z.boolean().optional(),
5383
+ /** Starting value the student can edit (shown read-only when disabled). */
5384
+ prefill: z.string().optional()
5101
5385
  });
5102
5386
  const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
5103
5387
  StringInputJSONSchema,
5104
5388
  NumericInputJSONSchema
5105
5389
  ]);
5390
+ const DropInputJSONSchema = z.object({
5391
+ type: z.literal(JSON_TYPES.DROP_INPUT),
5392
+ /** Box width in characters; absent means the default input size. */
5393
+ width: InlineInputWidthSchema,
5394
+ placeholder: z.string().max(32).optional()
5395
+ });
5106
5396
  const DragOptionsJSONSchema = z.object({
5107
5397
  type: z.literal(JSON_TYPES.DRAG_OPTIONS),
5108
5398
  options: z.array(z.string())
5109
5399
  });
5400
+ const DraggableOptionJSONSchema = z.object({
5401
+ name: z.string().min(1),
5402
+ content: z.array(LayoutItemSchema).min(1).refine(
5403
+ (content) => hasOptionContent(content, JSON_TYPES.SPACE),
5404
+ { message: "Option content cannot be empty or whitespace only" }
5405
+ )
5406
+ });
5407
+ const DraggableOptionsJSONSchema = z.object({
5408
+ type: z.literal(JSON_TYPES.DRAGGABLE_OPTIONS),
5409
+ reusable: z.boolean(),
5410
+ options: z.array(DraggableOptionJSONSchema).min(1)
5411
+ }).superRefine((widget, ctx) => {
5412
+ addDuplicateOptionNameIssues(widget.options, ctx);
5413
+ });
5110
5414
  const InlineDragOptionJSONSchema = z.object({
5111
5415
  type: z.literal(JSON_TYPES.INLINE_DRAG_OPTION),
5112
5416
  value: z.string().min(1)
@@ -5216,7 +5520,9 @@ const GeometryJSONSchema = z.object({
5216
5520
  });
5217
5521
  const WidgetJSONSchema = z.union([
5218
5522
  InlineInputJSONSchema,
5523
+ DropInputJSONSchema,
5219
5524
  DragOptionsJSONSchema,
5525
+ DraggableOptionsJSONSchema,
5220
5526
  InlineDragOptionJSONSchema,
5221
5527
  RadioOptionsJSONSchema,
5222
5528
  NumericKeypadJSONSchema,
@@ -5333,9 +5639,15 @@ function isInlineInputJSON(widget) {
5333
5639
  function isDisabledInputJSON(widget) {
5334
5640
  return isInlineInputJSON(widget) && widget.disabled === true;
5335
5641
  }
5642
+ function isDropInputJSON(widget) {
5643
+ return widget.type === JSON_TYPES.DROP_INPUT;
5644
+ }
5336
5645
  function isDragOptionsJSON(widget) {
5337
5646
  return widget.type === JSON_TYPES.DRAG_OPTIONS;
5338
5647
  }
5648
+ function isDraggableOptionsJSON(widget) {
5649
+ return widget.type === JSON_TYPES.DRAGGABLE_OPTIONS;
5650
+ }
5339
5651
  function isInlineDragOptionJSON(widget) {
5340
5652
  return widget.type === JSON_TYPES.INLINE_DRAG_OPTION;
5341
5653
  }
@@ -10869,7 +11181,16 @@ function toJSON(ast) {
10869
11181
  const answers = {};
10870
11182
  let gridSymmetryCounter = 0;
10871
11183
  let geometryCounter = 0;
10872
- const allInlines = collectAllInlines(ast.lines);
11184
+ const richOptionsTool = ast.tools?.find(
11185
+ (tool) => tool.type === AST_TYPES.DRAGGABLE_OPTIONS
11186
+ );
11187
+ const allInlines = [
11188
+ ...collectAllInlines(ast.lines),
11189
+ ...richOptionsTool?.options.flatMap((option) => option.content) ?? []
11190
+ ];
11191
+ const matcherCardInlines = new Set(collectAllInlines(ast.lines.filter(
11192
+ (block) => block.type === AST_TYPES.MATCHER_BLOCK || block.type === AST_TYPES.PAIR_MATCHER_BLOCK
11193
+ )));
10873
11194
  const inlineDragIds = /* @__PURE__ */ new Map();
10874
11195
  {
10875
11196
  let counter = 0;
@@ -10992,8 +11313,10 @@ function toJSON(ast) {
10992
11313
  layout.push(...lineItems);
10993
11314
  }
10994
11315
  }
11316
+ const hasDragBank = ast.tools?.some((tool) => tool.type === AST_TYPES.DRAG_OPTIONS) ?? false;
10995
11317
  for (const inline of allInlines) {
10996
11318
  if (inline.type === AST_TYPES.INLINE_INPUT) {
11319
+ const disabled = inline.disabled || hasDragBank && !!inline.prefill;
10997
11320
  if (inline.inputType === INPUT_TYPES.STRING) {
10998
11321
  widgets[inline.name] = {
10999
11322
  type: JSON_TYPES.INLINE_INPUT,
@@ -11001,7 +11324,8 @@ function toJSON(ast) {
11001
11324
  length: inline.length,
11002
11325
  ...inline.width !== void 0 && { width: inline.width },
11003
11326
  placeholder: inline.placeholder,
11004
- ...inline.disabled && { disabled: true }
11327
+ ...disabled && { disabled: true },
11328
+ ...inline.prefill !== void 0 && { prefill: inline.prefill }
11005
11329
  };
11006
11330
  } else {
11007
11331
  widgets[inline.name] = {
@@ -11010,12 +11334,20 @@ function toJSON(ast) {
11010
11334
  digits: inline.digits,
11011
11335
  ...inline.width !== void 0 && { width: inline.width },
11012
11336
  placeholder: inline.placeholder,
11013
- ...inline.disabled && { disabled: true }
11337
+ ...disabled && { disabled: true },
11338
+ ...inline.prefill !== void 0 && { prefill: inline.prefill }
11014
11339
  };
11015
11340
  }
11016
- if (inline.answer !== void 0) {
11341
+ if (inline.answer !== void 0 && !disabled) {
11017
11342
  answers[inline.name] = inline.answer;
11018
11343
  }
11344
+ } else if (inline.type === AST_TYPES.DROP_INPUT) {
11345
+ widgets[inline.name] = {
11346
+ type: JSON_TYPES.DROP_INPUT,
11347
+ ...inline.width !== void 0 && { width: inline.width },
11348
+ placeholder: inline.placeholder
11349
+ };
11350
+ answers[inline.name] = inline.answer;
11019
11351
  } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
11020
11352
  answers[inline.name] = inline.answer;
11021
11353
  }
@@ -11027,7 +11359,7 @@ function toJSON(ast) {
11027
11359
  simplified: inline.simplified ?? true,
11028
11360
  placeholder: inline.placeholder
11029
11361
  };
11030
- if (inline.answer !== void 0) {
11362
+ if (inline.answer !== void 0 && !matcherCardInlines.has(inline)) {
11031
11363
  answers[inline.name] = inline.answer;
11032
11364
  }
11033
11365
  }
@@ -11103,6 +11435,15 @@ function toJSON(ast) {
11103
11435
  // Randomise chip order per compile so the answer position isn't fixed.
11104
11436
  options: lodashExports.shuffle(tool.options)
11105
11437
  };
11438
+ } else if (tool.type === AST_TYPES.DRAGGABLE_OPTIONS) {
11439
+ widgets[DRAGGABLE_OPTIONS_WIDGET_ID] = {
11440
+ type: JSON_TYPES.DRAGGABLE_OPTIONS,
11441
+ reusable: tool.reusable,
11442
+ options: tool.options.map((option) => ({
11443
+ name: option.name,
11444
+ content: option.content.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
11445
+ }))
11446
+ };
11106
11447
  } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
11107
11448
  widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
11108
11449
  type: JSON_TYPES.NUMERIC_KEYPAD,
@@ -11568,6 +11909,12 @@ function transformInline(inline, idGenerator, inlineDragIds) {
11568
11909
  type: JSON_TYPES.WIDGET_REF,
11569
11910
  widgetId: inline.name
11570
11911
  };
11912
+ case AST_TYPES.DROP_INPUT:
11913
+ return {
11914
+ id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
11915
+ type: JSON_TYPES.WIDGET_REF,
11916
+ widgetId: inline.name
11917
+ };
11571
11918
  case AST_TYPES.FRACTION:
11572
11919
  return {
11573
11920
  id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
@@ -11624,6 +11971,7 @@ function transformValidation(validation, answers, widgets) {
11624
11971
  };
11625
11972
  } else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
11626
11973
  assertNoDisabledInExpression(validation.expression, widgets);
11974
+ assertNoDropInputs(Object.keys(widgets), widgets);
11627
11975
  const validVariableNames = getEnabledWidgetNames(widgets);
11628
11976
  const variables = extractVariables$1(validation.expression, validVariableNames);
11629
11977
  return {
@@ -11633,6 +11981,7 @@ function transformValidation(validation, answers, widgets) {
11633
11981
  };
11634
11982
  } else if (validation.type === AST_TYPES.MIXED_VALIDATION) {
11635
11983
  assertNoDisabledInExpression(validation.expression, widgets);
11984
+ assertNoDropInputs(extractVariables$1(validation.expression, Object.keys(widgets)), widgets);
11636
11985
  const validVariableNames = getEnabledWidgetNames(widgets);
11637
11986
  const variables = extractVariables$1(validation.expression, validVariableNames);
11638
11987
  const inExpression = new Set(variables);
@@ -11671,6 +12020,14 @@ function assertNoDisabledInExpression(expression, widgets) {
11671
12020
  );
11672
12021
  }
11673
12022
  }
12023
+ function assertNoDropInputs(names, widgets) {
12024
+ const dropInputs = names.filter((name) => widgets[name]?.type === JSON_TYPES.DROP_INPUT);
12025
+ if (dropInputs.length > 0) {
12026
+ throw new Error(
12027
+ `WIDGET_ANSWER: DROP_INPUT ${dropInputs.map((n) => `"${n}"`).join(", ")} cannot be used in expression validation — its value is an option name, not a number. Use type="simple"`
12028
+ );
12029
+ }
12030
+ }
11674
12031
  function getEnabledWidgetNames(widgets) {
11675
12032
  return Object.entries(widgets).filter(([, widget]) => !isDisabledInputJSON(widget)).map(([name]) => name);
11676
12033
  }
@@ -12239,7 +12596,20 @@ function revealGeometryWidget(json) {
12239
12596
  if (!revealAnswer(store, loaded.data).success) return null;
12240
12597
  return serializeGeometryState(store.getState().geometry);
12241
12598
  }
12599
+ function withDisabledPrefilledInputs(exercise) {
12600
+ if (exercise.widgets[DRAG_OPTIONS_WIDGET_ID]?.type !== JSON_TYPES.DRAG_OPTIONS) return exercise;
12601
+ const widgets = { ...exercise.widgets };
12602
+ let changed = false;
12603
+ for (const [id, widget] of Object.entries(widgets)) {
12604
+ if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill && !widget.disabled) {
12605
+ widgets[id] = { ...widget, disabled: true };
12606
+ changed = true;
12607
+ }
12608
+ }
12609
+ return changed ? { ...exercise, widgets } : exercise;
12610
+ }
12242
12611
  function withoutDisabledWidgets(exercise) {
12612
+ exercise = withDisabledPrefilledInputs(exercise);
12243
12613
  const validation = exercise.validation;
12244
12614
  if (!validation) return exercise;
12245
12615
  const disabled = new Set(
@@ -12262,6 +12632,7 @@ function withoutDisabledWidgets(exercise) {
12262
12632
  };
12263
12633
  }
12264
12634
  function findDisabledInExpression(exercise) {
12635
+ exercise = withDisabledPrefilledInputs(exercise);
12265
12636
  const expression = exercise.validation?.expression;
12266
12637
  if (!expression) return [];
12267
12638
  return extractVariables(expression, Object.keys(exercise.widgets)).filter((name) => exercise.widgets[name] && isDisabledInputJSON(exercise.widgets[name]));
@@ -12403,11 +12774,13 @@ function getMissingInputs(exercise, userInputs) {
12403
12774
  }
12404
12775
  function validateInputFormats(exercise, userInputs) {
12405
12776
  const errors = {};
12406
- for (const [widgetId, widget] of Object.entries(exercise.widgets)) {
12777
+ const { widgets } = withDisabledPrefilledInputs(exercise);
12778
+ for (const [widgetId, widget] of Object.entries(widgets)) {
12407
12779
  const value = userInputs[widgetId];
12408
12780
  if (value === void 0 || value === "") {
12409
12781
  continue;
12410
12782
  }
12783
+ if (isDisabledInputJSON(widget)) continue;
12411
12784
  if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
12412
12785
  const numValue = typeof value === "number" ? value : parseFloat(String(value));
12413
12786
  if (!Number.isFinite(numValue)) {
@@ -12425,6 +12798,8 @@ function validateInputFormats(exercise, userInputs) {
12425
12798
  const DragItemTypeSchema = z.enum([
12426
12799
  "drag-option",
12427
12800
  // Draggable answer options
12801
+ "rich-drag-option",
12802
+ // Rich option from DRAGGABLE_OPTIONS or DROP_INPUT
12428
12803
  "placed-chip",
12429
12804
  // Chip already placed on a number-line axis (re-drag)
12430
12805
  "ruler-handle",
@@ -12438,6 +12813,16 @@ const DragOptionDataSchema = z.object({
12438
12813
  value: z.string(),
12439
12814
  label: z.string()
12440
12815
  });
12816
+ const RichDragOptionDataSchema = z.object({
12817
+ optionName: z.string().min(1),
12818
+ origin: z.discriminatedUnion("kind", [
12819
+ z.object({ kind: z.literal("bank") }),
12820
+ z.object({
12821
+ kind: z.literal("drop-input"),
12822
+ inputName: z.string().min(1)
12823
+ })
12824
+ ])
12825
+ });
12441
12826
  const DragItemSchema = (dataSchema) => z.object({
12442
12827
  id: z.union([z.string(), z.number()]),
12443
12828
  // UniqueIdentifier from dnd-kit
@@ -12445,6 +12830,7 @@ const DragItemSchema = (dataSchema) => z.object({
12445
12830
  data: dataSchema
12446
12831
  });
12447
12832
  DragItemSchema(DragOptionDataSchema);
12833
+ DragItemSchema(RichDragOptionDataSchema);
12448
12834
  z.object({
12449
12835
  targetId: z.string(),
12450
12836
  item: z.discriminatedUnion("type", [
@@ -16225,12 +16611,16 @@ function useExerciseDnd() {
16225
16611
  }
16226
16612
  return ctx;
16227
16613
  }
16614
+ function useExerciseDndOptional() {
16615
+ return useContext(ExerciseDndContext);
16616
+ }
16228
16617
  const NO_EXERCISE = Symbol("no-exercise");
16229
16618
  const ExerciseDndProvider = ({
16230
16619
  children,
16231
16620
  droppedSources,
16232
16621
  onDragEnd,
16233
- exerciseKey
16622
+ exerciseKey,
16623
+ renderOverlay
16234
16624
  }) => {
16235
16625
  const [activeItem, setActiveItem] = useState(null);
16236
16626
  const [draggedExerciseKey, setDraggedExerciseKey] = useState(NO_EXERCISE);
@@ -16266,7 +16656,9 @@ const ExerciseDndProvider = ({
16266
16656
  const raw = active.data.current;
16267
16657
  const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
16268
16658
  setActiveItem(item);
16269
- if (item.type === "drag-option") setDraggedExerciseKey(exerciseKey);
16659
+ if (item.type === "drag-option" || item.type === "rich-drag-option") {
16660
+ setDraggedExerciseKey(exerciseKey);
16661
+ }
16270
16662
  playSound("drag-start");
16271
16663
  };
16272
16664
  const handleDragEnd = (event) => {
@@ -16309,14 +16701,16 @@ const ExerciseDndProvider = ({
16309
16701
  onDragCancel: handleDragCancel,
16310
16702
  children: [
16311
16703
  children,
16312
- /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: activeItem }) })
16704
+ /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: activeItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: activeItem, renderOverlay }) })
16313
16705
  ]
16314
16706
  }
16315
16707
  )
16316
16708
  }
16317
16709
  );
16318
16710
  };
16319
- const DragOverlayContent = ({ item }) => {
16711
+ const DragOverlayContent = ({ item, renderOverlay }) => {
16712
+ const customOverlay = renderOverlay?.(item);
16713
+ if (customOverlay != null) return /* @__PURE__ */ jsx(Fragment, { children: customOverlay });
16320
16714
  switch (item.type) {
16321
16715
  case "drag-option": {
16322
16716
  const data = item.data;
@@ -16335,22 +16729,22 @@ const Draggable = ({
16335
16729
  data,
16336
16730
  disabled = false,
16337
16731
  children,
16338
- className = ""
16732
+ className = "",
16733
+ behavior = "move"
16339
16734
  }) => {
16340
16735
  const dragItem = { id, type, data };
16341
16736
  const {
16342
16737
  attributes,
16343
16738
  listeners,
16344
16739
  setNodeRef,
16345
- transform,
16346
16740
  isDragging
16347
16741
  } = useDraggable({
16348
16742
  id,
16349
16743
  data: dragItem,
16350
16744
  disabled
16351
16745
  });
16746
+ const isCopy = behavior === "copy";
16352
16747
  const style = {
16353
- transform: CSS$1.Translate.toString(transform),
16354
16748
  cursor: disabled ? "default" : "grab",
16355
16749
  touchAction: "none"
16356
16750
  };
@@ -16359,7 +16753,7 @@ const Draggable = ({
16359
16753
  {
16360
16754
  ref: setNodeRef,
16361
16755
  style,
16362
- className: `draggable ${isDragging ? "dragging" : ""} ${className}`,
16756
+ className: `draggable ${isDragging && !isCopy ? "dragging" : ""} ${className}`,
16363
16757
  ...listeners,
16364
16758
  ...attributes,
16365
16759
  children
@@ -17101,6 +17495,16 @@ const SlotSizer = ({
17101
17495
  style: { ...style, visibility: "hidden" }
17102
17496
  }
17103
17497
  );
17498
+ const FixedInputChip = ({ value }) => /* @__PURE__ */ jsx(
17499
+ "button",
17500
+ {
17501
+ type: "button",
17502
+ className: "inline-input-chip",
17503
+ disabled: true,
17504
+ style: OVERLAY_STYLE,
17505
+ children: value
17506
+ }
17507
+ );
17104
17508
  const PlacedInputChip = ({
17105
17509
  widgetId,
17106
17510
  value,
@@ -17141,6 +17545,9 @@ const PlacedInputChip = ({
17141
17545
  }
17142
17546
  );
17143
17547
  };
17548
+ function getInputBoxWidth(chars) {
17549
+ return `${chars * 0.8 + 1}em`;
17550
+ }
17144
17551
  function getMaxInputLength(exercise, inputKey) {
17145
17552
  const widget = exercise.widgets[inputKey];
17146
17553
  if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
@@ -17170,6 +17577,8 @@ function findNextInput(container, currentKey) {
17170
17577
  }
17171
17578
  const KeypadContext = createContext(false);
17172
17579
  const useHasKeypad = () => useContext(KeypadContext);
17580
+ const DragBankContext = createContext(false);
17581
+ const useHasDragBank = () => useContext(DragBankContext);
17173
17582
  const InlineInput = ({
17174
17583
  widgetId,
17175
17584
  value,
@@ -17187,6 +17596,7 @@ const InlineInput = ({
17187
17596
  const playSound = useSound();
17188
17597
  const locked = useExerciseLocked();
17189
17598
  const hasKeypad = useHasKeypad();
17599
+ const hasDragBank = useHasDragBank();
17190
17600
  const handleChange = useCallback((newValue, fromDrop = false, sourceOptionId) => {
17191
17601
  onChange(widgetId, newValue, fromDrop, sourceOptionId);
17192
17602
  }, [onChange, widgetId]);
@@ -17194,9 +17604,10 @@ const InlineInput = ({
17194
17604
  return applyDropToInput(widgetId, item, onChange);
17195
17605
  }, [onChange, widgetId]);
17196
17606
  const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
17197
- const dynamicWidth = `${(width ?? expectedLength ?? 5) * 0.8 + 1}em`;
17607
+ const dynamicWidth = getInputBoxWidth(width ?? expectedLength ?? 5);
17198
17608
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
17199
17609
  const disabledClass = disabled ? " inline-input--disabled" : "";
17610
+ const isFixedChip = hasDragBank && disabled && value !== "";
17200
17611
  return /* @__PURE__ */ jsx(
17201
17612
  Droppable,
17202
17613
  {
@@ -17205,7 +17616,10 @@ const InlineInput = ({
17205
17616
  onDrop: handleDrop,
17206
17617
  disabled: locked || disabled,
17207
17618
  className: "inline-widget inline-input-droppable",
17208
- children: isDropped && value !== "" ? (
17619
+ children: isFixedChip ? /* @__PURE__ */ jsxs(Fragment, { children: [
17620
+ /* @__PURE__ */ jsx(SlotSizer, { className: "inline-input", style: { width: dynamicWidth }, placeholder }),
17621
+ /* @__PURE__ */ jsx(FixedInputChip, { value })
17622
+ ] }) : isDropped && value !== "" ? (
17209
17623
  // Show as chip only when value came from drag-drop
17210
17624
  /* @__PURE__ */ jsxs(Fragment, { children: [
17211
17625
  /* @__PURE__ */ jsx(
@@ -17262,11 +17676,13 @@ const DragOptionChip = ({
17262
17676
  onOptionClick,
17263
17677
  className = ""
17264
17678
  }) => {
17265
- const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17679
+ const { activeItem, usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17266
17680
  const locked = useExerciseLocked();
17267
17681
  const isUsed = usedOptionIds.has(optionId);
17682
+ const isPickedUp = activeItem?.type === "drag-option" && activeItem.id === optionId;
17268
17683
  const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
17269
- return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
17684
+ const emptySlot = /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true", children: /* @__PURE__ */ jsx("button", { className: "drag-option", disabled: true, children: value }) });
17685
+ return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? emptySlot : /* @__PURE__ */ jsx(
17270
17686
  Draggable,
17271
17687
  {
17272
17688
  id: optionId,
@@ -17274,7 +17690,7 @@ const DragOptionChip = ({
17274
17690
  data: { value, label: value },
17275
17691
  disabled: locked,
17276
17692
  className: "drag-option-wrapper",
17277
- children: /* @__PURE__ */ jsx(
17693
+ children: isPickedUp ? emptySlot : /* @__PURE__ */ jsx(
17278
17694
  "button",
17279
17695
  {
17280
17696
  className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
@@ -17392,100 +17808,476 @@ const InlineDragOption = ({
17392
17808
  }
17393
17809
  ) });
17394
17810
  };
17395
- const NumericKeypad = ({
17396
- value,
17397
- includeOperators = false,
17398
- includeDecimal = false,
17399
- onChange
17400
- }) => {
17401
- const playSound = useSound();
17402
- const locked = useExerciseLocked();
17403
- const handleDigit = (digit) => {
17404
- if (locked) return;
17405
- playSound("keypad-press");
17406
- onChange(value + digit);
17407
- };
17408
- const handleBackspace = () => {
17409
- if (locked) return;
17410
- playSound("keypad-press");
17411
- onChange(value.slice(0, -1));
17412
- };
17413
- return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
17414
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
17415
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
17416
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
17417
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
17418
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
17419
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
17420
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
17421
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
17422
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
17423
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
17424
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
17425
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
17426
- includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
17427
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
17428
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
17429
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
17430
- ] });
17431
- };
17432
- const FractionInput = ({
17433
- widgetId,
17434
- numerator,
17435
- denominator,
17436
- values,
17437
- droppedInputs,
17438
- validationStatus = null,
17439
- numValidationStatus = null,
17440
- denValidationStatus = null,
17441
- onChange,
17442
- onFocus,
17443
- placeholder
17444
- }) => {
17445
- const playSound = useSound();
17446
- const locked = useExerciseLocked();
17447
- const hasKeypad = useHasKeypad();
17448
- const numKey = getFractionKey(widgetId, "num");
17449
- const denKey = getFractionKey(widgetId, "den");
17450
- const numValue = values[numKey] ?? "";
17451
- const denValue = values[denKey] ?? "";
17452
- const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
17453
- const handleDrop = useCallback((subKey) => (item) => {
17454
- return applyDropToInput(subKey, item, onChange);
17455
- }, [onChange]);
17456
- const renderSlot = (subKey, fixedValue, currentValue, label, slotStatus) => {
17457
- if (fixedValue !== null) {
17458
- return /* @__PURE__ */ jsx("span", { className: "fraction-fixed", children: fixedValue });
17459
- }
17460
- const isDroppedHere = droppedInputs.has(subKey);
17461
- const slotClass = slotStatus ? ` widget--${slotStatus}` : statusClass;
17462
- return /* @__PURE__ */ jsx(
17463
- Droppable,
17464
- {
17465
- id: subKey,
17466
- accepts: ["drag-option", "placed-chip"],
17467
- onDrop: handleDrop(subKey),
17468
- disabled: locked,
17469
- className: "fraction-slot-droppable",
17470
- children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxs(Fragment, { children: [
17471
- /* @__PURE__ */ jsx(
17472
- SlotSizer,
17473
- {
17474
- className: "inline-input fraction-input",
17475
- size: placeholder ? placeholder.length : void 0,
17476
- placeholder
17477
- }
17478
- ),
17479
- /* @__PURE__ */ jsx(
17480
- PlacedInputChip,
17481
- {
17482
- widgetId: subKey,
17483
- value: currentValue,
17484
- className: `fraction-chip${slotClass}`,
17485
- onRemove: () => onChange(subKey, "", false)
17486
- }
17487
- )
17488
- ] }) : /* @__PURE__ */ jsx(
17811
+ function isArabic(text) {
17812
+ return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
17813
+ }
17814
+ function hasLatinChars(text) {
17815
+ return /[A-Za-z]/.test(text);
17816
+ }
17817
+ function getTextValue(item) {
17818
+ if ((item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT) && "value" in item) {
17819
+ return item.value;
17820
+ }
17821
+ return void 0;
17822
+ }
17823
+ function itemContainsArabic(item) {
17824
+ const text = getTextValue(item);
17825
+ if (text !== void 0) {
17826
+ return isArabic(text);
17827
+ }
17828
+ if (item.type === JSON_TYPES.GROUP && "items" in item) {
17829
+ return item.items.some((subItem) => itemContainsArabic(subItem));
17830
+ }
17831
+ return false;
17832
+ }
17833
+ function itemContainsLatin(item) {
17834
+ const text = getTextValue(item);
17835
+ if (text !== void 0) {
17836
+ return hasLatinChars(text);
17837
+ }
17838
+ if (item.type === JSON_TYPES.GROUP && "items" in item) {
17839
+ return item.items.some((subItem) => itemContainsLatin(subItem));
17840
+ }
17841
+ return false;
17842
+ }
17843
+ function shouldItemsUseRTL(items) {
17844
+ const hasArabic = items.some(itemContainsArabic);
17845
+ const hasLatin = items.some(itemContainsLatin);
17846
+ return hasArabic && !hasLatin;
17847
+ }
17848
+ function isTextItem(item) {
17849
+ return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
17850
+ }
17851
+ function groupLineItems(items) {
17852
+ const groups = [];
17853
+ let currentTextGroup = [];
17854
+ for (const item of items) {
17855
+ if (isTextItem(item)) {
17856
+ currentTextGroup.push(item);
17857
+ } else {
17858
+ if (currentTextGroup.length > 0) {
17859
+ const hasArabic = currentTextGroup.some(itemContainsArabic);
17860
+ const hasLatin = currentTextGroup.some(itemContainsLatin);
17861
+ const shouldApplyRTL = hasArabic && !hasLatin;
17862
+ groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
17863
+ currentTextGroup = [];
17864
+ }
17865
+ groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
17866
+ }
17867
+ }
17868
+ if (currentTextGroup.length > 0) {
17869
+ const hasArabic = currentTextGroup.some(itemContainsArabic);
17870
+ const hasLatin = currentTextGroup.some(itemContainsLatin);
17871
+ const shouldApplyRTL = hasArabic && !hasLatin;
17872
+ groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
17873
+ }
17874
+ return groups;
17875
+ }
17876
+ const EPSILON = 10 ** -INTERNAL_PRECISION;
17877
+ const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
17878
+ const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
17879
+ const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
17880
+ const NUMBER_LINE_AXIS_ENDS = {
17881
+ "arrow-right": { left: "tick", right: "arrow" },
17882
+ "arrow-left": { left: "arrow", right: "tick" },
17883
+ "arrow-both": { left: "arrow", right: "arrow" },
17884
+ "line": { left: "line", right: "line" },
17885
+ "line-left": { left: "line", right: "arrow" }
17886
+ };
17887
+ const toPercent = (value, min, max) => {
17888
+ const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
17889
+ return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
17890
+ };
17891
+ const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
17892
+ const relX = (chipCenterX - axisLeft) / axisWidth;
17893
+ const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
17894
+ const clamped = Math.max(0, Math.min(1, fraction));
17895
+ return min + clamped * (max - min);
17896
+ };
17897
+ const getMajorTicks = (min, max, step) => {
17898
+ if (step <= 0 || max <= min) return [min, max];
17899
+ const count = Math.floor((max - min) / step);
17900
+ const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
17901
+ const lastTick = ticks[ticks.length - 1];
17902
+ if (roundTick(lastTick) !== roundTick(max)) {
17903
+ ticks.push(max);
17904
+ } else {
17905
+ ticks[ticks.length - 1] = max;
17906
+ }
17907
+ return ticks;
17908
+ };
17909
+ const getMinorTickValues = (majorTicks, minorTicks) => {
17910
+ if (minorTicks <= 0 || majorTicks.length < 2) return [];
17911
+ const result = [];
17912
+ for (let i = 0; i < majorTicks.length - 1; i++) {
17913
+ const from = majorTicks[i];
17914
+ const to = majorTicks[i + 1];
17915
+ const interval = to - from;
17916
+ for (let j = 1; j <= minorTicks; j++) {
17917
+ result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
17918
+ }
17919
+ }
17920
+ return result;
17921
+ };
17922
+ const getSnapTicks = (majorTicks, minorTicks) => [
17923
+ ...majorTicks,
17924
+ ...getMinorTickValues(majorTicks, minorTicks)
17925
+ ];
17926
+ const snapToNearest = (raw, ticks) => {
17927
+ if (ticks.length === 0) return cleanFloat(raw);
17928
+ return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
17929
+ };
17930
+ const getPositionWidgetId = (content) => {
17931
+ for (const item of content) {
17932
+ if (isWidgetRef(item)) return item.widgetId;
17933
+ }
17934
+ return null;
17935
+ };
17936
+ const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
17937
+ function placeTableCells(rows, cols) {
17938
+ const placements = /* @__PURE__ */ new Map();
17939
+ const occupied = rows.map(() => []);
17940
+ const totalRows = rows.length;
17941
+ rows.forEach((row, rowIndex) => {
17942
+ const taken = occupied[rowIndex] ?? [];
17943
+ let col = 0;
17944
+ for (const cell of row.cells) {
17945
+ while (taken[col]) col++;
17946
+ const colspan = cell.colspan || 1;
17947
+ const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
17948
+ for (let r = rowIndex; r < rowIndex + rowspan; r++) {
17949
+ const line = occupied[r];
17950
+ if (!line) continue;
17951
+ for (let c = col; c < col + colspan; c++) line[c] = true;
17952
+ }
17953
+ placements.set(cell.id, {
17954
+ column: col + 1,
17955
+ row: rowIndex + 1,
17956
+ colspan,
17957
+ rowspan,
17958
+ isLastColumn: col + colspan >= cols,
17959
+ isLastRow: rowIndex + rowspan >= totalRows
17960
+ });
17961
+ col += colspan;
17962
+ }
17963
+ });
17964
+ return placements;
17965
+ }
17966
+ const EMPTY_VALUES$2 = {};
17967
+ const EMPTY_DROPPED_INPUTS = /* @__PURE__ */ new Set();
17968
+ const ignoreChange = () => {
17969
+ };
17970
+ const PLAIN_TEXT_TYPES = /* @__PURE__ */ new Set([JSON_TYPES.TEXT, JSON_TYPES.SPACE, JSON_TYPES.BOLD_TEXT]);
17971
+ function isPlainTextContent(content) {
17972
+ return content.every((item) => PLAIN_TEXT_TYPES.has(item.type));
17973
+ }
17974
+ const RichOptionContent = ({ content, widgets }) => {
17975
+ const renderItem = (item) => /* @__PURE__ */ jsx(
17976
+ LayoutRenderer,
17977
+ {
17978
+ item: item.type === JSON_TYPES.IMAGE ? { ...item, fullscreen: false } : item,
17979
+ widgets,
17980
+ values: EMPTY_VALUES$2,
17981
+ droppedInputs: EMPTY_DROPPED_INPUTS,
17982
+ onChange: ignoreChange
17983
+ },
17984
+ item.id
17985
+ );
17986
+ const hasRTLInlineWidget = content.some((item) => {
17987
+ if (item.type !== JSON_TYPES.WIDGET_REF) return false;
17988
+ const widget = widgets[item.widgetId];
17989
+ return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
17990
+ });
17991
+ const isRTL = hasRTLInlineWidget && content.some(itemContainsArabic);
17992
+ return /* @__PURE__ */ jsx("span", { className: "rich-option-content", dir: isRTL ? "rtl" : "ltr", children: groupLineItems(content).map((group, index) => group.isTextGroup ? /* @__PURE__ */ jsx("span", { className: "text-group", dir: group.shouldApplyRTL ? "rtl" : void 0, children: group.items.map(renderItem) }, index) : group.items.map(renderItem)) });
17993
+ };
17994
+ const DraggableOptions = ({
17995
+ widget,
17996
+ widgets,
17997
+ usedOptionNames
17998
+ }) => {
17999
+ const { exerciseKey } = useExerciseDnd();
18000
+ return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options draggable-options", children: widget.options.map((option) => /* @__PURE__ */ jsx(
18001
+ RichDragOption,
18002
+ {
18003
+ option,
18004
+ widgets,
18005
+ reusable: widget.reusable,
18006
+ used: !widget.reusable && usedOptionNames.has(option.name)
18007
+ },
18008
+ `${exerciseKey ?? ""}:${option.name}`
18009
+ )) }) });
18010
+ };
18011
+ const RichDragOption = ({ option, widgets, reusable, used }) => {
18012
+ const { activeItem, hasDraggedOption, playHintSound } = useExerciseDnd();
18013
+ const locked = useExerciseLocked();
18014
+ const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption, {
18015
+ ignoreBubbledAnimationEnd: true
18016
+ });
18017
+ const optionId = `rich-drag-option:${option.name}`;
18018
+ const data = { optionName: option.name, origin: { kind: "bank" } };
18019
+ const isPickedUp = !reusable && activeItem?.type === "rich-drag-option" && activeItem.id === optionId;
18020
+ const surfaceClass = isPlainTextContent(option.content) ? "drag-option" : "drag-option drag-option--rich";
18021
+ const emptySlot = /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true", children: /* @__PURE__ */ jsx("button", { type: "button", className: surfaceClass, disabled: true, children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets }) }) });
18022
+ return /* @__PURE__ */ jsx("div", { className: "drag-option-slot rich-drag-option-slot", children: used ? emptySlot : /* @__PURE__ */ jsx(
18023
+ Draggable,
18024
+ {
18025
+ id: optionId,
18026
+ type: "rich-drag-option",
18027
+ data,
18028
+ disabled: locked,
18029
+ behavior: reusable ? "copy" : "move",
18030
+ className: "drag-option-wrapper rich-drag-option-wrapper",
18031
+ children: isPickedUp ? emptySlot : /* @__PURE__ */ jsx(
18032
+ "button",
18033
+ {
18034
+ type: "button",
18035
+ className: `${surfaceClass} rich-drag-option${hinting && !locked ? " drag-option--hint" : ""}`,
18036
+ onAnimationStart: (event) => {
18037
+ if (hinting && event.target === event.currentTarget) playHintSound();
18038
+ },
18039
+ onAnimationEnd: onHintEnd,
18040
+ "aria-disabled": locked ? "true" : void 0,
18041
+ children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets })
18042
+ }
18043
+ )
18044
+ }
18045
+ ) });
18046
+ };
18047
+ const PlacedRichOption = ({
18048
+ inputName,
18049
+ option,
18050
+ widgets,
18051
+ onRemove,
18052
+ className = ""
18053
+ }) => {
18054
+ const locked = useExerciseLocked();
18055
+ const dragId = `${inputName}__rich-option`;
18056
+ const data = {
18057
+ optionName: option.name,
18058
+ origin: { kind: "drop-input", inputName }
18059
+ };
18060
+ const dragItem = { id: dragId, type: "rich-drag-option", data };
18061
+ const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
18062
+ id: dragId,
18063
+ data: dragItem,
18064
+ disabled: locked
18065
+ });
18066
+ useDndMonitor({
18067
+ onDragEnd(event) {
18068
+ if (isUndoDrop(event, dragId)) onRemove();
18069
+ }
18070
+ });
18071
+ return /* @__PURE__ */ jsx(
18072
+ "button",
18073
+ {
18074
+ ref: setNodeRef,
18075
+ type: "button",
18076
+ className: `inline-input-chip placed-rich-option ${className}${isDragging ? " dragging" : ""}`.trim(),
18077
+ onClick: (event) => {
18078
+ if (!locked && event.detail !== 0) onRemove();
18079
+ },
18080
+ title: locked ? void 0 : "Drag to move, or click to remove",
18081
+ style: {
18082
+ width: "100%",
18083
+ height: "100%",
18084
+ minWidth: 0,
18085
+ maxWidth: "none",
18086
+ margin: 0,
18087
+ boxSizing: "border-box",
18088
+ touchAction: "none",
18089
+ cursor: locked ? "default" : "grab"
18090
+ },
18091
+ ...listeners,
18092
+ ...attributes,
18093
+ children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets })
18094
+ }
18095
+ );
18096
+ };
18097
+ const dropSourceId = (optionName) => `rich-drag-option:${optionName}`;
18098
+ function applyRichDropToInput(targetInputName, item, onChange) {
18099
+ if (item.type !== "rich-drag-option") return false;
18100
+ const data = item.data;
18101
+ if (data.origin.kind === "drop-input") {
18102
+ if (data.origin.inputName === targetInputName) return false;
18103
+ onChange(data.origin.inputName, "", false);
18104
+ }
18105
+ onChange(targetInputName, data.optionName, true, dropSourceId(data.optionName));
18106
+ return true;
18107
+ }
18108
+ const ACCEPTED_DRAG_TYPES = ["rich-drag-option"];
18109
+ const DropInput = ({
18110
+ widgetId,
18111
+ value,
18112
+ width,
18113
+ placeholder,
18114
+ validationStatus = null,
18115
+ option,
18116
+ widgets = {},
18117
+ onChange = () => {
18118
+ }
18119
+ }) => {
18120
+ const locked = useExerciseLocked();
18121
+ const dnd = useExerciseDndOptional();
18122
+ const isEmpty = value === "";
18123
+ const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
18124
+ const hasChip = !isEmpty && Boolean(option) && Boolean(dnd);
18125
+ const boxStyle = width !== void 0 ? { width: getInputBoxWidth(width) } : void 0;
18126
+ const handleDrop = useCallback(
18127
+ (item) => applyRichDropToInput(widgetId, item, onChange),
18128
+ [widgetId, onChange]
18129
+ );
18130
+ const handleRemove = useCallback(
18131
+ () => onChange(widgetId, "", false),
18132
+ [widgetId, onChange]
18133
+ );
18134
+ const placeholderText = placeholder ? /* @__PURE__ */ jsx("span", { className: "drop-input-placeholder", children: placeholder }) : null;
18135
+ const input = /* @__PURE__ */ jsx(
18136
+ "div",
18137
+ {
18138
+ className: `inline-widget drop-input${hasChip ? " drop-input--filled" : ` inline-input${statusClass}`}${isEmpty ? " drop-input--empty" : ""}${locked ? " drop-input--locked" : ""}`,
18139
+ style: hasChip ? void 0 : boxStyle,
18140
+ role: "group",
18141
+ "aria-label": placeholder ?? widgetId,
18142
+ "aria-disabled": locked ? "true" : void 0,
18143
+ "data-widget-id": widgetId,
18144
+ children: isEmpty ? placeholderText : option ? dnd ? /* @__PURE__ */ jsxs(Fragment, { children: [
18145
+ /* @__PURE__ */ jsx("span", { className: "inline-input drop-input-sizer", style: boxStyle, "aria-hidden": "true", children: placeholderText }),
18146
+ /* @__PURE__ */ jsx(
18147
+ PlacedRichOption,
18148
+ {
18149
+ inputName: widgetId,
18150
+ option,
18151
+ widgets,
18152
+ onRemove: handleRemove,
18153
+ className: statusClass.trim()
18154
+ }
18155
+ )
18156
+ ] }) : /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets }) : dnd && !locked ? (
18157
+ // Value that resolves to no bank option (author answer/option-name
18158
+ // mismatch, legacy JSON). Keep it clearable so the field is not stuck.
18159
+ /* @__PURE__ */ jsx(
18160
+ "button",
18161
+ {
18162
+ type: "button",
18163
+ className: "drop-input-unresolved",
18164
+ onClick: (event) => {
18165
+ if (event.detail !== 0) handleRemove();
18166
+ },
18167
+ title: "Click to clear",
18168
+ children: value
18169
+ }
18170
+ )
18171
+ ) : value
18172
+ }
18173
+ );
18174
+ if (!dnd) return input;
18175
+ return /* @__PURE__ */ jsx(
18176
+ Droppable,
18177
+ {
18178
+ id: widgetId,
18179
+ accepts: ACCEPTED_DRAG_TYPES,
18180
+ onDrop: handleDrop,
18181
+ disabled: locked,
18182
+ className: "drop-input-droppable",
18183
+ children: input
18184
+ }
18185
+ );
18186
+ };
18187
+ const NumericKeypad = ({
18188
+ value,
18189
+ includeOperators = false,
18190
+ includeDecimal = false,
18191
+ onChange
18192
+ }) => {
18193
+ const playSound = useSound();
18194
+ const locked = useExerciseLocked();
18195
+ const handleDigit = (digit) => {
18196
+ if (locked) return;
18197
+ playSound("keypad-press");
18198
+ onChange(value + digit);
18199
+ };
18200
+ const handleBackspace = () => {
18201
+ if (locked) return;
18202
+ playSound("keypad-press");
18203
+ onChange(value.slice(0, -1));
18204
+ };
18205
+ return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
18206
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
18207
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
18208
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
18209
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
18210
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
18211
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
18212
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
18213
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
18214
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
18215
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
18216
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
18217
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
18218
+ includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
18219
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
18220
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
18221
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
18222
+ ] });
18223
+ };
18224
+ const FractionInput = ({
18225
+ widgetId,
18226
+ numerator,
18227
+ denominator,
18228
+ values,
18229
+ droppedInputs,
18230
+ validationStatus = null,
18231
+ numValidationStatus = null,
18232
+ denValidationStatus = null,
18233
+ onChange,
18234
+ onFocus,
18235
+ placeholder
18236
+ }) => {
18237
+ const playSound = useSound();
18238
+ const locked = useExerciseLocked();
18239
+ const hasKeypad = useHasKeypad();
18240
+ const numKey = getFractionKey(widgetId, "num");
18241
+ const denKey = getFractionKey(widgetId, "den");
18242
+ const numValue = values[numKey] ?? "";
18243
+ const denValue = values[denKey] ?? "";
18244
+ const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
18245
+ const handleDrop = useCallback((subKey) => (item) => {
18246
+ return applyDropToInput(subKey, item, onChange);
18247
+ }, [onChange]);
18248
+ const renderSlot = (subKey, fixedValue, currentValue, label, slotStatus) => {
18249
+ if (fixedValue !== null) {
18250
+ return /* @__PURE__ */ jsx("span", { className: "fraction-fixed", children: fixedValue });
18251
+ }
18252
+ const isDroppedHere = droppedInputs.has(subKey);
18253
+ const slotClass = slotStatus ? ` widget--${slotStatus}` : statusClass;
18254
+ return /* @__PURE__ */ jsx(
18255
+ Droppable,
18256
+ {
18257
+ id: subKey,
18258
+ accepts: ["drag-option", "placed-chip"],
18259
+ onDrop: handleDrop(subKey),
18260
+ disabled: locked,
18261
+ className: "fraction-slot-droppable",
18262
+ children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxs(Fragment, { children: [
18263
+ /* @__PURE__ */ jsx(
18264
+ SlotSizer,
18265
+ {
18266
+ className: "inline-input fraction-input",
18267
+ size: placeholder ? placeholder.length : void 0,
18268
+ placeholder
18269
+ }
18270
+ ),
18271
+ /* @__PURE__ */ jsx(
18272
+ PlacedInputChip,
18273
+ {
18274
+ widgetId: subKey,
18275
+ value: currentValue,
18276
+ className: `fraction-chip${slotClass}`,
18277
+ onRemove: () => onChange(subKey, "", false)
18278
+ }
18279
+ )
18280
+ ] }) : /* @__PURE__ */ jsx(
17489
18281
  "input",
17490
18282
  {
17491
18283
  id: getInputElementId(subKey),
@@ -19230,41 +20022,6 @@ const MatcherWidget = ({
19230
20022
  );
19231
20023
  };
19232
20024
  MatcherWidget.displayName = "MatcherWidget";
19233
- function isArabic(text) {
19234
- return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
19235
- }
19236
- function hasLatinChars(text) {
19237
- return /[A-Za-z]/.test(text);
19238
- }
19239
- function itemContainsArabic(item) {
19240
- if (item.type === JSON_TYPES.TEXT && "value" in item) {
19241
- return isArabic(item.value);
19242
- }
19243
- if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
19244
- return isArabic(item.value);
19245
- }
19246
- if (item.type === JSON_TYPES.GROUP && "items" in item) {
19247
- return item.items.some((subItem) => itemContainsArabic(subItem));
19248
- }
19249
- return false;
19250
- }
19251
- function itemContainsLatin(item) {
19252
- if (item.type === JSON_TYPES.TEXT && "value" in item) {
19253
- return hasLatinChars(item.value);
19254
- }
19255
- if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
19256
- return hasLatinChars(item.value);
19257
- }
19258
- if (item.type === JSON_TYPES.GROUP && "items" in item) {
19259
- return item.items.some((subItem) => itemContainsLatin(subItem));
19260
- }
19261
- return false;
19262
- }
19263
- function shouldItemsUseRTL(items) {
19264
- const hasArabic = items.some(itemContainsArabic);
19265
- const hasLatin = items.some(itemContainsLatin);
19266
- return hasArabic && !hasLatin;
19267
- }
19268
20025
  function usePairLines({
19269
20026
  containerRef,
19270
20027
  pairs,
@@ -19678,9 +20435,15 @@ PairMatcherWidget.displayName = "PairMatcherWidget";
19678
20435
  function isInlineInputWidget(widget) {
19679
20436
  return widget.type === "inline-input";
19680
20437
  }
20438
+ function isDropInputWidget(widget) {
20439
+ return widget.type === "drop-input";
20440
+ }
19681
20441
  function isDragOptionsWidget(widget) {
19682
20442
  return widget.type === "drag-options";
19683
20443
  }
20444
+ function isDraggableOptionsWidget(widget) {
20445
+ return widget.type === "draggable-options";
20446
+ }
19684
20447
  function isInlineDragOptionWidget(widget) {
19685
20448
  return widget.type === "inline-drag-option";
19686
20449
  }
@@ -19709,7 +20472,7 @@ function isGeometryWidget(widget) {
19709
20472
  return widget.type === JSON_TYPES.GEOMETRY;
19710
20473
  }
19711
20474
  const GeometryWidget = React.lazy(async () => ({
19712
- default: (await import("./GeometryWidget-h2yNZsas.mjs")).GeometryWidget
20475
+ default: (await import("./GeometryWidget-DZWM4LcD.mjs")).GeometryWidget
19713
20476
  }));
19714
20477
  const WidgetRenderer = ({
19715
20478
  widget,
@@ -19747,6 +20510,23 @@ const WidgetRenderer = ({
19747
20510
  }
19748
20511
  );
19749
20512
  }
20513
+ if (isDropInputWidget(widget)) {
20514
+ const optionsWidget = widgets?.[DRAGGABLE_OPTIONS_WIDGET_ID];
20515
+ const option = optionsWidget && isDraggableOptionsWidget(optionsWidget) ? optionsWidget.options.find((candidate) => candidate.name === strValue) : void 0;
20516
+ return /* @__PURE__ */ jsx(
20517
+ DropInput,
20518
+ {
20519
+ widgetId,
20520
+ value: strValue,
20521
+ width: widget.width,
20522
+ placeholder: widget.placeholder,
20523
+ validationStatus,
20524
+ option,
20525
+ widgets,
20526
+ onChange
20527
+ }
20528
+ );
20529
+ }
19750
20530
  if (isDragOptionsWidget(widget)) {
19751
20531
  return /* @__PURE__ */ jsx(
19752
20532
  DragOptions,
@@ -19757,6 +20537,25 @@ const WidgetRenderer = ({
19757
20537
  }
19758
20538
  );
19759
20539
  }
20540
+ if (isDraggableOptionsWidget(widget)) {
20541
+ const usedOptionNames = /* @__PURE__ */ new Set();
20542
+ if (!widget.reusable) {
20543
+ for (const [id, candidate] of Object.entries(widgets ?? {})) {
20544
+ const candidateValue = values[id];
20545
+ if (isDropInputWidget(candidate) && typeof candidateValue === "string" && candidateValue !== "") {
20546
+ usedOptionNames.add(candidateValue);
20547
+ }
20548
+ }
20549
+ }
20550
+ return /* @__PURE__ */ jsx(
20551
+ DraggableOptions,
20552
+ {
20553
+ widget,
20554
+ widgets: widgets ?? { [widgetId]: widget },
20555
+ usedOptionNames
20556
+ }
20557
+ );
20558
+ }
19760
20559
  if (isInlineDragOptionWidget(widget)) {
19761
20560
  return /* @__PURE__ */ jsx(
19762
20561
  InlineDragOption,
@@ -20106,67 +20905,6 @@ const ImageItem = ({
20106
20905
  overlay
20107
20906
  ] });
20108
20907
  };
20109
- const EPSILON = 10 ** -INTERNAL_PRECISION;
20110
- const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
20111
- const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
20112
- const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
20113
- const NUMBER_LINE_AXIS_ENDS = {
20114
- "arrow-right": { left: "tick", right: "arrow" },
20115
- "arrow-left": { left: "arrow", right: "tick" },
20116
- "arrow-both": { left: "arrow", right: "arrow" },
20117
- "line": { left: "line", right: "line" },
20118
- "line-left": { left: "line", right: "arrow" }
20119
- };
20120
- const toPercent = (value, min, max) => {
20121
- const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
20122
- return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
20123
- };
20124
- const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
20125
- const relX = (chipCenterX - axisLeft) / axisWidth;
20126
- const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
20127
- const clamped = Math.max(0, Math.min(1, fraction));
20128
- return min + clamped * (max - min);
20129
- };
20130
- const getMajorTicks = (min, max, step) => {
20131
- if (step <= 0 || max <= min) return [min, max];
20132
- const count = Math.floor((max - min) / step);
20133
- const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
20134
- const lastTick = ticks[ticks.length - 1];
20135
- if (roundTick(lastTick) !== roundTick(max)) {
20136
- ticks.push(max);
20137
- } else {
20138
- ticks[ticks.length - 1] = max;
20139
- }
20140
- return ticks;
20141
- };
20142
- const getMinorTickValues = (majorTicks, minorTicks) => {
20143
- if (minorTicks <= 0 || majorTicks.length < 2) return [];
20144
- const result = [];
20145
- for (let i = 0; i < majorTicks.length - 1; i++) {
20146
- const from = majorTicks[i];
20147
- const to = majorTicks[i + 1];
20148
- const interval = to - from;
20149
- for (let j = 1; j <= minorTicks; j++) {
20150
- result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
20151
- }
20152
- }
20153
- return result;
20154
- };
20155
- const getSnapTicks = (majorTicks, minorTicks) => [
20156
- ...majorTicks,
20157
- ...getMinorTickValues(majorTicks, minorTicks)
20158
- ];
20159
- const snapToNearest = (raw, ticks) => {
20160
- if (ticks.length === 0) return cleanFloat(raw);
20161
- return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
20162
- };
20163
- const getPositionWidgetId = (content) => {
20164
- for (const item of content) {
20165
- if (isWidgetRef(item)) return item.widgetId;
20166
- }
20167
- return null;
20168
- };
20169
- const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
20170
20908
  const getBandGeometry = (h, min, max) => {
20171
20909
  const leftPct = toPercent(Math.min(h.from, h.to), min, max);
20172
20910
  const rightPct = toPercent(Math.max(h.from, h.to), min, max);
@@ -21623,63 +22361,6 @@ const ClockRenderer = ({
21623
22361
  format === "12h" && /* @__PURE__ */ jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
21624
22362
  ] });
21625
22363
  };
21626
- function isTextItem(item) {
21627
- return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
21628
- }
21629
- function groupLineItems(items) {
21630
- const groups = [];
21631
- let currentTextGroup = [];
21632
- for (const item of items) {
21633
- if (isTextItem(item)) {
21634
- currentTextGroup.push(item);
21635
- } else {
21636
- if (currentTextGroup.length > 0) {
21637
- const hasArabic = currentTextGroup.some(itemContainsArabic);
21638
- const hasLatin = currentTextGroup.some(itemContainsLatin);
21639
- const shouldApplyRTL = hasArabic && !hasLatin;
21640
- groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
21641
- currentTextGroup = [];
21642
- }
21643
- groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
21644
- }
21645
- }
21646
- if (currentTextGroup.length > 0) {
21647
- const hasArabic = currentTextGroup.some(itemContainsArabic);
21648
- const hasLatin = currentTextGroup.some(itemContainsLatin);
21649
- const shouldApplyRTL = hasArabic && !hasLatin;
21650
- groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
21651
- }
21652
- return groups;
21653
- }
21654
- function placeTableCells(rows, cols) {
21655
- const placements = /* @__PURE__ */ new Map();
21656
- const occupied = rows.map(() => []);
21657
- const totalRows = rows.length;
21658
- rows.forEach((row, rowIndex) => {
21659
- const taken = occupied[rowIndex] ?? [];
21660
- let col = 0;
21661
- for (const cell of row.cells) {
21662
- while (taken[col]) col++;
21663
- const colspan = cell.colspan || 1;
21664
- const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
21665
- for (let r = rowIndex; r < rowIndex + rowspan; r++) {
21666
- const line = occupied[r];
21667
- if (!line) continue;
21668
- for (let c = col; c < col + colspan; c++) line[c] = true;
21669
- }
21670
- placements.set(cell.id, {
21671
- column: col + 1,
21672
- row: rowIndex + 1,
21673
- colspan,
21674
- rowspan,
21675
- isLastColumn: col + colspan >= cols,
21676
- isLastRow: rowIndex + rowspan >= totalRows
21677
- });
21678
- col += colspan;
21679
- }
21680
- });
21681
- return placements;
21682
- }
21683
22364
  const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
21684
22365
  const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
21685
22366
  const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
@@ -22011,7 +22692,7 @@ const LayoutRenderer = ({
22011
22692
  return null;
22012
22693
  }
22013
22694
  };
22014
- function LionHand({ className }) {
22695
+ function ChameleonHand({ className }) {
22015
22696
  const id = useId();
22016
22697
  const shadowId = `${id}-shadow`;
22017
22698
  const maskId = `${id}-mask`;
@@ -22038,34 +22719,100 @@ function LionHand({ className }) {
22038
22719
  y: "19",
22039
22720
  width: "49",
22040
22721
  height: "42",
22041
- children: /* @__PURE__ */ jsx("path", { d: "M49.7484 33.9668C58.158 45.6281 37.9572 61.4399 28.1745 60.1051C17.1397 58.5995 5.03802 42.2474 10.4349 38.3224C15.8318 34.3974 18.9715 32.4031 23.1181 30.1278C24.1486 29.5624 25.1742 29.0263 26.1947 28.5358C33.7314 24.764 52.8138 16.0308 57.0969 21.97C59.9201 25.8848 54.8651 28.9522 48.1097 31.8806C48.658 32.518 49.2042 33.2123 49.7484 33.9668Z", fill: "#FCD592" })
22722
+ children: /* @__PURE__ */ jsx(
22723
+ "path",
22724
+ {
22725
+ d: "M49.7484 33.9668C58.158 45.6281 37.9572 61.4399 28.1745 60.1051C17.1397 58.5995 5.03802 42.2474 10.4349 38.3224C15.8318 34.3974 18.9715 32.4031 23.1181 30.1278C24.1486 29.5624 25.1742 29.0263 26.1947 28.5358C33.7314 24.764 52.8138 16.0308 57.0969 21.97C59.9201 25.8848 54.8651 28.9522 48.1097 31.8806C48.658 32.518 49.2042 33.2123 49.7484 33.9668Z",
22726
+ fill: "#B7DB4E"
22727
+ }
22728
+ )
22042
22729
  }
22043
22730
  ),
22044
22731
  /* @__PURE__ */ jsxs("g", { mask: `url(#${maskId})`, children: [
22045
- /* @__PURE__ */ jsx("path", { d: "M49.7484 33.9672C58.158 45.6285 38.3118 60.3151 32.1037 64.7332C22.6672 71.4486 2.68204 43.9969 12.2424 37.1926C15.1884 35.0959 18.9716 32.4035 23.1182 30.1282C24.7308 29.2433 26.3314 28.4303 27.9195 27.7514C36.611 23.6398 54.4269 16.1501 58.4471 21.7248C61.34 25.7362 55.5686 28.9294 48.2024 31.9893C48.7196 32.5968 49.2349 33.2552 49.7484 33.9672Z", fill: "#FCD592" }),
22046
- /* @__PURE__ */ jsx("path", { d: "M12.2426 37.1924C2.68223 43.9967 22.6674 71.4485 32.1039 64.733C31.3434 63.6785 26.4317 61.1668 25.3134 59.6161C24.1951 58.0654 22.794 50.5211 22.123 49.5907C21.4521 48.6603 16.3219 45.7409 15.5637 44.4032C14.8055 43.0655 13.8979 39.4877 14.7258 35.4253L12.2426 37.1924Z", fill: "#6B291E" }),
22047
- /* @__PURE__ */ jsx("path", { d: "M50.4102 36.1737C50.6823 37.524 52.0035 38.4016 53.3614 38.1339C54.7192 37.8662 55.5994 36.5546 55.3274 35.2043L52.8688 35.689L50.4102 36.1737ZM38.8014 26.9523C37.4241 27.0934 36.425 28.3178 36.5697 29.6872C36.7144 31.0565 37.9482 32.0522 39.3255 31.9111L39.0634 29.4317L38.8014 26.9523ZM52.8688 35.689C55.3274 35.2043 55.327 35.2023 55.3266 35.2004C55.3265 35.1997 55.326 35.1977 55.3258 35.1963C55.3252 35.1935 55.3246 35.1906 55.324 35.1875C55.3227 35.1815 55.3213 35.1749 55.3198 35.1679C55.3168 35.1539 55.3133 35.1379 55.3092 35.1201C55.3011 35.0845 55.2907 35.0415 55.2779 34.9916C55.2522 34.8918 55.2163 34.764 55.1676 34.6127C55.0703 34.3106 54.9205 33.911 54.6961 33.4507C54.247 32.5296 53.4925 31.3535 52.2552 30.241C49.7078 27.9508 45.5021 26.2658 38.8014 26.9523L39.0634 29.4317L39.3255 31.9111C44.9159 31.3384 47.6128 32.782 48.8954 33.9352C49.5731 34.5445 49.9653 35.1687 50.1842 35.6177C50.2938 35.8424 50.3584 36.0201 50.3919 36.1242C50.4086 36.1759 50.4172 36.2084 50.4197 36.2181C50.4209 36.223 50.4206 36.222 50.419 36.2148C50.4181 36.2111 50.417 36.2059 50.4155 36.1991C50.4148 36.1957 50.414 36.1919 50.4131 36.1876C50.4127 36.1855 50.4122 36.1833 50.4117 36.181C50.4115 36.1798 50.4111 36.178 50.411 36.1774C50.4106 36.1755 50.4102 36.1737 52.8688 35.689Z", fill: "#A2634A" }),
22048
- /* @__PURE__ */ jsx("path", { d: "M54.7526 47.3947C55.5602 48.5146 57.1269 48.7734 58.2518 47.9729C59.3767 47.1723 59.634 45.6155 58.8264 44.4956L56.7895 45.9452L54.7526 47.3947ZM38.1539 36.5617C36.7805 36.7357 35.8113 37.9837 35.9891 39.3493C36.1669 40.7148 37.4244 41.6808 38.7978 41.5068L38.4758 39.0342L38.1539 36.5617ZM56.7895 45.9452L58.8264 44.4956C57.4277 42.5562 55.3182 40.1317 51.9378 38.4204C48.5385 36.6995 44.0772 35.8112 38.1539 36.5617L38.4758 39.0342L38.7978 41.5068C43.8796 40.863 47.3003 41.6624 49.6691 42.8616C52.0569 44.0704 53.6015 45.7985 54.7526 47.3947L56.7895 45.9452Z", fill: "#A2634A" })
22732
+ /* @__PURE__ */ jsx(
22733
+ "path",
22734
+ {
22735
+ d: "M49.7484 33.9672C58.158 45.6285 38.3118 60.3151 32.1037 64.7332C22.6672 71.4486 2.68204 43.9969 12.2424 37.1926C15.1884 35.0959 18.9716 32.4035 23.1182 30.1282C24.7308 29.2433 26.3314 28.4303 27.9195 27.7514C36.611 23.6398 54.4269 16.1501 58.4471 21.7248C61.34 25.7362 55.5686 28.9294 48.2024 31.9893C48.7196 32.5968 49.2349 33.2552 49.7484 33.9672Z",
22736
+ fill: "#B7DB4E"
22737
+ }
22738
+ ),
22739
+ /* @__PURE__ */ jsx(
22740
+ "path",
22741
+ {
22742
+ d: "M50.4102 36.1737C50.6823 37.524 52.0035 38.4016 53.3614 38.1339C54.7192 37.8662 55.5994 36.5546 55.3274 35.2043L52.8688 35.689L50.4102 36.1737ZM38.8014 26.9523C37.4241 27.0934 36.425 28.3178 36.5697 29.6872C36.7144 31.0565 37.9482 32.0522 39.3255 31.9111L39.0634 29.4317L38.8014 26.9523ZM52.8688 35.689C55.3274 35.2043 55.327 35.2023 55.3266 35.2004C55.3265 35.1997 55.326 35.1977 55.3258 35.1963C55.3252 35.1935 55.3246 35.1906 55.324 35.1875C55.3227 35.1815 55.3213 35.1749 55.3198 35.1679C55.3168 35.1539 55.3133 35.1379 55.3092 35.1201C55.3011 35.0845 55.2907 35.0415 55.2779 34.9916C55.2522 34.8918 55.2163 34.764 55.1676 34.6127C55.0703 34.3106 54.9205 33.911 54.6961 33.4507C54.247 32.5296 53.4925 31.3535 52.2552 30.241C49.7078 27.9508 45.5021 26.2658 38.8014 26.9523L39.0634 29.4317L39.3255 31.9111C44.9159 31.3384 47.6128 32.782 48.8954 33.9352C49.5731 34.5445 49.9653 35.1687 50.1842 35.6177C50.2938 35.8424 50.3584 36.0201 50.3919 36.1242C50.4086 36.1759 50.4172 36.2084 50.4197 36.2181C50.4209 36.223 50.4206 36.222 50.419 36.2148C50.4181 36.2111 50.417 36.2059 50.4155 36.1991C50.4148 36.1957 50.414 36.1919 50.4131 36.1876C50.4127 36.1855 50.4122 36.1833 50.4117 36.181C50.4115 36.1798 50.4111 36.178 50.411 36.1774C50.4106 36.1755 50.4102 36.1737 52.8688 35.689Z",
22743
+ fill: "#9CAF63"
22744
+ }
22745
+ ),
22746
+ /* @__PURE__ */ jsx(
22747
+ "path",
22748
+ {
22749
+ d: "M54.7526 47.3947C55.5602 48.5146 57.1269 48.7734 58.2518 47.9729C59.3767 47.1723 59.634 45.6155 58.8264 44.4956L56.7895 45.9452L54.7526 47.3947ZM38.1539 36.5617C36.7805 36.7357 35.8113 37.9837 35.9891 39.3493C36.1669 40.7148 37.4244 41.6808 38.7978 41.5068L38.4758 39.0342L38.1539 36.5617ZM56.7895 45.9452L58.8264 44.4956C57.4277 42.5562 55.3182 40.1317 51.9378 38.4204C48.5385 36.6995 44.0772 35.8112 38.1539 36.5617L38.4758 39.0342L38.7978 41.5068C43.8796 40.863 47.3003 41.6624 49.6691 42.8616C52.0569 44.0704 53.6015 45.7985 54.7526 47.3947L56.7895 45.9452Z",
22750
+ fill: "#9CAF63"
22751
+ }
22752
+ )
22049
22753
  ] })
22050
22754
  ] }),
22051
- /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("filter", { id: shadowId, x: "-1.99995", y: "0", width: "78.3658", height: "81.8518", filterUnits: "userSpaceOnUse", colorInterpolationFilters: "sRGB", children: [
22052
- /* @__PURE__ */ jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22053
- /* @__PURE__ */ jsx("feColorMatrix", { in: "SourceAlpha", type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0", result: "hardAlpha" }),
22054
- /* @__PURE__ */ jsx("feOffset", { dy: "2" }),
22055
- /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: "1" }),
22056
- /* @__PURE__ */ jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22057
- /* @__PURE__ */ jsx("feColorMatrix", { type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" }),
22058
- /* @__PURE__ */ jsx("feBlend", { mode: "normal", in2: "BackgroundImageFix", result: "effect1_dropShadow" }),
22059
- /* @__PURE__ */ jsx("feBlend", { mode: "normal", in: "SourceGraphic", in2: "effect1_dropShadow", result: "shape" })
22060
- ] }) })
22755
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
22756
+ "filter",
22757
+ {
22758
+ id: shadowId,
22759
+ x: "-1.99995",
22760
+ y: "0",
22761
+ width: "78.3658",
22762
+ height: "81.8518",
22763
+ filterUnits: "userSpaceOnUse",
22764
+ colorInterpolationFilters: "sRGB",
22765
+ children: [
22766
+ /* @__PURE__ */ jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22767
+ /* @__PURE__ */ jsx(
22768
+ "feColorMatrix",
22769
+ {
22770
+ in: "SourceAlpha",
22771
+ type: "matrix",
22772
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
22773
+ result: "hardAlpha"
22774
+ }
22775
+ ),
22776
+ /* @__PURE__ */ jsx("feOffset", { dy: "2" }),
22777
+ /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: "1" }),
22778
+ /* @__PURE__ */ jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22779
+ /* @__PURE__ */ jsx(
22780
+ "feColorMatrix",
22781
+ {
22782
+ type: "matrix",
22783
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
22784
+ }
22785
+ ),
22786
+ /* @__PURE__ */ jsx(
22787
+ "feBlend",
22788
+ {
22789
+ mode: "normal",
22790
+ in2: "BackgroundImageFix",
22791
+ result: "effect1_dropShadow"
22792
+ }
22793
+ ),
22794
+ /* @__PURE__ */ jsx(
22795
+ "feBlend",
22796
+ {
22797
+ mode: "normal",
22798
+ in: "SourceGraphic",
22799
+ in2: "effect1_dropShadow",
22800
+ result: "shape"
22801
+ }
22802
+ )
22803
+ ]
22804
+ }
22805
+ ) })
22061
22806
  ]
22062
22807
  }
22063
22808
  );
22064
22809
  }
22065
22810
  const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
22811
+ const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22066
22812
  const CYCLE_MS = 1400;
22067
22813
  const PRESS_AT_MS = 650;
22068
22814
  const PRESS_HOLD_MS = 220;
22815
+ const MAX_SPEECH_MS = 3e4;
22069
22816
  const HOLE_PAD = 10;
22070
22817
  const HOLE_RADIUS = 18;
22071
22818
  const HAND_W = 72;
@@ -22083,7 +22830,6 @@ function toRect(t) {
22083
22830
  if (r.width <= 0 || r.height <= 0) return null;
22084
22831
  return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
22085
22832
  }
22086
- const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22087
22833
  const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22088
22834
  const { steps } = tutorial;
22089
22835
  const [stepIndex, setStepIndex] = useState(
@@ -22119,6 +22865,8 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22119
22865
  tick();
22120
22866
  return () => cancelAnimationFrame(raf);
22121
22867
  }, [step]);
22868
+ const showBubble = options.showBubble !== false;
22869
+ const autoAdvance = options.autoAdvance === true || !showBubble;
22122
22870
  const [tapIndex, setTapIndex] = useState(0);
22123
22871
  const [tapCount, setTapCount] = useState(0);
22124
22872
  const [shownStep, setShownStep] = useState(stepIndex);
@@ -22128,11 +22876,13 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22128
22876
  setTapCount(0);
22129
22877
  }
22130
22878
  useEffect(() => {
22879
+ const targets = step.taps(ctxRef.current);
22880
+ if (autoAdvance && tapIndex >= targets.length) return;
22131
22881
  let release;
22132
22882
  const press = setTimeout(() => {
22133
22883
  setTapCount((n) => n + 1);
22134
- const targets = step.taps(ctxRef.current);
22135
- const el = targets[tapIndex % Math.max(1, targets.length)];
22884
+ const targets2 = step.taps(ctxRef.current);
22885
+ const el = targets2[tapIndex % Math.max(1, targets2.length)];
22136
22886
  if (el instanceof Element) {
22137
22887
  el.classList.add("met-tutorial-pressed");
22138
22888
  release = setTimeout(() => el.classList.remove("met-tutorial-pressed"), PRESS_HOLD_MS);
@@ -22145,23 +22895,51 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22145
22895
  if (release) clearTimeout(release);
22146
22896
  root.querySelectorAll(".met-tutorial-pressed").forEach((el) => el.classList.remove("met-tutorial-pressed"));
22147
22897
  };
22148
- }, [step, tapIndex, root]);
22898
+ }, [step, tapIndex, root, autoAdvance]);
22149
22899
  const text = options.texts?.[step.id] ?? step.text;
22150
22900
  const textRef = useRef(text);
22151
22901
  textRef.current = text;
22902
+ const onCloseRef = useRef(onClose);
22903
+ onCloseRef.current = onClose;
22904
+ const isLast = stepIndex === steps.length - 1;
22905
+ const [completedStep, setCompletedStep] = useState(null);
22152
22906
  useEffect(() => {
22153
22907
  optionsRef.current.onStep?.(stepIndex, step.id);
22154
- optionsRef.current.onSpeak?.(step.id, textRef.current);
22155
- }, [stepIndex, step]);
22156
- const isLast = stepIndex === steps.length - 1;
22908
+ const speech = new Promise((resolve2) => {
22909
+ resolve2(optionsRef.current.onSpeak?.(step.id, textRef.current));
22910
+ }).catch(() => {
22911
+ });
22912
+ if (!autoAdvance) return;
22913
+ const duration = Math.max(2, step.taps(ctxRef.current).length) * CYCLE_MS;
22914
+ let cancelled = false;
22915
+ let timer;
22916
+ const animation = new Promise((resolve2) => {
22917
+ timer = setTimeout(resolve2, duration);
22918
+ });
22919
+ let speechTimer;
22920
+ const speechLimit = new Promise((resolve2) => {
22921
+ speechTimer = setTimeout(resolve2, MAX_SPEECH_MS);
22922
+ });
22923
+ void Promise.all([Promise.race([speech, speechLimit]), animation]).then(() => {
22924
+ if (cancelled) return;
22925
+ if (isLast) {
22926
+ if (showBubble) setCompletedStep(stepIndex);
22927
+ else onCloseRef.current(true);
22928
+ } else setStepIndex((i) => i + 1);
22929
+ });
22930
+ return () => {
22931
+ cancelled = true;
22932
+ clearTimeout(timer);
22933
+ clearTimeout(speechTimer);
22934
+ };
22935
+ }, [stepIndex, step, autoAdvance, isLast, showBubble]);
22936
+ const showNext = !autoAdvance || isLast && completedStep === stepIndex;
22157
22937
  const next = () => {
22158
22938
  if (isLast) onClose(true);
22159
22939
  else setStepIndex((i) => i + 1);
22160
22940
  };
22161
22941
  const overlayRef = useRef(null);
22162
22942
  const nextRef = useRef(null);
22163
- const onCloseRef = useRef(onClose);
22164
- onCloseRef.current = onClose;
22165
22943
  useEffect(() => {
22166
22944
  const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22167
22945
  previous?.blur();
@@ -22194,15 +22972,15 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22194
22972
  }, []);
22195
22973
  const bubbleShown = layout !== null;
22196
22974
  useEffect(() => {
22197
- nextRef.current?.focus({ focusVisible: false });
22198
- }, [stepIndex, bubbleShown]);
22975
+ (nextRef.current ?? overlayRef.current)?.focus({ focusVisible: false });
22976
+ }, [stepIndex, bubbleShown, showNext]);
22199
22977
  const maskId = `met-tutorial-mask-${useId()}`;
22200
22978
  if (typeof document === "undefined") return null;
22201
22979
  const holes = (layout?.spotlight ?? []).filter((r) => r !== null);
22202
- const tapTarget = layout?.taps.length ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22980
+ const tapTarget = layout?.taps.length && (!autoAdvance || tapIndex < layout.taps.length) ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22203
22981
  const Scene = step.Scene;
22204
22982
  return createPortal(
22205
- /* @__PURE__ */ jsxs("div", { ref: overlayRef, className: "met-tutorial", role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22983
+ /* @__PURE__ */ jsxs("div", { ref: overlayRef, className: "met-tutorial", tabIndex: -1, role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22206
22984
  /* @__PURE__ */ jsxs("svg", { className: "met-tutorial-dim", "aria-hidden": "true", children: [
22207
22985
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", { id: maskId, children: [
22208
22986
  /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", fill: "white" }),
@@ -22237,7 +23015,7 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22237
23015
  )),
22238
23016
  Scene && layout && /* @__PURE__ */ jsx(Scene, { ctx, spotlight: layout.spotlight, taps: layout.taps, tapCount }),
22239
23017
  tapTarget && /* @__PURE__ */ jsx(Hand, { target: tapTarget, tapKey: `${stepIndex}-${tapIndex}` }),
22240
- layout && /* @__PURE__ */ jsx(
23018
+ layout && showBubble && /* @__PURE__ */ jsx(
22241
23019
  Bubble,
22242
23020
  {
22243
23021
  layout,
@@ -22245,7 +23023,8 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22245
23023
  stepIndex,
22246
23024
  stepCount: steps.length,
22247
23025
  nextLabel: options.nextLabel ?? DEFAULT_NEXT_LABEL,
22248
- onSpeak: options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
23026
+ showNext,
23027
+ onSpeak: !autoAdvance && options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22249
23028
  onNext: next,
22250
23029
  nextRef
22251
23030
  }
@@ -22273,12 +23052,12 @@ const Hand = ({ target, tapKey }) => {
22273
23052
  style: { left: TUTORIAL_HAND_TIP.x * HAND_W, top: TUTORIAL_HAND_TIP.y * HAND_H }
22274
23053
  }
22275
23054
  ),
22276
- /* @__PURE__ */ jsx(LionHand, {})
23055
+ /* @__PURE__ */ jsx(ChameleonHand, {})
22277
23056
  ] }, tapKey)
22278
23057
  }
22279
23058
  );
22280
23059
  };
22281
- const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext, nextRef }) => {
23060
+ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, showNext, onSpeak, onNext, nextRef }) => {
22282
23061
  const { anchor, root, vw, vh } = layout;
22283
23062
  const clampLeft = (x) => Math.min(Math.max(EDGE, x), Math.max(EDGE, vw - BUBBLE_W - EDGE));
22284
23063
  const style = anchor ? { left: clampLeft(anchor.x), bottom: Math.max(EDGE, vh - anchor.y + BUBBLE_GAP), width: BUBBLE_W } : { left: clampLeft((root?.x ?? 0) + EDGE * 2), top: (root?.y ?? 0) + EDGE * 2, width: BUBBLE_W };
@@ -22289,7 +23068,7 @@ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext
22289
23068
  /* @__PURE__ */ jsx("path", { d: "M4 9v6h4l5 4V5L8 9H4z", fill: "currentColor" }),
22290
23069
  /* @__PURE__ */ jsx("path", { d: "M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
22291
23070
  ] }) }),
22292
- /* @__PURE__ */ jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
23071
+ showNext && /* @__PURE__ */ jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22293
23072
  ] }),
22294
23073
  stepCount > 1 && /* @__PURE__ */ jsx("div", { className: "met-tutorial-dots", "aria-hidden": "true", children: Array.from({ length: stepCount }, (_, i) => /* @__PURE__ */ jsx("span", { className: i === stepIndex ? "is-current" : i < stepIndex ? "is-done" : "" }, i)) })
22295
23074
  ] });
@@ -22307,9 +23086,18 @@ function key(ctx, k) {
22307
23086
  }
22308
23087
  function answerBox(ctx) {
22309
23088
  const inputs = getTextInputs(ctx.root);
22310
- return inputs.find(canTakeInput) ?? inputs[0] ?? null;
23089
+ return inputs.find(canTakeInput) ?? inputs.find(canAdvanceTo) ?? null;
23090
+ }
23091
+ function answerDigits(ctx) {
23092
+ const id = answerBox(ctx)?.id.replace(/-input$/, "");
23093
+ const expected = id ? ctx.json.validation?.answers?.[id] : void 0;
23094
+ if (typeof expected !== "number" && typeof expected !== "string") return null;
23095
+ const value = String(expected);
23096
+ return /^\d+$/.test(value) ? [...value] : null;
22311
23097
  }
22312
23098
  function answerSlots(ctx) {
23099
+ const digits = answerDigits(ctx);
23100
+ if (digits) return digits.length;
22313
23101
  const id = answerBox(ctx)?.id.replace(/-input$/, "");
22314
23102
  const max = id ? getMaxInputLength(ctx.json, id) : void 0;
22315
23103
  return Math.min(MAX_SLOTS, Math.max(1, max ?? DEFAULT_SLOTS));
@@ -22325,40 +23113,49 @@ function checkTarget(ctx) {
22325
23113
  STAND_IN_CHECK_SIZE
22326
23114
  );
22327
23115
  }
22328
- const demoKeys = (ctx) => DEMO_KEYS.map((k) => key(ctx, k));
22329
- const StandInFill = ({ box, slots, filled }) => /* @__PURE__ */ jsx(
23116
+ const demoKeys = (ctx) => (answerDigits(ctx) ?? DEMO_KEYS).map((k) => key(ctx, k));
23117
+ const StandInFill = ({ box, slots, filled, digits }) => /* @__PURE__ */ jsx(
22330
23118
  "div",
22331
23119
  {
22332
23120
  className: "met-tutorial-fill",
23121
+ dir: "ltr",
22333
23122
  style: { left: box.x, top: box.y, width: box.width, height: box.height },
22334
- children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? "●" : "_" }, i))
23123
+ children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? digits?.[i] ?? "●" : "_" }, i))
22335
23124
  }
22336
23125
  );
22337
- const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22338
23126
  const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22339
23127
  const box = spotlight[1];
22340
23128
  if (!box) return null;
22341
23129
  const slots = answerSlots(ctx);
22342
- const filled = tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
23130
+ const digits = answerDigits(ctx);
23131
+ const filled = digits ? Math.min(tapCount, slots) : tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22343
23132
  const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
22344
23133
  let arc = null;
22345
23134
  if (from) {
22346
- const a = center(from);
22347
- const b = center(box);
23135
+ const a = centerOf(from);
23136
+ const b = centerOf(box);
22348
23137
  const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
22349
23138
  arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
22350
23139
  }
22351
23140
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22352
23141
  arc && /* @__PURE__ */ jsx("svg", { className: "met-tutorial-arc", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: arc }) }),
22353
- arc && /* @__PURE__ */ jsx("div", { className: "met-tutorial-hop", style: { offsetPath: `path('${arc}')` } }, tapCount),
22354
- /* @__PURE__ */ jsx(StandInFill, { box, slots, filled })
23142
+ arc && /* @__PURE__ */ jsx(
23143
+ "div",
23144
+ {
23145
+ className: `met-tutorial-hop${digits ? " met-tutorial-hop--digit" : ""}`,
23146
+ style: { offsetPath: `path('${arc}')` },
23147
+ children: digits?.[(tapCount - 1) % digits.length]
23148
+ },
23149
+ tapCount
23150
+ ),
23151
+ /* @__PURE__ */ jsx(StandInFill, { box, slots, filled, digits })
22355
23152
  ] });
22356
23153
  };
22357
23154
  const CheckScene = ({ ctx, spotlight }) => {
22358
23155
  const box = spotlight[0];
22359
23156
  const check = spotlight[1];
22360
23157
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22361
- box && /* @__PURE__ */ jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx) }),
23158
+ box && /* @__PURE__ */ jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx), digits: answerDigits(ctx) }),
22362
23159
  check && !ctx.submitTarget && /* @__PURE__ */ jsx(
22363
23160
  "div",
22364
23161
  {
@@ -22399,13 +23196,82 @@ const WIDGET_TUTORIALS = [keypadTutorial];
22399
23196
  function findWidgetTutorial(json) {
22400
23197
  return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
22401
23198
  }
23199
+ function hasMatchingLength(value, expected) {
23200
+ return (typeof expected === "string" || typeof expected === "number") && (typeof value === "string" || typeof value === "number") && String(value).length > 0 && String(value).length === String(expected).length;
23201
+ }
23202
+ function isSelectionGroups(value) {
23203
+ return Array.isArray(value) && value.every(
23204
+ (group) => Array.isArray(group) && group.length > 0 && group.every((id) => typeof id === "string")
23205
+ );
23206
+ }
23207
+ function hasMatchingSelectionCount(value, expected, itemIds, pairsOnly) {
23208
+ if (!isSelectionGroups(value) || !isSelectionGroups(expected)) return false;
23209
+ if (pairsOnly && (value.some((group) => group.length !== 2) || expected.some((group) => group.length !== 2)))
23210
+ return false;
23211
+ const selected = value.flat();
23212
+ const requiredCount = expected.flat().length;
23213
+ const declared = new Set(itemIds);
23214
+ return requiredCount > 0 && selected.length === requiredCount && new Set(selected).size === selected.length && selected.every((id) => declared.has(id));
23215
+ }
23216
+ function getAnswerLengthCompletion(json, answerable, values, touched) {
23217
+ const fields = [];
23218
+ const addTypedField = (id, widgetId, expected) => {
23219
+ const value = values[id];
23220
+ const hasKnownLength = typeof expected === "string" || typeof expected === "number";
23221
+ const hasScalarValue = typeof value === "string" || typeof value === "number";
23222
+ fields.push({
23223
+ id,
23224
+ widgetId,
23225
+ matches: hasMatchingLength(value, expected),
23226
+ // Unknown answer lengths use normal completion; known ones use grading's normalization.
23227
+ guideMatches: !hasKnownLength || touched.has(widgetId) && hasScalarValue && hasMatchingLength(normalize(value), normalize(expected))
23228
+ });
23229
+ };
23230
+ for (const widgetId of answerable) {
23231
+ const widget = json.widgets[widgetId];
23232
+ const expected = json.validation?.answers?.[widgetId];
23233
+ if (widget?.type === JSON_TYPES.INLINE_INPUT && !widget.disabled) {
23234
+ addTypedField(widgetId, widgetId, expected);
23235
+ } else if (widget?.type === JSON_TYPES.FRACTION) {
23236
+ const parts = typeof expected === "string" ? expected.split("/") : [];
23237
+ const validParts = parts.length === 2;
23238
+ if (widget.numerator === null) {
23239
+ const id = getFractionKey(widgetId, "num");
23240
+ addTypedField(id, widgetId, validParts ? parts[0] : void 0);
23241
+ }
23242
+ if (widget.denominator === null) {
23243
+ const id = getFractionKey(widgetId, "den");
23244
+ addTypedField(id, widgetId, validParts ? parts[1] : void 0);
23245
+ }
23246
+ } else if (widget?.type === JSON_TYPES.MATCHER || widget?.type === JSON_TYPES.PAIR_MATCHER) {
23247
+ const itemIds = widget.type === JSON_TYPES.MATCHER ? widget.rows.flatMap((row) => row.items.map((item) => item.itemId)) : widget.groups.flatMap((group) => group.map((item) => item.itemId));
23248
+ const matches = hasMatchingSelectionCount(
23249
+ values[widgetId],
23250
+ expected,
23251
+ itemIds,
23252
+ widget.type === JSON_TYPES.PAIR_MATCHER
23253
+ );
23254
+ fields.push({
23255
+ id: widgetId,
23256
+ widgetId,
23257
+ matches,
23258
+ guideMatches: expected == null || touched.has(widgetId) && matches
23259
+ });
23260
+ }
23261
+ }
23262
+ const answerLengthMatchedIds = fields.filter(({ widgetId, matches }) => touched.has(widgetId) && matches).map(({ id }) => id);
23263
+ return {
23264
+ areSubmitGuideCountsMatched: fields.every(({ guideMatches }) => guideMatches),
23265
+ isAnswerLengthMatched: fields.length > 0 && answerLengthMatchedIds.length === fields.length,
23266
+ answerLengthMatchedIds
23267
+ };
23268
+ }
22402
23269
  const TOOL_ASPECT_RATIO = {
22403
23270
  ruler: RULER_ASPECT_RATIO,
22404
23271
  protractor: PROTRACTOR_ASPECT_RATIO,
22405
23272
  "set-square": SET_SQUARE_ASPECT_RATIO
22406
23273
  };
22407
23274
  const FLIP_BTN_EXTRA = 44;
22408
- const INPUT_FINISH_IDLE_MS = 1e3;
22409
23275
  function warnOnInvalidThemeColors(theme) {
22410
23276
  if (process.env.NODE_ENV === "production" || !theme) return;
22411
23277
  if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
@@ -22421,16 +23287,15 @@ function buildInitialValues(json) {
22421
23287
  if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) {
22422
23288
  initial[id] = [];
22423
23289
  } else if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
22424
- initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
22425
- cell,
22426
- widget.rows,
22427
- widget.cols,
22428
- widget.interactiveArea
22429
- ));
23290
+ initial[id] = widget.cells.filter(
23291
+ (cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
23292
+ );
22430
23293
  } else if (widget.type === JSON_TYPES.CHART_BAR || widget.type === JSON_TYPES.CHART_PICTO) {
22431
23294
  initial[id] = String(widget.value);
22432
23295
  } else if (widget.type === JSON_TYPES.GEOMETRY) {
22433
23296
  initial[id] = serializeGeometryState(createEmptyGeometryAttempt());
23297
+ } else if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill !== void 0) {
23298
+ initial[id] = widget.prefill;
22434
23299
  }
22435
23300
  }
22436
23301
  return initial;
@@ -22443,9 +23308,6 @@ function hasJustFilledBox(json, key2, previous, value) {
22443
23308
  if (maxLength === void 0) return false;
22444
23309
  return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22445
23310
  }
22446
- function isTypedInputKey(json, key2) {
22447
- return json.widgets[key2]?.type === JSON_TYPES.INLINE_INPUT || Boolean(parseFractionKey(key2));
22448
- }
22449
23311
  function buildInitialValuesSignature(json) {
22450
23312
  return JSON.stringify({
22451
23313
  widgets: json.widgets,
@@ -22466,8 +23328,10 @@ function buildRevealedValues(json) {
22466
23328
  const widget = json.widgets[widgetId];
22467
23329
  if (widget?.type === JSON_TYPES.FRACTION && typeof expected === "string") {
22468
23330
  const [num, den] = expected.split("/", 2);
22469
- if (widget.numerator === null && num !== void 0) revealed[getFractionKey(widgetId, "num")] = num;
22470
- if (widget.denominator === null && den !== void 0) revealed[getFractionKey(widgetId, "den")] = den;
23331
+ if (widget.numerator === null && num !== void 0)
23332
+ revealed[getFractionKey(widgetId, "num")] = num;
23333
+ if (widget.denominator === null && den !== void 0)
23334
+ revealed[getFractionKey(widgetId, "den")] = den;
22471
23335
  continue;
22472
23336
  }
22473
23337
  revealed[widgetId] = typeof expected === "number" ? String(expected) : expected;
@@ -22499,490 +23363,571 @@ function buildRevealedDropSources(json, revealedValues) {
22499
23363
  const pool = [];
22500
23364
  const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
22501
23365
  if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
22502
- bank.options.forEach((value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) }));
23366
+ bank.options.forEach(
23367
+ (value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) })
23368
+ );
22503
23369
  }
22504
23370
  for (const [widgetId, widget] of Object.entries(json.widgets)) {
22505
- if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION) pool.push({ optionId: widgetId, value: widget.value });
23371
+ if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION)
23372
+ pool.push({ optionId: widgetId, value: widget.value });
22506
23373
  }
22507
23374
  const sources = /* @__PURE__ */ new Map();
22508
23375
  if (pool.length === 0) return sources;
22509
23376
  const used = /* @__PURE__ */ new Set();
22510
23377
  for (const [inputId, value] of Object.entries(revealedValues)) {
22511
23378
  if (typeof value !== "string" || value === "") continue;
22512
- const match = pool.find((o) => !used.has(o.optionId) && normalize(o.value) === normalize(value));
23379
+ if (json.widgets[inputId]?.type !== JSON_TYPES.INLINE_INPUT && !isFractionKey(inputId))
23380
+ continue;
23381
+ const match = pool.find(
23382
+ (o) => !used.has(o.optionId) && normalize(o.value) === normalize(value)
23383
+ );
22513
23384
  if (!match) continue;
22514
23385
  used.add(match.optionId);
22515
23386
  sources.set(inputId, match.optionId);
22516
23387
  }
22517
23388
  return sources;
22518
23389
  }
22519
- const ExerciseRenderer = forwardRef(({
22520
- json,
22521
- onSubmit,
22522
- onChange,
22523
- showSubmitButton = false,
22524
- className = "",
22525
- soundEnabled = true,
22526
- sounds,
22527
- theme
22528
- }, ref) => {
22529
- const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
22530
- const [values, setValues] = useState(() => buildInitialValues(json));
22531
- const valuesRef = useRef(values);
22532
- valuesRef.current = values;
22533
- const touchedRef = useRef(/* @__PURE__ */ new Set());
22534
- const pendingRef = useRef(/* @__PURE__ */ new Map());
22535
- const onChangeRef = useRef(onChange);
22536
- onChangeRef.current = onChange;
22537
- const clearPending = (key2) => {
22538
- const timer = pendingRef.current.get(key2);
22539
- if (timer === void 0) return false;
22540
- clearTimeout(timer);
22541
- pendingRef.current.delete(key2);
22542
- return true;
22543
- };
22544
- const clearAllPending = () => {
22545
- pendingRef.current.forEach((timer) => clearTimeout(timer));
22546
- pendingRef.current = /* @__PURE__ */ new Map();
22547
- };
22548
- const finishPending = (key2) => {
22549
- if (clearPending(key2)) onChangeRef.current?.(valuesRef.current);
22550
- };
22551
- useEffect(() => clearAllPending, []);
22552
- const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
22553
- const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
22554
- const [focusedInputId, setFocusedInputId] = useState(null);
22555
- const [validationDetails, setValidationDetails] = useState(null);
22556
- const rootRef = useRef(null);
22557
- const [resetKey, setResetKey] = useState(0);
22558
- const [revealed, setRevealed] = useState(false);
22559
- const tutorial = useMemo(() => findWidgetTutorial(json), [json]);
22560
- const [tutorialRun, setTutorialRun] = useState(null);
22561
- const themeStyle = useMemo(() => {
22562
- const style = {};
22563
- if (theme?.correct) style["--exercise-correct"] = theme.correct;
22564
- if (theme?.incorrect) style["--exercise-incorrect"] = theme.incorrect;
22565
- return Object.keys(style).length > 0 ? style : void 0;
22566
- }, [theme?.correct, theme?.incorrect]);
22567
- useEffect(() => {
22568
- warnOnInvalidThemeColors({ correct: theme?.correct, incorrect: theme?.incorrect });
22569
- }, [theme?.correct, theme?.incorrect]);
22570
- const playSound = useSoundPlayer(soundEnabled, sounds);
22571
- const externallyLocked = useExerciseLocked();
22572
- useLayoutEffect(() => {
22573
- if (loadedValuesSignature === initialValuesSignature) return;
22574
- setValues(buildInitialValues(json));
22575
- setDroppedSources(/* @__PURE__ */ new Map());
22576
- setFocusedInputId(null);
22577
- setValidationDetails(null);
22578
- setResetKey((k) => k + 1);
22579
- setLoadedValuesSignature(initialValuesSignature);
22580
- touchedRef.current = /* @__PURE__ */ new Set();
22581
- setTutorialRun(null);
22582
- clearAllPending();
22583
- }, [json, initialValuesSignature, loadedValuesSignature]);
22584
- const locked = validationDetails !== null || externallyLocked;
22585
- const lastResultRef = useRef(null);
22586
- const runSubmit = () => {
22587
- if (locked) return lastResultRef.current ?? validate(json, values);
22588
- clearAllPending();
22589
- const result = validate(json, values);
22590
- playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
22591
- const details = { ...result.details };
22592
- const slotValidation = (id, raw, expected) => ({
22593
- widgetId: id,
22594
- userValue: typeof raw === "string" ? raw : void 0,
22595
- isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
22596
- });
22597
- for (const [widgetId, widget] of Object.entries(json.widgets)) {
22598
- if (widget.type !== JSON_TYPES.FRACTION) continue;
22599
- if (!(widgetId in details)) continue;
22600
- const answer = json.validation?.answers?.[widgetId];
22601
- if (!answer) continue;
22602
- const [expectedNum, expectedDen] = answer.split("/", 2);
22603
- if (!expectedNum || !expectedDen) continue;
22604
- if (details[widgetId]?.isCorrect) {
22605
- if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
22606
- if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
22607
- } else {
22608
- if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
22609
- if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
22610
- }
22611
- }
22612
- setValidationDetails(details);
22613
- onSubmit?.(values);
22614
- lastResultRef.current = result;
22615
- return result;
22616
- };
22617
- useImperativeHandle(ref, () => ({
22618
- submit: runSubmit,
22619
- getValues: () => values,
22620
- getCompletion: () => {
22621
- const answerable = getAnswerableInputs(json);
22622
- const missingSet = new Set(getMissingInputs(json, valuesRef.current));
22623
- for (const id of answerable) {
22624
- if (!touchedRef.current.has(id)) missingSet.add(id);
22625
- }
22626
- const isAnyFilled = answerable.some((id) => !missingSet.has(id));
22627
- for (const key2 of pendingRef.current.keys()) {
22628
- const id = toBaseId(key2);
22629
- if (answerable.includes(id)) missingSet.add(id);
22630
- }
22631
- const missing = [...missingSet];
22632
- const filledIds = answerable.filter((id) => !missingSet.has(id));
22633
- return {
22634
- total: answerable.length,
22635
- filled: filledIds.length,
22636
- missing,
22637
- filledIds,
22638
- isComplete: answerable.length > 0 && missing.length === 0,
22639
- isAnyFilled
22640
- };
22641
- },
22642
- reset: () => {
22643
- const initial = buildInitialValues(json);
22644
- valuesRef.current = initial;
22645
- setValues(initial);
23390
+ const ExerciseRenderer = forwardRef(
23391
+ ({
23392
+ json: suppliedJson,
23393
+ onSubmit,
23394
+ onChange,
23395
+ showSubmitButton = false,
23396
+ className = "",
23397
+ soundEnabled = true,
23398
+ sounds,
23399
+ theme
23400
+ }, ref) => {
23401
+ const json = useMemo(() => withDisabledPrefilledInputs(suppliedJson), [suppliedJson]);
23402
+ const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
23403
+ const [values, setValues] = useState(
23404
+ () => buildInitialValues(json)
23405
+ );
23406
+ const valuesRef = useRef(values);
23407
+ valuesRef.current = values;
23408
+ const touchedRef = useRef(/* @__PURE__ */ new Set());
23409
+ const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
23410
+ const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
23411
+ const [focusedInputId, setFocusedInputId] = useState(null);
23412
+ const [validationDetails, setValidationDetails] = useState(null);
23413
+ const rootRef = useRef(null);
23414
+ const [resetKey, setResetKey] = useState(0);
23415
+ const [revealed, setRevealed] = useState(false);
23416
+ const tutorial = useMemo(() => findWidgetTutorial(json), [json]);
23417
+ const [tutorialRun, setTutorialRun] = useState(null);
23418
+ const themeStyle = useMemo(() => {
23419
+ const style = {};
23420
+ if (theme?.correct) style["--exercise-correct"] = theme.correct;
23421
+ if (theme?.incorrect) style["--exercise-incorrect"] = theme.incorrect;
23422
+ return Object.keys(style).length > 0 ? style : void 0;
23423
+ }, [theme?.correct, theme?.incorrect]);
23424
+ useEffect(() => {
23425
+ warnOnInvalidThemeColors({ correct: theme?.correct, incorrect: theme?.incorrect });
23426
+ }, [theme?.correct, theme?.incorrect]);
23427
+ const playSound = useSoundPlayer(soundEnabled, sounds);
23428
+ const externallyLocked = useExerciseLocked();
23429
+ useLayoutEffect(() => {
23430
+ if (loadedValuesSignature === initialValuesSignature) return;
23431
+ setValues(buildInitialValues(json));
22646
23432
  setDroppedSources(/* @__PURE__ */ new Map());
22647
23433
  setFocusedInputId(null);
22648
23434
  setValidationDetails(null);
22649
23435
  setResetKey((k) => k + 1);
22650
- setRevealed(false);
22651
23436
  setLoadedValuesSignature(initialValuesSignature);
22652
23437
  touchedRef.current = /* @__PURE__ */ new Set();
22653
- clearAllPending();
22654
- },
22655
- revealAnswers: () => {
22656
- const revealedValues = buildRevealedValues(json);
22657
- if (Object.keys(revealedValues).length === 0) return;
22658
- clearAllPending();
22659
- playSound("answer-reveal");
22660
- const details = {};
22661
- for (const [widgetId, userValue] of Object.entries(revealedValues)) {
22662
- const widget = json.widgets[widgetId];
22663
- if (widget?.type === JSON_TYPES.GEOMETRY) {
22664
- const { isCorrect, result } = checkGeometryWidget(widget.json, userValue, widgetId);
22665
- details[widgetId] = { widgetId, userValue, isCorrect, geometryFeedback: result };
23438
+ setTutorialRun(null);
23439
+ }, [json, initialValuesSignature, loadedValuesSignature]);
23440
+ const locked = validationDetails !== null || externallyLocked;
23441
+ const lastResultRef = useRef(null);
23442
+ const runSubmit = () => {
23443
+ if (locked) return lastResultRef.current ?? validate(json, values);
23444
+ const result = validate(json, values);
23445
+ playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
23446
+ const details = { ...result.details };
23447
+ const slotValidation = (id, raw, expected) => ({
23448
+ widgetId: id,
23449
+ userValue: typeof raw === "string" ? raw : void 0,
23450
+ isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
23451
+ });
23452
+ for (const [widgetId, widget] of Object.entries(json.widgets)) {
23453
+ if (widget.type !== JSON_TYPES.FRACTION) continue;
23454
+ if (!(widgetId in details)) continue;
23455
+ const answer = json.validation?.answers?.[widgetId];
23456
+ if (!answer) continue;
23457
+ const [expectedNum, expectedDen] = answer.split("/", 2);
23458
+ if (!expectedNum || !expectedDen) continue;
23459
+ if (details[widgetId]?.isCorrect) {
23460
+ if (widget.numerator === null)
23461
+ details[`${widgetId}__num`] = {
23462
+ widgetId: `${widgetId}__num`,
23463
+ userValue: void 0,
23464
+ isCorrect: true
23465
+ };
23466
+ if (widget.denominator === null)
23467
+ details[`${widgetId}__den`] = {
23468
+ widgetId: `${widgetId}__den`,
23469
+ userValue: void 0,
23470
+ isCorrect: true
23471
+ };
22666
23472
  } else {
22667
- details[widgetId] = { widgetId, userValue, isCorrect: true };
23473
+ if (widget.numerator === null)
23474
+ details[`${widgetId}__num`] = slotValidation(
23475
+ `${widgetId}__num`,
23476
+ values[`${widgetId}__num`],
23477
+ expectedNum
23478
+ );
23479
+ if (widget.denominator === null)
23480
+ details[`${widgetId}__den`] = slotValidation(
23481
+ `${widgetId}__den`,
23482
+ values[`${widgetId}__den`],
23483
+ expectedDen
23484
+ );
22668
23485
  }
23486
+ }
23487
+ setValidationDetails(details);
23488
+ onSubmit?.(values);
23489
+ lastResultRef.current = result;
23490
+ return result;
23491
+ };
23492
+ useImperativeHandle(
23493
+ ref,
23494
+ () => ({
23495
+ submit: runSubmit,
23496
+ getValues: () => values,
23497
+ getCompletion: () => {
23498
+ const answerable = getAnswerableInputs(json);
23499
+ const missingSet = new Set(getMissingInputs(json, valuesRef.current));
23500
+ for (const id of answerable) {
23501
+ if (!touchedRef.current.has(id)) missingSet.add(id);
23502
+ }
23503
+ const missing = [...missingSet];
23504
+ const filledIds = answerable.filter((id) => !missingSet.has(id));
23505
+ const isComplete = answerable.length > 0 && missing.length === 0;
23506
+ const { areSubmitGuideCountsMatched, ...answerLengthCompletion } = getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current);
23507
+ return {
23508
+ total: answerable.length,
23509
+ filled: filledIds.length,
23510
+ missing,
23511
+ filledIds,
23512
+ isComplete,
23513
+ isSubmitGuideReady: isComplete && areSubmitGuideCountsMatched,
23514
+ isAnyFilled: filledIds.length > 0,
23515
+ ...answerLengthCompletion
23516
+ };
23517
+ },
23518
+ reset: () => {
23519
+ const initial = buildInitialValues(json);
23520
+ valuesRef.current = initial;
23521
+ setValues(initial);
23522
+ setDroppedSources(/* @__PURE__ */ new Map());
23523
+ setFocusedInputId(null);
23524
+ setValidationDetails(null);
23525
+ setResetKey((k) => k + 1);
23526
+ setRevealed(false);
23527
+ setLoadedValuesSignature(initialValuesSignature);
23528
+ touchedRef.current = /* @__PURE__ */ new Set();
23529
+ },
23530
+ revealAnswers: () => {
23531
+ const revealedValues = buildRevealedValues(json);
23532
+ if (Object.keys(revealedValues).length === 0) return;
23533
+ playSound("answer-reveal");
23534
+ const details = {};
23535
+ for (const [widgetId, userValue] of Object.entries(revealedValues)) {
23536
+ const widget = json.widgets[widgetId];
23537
+ if (widget?.type === JSON_TYPES.GEOMETRY) {
23538
+ const { isCorrect, result } = checkGeometryWidget(widget.json, userValue, widgetId);
23539
+ details[widgetId] = { widgetId, userValue, isCorrect, geometryFeedback: result };
23540
+ } else {
23541
+ details[widgetId] = { widgetId, userValue, isCorrect: true };
23542
+ }
23543
+ touchedRef.current.add(toBaseId(widgetId));
23544
+ }
23545
+ const next = { ...valuesRef.current, ...revealedValues };
23546
+ valuesRef.current = next;
23547
+ setValues(next);
23548
+ setDroppedSources(buildRevealedDropSources(json, revealedValues));
23549
+ setValidationDetails(details);
23550
+ setRevealed(true);
23551
+ setResetKey((k) => k + 1);
23552
+ },
23553
+ focusFirstInput: () => {
23554
+ const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
23555
+ if (!container) return;
23556
+ const candidates = getTextInputs(container);
23557
+ const target = candidates.find(canTakeInput) ?? // A locked exercise is read-only on purpose — nothing to focus until
23558
+ // reset(). Unlocked, a read-only field is not the lock, so refusing to
23559
+ // focus it would strand the student with no caret; take the first one
23560
+ // that isn't disabled or a chip's invisible SlotSizer.
23561
+ (locked ? void 0 : candidates.find(canAdvanceTo));
23562
+ target?.focus();
23563
+ },
23564
+ hasTutorial: () => tutorial !== null,
23565
+ tutorialId: () => tutorial?.id ?? null,
23566
+ startTutorial: (options = {}) => {
23567
+ if (!tutorial || !rootRef.current) return false;
23568
+ setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
23569
+ return true;
23570
+ },
23571
+ stopTutorial: () => setTutorialRun(null)
23572
+ }),
23573
+ [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]
23574
+ );
23575
+ const focusNextInput = (currentKey) => {
23576
+ if (!rootRef.current) return;
23577
+ findNextInput(rootRef.current, currentKey)?.focus();
23578
+ };
23579
+ const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
23580
+ if (locked) return;
23581
+ if (!silent) {
23582
+ setRevealed(false);
22669
23583
  touchedRef.current.add(toBaseId(widgetId));
22670
23584
  }
22671
- const next = { ...valuesRef.current, ...revealedValues };
23585
+ const previous = valuesRef.current[widgetId];
23586
+ const next = { ...valuesRef.current, [widgetId]: value };
22672
23587
  valuesRef.current = next;
22673
23588
  setValues(next);
22674
- setDroppedSources(buildRevealedDropSources(json, revealedValues));
22675
- setValidationDetails(details);
22676
- setRevealed(true);
22677
- setResetKey((k) => k + 1);
22678
- },
22679
- focusFirstInput: () => {
22680
- const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
22681
- if (!container) return;
22682
- const candidates = getTextInputs(container);
22683
- const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
22684
- target?.focus();
22685
- },
22686
- hasTutorial: () => tutorial !== null,
22687
- tutorialId: () => tutorial?.id ?? null,
22688
- startTutorial: (options = {}) => {
22689
- if (!tutorial || !rootRef.current) return false;
22690
- setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
22691
- return true;
22692
- },
22693
- stopTutorial: () => setTutorialRun(null)
22694
- }), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
22695
- const focusNextInput = (currentKey) => {
22696
- if (!rootRef.current) return;
22697
- findNextInput(rootRef.current, currentKey)?.focus();
22698
- };
22699
- const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
22700
- if (locked) return;
22701
- if (!silent) {
22702
- setRevealed(false);
22703
- touchedRef.current.add(toBaseId(widgetId));
22704
- }
22705
- const previous = valuesRef.current[widgetId];
22706
- const next = { ...valuesRef.current, [widgetId]: value };
22707
- valuesRef.current = next;
22708
- setValues(next);
22709
- setDroppedSources((prev) => {
22710
- const next2 = new Map(prev);
22711
- if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
22712
- next2.set(widgetId, sourceOptionId);
22713
- } else {
22714
- next2.delete(widgetId);
22715
- }
22716
- return next2;
22717
- });
22718
- if (!silent && isTypedInputKey(json, widgetId)) {
22719
- clearPending(widgetId);
22720
- const maxLength = getMaxInputLength(json, widgetId);
22721
- const isFull = maxLength !== void 0 && String(value).length >= maxLength;
22722
- if (!fromDrop && value !== "" && !isFull) {
22723
- pendingRef.current.set(
22724
- widgetId,
22725
- setTimeout(() => finishPending(widgetId), INPUT_FINISH_IDLE_MS)
22726
- );
22727
- }
22728
- }
22729
- onChange?.(next);
22730
- if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22731
- focusNextInput(widgetId);
22732
- }
22733
- };
22734
- const handleInputFocus = (key2) => {
22735
- for (const pendingKey of [...pendingRef.current.keys()]) {
22736
- if (pendingKey !== key2) finishPending(pendingKey);
22737
- }
22738
- setFocusedInputId(key2);
22739
- };
22740
- const handleSubmit = () => {
22741
- runSubmit();
22742
- };
22743
- const lineGroups = useMemo(() => {
22744
- const isRTLInlineWidget = (item) => {
22745
- if (item.type === JSON_TYPES.WIDGET_REF) {
22746
- const widget = json.widgets[item.widgetId];
22747
- return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
23589
+ setDroppedSources((prev) => {
23590
+ const next2 = new Map(prev);
23591
+ if (fromDrop && typeof value === "string" && value !== "" && sourceOptionId) {
23592
+ next2.set(widgetId, sourceOptionId);
23593
+ } else {
23594
+ next2.delete(widgetId);
23595
+ }
23596
+ return next2;
23597
+ });
23598
+ onChange?.(next);
23599
+ if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
23600
+ focusNextInput(widgetId);
22748
23601
  }
22749
- return RTL_INLINE_WIDGET_TYPES.has(item.type);
22750
23602
  };
22751
- const groups = [];
22752
- let currentGroup = [];
22753
- for (const item of json.layout) {
22754
- if (item.type === JSON_TYPES.QUESTION_TEXT) {
22755
- groups.push({
22756
- items: [item],
22757
- hasArabic: itemContainsArabic(item),
22758
- hasRTLInlineWidget: false,
22759
- subGroups: []
22760
- // Question text doesn't need sub-grouping
22761
- });
22762
- continue;
22763
- }
22764
- if (item.type === JSON_TYPES.TABLE) {
22765
- if (currentGroup.length > 0) {
22766
- const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
22767
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22768
- const subGroups = groupLineItems(currentGroup);
22769
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22770
- currentGroup = [];
22771
- }
22772
- groups.push({
22773
- items: [item],
22774
- hasArabic: false,
22775
- hasRTLInlineWidget: false,
22776
- subGroups: []
22777
- });
22778
- continue;
22779
- }
22780
- if (item.type === JSON_TYPES.NUMBER_LINE) {
22781
- if (currentGroup.length > 0) {
22782
- const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
22783
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22784
- const subGroups = groupLineItems(currentGroup);
22785
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22786
- currentGroup = [];
22787
- }
22788
- groups.push({
22789
- items: [item],
22790
- hasArabic: false,
22791
- hasRTLInlineWidget: false,
22792
- subGroups: []
22793
- });
22794
- continue;
22795
- }
22796
- if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
22797
- if (currentGroup.length > 0) {
22798
- const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
22799
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22800
- const subGroups = groupLineItems(currentGroup);
22801
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22802
- currentGroup = [];
22803
- }
22804
- groups.push({
22805
- items: [item],
22806
- hasArabic: false,
22807
- hasRTLInlineWidget: false,
22808
- subGroups: []
22809
- });
22810
- continue;
22811
- }
22812
- if (item.type === JSON_TYPES.CHART) {
22813
- if (currentGroup.length > 0) {
22814
- const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
22815
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22816
- const subGroups = groupLineItems(currentGroup);
22817
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22818
- currentGroup = [];
22819
- }
22820
- groups.push({
22821
- items: [item],
22822
- hasArabic: false,
22823
- hasRTLInlineWidget: false,
22824
- subGroups: []
22825
- });
22826
- continue;
22827
- }
22828
- if (item.type === JSON_TYPES.LINEBREAK) {
22829
- if (currentGroup.length > 0) {
22830
- const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
22831
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22832
- const subGroups = groupLineItems(currentGroup);
22833
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22834
- currentGroup = [];
22835
- }
22836
- } else {
22837
- currentGroup.push(item);
22838
- }
22839
- }
22840
- if (currentGroup.length > 0) {
22841
- const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
22842
- const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
22843
- const subGroups = groupLineItems(currentGroup);
22844
- groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
22845
- }
22846
- return groups;
22847
- }, [json.layout, json.widgets]);
22848
- const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
22849
- const dragOptionsWidget = json.widgets["_drag_options"];
22850
- const rulerWidget = json.widgets[RULER_WIDGET_ID];
22851
- const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
22852
- const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
22853
- const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
22854
- const { tools, toolsMinHeight } = useMemo(() => {
22855
- const list = [];
22856
- const PAD = 12;
22857
- const STAGGER = 24;
22858
- let maxH = 0;
22859
- const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
22860
- const add2 = (id, type, width) => {
22861
- const offset = list.length * STAGGER;
22862
- list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
22863
- const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
22864
- const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
22865
- if (h > maxH) maxH = h;
23603
+ const handleSubmit = () => {
23604
+ runSubmit();
22866
23605
  };
22867
- if (rulerWidget) add2("ruler-1", "ruler", Math.round(getSize(rulerWidget) * 240));
22868
- if (protractorWidget) add2("protractor-1", "protractor", Math.round(getSize(protractorWidget) * 180));
22869
- if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
22870
- return { tools: list, toolsMinHeight: maxH };
22871
- }, [rulerWidget, protractorWidget, setSquareWidget]);
22872
- return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22873
- /* @__PURE__ */ jsxs(
22874
- "div",
23606
+ const lineGroups = useMemo(() => {
23607
+ const isRTLInlineWidget = (item) => {
23608
+ if (item.type === JSON_TYPES.WIDGET_REF) {
23609
+ const widget = json.widgets[item.widgetId];
23610
+ return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
23611
+ }
23612
+ return RTL_INLINE_WIDGET_TYPES.has(item.type);
23613
+ };
23614
+ const groups = [];
23615
+ let currentGroup = [];
23616
+ for (const item of json.layout) {
23617
+ if (item.type === JSON_TYPES.QUESTION_TEXT) {
23618
+ groups.push({
23619
+ items: [item],
23620
+ hasArabic: itemContainsArabic(item),
23621
+ hasRTLInlineWidget: false,
23622
+ subGroups: []
23623
+ // Question text doesn't need sub-grouping
23624
+ });
23625
+ continue;
23626
+ }
23627
+ if (item.type === JSON_TYPES.TABLE) {
23628
+ if (currentGroup.length > 0) {
23629
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
23630
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23631
+ const subGroups = groupLineItems(currentGroup);
23632
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23633
+ currentGroup = [];
23634
+ }
23635
+ groups.push({
23636
+ items: [item],
23637
+ hasArabic: false,
23638
+ hasRTLInlineWidget: false,
23639
+ subGroups: []
23640
+ });
23641
+ continue;
23642
+ }
23643
+ if (item.type === JSON_TYPES.NUMBER_LINE) {
23644
+ if (currentGroup.length > 0) {
23645
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
23646
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23647
+ const subGroups = groupLineItems(currentGroup);
23648
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23649
+ currentGroup = [];
23650
+ }
23651
+ groups.push({
23652
+ items: [item],
23653
+ hasArabic: false,
23654
+ hasRTLInlineWidget: false,
23655
+ subGroups: []
23656
+ });
23657
+ continue;
23658
+ }
23659
+ if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
23660
+ if (currentGroup.length > 0) {
23661
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
23662
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23663
+ const subGroups = groupLineItems(currentGroup);
23664
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23665
+ currentGroup = [];
23666
+ }
23667
+ groups.push({
23668
+ items: [item],
23669
+ hasArabic: false,
23670
+ hasRTLInlineWidget: false,
23671
+ subGroups: []
23672
+ });
23673
+ continue;
23674
+ }
23675
+ if (item.type === JSON_TYPES.CHART) {
23676
+ if (currentGroup.length > 0) {
23677
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
23678
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23679
+ const subGroups = groupLineItems(currentGroup);
23680
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23681
+ currentGroup = [];
23682
+ }
23683
+ groups.push({
23684
+ items: [item],
23685
+ hasArabic: false,
23686
+ hasRTLInlineWidget: false,
23687
+ subGroups: []
23688
+ });
23689
+ continue;
23690
+ }
23691
+ if (item.type === JSON_TYPES.LINEBREAK) {
23692
+ if (currentGroup.length > 0) {
23693
+ const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
23694
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23695
+ const subGroups = groupLineItems(currentGroup);
23696
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23697
+ currentGroup = [];
23698
+ }
23699
+ } else {
23700
+ currentGroup.push(item);
23701
+ }
23702
+ }
23703
+ if (currentGroup.length > 0) {
23704
+ const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
23705
+ const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
23706
+ const subGroups = groupLineItems(currentGroup);
23707
+ groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
23708
+ }
23709
+ return groups;
23710
+ }, [json.layout, json.widgets]);
23711
+ const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
23712
+ const dragOptionsWidget = json.widgets["_drag_options"];
23713
+ const draggableOptionsWidget = json.widgets[DRAGGABLE_OPTIONS_WIDGET_ID];
23714
+ const rulerWidget = json.widgets[RULER_WIDGET_ID];
23715
+ const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
23716
+ const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
23717
+ const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
23718
+ const { tools, toolsMinHeight } = useMemo(() => {
23719
+ const list = [];
23720
+ const PAD = 12;
23721
+ const STAGGER = 24;
23722
+ let maxH = 0;
23723
+ const getSize = (w) => typeof w === "object" && w !== null && "size" in w && typeof w.size === "number" ? w.size : 1;
23724
+ const add2 = (id, type, width) => {
23725
+ const offset = list.length * STAGGER;
23726
+ list.push({ id, type, width, initialPosition: { x: PAD + offset, y: PAD + offset } });
23727
+ const extra = type === "set-square" ? FLIP_BTN_EXTRA : 0;
23728
+ const h = PAD + Math.ceil(width * (TOOL_ASPECT_RATIO[type] ?? 0)) + extra + PAD;
23729
+ if (h > maxH) maxH = h;
23730
+ };
23731
+ if (rulerWidget) add2("ruler-1", "ruler", Math.round(getSize(rulerWidget) * 240));
23732
+ if (protractorWidget)
23733
+ add2("protractor-1", "protractor", Math.round(getSize(protractorWidget) * 180));
23734
+ if (setSquareWidget)
23735
+ add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
23736
+ return { tools: list, toolsMinHeight: maxH };
23737
+ }, [rulerWidget, protractorWidget, setSquareWidget]);
23738
+ return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(
23739
+ ExerciseDndProvider,
22875
23740
  {
22876
- className: `exercise-layout-container ${numericKeypadWidget ? "has-keypad" : ""}`,
22877
- style: toolsMinHeight > 0 ? { minHeight: toolsMinHeight } : void 0,
22878
- children: [
22879
- /* @__PURE__ */ jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsx(
22880
- "div",
22881
- {
22882
- className: "exercise-line",
22883
- dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
22884
- children: group.subGroups.length > 0 ? (
22885
- // Render sub-groups for regular lines
22886
- group.subGroups.map((subGroup, subIdx) => subGroup.isTextGroup ? (
22887
- // Text group - only apply RTL for pure Arabic (no mixed languages)
22888
- /* @__PURE__ */ jsx("span", { className: "text-group", dir: subGroup.shouldApplyRTL ? "rtl" : void 0, children: subGroup.items.map((item) => /* @__PURE__ */ jsx(
22889
- LayoutRenderer,
22890
- {
22891
- item,
22892
- widgets: json.widgets,
22893
- values,
22894
- droppedInputs,
22895
- validationDetails,
22896
- onChange: handleChange,
22897
- onInputFocus: handleInputFocus,
22898
- resetKey
22899
- },
22900
- item.id
22901
- )) }, subIdx)
22902
- ) : (
22903
- // Widget group - no dir wrapper, render directly
22904
- subGroup.items.map((item) => /* @__PURE__ */ jsx(
22905
- LayoutRenderer,
22906
- {
22907
- item,
22908
- widgets: json.widgets,
22909
- values,
22910
- droppedInputs,
22911
- validationDetails,
22912
- onChange: handleChange,
22913
- onInputFocus: handleInputFocus,
22914
- resetKey
22915
- },
22916
- item.id
22917
- ))
22918
- ))
22919
- ) : (
22920
- // For QUESTION_TEXT items (no sub-groups)
22921
- group.items.map((item) => /* @__PURE__ */ jsx(
22922
- LayoutRenderer,
23741
+ droppedSources,
23742
+ exerciseKey: loadedValuesSignature,
23743
+ renderOverlay: (item) => {
23744
+ if (item.type !== "rich-drag-option" || draggableOptionsWidget?.type !== JSON_TYPES.DRAGGABLE_OPTIONS) {
23745
+ return null;
23746
+ }
23747
+ const data = item.data;
23748
+ const option = draggableOptionsWidget.options.find(
23749
+ (candidate) => candidate.name === data.optionName
23750
+ );
23751
+ return option ? (
23752
+ // The overlay renders outside the root below, so it needs the root's class
23753
+ // and theme style for nested widgets to resolve the --exercise-* variables.
23754
+ /* @__PURE__ */ jsx(
23755
+ "div",
23756
+ {
23757
+ className: `math-exercise-renderer rich-drag-overlay-root ${className}`,
23758
+ style: themeStyle,
23759
+ "aria-hidden": "true",
23760
+ children: /* @__PURE__ */ jsx(
23761
+ "div",
22923
23762
  {
22924
- item,
22925
- widgets: json.widgets,
22926
- values,
22927
- droppedInputs,
22928
- validationDetails,
22929
- onChange: handleChange,
22930
- onInputFocus: handleInputFocus,
22931
- resetKey
22932
- },
22933
- item.id
22934
- ))
22935
- )
22936
- },
22937
- groupIdx
22938
- )) }),
22939
- numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsx(
22940
- WidgetRenderer,
22941
- {
22942
- widget: numericKeypadWidget,
22943
- widgetId: "_numeric_keypad",
22944
- value: focusedInputId ? values[focusedInputId] || "" : "",
22945
- onChange: () => {
22946
- },
22947
- onKeypadChange: (newValue) => {
22948
- if (!focusedInputId) return;
22949
- const maxLength = getMaxInputLength(json, focusedInputId);
22950
- if (maxLength !== void 0 && newValue.length > maxLength) return;
22951
- handleChange(focusedInputId, newValue, false);
23763
+ className: `drag-overlay-option rich-drag-overlay-option${isPlainTextContent(option.content) ? "" : " drag-overlay-option--rich"}`,
23764
+ children: /* @__PURE__ */ jsx(RichOptionContent, { content: option.content, widgets: json.widgets })
23765
+ }
23766
+ )
22952
23767
  }
22953
- }
22954
- ) }),
22955
- tools.length > 0 && /* @__PURE__ */ jsx(ToolsOverlay, { tools, resetKey })
22956
- ]
22957
- }
22958
- ),
22959
- dragOptionsWidget && dragOptionsWidget.type === "drag-options" && /* @__PURE__ */ jsx("div", { className: "drag-options-container", children: /* @__PURE__ */ jsx(
22960
- WidgetRenderer,
22961
- {
22962
- widget: dragOptionsWidget,
22963
- widgetId: "_drag_options",
22964
- value: "",
22965
- onChange: () => {
22966
- }
23768
+ )
23769
+ ) : null;
23770
+ },
23771
+ children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsx(DragBankContext.Provider, { value: json.widgets[DRAG_OPTIONS_WIDGET_ID] !== void 0, children: /* @__PURE__ */ jsxs(
23772
+ "div",
23773
+ {
23774
+ ref: rootRef,
23775
+ className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`,
23776
+ style: themeStyle,
23777
+ children: [
23778
+ /* @__PURE__ */ jsxs(
23779
+ "div",
23780
+ {
23781
+ className: `exercise-layout-container ${numericKeypadWidget ? "has-keypad" : ""}`,
23782
+ style: toolsMinHeight > 0 ? { minHeight: toolsMinHeight } : void 0,
23783
+ children: [
23784
+ /* @__PURE__ */ jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsx(
23785
+ "div",
23786
+ {
23787
+ className: "exercise-line",
23788
+ dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
23789
+ children: group.subGroups.length > 0 ? (
23790
+ // Render sub-groups for regular lines
23791
+ group.subGroups.map(
23792
+ (subGroup, subIdx) => subGroup.isTextGroup ? (
23793
+ // Text group - only apply RTL for pure Arabic (no mixed languages)
23794
+ /* @__PURE__ */ jsx(
23795
+ "span",
23796
+ {
23797
+ className: "text-group",
23798
+ dir: subGroup.shouldApplyRTL ? "rtl" : void 0,
23799
+ children: subGroup.items.map((item) => /* @__PURE__ */ jsx(
23800
+ LayoutRenderer,
23801
+ {
23802
+ item,
23803
+ widgets: json.widgets,
23804
+ values,
23805
+ droppedInputs,
23806
+ validationDetails,
23807
+ onChange: handleChange,
23808
+ onInputFocus: setFocusedInputId,
23809
+ resetKey
23810
+ },
23811
+ item.id
23812
+ ))
23813
+ },
23814
+ subIdx
23815
+ )
23816
+ ) : (
23817
+ // Widget group - no dir wrapper, render directly
23818
+ subGroup.items.map((item) => /* @__PURE__ */ jsx(
23819
+ LayoutRenderer,
23820
+ {
23821
+ item,
23822
+ widgets: json.widgets,
23823
+ values,
23824
+ droppedInputs,
23825
+ validationDetails,
23826
+ onChange: handleChange,
23827
+ onInputFocus: setFocusedInputId,
23828
+ resetKey
23829
+ },
23830
+ item.id
23831
+ ))
23832
+ )
23833
+ )
23834
+ ) : (
23835
+ // For QUESTION_TEXT items (no sub-groups)
23836
+ group.items.map((item) => /* @__PURE__ */ jsx(
23837
+ LayoutRenderer,
23838
+ {
23839
+ item,
23840
+ widgets: json.widgets,
23841
+ values,
23842
+ droppedInputs,
23843
+ validationDetails,
23844
+ onChange: handleChange,
23845
+ onInputFocus: setFocusedInputId,
23846
+ resetKey
23847
+ },
23848
+ item.id
23849
+ ))
23850
+ )
23851
+ },
23852
+ groupIdx
23853
+ )) }),
23854
+ numericKeypadWidget && numericKeypadWidget.type === "numeric-keypad" && /* @__PURE__ */ jsx("div", { className: "numeric-keypad-container", children: /* @__PURE__ */ jsx(
23855
+ WidgetRenderer,
23856
+ {
23857
+ widget: numericKeypadWidget,
23858
+ widgetId: "_numeric_keypad",
23859
+ value: focusedInputId ? values[focusedInputId] || "" : "",
23860
+ onChange: () => {
23861
+ },
23862
+ onKeypadChange: (newValue) => {
23863
+ if (!focusedInputId) return;
23864
+ const maxLength = getMaxInputLength(json, focusedInputId);
23865
+ const currentLength = String(values[focusedInputId] ?? "").length;
23866
+ if (maxLength !== void 0 && newValue.length > maxLength && newValue.length > currentLength)
23867
+ return;
23868
+ handleChange(focusedInputId, newValue, false);
23869
+ }
23870
+ }
23871
+ ) }),
23872
+ tools.length > 0 && /* @__PURE__ */ jsx(ToolsOverlay, { tools, resetKey })
23873
+ ]
23874
+ }
23875
+ ),
23876
+ dragOptionsWidget && dragOptionsWidget.type === "drag-options" && /* @__PURE__ */ jsx("div", { className: "drag-options-container", children: /* @__PURE__ */ jsx(
23877
+ WidgetRenderer,
23878
+ {
23879
+ widget: dragOptionsWidget,
23880
+ widgetId: "_drag_options",
23881
+ value: "",
23882
+ onChange: () => {
23883
+ }
23884
+ }
23885
+ ) }),
23886
+ draggableOptionsWidget && draggableOptionsWidget.type === JSON_TYPES.DRAGGABLE_OPTIONS && // Keeps the legacy class too: host CSS uses :has(.drag-options-container) to
23887
+ // tell drag exercises from typed ones.
23888
+ /* @__PURE__ */ jsx("div", { className: "drag-options-container draggable-options-container", children: /* @__PURE__ */ jsx(
23889
+ WidgetRenderer,
23890
+ {
23891
+ widget: draggableOptionsWidget,
23892
+ widgetId: DRAGGABLE_OPTIONS_WIDGET_ID,
23893
+ value: "",
23894
+ values,
23895
+ widgets: json.widgets,
23896
+ onChange: () => {
23897
+ }
23898
+ }
23899
+ ) }),
23900
+ showSubmitButton && /* @__PURE__ */ jsx(
23901
+ "button",
23902
+ {
23903
+ className: "exercise-submit-btn",
23904
+ disabled: locked,
23905
+ onClick: handleSubmit,
23906
+ children: "Check Answer"
23907
+ }
23908
+ ),
23909
+ tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsx(
23910
+ TutorialOverlay,
23911
+ {
23912
+ tutorial,
23913
+ root: rootRef.current,
23914
+ json,
23915
+ options: tutorialRun.options,
23916
+ onClose: (finished) => {
23917
+ setTutorialRun(null);
23918
+ tutorialRun.options.onClose?.();
23919
+ if (finished) tutorialRun.options.onDone?.();
23920
+ }
23921
+ },
23922
+ tutorialRun.run
23923
+ )
23924
+ ]
23925
+ }
23926
+ ) }) })
22967
23927
  }
22968
- ) }),
22969
- showSubmitButton && /* @__PURE__ */ jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" }),
22970
- tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsx(
22971
- TutorialOverlay,
22972
- {
22973
- tutorial,
22974
- root: rootRef.current,
22975
- json,
22976
- options: tutorialRun.options,
22977
- onClose: (finished) => {
22978
- setTutorialRun(null);
22979
- if (finished) tutorialRun.options.onDone?.();
22980
- }
22981
- },
22982
- tutorialRun.run
22983
- )
22984
- ] }) }) }) }) });
22985
- });
23928
+ ) }) });
23929
+ }
23930
+ );
22986
23931
  ExerciseRenderer.displayName = "ExerciseRenderer";
22987
23932
  function renderExercise(options) {
22988
23933
  const container = typeof options.container === "string" ? document.querySelector(options.container) : options.container;
@@ -23020,7 +23965,7 @@ function compileWithAST(dsl) {
23020
23965
  return { ast, json };
23021
23966
  }
23022
23967
  export {
23023
- isImageItem as $,
23968
+ isTableRowItem as $,
23024
23969
  AST_TYPES as A,
23025
23970
  isQuestionText as B,
23026
23971
  isTextItem$1 as C,
@@ -23036,275 +23981,291 @@ export {
23036
23981
  isGroupItem as M,
23037
23982
  isOperatorItem as N,
23038
23983
  isInlineInputJSON as O,
23039
- isDragOptionsJSON as P,
23040
- isInlineDragOptionJSON as Q,
23041
- isRadioOptionsJSON as R,
23042
- isNumericKeypadJSON as S,
23043
- isFractionJSON as T,
23044
- isGridSymmetryJSON as U,
23045
- isChartItem as V,
23046
- isChartBarWidgetJSON as W,
23047
- isChartPictoWidgetJSON as X,
23048
- isTableItem as Y,
23049
- isTableRowItem as Z,
23050
- isTableCellItem as _,
23984
+ isDropInputJSON as P,
23985
+ isDragOptionsJSON as Q,
23986
+ isDraggableOptionsJSON as R,
23987
+ isInlineDragOptionJSON as S,
23988
+ isRadioOptionsJSON as T,
23989
+ isNumericKeypadJSON as U,
23990
+ isFractionJSON as V,
23991
+ isGridSymmetryJSON as W,
23992
+ isChartItem as X,
23993
+ isChartBarWidgetJSON as Y,
23994
+ isChartPictoWidgetJSON as Z,
23995
+ isTableItem as _,
23051
23996
  compileWithAST as a,
23052
- validateExerciseJSONSafe as a$,
23053
- isVerticalOperationItem as a0,
23054
- isClockItem as a1,
23055
- isRulerWidget as a2,
23056
- isProtractorWidget as a3,
23057
- isSetSquareWidget as a4,
23058
- isGeometryJSON as a5,
23059
- ExerciseJSONSchema as a6,
23060
- ExerciseMetaSchema as a7,
23061
- ValidationJSONSchema as a8,
23062
- LayoutItemSchema as a9,
23063
- VopColorCellItemSchema as aA,
23064
- VopBoldCellItemSchema as aB,
23065
- VopHighlightCellItemSchema as aC,
23066
- ClockItemSchema as aD,
23067
- ChartItemSchema as aE,
23068
- ChartAxisJSONSchema as aF,
23069
- ChartBarJSONSchema as aG,
23070
- ChartPictoJSONSchema as aH,
23071
- ChartSeriesJSONSchema as aI,
23072
- ChartSeriesBarJSONSchema as aJ,
23073
- ChartGroupJSONSchema as aK,
23074
- WidgetJSONSchema as aL,
23075
- InlineInputJSONSchema as aM,
23076
- DragOptionsJSONSchema as aN,
23077
- RadioOptionsJSONSchema as aO,
23078
- NumericKeypadJSONSchema as aP,
23079
- FractionJSONSchema as aQ,
23080
- MatcherWidgetJSONSchema as aR,
23081
- PairMatcherWidgetJSONSchema as aS,
23082
- GridSymmetryJSONSchema as aT,
23083
- ChartBarWidgetJSONSchema as aU,
23084
- ChartPictoWidgetJSONSchema as aV,
23085
- RulerWidgetSchema as aW,
23086
- ProtractorWidgetSchema as aX,
23087
- SetSquareWidgetSchema as aY,
23088
- GeometryJSONSchema as aZ,
23089
- validateExerciseJSON as a_,
23090
- QuestionTextSchema as aa,
23091
- TextItemSchema as ab,
23092
- WidgetRefSchema as ac,
23093
- HighlightedTextSchema as ad,
23094
- ColorTextSchema as ae,
23095
- BoldTextSchema as af,
23096
- ImageItemSchema as ag,
23097
- SpaceItemSchema as ah,
23098
- LinebreakSchema as ai,
23099
- GroupItemSchema as aj,
23100
- OperatorItemSchema as ak,
23101
- OperatorSchema as al,
23102
- TableItemSchema as am,
23103
- TableRowItemSchema as an,
23104
- TableCellItemSchema as ao,
23105
- NumberLineItemSchema as ap,
23106
- NumberLinePositionItemSchema as aq,
23107
- NumberLineCounterItemSchema as ar,
23108
- NumberLineHighlightItemSchema as as,
23109
- NumberLineCursorItemSchema as at,
23110
- NumberLineArrowItemSchema as au,
23111
- VerticalOperationItemSchema as av,
23112
- VopRowItemSchema as aw,
23113
- VopCarryRowItemSchema as ax,
23114
- VopSeparatorItemSchema as ay,
23115
- VopCellItemSchema as az,
23997
+ RulerWidgetSchema as a$,
23998
+ isTableCellItem as a0,
23999
+ isImageItem as a1,
24000
+ isVerticalOperationItem as a2,
24001
+ isClockItem as a3,
24002
+ isRulerWidget as a4,
24003
+ isProtractorWidget as a5,
24004
+ isSetSquareWidget as a6,
24005
+ isGeometryJSON as a7,
24006
+ ExerciseJSONSchema as a8,
24007
+ ExerciseMetaSchema as a9,
24008
+ VopSeparatorItemSchema as aA,
24009
+ VopCellItemSchema as aB,
24010
+ VopColorCellItemSchema as aC,
24011
+ VopBoldCellItemSchema as aD,
24012
+ VopHighlightCellItemSchema as aE,
24013
+ ClockItemSchema as aF,
24014
+ ChartItemSchema as aG,
24015
+ ChartAxisJSONSchema as aH,
24016
+ ChartBarJSONSchema as aI,
24017
+ ChartPictoJSONSchema as aJ,
24018
+ ChartSeriesJSONSchema as aK,
24019
+ ChartSeriesBarJSONSchema as aL,
24020
+ ChartGroupJSONSchema as aM,
24021
+ WidgetJSONSchema as aN,
24022
+ InlineInputJSONSchema as aO,
24023
+ DropInputJSONSchema as aP,
24024
+ DragOptionsJSONSchema as aQ,
24025
+ DraggableOptionJSONSchema as aR,
24026
+ DraggableOptionsJSONSchema as aS,
24027
+ RadioOptionsJSONSchema as aT,
24028
+ NumericKeypadJSONSchema as aU,
24029
+ FractionJSONSchema as aV,
24030
+ MatcherWidgetJSONSchema as aW,
24031
+ PairMatcherWidgetJSONSchema as aX,
24032
+ GridSymmetryJSONSchema as aY,
24033
+ ChartBarWidgetJSONSchema as aZ,
24034
+ ChartPictoWidgetJSONSchema as a_,
24035
+ ValidationJSONSchema as aa,
24036
+ LayoutItemSchema as ab,
24037
+ QuestionTextSchema as ac,
24038
+ TextItemSchema as ad,
24039
+ WidgetRefSchema as ae,
24040
+ HighlightedTextSchema as af,
24041
+ ColorTextSchema as ag,
24042
+ BoldTextSchema as ah,
24043
+ ImageItemSchema as ai,
24044
+ SpaceItemSchema as aj,
24045
+ LinebreakSchema as ak,
24046
+ GroupItemSchema as al,
24047
+ OperatorItemSchema as am,
24048
+ OperatorSchema as an,
24049
+ TableItemSchema as ao,
24050
+ TableRowItemSchema as ap,
24051
+ TableCellItemSchema as aq,
24052
+ NumberLineItemSchema as ar,
24053
+ NumberLinePositionItemSchema as as,
24054
+ NumberLineCounterItemSchema as at,
24055
+ NumberLineHighlightItemSchema as au,
24056
+ NumberLineCursorItemSchema as av,
24057
+ NumberLineArrowItemSchema as aw,
24058
+ VerticalOperationItemSchema as ax,
24059
+ VopRowItemSchema as ay,
24060
+ VopCarryRowItemSchema as az,
23116
24061
  tokenizeSafe as b,
23117
- TextBlockSchema as b$,
23118
- validateWidgetJSON as b0,
23119
- validateWidgetJSONSafe as b1,
23120
- EngineError as b2,
23121
- LexerError as b3,
23122
- ParseError as b4,
23123
- TransformError as b5,
23124
- ValidationError as b6,
23125
- ExerciseRenderer as b7,
23126
- LayoutRenderer as b8,
23127
- WidgetRenderer as b9,
23128
- CLOCK_MODES as bA,
23129
- CLOCK_FORMATS as bB,
23130
- CLOCK_TIME_REGEX as bC,
23131
- ClockInlineSchema as bD,
23132
- InlineNodeSchema as bE,
23133
- createFreeText as bF,
23134
- createSpace as bG,
23135
- createHighlightedText as bH,
23136
- createColorText as bI,
23137
- createBoldText as bJ,
23138
- createStringInput as bK,
23139
- createNumericInput as bL,
23140
- createImage as bM,
23141
- createFraction as bN,
23142
- createVisualFraction as bO,
23143
- createInlineDragOption as bP,
23144
- createDisplayClock as bQ,
23145
- createInteractiveClock as bR,
23146
- isFreeText as bS,
23147
- isSpace as bT,
23148
- isInlineInput as bU,
23149
- isFraction as bV,
23150
- isImage as bW,
23151
- isClock as bX,
23152
- isInlineNode as bY,
23153
- isVisualFractionInline as bZ,
23154
- isInlineDragOption as b_,
23155
- renderExercise as ba,
23156
- SoundProvider as bb,
23157
- useSound as bc,
23158
- ExerciseLockProvider as bd,
23159
- WIDGET_TUTORIALS as be,
23160
- findWidgetTutorial as bf,
23161
- isPairMatcherWidget as bg,
23162
- isMatcherWidget as bh,
23163
- VISUAL_FRACTION_SHAPES as bi,
23164
- VISUAL_FRACTION_SHAPE as bj,
23165
- VISUAL_FRACTION_DEFAULT_SIZE as bk,
23166
- VISUAL_FRACTION_MAX_DIVISIONS as bl,
23167
- INLINE_INPUT_MAX_WIDTH as bm,
23168
- InlineInputWidthSchema as bn,
23169
- FreeTextInlineSchema as bo,
23170
- SpaceInlineSchema as bp,
23171
- HighlightedTextInlineSchema as bq,
23172
- ColorTextInlineSchema as br,
23173
- BoldTextInlineSchema as bs,
23174
- InlineInputWidgetSchema as bt,
23175
- FractionInlineSchema as bu,
23176
- ImageInlineSchema as bv,
23177
- VisualFractionInlineSchema as bw,
23178
- validateVisualFractionCapacity as bx,
23179
- InlineDragOptionSchema as by,
23180
- CLOCK_TYPES as bz,
24062
+ isInlineInput as b$,
24063
+ ProtractorWidgetSchema as b0,
24064
+ SetSquareWidgetSchema as b1,
24065
+ GeometryJSONSchema as b2,
24066
+ validateExerciseJSON as b3,
24067
+ validateExerciseJSONSafe as b4,
24068
+ validateWidgetJSON as b5,
24069
+ validateWidgetJSONSafe as b6,
24070
+ EngineError as b7,
24071
+ LexerError as b8,
24072
+ ParseError as b9,
24073
+ FractionInlineSchema as bA,
24074
+ ImageInlineSchema as bB,
24075
+ VisualFractionInlineSchema as bC,
24076
+ validateVisualFractionCapacity as bD,
24077
+ InlineDragOptionSchema as bE,
24078
+ CLOCK_TYPES as bF,
24079
+ CLOCK_MODES as bG,
24080
+ CLOCK_FORMATS as bH,
24081
+ CLOCK_TIME_REGEX as bI,
24082
+ ClockInlineSchema as bJ,
24083
+ InlineNodeSchema as bK,
24084
+ createFreeText as bL,
24085
+ createSpace as bM,
24086
+ createHighlightedText as bN,
24087
+ createColorText as bO,
24088
+ createBoldText as bP,
24089
+ createStringInput as bQ,
24090
+ createNumericInput as bR,
24091
+ createDropInput as bS,
24092
+ createImage as bT,
24093
+ createFraction as bU,
24094
+ createVisualFraction as bV,
24095
+ createInlineDragOption as bW,
24096
+ createDisplayClock as bX,
24097
+ createInteractiveClock as bY,
24098
+ isFreeText as bZ,
24099
+ isSpace as b_,
24100
+ TransformError as ba,
24101
+ ValidationError as bb,
24102
+ ExerciseRenderer as bc,
24103
+ LayoutRenderer as bd,
24104
+ WidgetRenderer as be,
24105
+ renderExercise as bf,
24106
+ SoundProvider as bg,
24107
+ useSound as bh,
24108
+ ExerciseLockProvider as bi,
24109
+ WIDGET_TUTORIALS as bj,
24110
+ findWidgetTutorial as bk,
24111
+ isPairMatcherWidget as bl,
24112
+ isMatcherWidget as bm,
24113
+ VISUAL_FRACTION_SHAPES as bn,
24114
+ VISUAL_FRACTION_SHAPE as bo,
24115
+ VISUAL_FRACTION_DEFAULT_SIZE as bp,
24116
+ VISUAL_FRACTION_MAX_DIVISIONS as bq,
24117
+ INLINE_INPUT_MAX_WIDTH as br,
24118
+ InlineInputWidthSchema as bs,
24119
+ FreeTextInlineSchema as bt,
24120
+ SpaceInlineSchema as bu,
24121
+ HighlightedTextInlineSchema as bv,
24122
+ ColorTextInlineSchema as bw,
24123
+ BoldTextInlineSchema as bx,
24124
+ InlineInputWidgetSchema as by,
24125
+ DropInputSchema as bz,
23181
24126
  compile as c,
23182
- createVopEmptyCell as c$,
23183
- LineBlockSchema as c0,
23184
- createTextBlock as c1,
23185
- createLineBlock as c2,
23186
- isTextBlock as c3,
23187
- isLineBlock as c4,
23188
- DisplayInlineNodeSchema as c5,
23189
- MatcherItemSchema as c6,
23190
- createMatcherItem as c7,
23191
- isMatcherItem as c8,
23192
- MatcherRowSchema as c9,
23193
- ExpressionValidationSchema as cA,
23194
- MixedValidationSchema as cB,
23195
- ValidationNodeSchema as cC,
23196
- createSimpleValidation as cD,
23197
- createExpressionValidation as cE,
23198
- createMixedValidation as cF,
23199
- isSimpleValidation as cG,
23200
- isExpressionValidation as cH,
23201
- isMixedValidation as cI,
23202
- isValidationNode as cJ,
23203
- ContentBlockSchema as cK,
23204
- ExerciseDocumentSchema as cL,
23205
- createExerciseDocument as cM,
23206
- isExerciseDocument as cN,
23207
- VOP_OPERATORS as cO,
23208
- normalizeVopOperator as cP,
23209
- VopDigitCellSchema as cQ,
23210
- VopLabelCellSchema as cR,
23211
- VopInputCellSchema as cS,
23212
- VopEmptyCellSchema as cT,
23213
- VopColorCellSchema as cU,
23214
- VopBoldCellSchema as cV,
23215
- VopHighlightCellSchema as cW,
23216
- VopCellSchema as cX,
23217
- createVopDigitCell as cY,
23218
- createVopInputCell as cZ,
23219
- createVopLabelCell as c_,
23220
- createMatcherRow as ca,
23221
- isMatcherRow as cb,
23222
- MatcherBlockSchema as cc,
23223
- createMatcherBlock as cd,
23224
- isMatcherBlock as ce,
23225
- PairMatcherBlockSchema as cf,
23226
- createPairMatcherBlock as cg,
23227
- isPairMatcherBlock as ch,
23228
- DragOptionsToolSchema as ci,
23229
- NumericKeypadToolSchema as cj,
23230
- RulerToolSchema as ck,
23231
- ProtractorToolSchema as cl,
23232
- SetSquareToolSchema as cm,
23233
- ToolNodeSchema as cn,
23234
- createDragOptions as co,
23235
- createNumericKeypad as cp,
23236
- createRulerTool as cq,
23237
- createProtractorTool as cr,
23238
- createSetSquareTool as cs,
23239
- isDragOptions as ct,
23240
- isNumericKeypad as cu,
23241
- isRulerTool as cv,
23242
- isProtractorTool as cw,
23243
- isSetSquareTool as cx,
23244
- isToolNode as cy,
23245
- SimpleValidationSchema as cz,
24127
+ ExerciseDocumentSchema as c$,
24128
+ isDropInput as c0,
24129
+ isFraction as c1,
24130
+ isImage as c2,
24131
+ isClock as c3,
24132
+ isInlineNode as c4,
24133
+ isVisualFractionInline as c5,
24134
+ isInlineDragOption as c6,
24135
+ TextBlockSchema as c7,
24136
+ LineBlockSchema as c8,
24137
+ createTextBlock as c9,
24138
+ ToolNodeSchema as cA,
24139
+ createDragOptions as cB,
24140
+ createDraggableOptions as cC,
24141
+ createDraggableOption as cD,
24142
+ createNumericKeypad as cE,
24143
+ createRulerTool as cF,
24144
+ createProtractorTool as cG,
24145
+ createSetSquareTool as cH,
24146
+ isDragOptions as cI,
24147
+ isDraggableOptions as cJ,
24148
+ isNumericKeypad as cK,
24149
+ isRulerTool as cL,
24150
+ isProtractorTool as cM,
24151
+ isSetSquareTool as cN,
24152
+ isToolNode as cO,
24153
+ SimpleValidationSchema as cP,
24154
+ ExpressionValidationSchema as cQ,
24155
+ MixedValidationSchema as cR,
24156
+ ValidationNodeSchema as cS,
24157
+ createSimpleValidation as cT,
24158
+ createExpressionValidation as cU,
24159
+ createMixedValidation as cV,
24160
+ isSimpleValidation as cW,
24161
+ isExpressionValidation as cX,
24162
+ isMixedValidation as cY,
24163
+ isValidationNode as cZ,
24164
+ ContentBlockSchema as c_,
24165
+ createLineBlock as ca,
24166
+ isTextBlock as cb,
24167
+ isLineBlock as cc,
24168
+ DisplayInlineNodeSchema as cd,
24169
+ MatcherItemSchema as ce,
24170
+ createMatcherItem as cf,
24171
+ isMatcherItem as cg,
24172
+ MatcherRowSchema as ch,
24173
+ createMatcherRow as ci,
24174
+ isMatcherRow as cj,
24175
+ MatcherBlockSchema as ck,
24176
+ createMatcherBlock as cl,
24177
+ isMatcherBlock as cm,
24178
+ PairMatcherBlockSchema as cn,
24179
+ createPairMatcherBlock as co,
24180
+ isPairMatcherBlock as cp,
24181
+ DragOptionsToolSchema as cq,
24182
+ hasOptionContent as cr,
24183
+ isOptionContentNode as cs,
24184
+ DraggableOptionSchema as ct,
24185
+ addDuplicateOptionNameIssues as cu,
24186
+ DraggableOptionsToolSchema as cv,
24187
+ NumericKeypadToolSchema as cw,
24188
+ RulerToolSchema as cx,
24189
+ ProtractorToolSchema as cy,
24190
+ SetSquareToolSchema as cz,
23246
24191
  parse as d,
23247
- createVopColorCell as d0,
23248
- createVopBoldCell as d1,
23249
- createVopHighlightCell as d2,
23250
- VopRowSchema as d3,
23251
- createVopRow as d4,
23252
- VopCarryRowSchema as d5,
23253
- createVopCarryRow as d6,
23254
- VopSeparatorSchema as d7,
23255
- createVopSeparator as d8,
23256
- VerticalOperationBlockSchema as d9,
23257
- isChartAxis as dA,
23258
- ChartPictoSchema as dB,
23259
- createChartPicto as dC,
23260
- isChartPicto as dD,
23261
- ChartBarSchema as dE,
23262
- createChartBar as dF,
23263
- isChartBar as dG,
23264
- ChartSeriesSchema as dH,
23265
- createChartSeries as dI,
23266
- isChartSeries as dJ,
23267
- ChartSeriesBarSchema as dK,
23268
- createChartSeriesBar as dL,
23269
- isChartSeriesBar as dM,
23270
- ChartGroupSchema as dN,
23271
- createChartGroup as dO,
23272
- isChartGroup as dP,
23273
- alignGroupBarsToSeries as dQ,
23274
- ChartBlockSchema as dR,
23275
- resolveChartAxes as dS,
23276
- validateUniqueIds as dT,
23277
- createChartBlock as dU,
23278
- isChartBlock as dV,
23279
- GeometryBlockSchema as dW,
23280
- createGeometryBlock as dX,
23281
- isGeometryBlock as dY,
23282
- createVerticalOperationBlock as da,
23283
- isVopCell as db,
23284
- isVopRow as dc,
23285
- isVopCarryRow as dd,
23286
- isVopSeparator as de,
23287
- isVerticalOperationBlock as df,
23288
- GRID_SYMMETRY_AXES as dg,
23289
- GRID_SYMMETRY_INTERACTIVE_AREAS as dh,
23290
- GRID_SYMMETRY_MAX_ROWS as di,
23291
- GRID_SYMMETRY_MAX_COLS as dj,
23292
- GRID_SYMMETRY_MIN_CELL_SIZE as dk,
23293
- GRID_SYMMETRY_MAX_CELL_SIZE as dl,
23294
- isCellInInteractiveArea as dm,
23295
- reflectGridSymmetryCell as dn,
23296
- computeGridSymmetryExpectedCells as dp,
23297
- computeGridSymmetryCellsToFill as dq,
23298
- GridSymmetryBlockSchema as dr,
23299
- createGridSymmetryBlock as ds,
23300
- isGridSymmetryBlock as dt,
23301
- CHART_TYPES as du,
23302
- CHART_AXIS_DEFAULTS as dv,
23303
- CHART_MAX_AXIS_TICKS as dw,
23304
- CHART_MAX_PICTO_SLOTS as dx,
23305
- ChartAxisSchema as dy,
23306
- createChartAxis as dz,
24192
+ createChartSeriesBar as d$,
24193
+ createExerciseDocument as d0,
24194
+ isExerciseDocument as d1,
24195
+ VOP_OPERATORS as d2,
24196
+ normalizeVopOperator as d3,
24197
+ VopDigitCellSchema as d4,
24198
+ VopLabelCellSchema as d5,
24199
+ VopInputCellSchema as d6,
24200
+ VopEmptyCellSchema as d7,
24201
+ VopColorCellSchema as d8,
24202
+ VopBoldCellSchema as d9,
24203
+ GRID_SYMMETRY_MAX_COLS as dA,
24204
+ GRID_SYMMETRY_MIN_CELL_SIZE as dB,
24205
+ GRID_SYMMETRY_MAX_CELL_SIZE as dC,
24206
+ isCellInInteractiveArea as dD,
24207
+ reflectGridSymmetryCell as dE,
24208
+ computeGridSymmetryExpectedCells as dF,
24209
+ computeGridSymmetryCellsToFill as dG,
24210
+ GridSymmetryBlockSchema as dH,
24211
+ createGridSymmetryBlock as dI,
24212
+ isGridSymmetryBlock as dJ,
24213
+ CHART_TYPES as dK,
24214
+ CHART_AXIS_DEFAULTS as dL,
24215
+ CHART_MAX_AXIS_TICKS as dM,
24216
+ CHART_MAX_PICTO_SLOTS as dN,
24217
+ ChartAxisSchema as dO,
24218
+ createChartAxis as dP,
24219
+ isChartAxis as dQ,
24220
+ ChartPictoSchema as dR,
24221
+ createChartPicto as dS,
24222
+ isChartPicto as dT,
24223
+ ChartBarSchema as dU,
24224
+ createChartBar as dV,
24225
+ isChartBar as dW,
24226
+ ChartSeriesSchema as dX,
24227
+ createChartSeries as dY,
24228
+ isChartSeries as dZ,
24229
+ ChartSeriesBarSchema as d_,
24230
+ VopHighlightCellSchema as da,
24231
+ VopCellSchema as db,
24232
+ createVopDigitCell as dc,
24233
+ createVopInputCell as dd,
24234
+ createVopLabelCell as de,
24235
+ createVopEmptyCell as df,
24236
+ createVopColorCell as dg,
24237
+ createVopBoldCell as dh,
24238
+ createVopHighlightCell as di,
24239
+ VopRowSchema as dj,
24240
+ createVopRow as dk,
24241
+ VopCarryRowSchema as dl,
24242
+ createVopCarryRow as dm,
24243
+ VopSeparatorSchema as dn,
24244
+ createVopSeparator as dp,
24245
+ VerticalOperationBlockSchema as dq,
24246
+ createVerticalOperationBlock as dr,
24247
+ isVopCell as ds,
24248
+ isVopRow as dt,
24249
+ isVopCarryRow as du,
24250
+ isVopSeparator as dv,
24251
+ isVerticalOperationBlock as dw,
24252
+ GRID_SYMMETRY_AXES as dx,
24253
+ GRID_SYMMETRY_INTERACTIVE_AREAS as dy,
24254
+ GRID_SYMMETRY_MAX_ROWS as dz,
23307
24255
  parseSafe as e,
24256
+ isChartSeriesBar as e0,
24257
+ ChartGroupSchema as e1,
24258
+ createChartGroup as e2,
24259
+ isChartGroup as e3,
24260
+ alignGroupBarsToSeries as e4,
24261
+ ChartBlockSchema as e5,
24262
+ resolveChartAxes as e6,
24263
+ validateUniqueIds as e7,
24264
+ createChartBlock as e8,
24265
+ isChartBlock as e9,
24266
+ GeometryBlockSchema as ea,
24267
+ createGeometryBlock as eb,
24268
+ isGeometryBlock as ec,
23308
24269
  toAST as f,
23309
24270
  getFractionKey as g,
23310
24271
  toJSON as h,
@@ -23327,4 +24288,4 @@ export {
23327
24288
  extractVariables as y,
23328
24289
  validateBooleanExpression as z
23329
24290
  };
23330
- //# sourceMappingURL=index-CAvdtiDQ.mjs.map
24291
+ //# sourceMappingURL=index-BLrnKegW.mjs.map