@mathscan/math-exercise-engine 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40,6 +40,10 @@ const AST_TYPES = {
40
40
  NUMBER_LINE_BLOCK: "NumberLineBlock",
41
41
  /** A position marker on the number line */
42
42
  NUMBER_LINE_POSITION: "NumberLinePosition",
43
+ /** A dynamic subdivision counter embedded in a number line */
44
+ NUMBER_LINE_COUNTER: "NumberLineCounter",
45
+ /** A highlighted range on the number line axis */
46
+ NUMBER_LINE_HIGHLIGHT: "NumberLineHighlight",
43
47
  // ═══════════════════════════════════════════════════════════════════════════
44
48
  // Tools
45
49
  // ═══════════════════════════════════════════════════════════════════════════
@@ -96,6 +100,10 @@ const JSON_TYPES = {
96
100
  NUMBER_LINE: "number-line",
97
101
  /** Position marker within a number line */
98
102
  NUMBER_LINE_POSITION: "number-line-position",
103
+ /** Dynamic subdivision counter embedded in a number line */
104
+ NUMBER_LINE_COUNTER: "number-line-counter",
105
+ /** Highlighted range within a number line */
106
+ NUMBER_LINE_HIGHLIGHT: "number-line-highlight",
99
107
  // ═══════════════════════════════════════════════════════════════════════════
100
108
  // Widget Types
101
109
  // ═══════════════════════════════════════════════════════════════════════════
@@ -122,8 +130,12 @@ const INPUT_TYPES = {
122
130
  NUMERIC: "numeric"
123
131
  };
124
132
  const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
133
+ const INTERNAL_PRECISION = 9;
134
+ const DISPLAY_PRECISION = 2;
125
135
  const SIDE_PAD = 7;
126
136
  const CSS_VAR_SIDE_PAD = "--nl-side-pad";
137
+ const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
138
+ const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
127
139
  const Newline = chevrotain.createToken({
128
140
  name: "Newline",
129
141
  pattern: /\r?\n/,
@@ -217,6 +229,14 @@ const CELL = chevrotain.createToken({
217
229
  name: "CELL",
218
230
  pattern: /CELL/
219
231
  });
232
+ const COUNTER = chevrotain.createToken({
233
+ name: "COUNTER",
234
+ pattern: /COUNTER/
235
+ });
236
+ const HIGHLIGHT = chevrotain.createToken({
237
+ name: "HIGHLIGHT",
238
+ pattern: /HIGHLIGHT(?!_TEXT)/
239
+ });
220
240
  const NumberLiteral = chevrotain.createToken({
221
241
  name: "NumberLiteral",
222
242
  pattern: /-?\d+(\.\d+)?/
@@ -324,6 +344,8 @@ const allTokens = [
324
344
  TABLE,
325
345
  ROW,
326
346
  CELL,
347
+ COUNTER,
348
+ HIGHLIGHT,
327
349
  // Literals
328
350
  NumberLiteral,
329
351
  StringLiteral,
@@ -431,10 +453,7 @@ class MathExerciseParser extends chevrotain.CstParser {
431
453
  });
432
454
  this.exerciseDocument = this.RULE("exerciseDocument", () => {
433
455
  this.MANY(() => {
434
- this.OR([
435
- { ALT: () => this.CONSUME(Newline) },
436
- { ALT: () => this.CONSUME(Space) }
437
- ]);
456
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
438
457
  });
439
458
  this.OPTION(() => {
440
459
  this.SUBRULE(this.question);
@@ -450,10 +469,7 @@ class MathExerciseParser extends chevrotain.CstParser {
450
469
  this.SUBRULE(this.validation);
451
470
  });
452
471
  this.MANY4(() => {
453
- this.OR2([
454
- { ALT: () => this.CONSUME2(Newline) },
455
- { ALT: () => this.CONSUME2(Space) }
456
- ]);
472
+ this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
457
473
  });
458
474
  });
459
475
  this.question = this.RULE("question", () => {
@@ -462,10 +478,7 @@ class MathExerciseParser extends chevrotain.CstParser {
462
478
  this.CONSUME(StringLiteral, { LABEL: "value" });
463
479
  this.CONSUME(RBracket);
464
480
  this.MANY(() => {
465
- this.OR([
466
- { ALT: () => this.CONSUME(Newline) },
467
- { ALT: () => this.CONSUME(Space) }
468
- ]);
481
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
469
482
  });
470
483
  });
471
484
  this.contentBlock = this.RULE("contentBlock", () => {
@@ -487,77 +500,50 @@ class MathExerciseParser extends chevrotain.CstParser {
487
500
  });
488
501
  this.table = this.RULE("table", () => {
489
502
  this.MANY(() => {
490
- this.OR2([
491
- { ALT: () => this.CONSUME(Space) },
492
- { ALT: () => this.CONSUME(Newline) }
493
- ]);
503
+ this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
494
504
  });
495
505
  this.CONSUME(TABLE);
496
506
  this.CONSUME(LBracket);
497
507
  this.SUBRULE(this.parameterList);
498
508
  this.CONSUME(RBracket);
499
509
  this.MANY2(() => {
500
- this.OR3([
501
- { ALT: () => this.CONSUME2(Space) },
502
- { ALT: () => this.CONSUME2(Newline) }
503
- ]);
510
+ this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
504
511
  });
505
512
  this.CONSUME(LBrace);
506
513
  this.MANY3(() => {
507
- this.OR4([
508
- { ALT: () => this.CONSUME3(Space) },
509
- { ALT: () => this.CONSUME3(Newline) }
510
- ]);
514
+ this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
511
515
  });
512
516
  this.AT_LEAST_ONE(() => {
513
517
  this.SUBRULE(this.row);
514
518
  });
515
519
  this.CONSUME(RBrace);
516
520
  this.MANY4(() => {
517
- this.OR5([
518
- { ALT: () => this.CONSUME4(Newline) },
519
- { ALT: () => this.CONSUME4(Space) }
520
- ]);
521
+ this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
521
522
  });
522
523
  });
523
524
  this.row = this.RULE("row", () => {
524
525
  this.MANY(() => {
525
- this.OR([
526
- { ALT: () => this.CONSUME(Space) },
527
- { ALT: () => this.CONSUME(Newline) }
528
- ]);
526
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
529
527
  });
530
528
  this.CONSUME(ROW);
531
529
  this.MANY2(() => {
532
- this.OR2([
533
- { ALT: () => this.CONSUME2(Space) },
534
- { ALT: () => this.CONSUME2(Newline) }
535
- ]);
530
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
536
531
  });
537
532
  this.CONSUME(LBrace);
538
533
  this.MANY3(() => {
539
- this.OR3([
540
- { ALT: () => this.CONSUME3(Space) },
541
- { ALT: () => this.CONSUME3(Newline) }
542
- ]);
534
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
543
535
  });
544
536
  this.AT_LEAST_ONE(() => {
545
537
  this.SUBRULE(this.cell);
546
538
  });
547
539
  this.CONSUME(RBrace);
548
540
  this.MANY4(() => {
549
- this.OR4([
550
- { ALT: () => this.CONSUME4(Space) },
551
- { ALT: () => this.CONSUME4(Newline) }
552
- ]);
541
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
553
542
  });
554
543
  });
555
544
  this.cell = this.RULE("cell", () => {
556
545
  this.MANY(() => {
557
- this.OR([
558
- { ALT: () => this.CONSUME(Space) },
559
- { ALT: () => this.CONSUME(Newline) }
560
- ]);
546
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
561
547
  });
562
548
  this.CONSUME(CELL);
563
549
  this.OPTION(() => {
@@ -566,10 +552,7 @@ class MathExerciseParser extends chevrotain.CstParser {
566
552
  this.CONSUME(RBracket);
567
553
  });
568
554
  this.MANY2(() => {
569
- this.OR2([
570
- { ALT: () => this.CONSUME2(Space) },
571
- { ALT: () => this.CONSUME2(Newline) }
572
- ]);
555
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
573
556
  });
574
557
  this.CONSUME(LBrace);
575
558
  this.MANY3(() => {
@@ -580,70 +563,47 @@ class MathExerciseParser extends chevrotain.CstParser {
580
563
  });
581
564
  this.CONSUME(RBrace);
582
565
  this.MANY4(() => {
583
- this.OR4([
584
- { ALT: () => this.CONSUME4(Space) },
585
- { ALT: () => this.CONSUME4(Newline) }
586
- ]);
566
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
587
567
  });
588
568
  });
589
569
  this.numberLine = this.RULE("numberLine", () => {
590
570
  this.MANY(() => {
591
- this.OR2([
592
- { ALT: () => this.CONSUME(Space) },
593
- { ALT: () => this.CONSUME(Newline) }
594
- ]);
571
+ this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
595
572
  });
596
573
  this.CONSUME(NUMBER_LINE);
597
574
  this.CONSUME(LBracket);
598
575
  this.SUBRULE(this.parameterList);
599
576
  this.CONSUME(RBracket);
600
577
  this.MANY2(() => {
601
- this.OR3([
602
- { ALT: () => this.CONSUME2(Space) },
603
- { ALT: () => this.CONSUME2(Newline) }
604
- ]);
578
+ this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
605
579
  });
606
580
  this.CONSUME(LBrace);
607
581
  this.MANY3(() => {
608
- this.OR4([
609
- { ALT: () => this.CONSUME3(Space) },
610
- { ALT: () => this.CONSUME3(Newline) }
611
- ]);
582
+ this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
612
583
  });
613
584
  this.MANY4(() => {
614
- this.SUBRULE(this.position);
585
+ this.OR6([
586
+ { ALT: () => this.SUBRULE(this.position) },
587
+ { ALT: () => this.SUBRULE(this.counter) },
588
+ { ALT: () => this.SUBRULE(this.highlight) }
589
+ ]);
615
590
  });
616
591
  this.CONSUME(RBrace);
617
592
  this.MANY5(() => {
618
- this.OR5([
619
- { ALT: () => this.CONSUME4(Newline) },
620
- { ALT: () => this.CONSUME4(Space) }
621
- ]);
593
+ this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
622
594
  });
623
595
  });
624
596
  this.position = this.RULE("position", () => {
625
- this.MANY(() => {
626
- this.OR([
627
- { ALT: () => this.CONSUME(Space) },
628
- { ALT: () => this.CONSUME(Newline) }
629
- ]);
630
- });
631
597
  this.CONSUME(POSITION);
632
598
  this.CONSUME(LBracket);
633
599
  this.SUBRULE(this.parameterList);
634
600
  this.CONSUME(RBracket);
635
601
  this.MANY2(() => {
636
- this.OR2([
637
- { ALT: () => this.CONSUME2(Space) },
638
- { ALT: () => this.CONSUME2(Newline) }
639
- ]);
602
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
640
603
  });
641
604
  this.CONSUME(LBrace);
642
605
  this.MANY3(() => {
643
- this.OR3([
644
- { ALT: () => this.CONSUME3(Space) },
645
- { ALT: () => this.CONSUME3(Newline) }
646
- ]);
606
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
647
607
  });
648
608
  this.MANY4(() => {
649
609
  this.OR4([
@@ -653,10 +613,25 @@ class MathExerciseParser extends chevrotain.CstParser {
653
613
  });
654
614
  this.CONSUME(RBrace);
655
615
  this.MANY5(() => {
656
- this.OR5([
657
- { ALT: () => this.CONSUME5(Space) },
658
- { ALT: () => this.CONSUME5(Newline) }
659
- ]);
616
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
617
+ });
618
+ });
619
+ this.counter = this.RULE("counter", () => {
620
+ this.CONSUME(COUNTER);
621
+ this.CONSUME(LBracket);
622
+ this.SUBRULE(this.parameterList);
623
+ this.CONSUME(RBracket);
624
+ this.MANY2(() => {
625
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
626
+ });
627
+ });
628
+ this.highlight = this.RULE("highlight", () => {
629
+ this.CONSUME(HIGHLIGHT);
630
+ this.CONSUME(LBracket);
631
+ this.SUBRULE(this.parameterList);
632
+ this.CONSUME(RBracket);
633
+ this.MANY2(() => {
634
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
660
635
  });
661
636
  });
662
637
  this.line = this.RULE("line", () => {
@@ -753,28 +728,19 @@ class MathExerciseParser extends chevrotain.CstParser {
753
728
  this.SUBRULE(this.stringList);
754
729
  this.CONSUME(RBracket);
755
730
  this.MANY(() => {
756
- this.OR([
757
- { ALT: () => this.CONSUME(Newline) },
758
- { ALT: () => this.CONSUME(Space) }
759
- ]);
731
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
760
732
  });
761
733
  });
762
734
  this.radioButtons = this.RULE("radioButtons", () => {
763
735
  this.MANY(() => {
764
- this.OR([
765
- { ALT: () => this.CONSUME(Newline) },
766
- { ALT: () => this.CONSUME(Space) }
767
- ]);
736
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
768
737
  });
769
738
  this.CONSUME(RADIO_BUTTONS_OPTIONS);
770
739
  this.CONSUME(LBracket);
771
740
  this.SUBRULE(this.parameterList);
772
741
  this.CONSUME(RBracket);
773
742
  this.MANY2(() => {
774
- this.OR2([
775
- { ALT: () => this.CONSUME2(Newline) },
776
- { ALT: () => this.CONSUME2(Space) }
777
- ]);
743
+ this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
778
744
  });
779
745
  });
780
746
  this.numericKeypad = this.RULE("numericKeypad", () => {
@@ -785,10 +751,7 @@ class MathExerciseParser extends chevrotain.CstParser {
785
751
  });
786
752
  this.CONSUME(RBracket);
787
753
  this.MANY(() => {
788
- this.OR([
789
- { ALT: () => this.CONSUME(Newline) },
790
- { ALT: () => this.CONSUME(Space) }
791
- ]);
754
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
792
755
  });
793
756
  });
794
757
  this.validation = this.RULE("validation", () => {
@@ -1023,7 +986,7 @@ function isLineBlock(value) {
1023
986
  }
1024
987
  const NumberLinePositionSchema = zod.z.object({
1025
988
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_POSITION),
1026
- value: zod.z.number(),
989
+ value: zod.z.number().finite({ message: "POSITION: value must be a finite number" }),
1027
990
  inlines: zod.z.array(InlineNodeSchema).min(1)
1028
991
  });
1029
992
  function createNumberLinePosition(value, inlines) {
@@ -1043,10 +1006,50 @@ function createNumberLinePosition(value, inlines) {
1043
1006
  inlines
1044
1007
  });
1045
1008
  }
1009
+ const NumberLineCounterSchema = zod.z.object({
1010
+ type: zod.z.literal(AST_TYPES.NUMBER_LINE_COUNTER),
1011
+ min: zod.z.number().int().min(1, { message: "COUNTER: min must be >= 1" }),
1012
+ max: zod.z.number().int().positive({ message: "COUNTER: max must be > 0" }),
1013
+ default: zod.z.number().int().nonnegative().optional(),
1014
+ name: zod.z.string().optional()
1015
+ }).refine((d) => d.min <= d.max, {
1016
+ message: "COUNTER: min must be <= max"
1017
+ });
1018
+ function createNumberLineCounter(max, min, defaultValue2, name) {
1019
+ return NumberLineCounterSchema.parse({
1020
+ type: AST_TYPES.NUMBER_LINE_COUNTER,
1021
+ min: min ?? 1,
1022
+ max,
1023
+ ...defaultValue2 !== void 0 && { default: defaultValue2 },
1024
+ ...name !== void 0 && { name }
1025
+ });
1026
+ }
1027
+ const NumberLineHighlightSchema = zod.z.object({
1028
+ type: zod.z.literal(AST_TYPES.NUMBER_LINE_HIGHLIGHT),
1029
+ from: zod.z.number().finite({ message: "HIGHLIGHT: from must be a finite number" }),
1030
+ to: zod.z.number().finite({ message: "HIGHLIGHT: to must be a finite number" }),
1031
+ color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS, {
1032
+ errorMap: () => ({
1033
+ message: `HIGHLIGHT: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")}`
1034
+ })
1035
+ }).optional(),
1036
+ label: zod.z.string().optional()
1037
+ }).refine((d) => d.from < d.to, {
1038
+ message: "HIGHLIGHT: from must be less than to"
1039
+ });
1040
+ function createNumberLineHighlight(from, to, color, label) {
1041
+ return NumberLineHighlightSchema.parse({
1042
+ type: AST_TYPES.NUMBER_LINE_HIGHLIGHT,
1043
+ from,
1044
+ to,
1045
+ ...color !== void 0 && { color },
1046
+ ...label !== void 0 && { label }
1047
+ });
1048
+ }
1046
1049
  const NumberLineBlockSchema = zod.z.object({
1047
1050
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
1048
- min: zod.z.number(),
1049
- max: zod.z.number(),
1051
+ min: zod.z.number().finite({ message: "NUMBER_LINE: min must be a finite number" }),
1052
+ max: zod.z.number().finite({ message: "NUMBER_LINE: max must be a finite number" }),
1050
1053
  step: zod.z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
1051
1054
  minorTicks: zod.z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
1052
1055
  style: zod.z.enum(NUMBER_LINE_STYLES, {
@@ -1054,6 +1057,13 @@ const NumberLineBlockSchema = zod.z.object({
1054
1057
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
1055
1058
  })
1056
1059
  }).optional(),
1060
+ labelVisibility: zod.z.enum(NUMBER_LINE_LABELS, {
1061
+ errorMap: () => ({
1062
+ message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
1063
+ })
1064
+ }).optional(),
1065
+ counter: NumberLineCounterSchema.optional(),
1066
+ highlights: zod.z.array(NumberLineHighlightSchema).optional(),
1057
1067
  positions: zod.z.array(NumberLinePositionSchema)
1058
1068
  }).refine((data) => data.min < data.max, {
1059
1069
  message: "NUMBER_LINE: min must be less than max"
@@ -1074,8 +1084,24 @@ const NumberLineBlockSchema = zod.z.object({
1074
1084
  }
1075
1085
  seen.add(pos.value);
1076
1086
  }
1087
+ for (const h of data.highlights ?? []) {
1088
+ if (h.from < data.min || h.to > data.max) {
1089
+ ctx.addIssue({
1090
+ code: zod.z.ZodIssueCode.custom,
1091
+ message: `NUMBER_LINE: HIGHLIGHT range [${h.from}, ${h.to}] must be within [${data.min}, ${data.max}]`
1092
+ });
1093
+ }
1094
+ }
1095
+ if (data.counter?.default !== void 0) {
1096
+ if (data.counter.default < data.counter.min || data.counter.default > data.counter.max) {
1097
+ ctx.addIssue({
1098
+ code: zod.z.ZodIssueCode.custom,
1099
+ message: `NUMBER_LINE: COUNTER default (${data.counter.default}) must be within [${data.counter.min}, ${data.counter.max}]`
1100
+ });
1101
+ }
1102
+ }
1077
1103
  });
1078
- function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
1104
+ function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights) {
1079
1105
  return NumberLineBlockSchema.parse({
1080
1106
  type: AST_TYPES.NUMBER_LINE_BLOCK,
1081
1107
  min,
@@ -1083,6 +1109,9 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
1083
1109
  step,
1084
1110
  minorTicks,
1085
1111
  style,
1112
+ labelVisibility,
1113
+ counter,
1114
+ highlights,
1086
1115
  positions
1087
1116
  });
1088
1117
  }
@@ -1293,8 +1322,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1293
1322
  const step = params.step !== void 0 ? Number(params.step) : void 0;
1294
1323
  const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
1295
1324
  const style = params.style;
1325
+ const labelVisibility = params.labelVisibility;
1296
1326
  const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
1297
- return createNumberLineBlock(min, max, positions, step, minorTicks, style);
1327
+ const rawCounters = ctx.counter;
1328
+ if (rawCounters && rawCounters.length > 1) {
1329
+ throw new Error("NUMBER_LINE: only one COUNTER block is allowed");
1330
+ }
1331
+ const counter = rawCounters ? this.visit(rawCounters) : void 0;
1332
+ const highlights = ctx.highlight ? ctx.highlight.map((h) => this.visit(h)) : [];
1333
+ return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights);
1298
1334
  }
1299
1335
  position(ctx) {
1300
1336
  const params = this.visit(ctx.parameterList);
@@ -1305,6 +1341,28 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1305
1341
  const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
1306
1342
  return createNumberLinePosition(value, inlines);
1307
1343
  }
1344
+ counter(ctx) {
1345
+ const params = this.visit(ctx.parameterList);
1346
+ if (params.max === void 0) {
1347
+ throw new Error("COUNTER requires parameter: max");
1348
+ }
1349
+ const max = Number(params.max);
1350
+ const min = params.min !== void 0 ? Number(params.min) : void 0;
1351
+ const defaultValue2 = params.default !== void 0 ? Number(params.default) : void 0;
1352
+ const name = params.name;
1353
+ return createNumberLineCounter(max, min, defaultValue2, name);
1354
+ }
1355
+ highlight(ctx) {
1356
+ const params = this.visit(ctx.parameterList);
1357
+ if (params.from === void 0 || params.to === void 0) {
1358
+ throw new Error("HIGHLIGHT requires parameters: from and to");
1359
+ }
1360
+ const from = Number(params.from);
1361
+ const to = Number(params.to);
1362
+ const color = params.color;
1363
+ const label = params.label;
1364
+ return createNumberLineHighlight(from, to, color, label);
1365
+ }
1308
1366
  // ═══════════════════════════════════════════════════════════════════════════
1309
1367
  // Line Block
1310
1368
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1635,17 +1693,36 @@ const TableItemSchema = zod.z.object({
1635
1693
  const NumberLinePositionItemSchema = zod.z.object({
1636
1694
  id: zod.z.string(),
1637
1695
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
1638
- value: zod.z.number(),
1639
- content: zod.z.array(zod.z.lazy(() => LayoutItemSchema))
1696
+ value: zod.z.number().finite(),
1697
+ content: zod.z.array(LayoutItemSchema)
1698
+ });
1699
+ const NumberLineCounterItemSchema = zod.z.object({
1700
+ id: zod.z.string(),
1701
+ type: zod.z.literal(JSON_TYPES.NUMBER_LINE_COUNTER),
1702
+ min: zod.z.number().int().min(1),
1703
+ max: zod.z.number().int().positive(),
1704
+ default: zod.z.number().int().nonnegative().optional(),
1705
+ name: zod.z.string().optional()
1706
+ });
1707
+ const NumberLineHighlightItemSchema = zod.z.object({
1708
+ id: zod.z.string(),
1709
+ type: zod.z.literal(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
1710
+ from: zod.z.number().finite(),
1711
+ to: zod.z.number().finite(),
1712
+ color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
1713
+ label: zod.z.string().optional()
1640
1714
  });
1641
1715
  const NumberLineItemSchema = zod.z.object({
1642
1716
  id: zod.z.string(),
1643
1717
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
1644
- min: zod.z.number(),
1645
- max: zod.z.number(),
1718
+ min: zod.z.number().finite(),
1719
+ max: zod.z.number().finite(),
1646
1720
  step: zod.z.number().positive(),
1647
1721
  minorTicks: zod.z.number().int().nonnegative().optional(),
1648
1722
  style: zod.z.enum(NUMBER_LINE_STYLES).optional(),
1723
+ labelVisibility: zod.z.enum(NUMBER_LINE_LABELS).optional(),
1724
+ counter: NumberLineCounterItemSchema.optional(),
1725
+ highlights: zod.z.array(NumberLineHighlightItemSchema).optional(),
1649
1726
  positions: zod.z.array(NumberLinePositionItemSchema)
1650
1727
  });
1651
1728
  const StringInputJSONSchema = zod.z.object({
@@ -1719,7 +1796,7 @@ function isQuestionText(item) {
1719
1796
  function isTextItem$1(item) {
1720
1797
  return item.type === JSON_TYPES.TEXT;
1721
1798
  }
1722
- function isWidgetRef$1(item) {
1799
+ function isWidgetRef(item) {
1723
1800
  return item.type === JSON_TYPES.WIDGET_REF;
1724
1801
  }
1725
1802
  function isHighlightedText(item) {
@@ -1892,16 +1969,42 @@ function transformNumberLine(numberLine, idGenerator) {
1892
1969
  content
1893
1970
  };
1894
1971
  });
1972
+ let counterItem;
1973
+ if (numberLine.counter !== void 0) {
1974
+ const c = numberLine.counter;
1975
+ counterItem = {
1976
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_COUNTER),
1977
+ type: JSON_TYPES.NUMBER_LINE_COUNTER,
1978
+ min: c.min,
1979
+ max: c.max,
1980
+ ...c.default !== void 0 && { default: c.default },
1981
+ ...c.name !== void 0 && { name: c.name }
1982
+ };
1983
+ }
1984
+ let highlights;
1985
+ if (numberLine.highlights !== void 0 && numberLine.highlights.length > 0) {
1986
+ highlights = numberLine.highlights.map((h) => ({
1987
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
1988
+ type: JSON_TYPES.NUMBER_LINE_HIGHLIGHT,
1989
+ from: h.from,
1990
+ to: h.to,
1991
+ ...h.color !== void 0 && { color: h.color },
1992
+ ...h.label !== void 0 && { label: h.label }
1993
+ }));
1994
+ }
1895
1995
  const item = {
1896
1996
  id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
1897
1997
  type: JSON_TYPES.NUMBER_LINE,
1898
1998
  min: numberLine.min,
1899
1999
  max: numberLine.max,
1900
2000
  step: numberLine.step,
2001
+ ...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
2002
+ ...numberLine.style !== void 0 && { style: numberLine.style },
2003
+ ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
2004
+ ...counterItem !== void 0 && { counter: counterItem },
2005
+ ...highlights !== void 0 && { highlights },
1901
2006
  positions
1902
2007
  };
1903
- if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
1904
- if (numberLine.style !== void 0) item.style = numberLine.style;
1905
2008
  return item;
1906
2009
  }
1907
2010
  function transformTable(table, idGenerator) {
@@ -2024,6 +2127,49 @@ function extractVariables$1(expression, validVariables) {
2024
2127
  }
2025
2128
  return uniqueVars;
2026
2129
  }
2130
+ function normalize(value) {
2131
+ if (typeof value === "number") {
2132
+ return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
2133
+ }
2134
+ const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
2135
+ const asNum = Number(trimmed);
2136
+ if (trimmed !== "" && !isNaN(asNum)) {
2137
+ return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
2138
+ }
2139
+ return trimmed;
2140
+ }
2141
+ const NL_DRAG_PREFIX = "\0nl-drag\0";
2142
+ const isNLDragEncoded = (v) => v.startsWith(NL_DRAG_PREFIX);
2143
+ const encodeNLDragValue = (label, position) => `${NL_DRAG_PREFIX}${label}@${String(position)}`;
2144
+ function isNLDragCorrect(encoded, expectedLabel, expectedPosition) {
2145
+ const payload = encoded.slice(NL_DRAG_PREFIX.length);
2146
+ const atIdx = payload.lastIndexOf("@");
2147
+ const label = normalize(payload.slice(0, atIdx));
2148
+ const position = normalize(payload.slice(atIdx + 1));
2149
+ return label === normalize(expectedLabel) && position === normalize(expectedPosition);
2150
+ }
2151
+ function collectNumberLines(items) {
2152
+ const result = [];
2153
+ for (const item of items) {
2154
+ if (item.type === JSON_TYPES.NUMBER_LINE) result.push(item);
2155
+ else if (item.type === JSON_TYPES.GROUP) result.push(...collectNumberLines(item.items));
2156
+ }
2157
+ return result;
2158
+ }
2159
+ function buildPositionMap(exercise) {
2160
+ const map = {};
2161
+ for (const nl of collectNumberLines(exercise.layout)) {
2162
+ for (const pos of nl.positions) {
2163
+ for (const c of pos.content) {
2164
+ if (isWidgetRef(c)) {
2165
+ map[c.widgetId] = pos.value;
2166
+ break;
2167
+ }
2168
+ }
2169
+ }
2170
+ }
2171
+ return map;
2172
+ }
2027
2173
  function validateSimple(exercise, userInputs) {
2028
2174
  const details = {};
2029
2175
  let allCorrect = true;
@@ -2037,10 +2183,11 @@ function validateSimple(exercise, userInputs) {
2037
2183
  score: 0
2038
2184
  };
2039
2185
  }
2186
+ const positionMap = buildPositionMap(exercise);
2040
2187
  for (const [widgetId, expected] of Object.entries(answers)) {
2041
2188
  totalCount++;
2042
2189
  const userValue = userInputs[widgetId];
2043
- const isCorrect = areValuesEqual(userValue, expected);
2190
+ const isCorrect = areValuesEqual(userValue, expected, positionMap[widgetId]);
2044
2191
  details[widgetId] = {
2045
2192
  widgetId,
2046
2193
  userValue,
@@ -2068,19 +2215,13 @@ function validateSimple(exercise, userInputs) {
2068
2215
  score: totalCount > 0 ? correctCount / totalCount : 0
2069
2216
  };
2070
2217
  }
2071
- function areValuesEqual(userValue, expected) {
2072
- if (userValue === void 0 || userValue === null) {
2073
- return false;
2218
+ function areValuesEqual(userValue, expected, expectedPosition) {
2219
+ if (userValue === void 0 || userValue === null) return false;
2220
+ if (typeof userValue === "string" && isNLDragEncoded(userValue)) {
2221
+ if (expectedPosition === void 0) return false;
2222
+ return isNLDragCorrect(userValue, expected, expectedPosition);
2074
2223
  }
2075
- const normalizedUser = normalize(userValue);
2076
- const normalizedExpected = normalize(expected);
2077
- return normalizedUser === normalizedExpected;
2078
- }
2079
- function normalize(value) {
2080
- if (typeof value === "number") {
2081
- return String(Math.round(value * 1e10) / 1e10);
2082
- }
2083
- return String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
2224
+ return normalize(userValue) === normalize(expected);
2084
2225
  }
2085
2226
  const math = mathjs.create(mathjs.all);
2086
2227
  const BLOCKED_FUNCTIONS = [
@@ -6622,7 +6763,10 @@ const WidgetRenderer = ({
6622
6763
  }
6623
6764
  return null;
6624
6765
  };
6625
- const cleanFloat = (n) => parseFloat(n.toFixed(10));
6766
+ const EPSILON = 10 ** -INTERNAL_PRECISION;
6767
+ const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
6768
+ const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
6769
+ const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
6626
6770
  const toPercent = (value, min, max) => {
6627
6771
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
6628
6772
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -6638,19 +6782,24 @@ const getMajorTicks = (min, max, step) => {
6638
6782
  const count = Math.floor((max - min) / step);
6639
6783
  const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
6640
6784
  const lastTick = ticks[ticks.length - 1];
6641
- if (Math.abs(lastTick - max) > 1e-9) ticks.push(max);
6785
+ if (!almostEqual(lastTick, max)) {
6786
+ ticks.push(max);
6787
+ } else {
6788
+ ticks[ticks.length - 1] = max;
6789
+ }
6642
6790
  return ticks;
6643
6791
  };
6644
6792
  const getMinorTickValues = (majorTicks, minorTicks) => {
6645
- if (minorTicks <= 0) return [];
6793
+ if (minorTicks <= 0 || majorTicks.length < 2) return [];
6646
6794
  const result = [];
6647
- majorTicks.slice(0, -1).forEach((tick, idx) => {
6648
- const next = majorTicks[idx + 1];
6649
- const interval = next - tick;
6795
+ for (let i = 0; i < majorTicks.length - 1; i++) {
6796
+ const from = majorTicks[i];
6797
+ const to = majorTicks[i + 1];
6798
+ const interval = to - from;
6650
6799
  for (let j = 1; j <= minorTicks; j++) {
6651
- result.push(cleanFloat(tick + j / (minorTicks + 1) * interval));
6800
+ result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
6652
6801
  }
6653
- });
6802
+ }
6654
6803
  return result;
6655
6804
  };
6656
6805
  const getSnapTicks = (majorTicks, minorTicks) => [
@@ -6659,19 +6808,41 @@ const getSnapTicks = (majorTicks, minorTicks) => [
6659
6808
  ];
6660
6809
  const snapToNearest = (raw, ticks) => {
6661
6810
  if (ticks.length === 0) return cleanFloat(raw);
6662
- const snapped = ticks.reduce(
6663
- (best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
6664
- );
6665
- return cleanFloat(snapped);
6811
+ return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
6666
6812
  };
6667
- const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
6668
6813
  const getPositionWidgetId = (content) => {
6669
6814
  for (const item of content) {
6670
6815
  if (isWidgetRef(item)) return item.widgetId;
6671
6816
  }
6672
6817
  return null;
6673
6818
  };
6674
- const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
6819
+ const toHighlightColor = (color) => `var(--nl-highlight-${color})`;
6820
+ const getBandGeometry = (h, min, max) => {
6821
+ const leftPct = toPercent(Math.min(h.from, h.to), min, max);
6822
+ const rightPct = toPercent(Math.max(h.from, h.to), min, max);
6823
+ return { leftPct, widthPct: rightPct - leftPct };
6824
+ };
6825
+ const HighlightBands = ({ highlights, min, max }) => {
6826
+ if (!highlights?.length) return null;
6827
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: highlights.map((h) => {
6828
+ const { leftPct, widthPct } = getBandGeometry(h, min, max);
6829
+ if (widthPct <= 0) return null;
6830
+ return /* @__PURE__ */ jsxRuntime.jsx(
6831
+ "div",
6832
+ {
6833
+ className: "number-line-highlight-band",
6834
+ "aria-hidden": "true",
6835
+ style: {
6836
+ left: `${leftPct}%`,
6837
+ width: `${widthPct}%`,
6838
+ ...h.color && { "--nl-highlight-color": toHighlightColor(h.color) }
6839
+ }
6840
+ },
6841
+ h.id
6842
+ );
6843
+ }) });
6844
+ };
6845
+ const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
6675
6846
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
6676
6847
  id: widgetId,
6677
6848
  data: { type: "placed-chip", widgetId, label }
@@ -6679,23 +6850,49 @@ const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
6679
6850
  return /* @__PURE__ */ jsxRuntime.jsx(
6680
6851
  "div",
6681
6852
  {
6682
- className: "number-line-placed-chip",
6683
- style: { left: `${leftPercent}%`, opacity: isDragging ? 0.3 : 1 },
6684
- children: /* @__PURE__ */ jsxRuntime.jsx(
6685
- "button",
6686
- {
6687
- ref: setNodeRef,
6688
- className: "number-line-placed-chip-btn",
6689
- title: "Drag to move · Click to remove",
6690
- onClick: onRemove,
6691
- ...listeners,
6692
- ...attributes,
6693
- children: label
6694
- }
6695
- )
6853
+ className: `number-line-placed-chip${isLanding ? " nl-chip-landing" : ""}${isDragging ? " nl-chip-dragging" : ""}`,
6854
+ style: { left: `${leftPercent}%` },
6855
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-placed-chip-inner", children: [
6856
+ /* @__PURE__ */ jsxRuntime.jsx(
6857
+ "button",
6858
+ {
6859
+ ref: setNodeRef,
6860
+ className: "number-line-placed-chip-btn",
6861
+ "aria-label": `${label}, drag to move`,
6862
+ ...listeners,
6863
+ ...attributes,
6864
+ children: label
6865
+ }
6866
+ ),
6867
+ /* @__PURE__ */ jsxRuntime.jsx(
6868
+ "button",
6869
+ {
6870
+ type: "button",
6871
+ className: "number-line-placed-chip-remove",
6872
+ "aria-label": "Remove",
6873
+ onClick: onRemove,
6874
+ children: "✕"
6875
+ }
6876
+ )
6877
+ ] })
6696
6878
  }
6697
6879
  );
6698
6880
  };
6881
+ function findNearestFreeWidgetSlot(positions, axisValue, values, placedChips, excludeWid) {
6882
+ let best;
6883
+ let bestDist = Infinity;
6884
+ for (const pos of positions) {
6885
+ const wid = getPositionWidgetId(pos.content);
6886
+ if (!wid) continue;
6887
+ if (wid !== excludeWid && (values[wid] || placedChips.has(wid))) continue;
6888
+ const dist = Math.abs(pos.value - axisValue);
6889
+ if (dist < bestDist) {
6890
+ bestDist = dist;
6891
+ best = pos;
6892
+ }
6893
+ }
6894
+ return best;
6895
+ }
6699
6896
  const FreeDragAxis = ({
6700
6897
  axisDropId,
6701
6898
  min,
@@ -6706,17 +6903,51 @@ const FreeDragAxis = ({
6706
6903
  showArrowRight,
6707
6904
  positions,
6708
6905
  values,
6709
- onChange
6906
+ onChange,
6907
+ highlights,
6908
+ snapKey
6710
6909
  }) => {
6711
6910
  const axisRef = React.useRef(null);
6712
6911
  const snapTicks = React.useMemo(
6713
6912
  () => getSnapTicks(majorTicks, minorTicks),
6714
6913
  [majorTicks, minorTicks]
6715
6914
  );
6716
- const [dropLabels, setDropLabels] = React.useState(/* @__PURE__ */ new Map());
6717
- const [dropSources, setDropSources] = React.useState(/* @__PURE__ */ new Map());
6915
+ const minorValues = React.useMemo(
6916
+ () => getMinorTickValues(majorTicks, minorTicks),
6917
+ [majorTicks, minorTicks]
6918
+ );
6919
+ const [placedChips, setPlacedChips] = React.useState(/* @__PURE__ */ new Map());
6920
+ const [hoverSnap, setHoverSnap] = React.useState(null);
6921
+ const [landingChips, setLandingChips] = React.useState(/* @__PURE__ */ new Set());
6922
+ const landingTimersRef = React.useRef(/* @__PURE__ */ new Map());
6923
+ React.useEffect(() => () => landingTimersRef.current.forEach(clearTimeout), []);
6924
+ const onChangeRef = React.useRef(onChange);
6925
+ const placedChipsRef = React.useRef(placedChips);
6926
+ React.useEffect(() => {
6927
+ onChangeRef.current = onChange;
6928
+ }, [onChange]);
6718
6929
  React.useEffect(() => {
6719
- setDropLabels((prev) => {
6930
+ placedChipsRef.current = placedChips;
6931
+ }, [placedChips]);
6932
+ const isFirstRender = React.useRef(true);
6933
+ React.useEffect(() => {
6934
+ if (isFirstRender.current) {
6935
+ isFirstRender.current = false;
6936
+ return;
6937
+ }
6938
+ for (const [widgetId, chip] of placedChipsRef.current) {
6939
+ onChangeRef.current(widgetId, "", false, chip.sourceId);
6940
+ }
6941
+ landingTimersRef.current.forEach(clearTimeout);
6942
+ landingTimersRef.current.clear();
6943
+ const empty = /* @__PURE__ */ new Map();
6944
+ placedChipsRef.current = empty;
6945
+ setPlacedChips(empty);
6946
+ setHoverSnap(null);
6947
+ setLandingChips(/* @__PURE__ */ new Set());
6948
+ }, [snapKey]);
6949
+ React.useEffect(() => {
6950
+ setPlacedChips((prev) => {
6720
6951
  let changed = false;
6721
6952
  const next = new Map(prev);
6722
6953
  for (const [wid] of next) {
@@ -6725,21 +6956,13 @@ const FreeDragAxis = ({
6725
6956
  changed = true;
6726
6957
  }
6727
6958
  }
6728
- return changed ? next : prev;
6729
- });
6730
- setDropSources((prev) => {
6731
- let changed = false;
6732
- const next = new Map(prev);
6733
- for (const [wid] of next) {
6734
- if (!values[wid]) {
6735
- next.delete(wid);
6736
- changed = true;
6737
- }
6959
+ if (changed) {
6960
+ placedChipsRef.current = next;
6738
6961
  }
6739
6962
  return changed ? next : prev;
6740
6963
  });
6741
6964
  }, [values]);
6742
- const { setNodeRef, isOver } = useDroppable({ id: axisDropId });
6965
+ const { setNodeRef } = useDroppable({ id: axisDropId });
6743
6966
  const mergedRef = React.useCallback((node) => {
6744
6967
  axisRef.current = node;
6745
6968
  setNodeRef(node);
@@ -6747,91 +6970,163 @@ const FreeDragAxis = ({
6747
6970
  const snapFromEvent = React.useCallback((translated) => {
6748
6971
  if (!axisRef.current) return null;
6749
6972
  const axisRect = axisRef.current.getBoundingClientRect();
6973
+ if (axisRect.width === 0) return null;
6750
6974
  const chipCenterX = translated.left + translated.width / 2;
6751
6975
  const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
6752
6976
  return snapToNearest(raw, snapTicks);
6753
6977
  }, [min, max, snapTicks]);
6978
+ const isTickOccupied = React.useCallback(
6979
+ (tick, excludeWid) => [...placedChipsRef.current.entries()].some(
6980
+ ([wid, c]) => wid !== excludeWid && almostEqual(c.position, tick)
6981
+ ),
6982
+ []
6983
+ // reads placedChipsRef (a ref — stable, no dep needed)
6984
+ );
6985
+ const findNearestFreeWidgetSlotCb = React.useCallback(
6986
+ (axisValue, excludeWid) => findNearestFreeWidgetSlot(positions, axisValue, values, placedChipsRef.current, excludeWid),
6987
+ [positions, values]
6988
+ );
6989
+ const triggerLanding = React.useCallback((widgetId) => {
6990
+ setLandingChips((prev) => new Set(prev).add(widgetId));
6991
+ clearTimeout(landingTimersRef.current.get(widgetId));
6992
+ landingTimersRef.current.set(widgetId, setTimeout(() => {
6993
+ setLandingChips((prev) => {
6994
+ const s = new Set(prev);
6995
+ s.delete(widgetId);
6996
+ return s;
6997
+ });
6998
+ landingTimersRef.current.delete(widgetId);
6999
+ }, 300));
7000
+ }, []);
6754
7001
  useDndMonitor({
7002
+ onDragMove(event) {
7003
+ if (!event.over || event.over.id !== axisDropId) {
7004
+ setHoverSnap(null);
7005
+ return;
7006
+ }
7007
+ const translated = event.active.rect.current.translated;
7008
+ if (!translated) return;
7009
+ setHoverSnap(snapFromEvent(translated));
7010
+ },
7011
+ onDragCancel() {
7012
+ setHoverSnap(null);
7013
+ },
6755
7014
  onDragEnd(event) {
7015
+ setHoverSnap(null);
6756
7016
  if (!event.over || event.over.id !== axisDropId) return;
6757
7017
  if (!axisRef.current) return;
6758
7018
  const translated = event.active.rect.current.translated;
6759
7019
  if (!translated) return;
6760
- const placed = snapFromEvent(translated);
6761
- if (placed === null) return;
7020
+ const rawPlaced = snapFromEvent(translated);
7021
+ if (rawPlaced === null) {
7022
+ if (process.env.NODE_ENV !== "production") {
7023
+ console.warn("[FreeDragAxis] snap calculation failed — axis may not be laid out yet");
7024
+ }
7025
+ return;
7026
+ }
7027
+ const placed = cleanFloat(rawPlaced);
6762
7028
  const data = event.active.data.current;
6763
- if (data?.type === "placed-chip") {
6764
- const widgetId = data.widgetId;
7029
+ if (!data) return;
7030
+ if (data.type === "placed-chip") {
7031
+ const oldWidgetId = data.widgetId;
6765
7032
  const label = data.label;
6766
- const originalSourceId = dropSources.get(widgetId) ?? widgetId;
6767
- onChange(widgetId, String(placed), true, originalSourceId);
6768
- setDropLabels((prev) => new Map(prev).set(widgetId, label));
7033
+ const sourceId2 = placedChipsRef.current.get(oldWidgetId)?.sourceId ?? oldWidgetId;
7034
+ if (isTickOccupied(placed, oldWidgetId)) return;
7035
+ const targetPos2 = findNearestFreeWidgetSlotCb(placed, oldWidgetId);
7036
+ if (!targetPos2) return;
7037
+ const newWidgetId = getPositionWidgetId(targetPos2.content);
7038
+ if (!newWidgetId) return;
7039
+ if (newWidgetId !== oldWidgetId) {
7040
+ onChangeRef.current(oldWidgetId, "", false, sourceId2);
7041
+ }
7042
+ onChangeRef.current(newWidgetId, encodeNLDragValue(label, placed), true, sourceId2);
7043
+ setPlacedChips((prev) => {
7044
+ const next = new Map(prev);
7045
+ if (newWidgetId !== oldWidgetId) next.delete(oldWidgetId);
7046
+ next.set(newWidgetId, { label, position: placed, sourceId: sourceId2 });
7047
+ placedChipsRef.current = next;
7048
+ return next;
7049
+ });
7050
+ triggerLanding(newWidgetId);
6769
7051
  return;
6770
7052
  }
6771
- const draggedItem = event.active.data.current;
6772
- if (draggedItem?.type !== "drag-option") return;
6773
- const targetPos = positions.find((pos) => {
6774
- const wid = getPositionWidgetId(pos.content);
6775
- return wid && !values[wid];
6776
- });
7053
+ if (data.type !== "drag-option") return;
7054
+ if (isTickOccupied(placed)) return;
7055
+ const targetPos = findNearestFreeWidgetSlotCb(placed);
6777
7056
  if (!targetPos) return;
6778
7057
  const targetWidgetId = getPositionWidgetId(targetPos.content);
6779
7058
  if (!targetWidgetId) return;
6780
- const chipLabel = draggedItem.data.value;
6781
- const sourceOptId = String(event.active.id);
6782
- onChange(targetWidgetId, String(placed), true, sourceOptId);
6783
- setDropLabels((prev) => new Map(prev).set(targetWidgetId, chipLabel));
6784
- setDropSources((prev) => new Map(prev).set(targetWidgetId, sourceOptId));
7059
+ const chipLabel = data.data.value;
7060
+ const sourceId = String(event.active.id);
7061
+ onChangeRef.current(targetWidgetId, encodeNLDragValue(chipLabel, placed), true, sourceId);
7062
+ setPlacedChips((prev) => {
7063
+ const next = new Map(prev).set(targetWidgetId, { label: chipLabel, position: placed, sourceId });
7064
+ placedChipsRef.current = next;
7065
+ return next;
7066
+ });
7067
+ triggerLanding(targetWidgetId);
6785
7068
  }
6786
7069
  });
6787
7070
  return /* @__PURE__ */ jsxRuntime.jsxs(
6788
7071
  "div",
6789
7072
  {
6790
7073
  ref: mergedRef,
6791
- className: `number-line-axis-row nl-axis-drag-mode${isOver ? " nl-axis-drag-over" : ""}`,
7074
+ className: "number-line-axis-row nl-axis-drag-mode",
6792
7075
  children: [
6793
7076
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track" }),
6794
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
6795
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
6796
- getMinorTickValues(majorTicks, minorTicks).map((v) => /* @__PURE__ */ jsxRuntime.jsx(
7077
+ showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
7078
+ showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
7079
+ /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
7080
+ minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
6797
7081
  "div",
6798
7082
  {
6799
- className: "number-line-minor-tick",
7083
+ className: `number-line-minor-tick${hoverSnap === v ? " nl-tick-hover" : ""}`,
6800
7084
  style: { left: `${toPercent(v, min, max)}%` }
6801
7085
  },
6802
- `minor-${v}`
7086
+ `minor-${cleanFloat(v)}`
6803
7087
  )),
6804
7088
  majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
6805
7089
  "div",
6806
7090
  {
6807
- className: "number-line-major-tick",
7091
+ className: `number-line-major-tick${hoverSnap === tick ? " nl-tick-hover" : ""}`,
6808
7092
  style: { left: `${toPercent(tick, min, max)}%` }
6809
7093
  },
6810
- `major-${tick}`
7094
+ `major-${cleanFloat(tick)}`
7095
+ )),
7096
+ hoverSnap !== null && /* @__PURE__ */ jsxRuntime.jsx(
7097
+ "div",
7098
+ {
7099
+ className: "nl-snap-preview",
7100
+ style: { left: `${toPercent(hoverSnap, min, max)}%` },
7101
+ "aria-hidden": "true"
7102
+ }
7103
+ ),
7104
+ positions.filter((pos) => getPositionWidgetId(pos.content) === null).map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
7105
+ "div",
7106
+ {
7107
+ className: "number-line-position-dot",
7108
+ style: { left: `${toPercent(pos.value, min, max)}%` }
7109
+ },
7110
+ `dot-${pos.id}`
6811
7111
  )),
6812
7112
  positions.map((pos) => {
6813
7113
  const wid = getPositionWidgetId(pos.content);
6814
- const dropPos = wid ? parseFloat(values[wid] ?? "") : NaN;
6815
- const label = wid ? dropLabels.get(wid) : void 0;
6816
- if (!wid || isNaN(dropPos) || !label) return null;
7114
+ const chip = wid ? placedChips.get(wid) : void 0;
7115
+ if (!wid || !chip) return null;
6817
7116
  return /* @__PURE__ */ jsxRuntime.jsx(
6818
7117
  PlacedChip,
6819
7118
  {
6820
7119
  widgetId: wid,
6821
- label,
6822
- leftPercent: toPercent(dropPos, min, max),
7120
+ label: chip.label,
7121
+ leftPercent: toPercent(chip.position, min, max),
7122
+ isLanding: landingChips.has(wid),
6823
7123
  onRemove: () => {
6824
- const sourceId = dropSources.get(wid);
6825
- onChange(wid, "", false, sourceId);
6826
- setDropLabels((prev) => {
6827
- const m = new Map(prev);
6828
- m.delete(wid);
6829
- return m;
6830
- });
6831
- setDropSources((prev) => {
6832
- const m = new Map(prev);
6833
- m.delete(wid);
6834
- return m;
7124
+ onChangeRef.current(wid, "", false, chip.sourceId);
7125
+ setPlacedChips((prev) => {
7126
+ const next = new Map(prev);
7127
+ next.delete(wid);
7128
+ placedChipsRef.current = next;
7129
+ return next;
6835
7130
  });
6836
7131
  }
6837
7132
  },
@@ -6842,6 +7137,132 @@ const FreeDragAxis = ({
6842
7137
  }
6843
7138
  );
6844
7139
  };
7140
+ const AxisTicks = ({
7141
+ min,
7142
+ max,
7143
+ majorTicks,
7144
+ minorValues,
7145
+ showArrowLeft,
7146
+ showArrowRight,
7147
+ highlights,
7148
+ positions
7149
+ }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
7150
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
7151
+ showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
7152
+ showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
7153
+ /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
7154
+ minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
7155
+ "div",
7156
+ {
7157
+ className: "number-line-minor-tick",
7158
+ "aria-hidden": "true",
7159
+ style: { left: `${toPercent(v, min, max)}%` }
7160
+ },
7161
+ `minor-${v}`
7162
+ )),
7163
+ majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
7164
+ "div",
7165
+ {
7166
+ className: "number-line-major-tick",
7167
+ "aria-hidden": "true",
7168
+ style: { left: `${toPercent(tick, min, max)}%` }
7169
+ },
7170
+ `major-${tick}`
7171
+ )),
7172
+ positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
7173
+ "div",
7174
+ {
7175
+ className: "number-line-position-dot",
7176
+ "aria-hidden": "true",
7177
+ style: { left: `${toPercent(pos.value, min, max)}%` }
7178
+ },
7179
+ `dot-${pos.id}`
7180
+ ))
7181
+ ] });
7182
+ const isLabelVisible = (tick, min, max, labelVisibility) => {
7183
+ switch (labelVisibility) {
7184
+ case "none":
7185
+ return false;
7186
+ case "min-only":
7187
+ return tick === roundTick(min);
7188
+ case "max-only":
7189
+ return tick === roundTick(max);
7190
+ case "extremes":
7191
+ return tick === roundTick(min) || tick === roundTick(max);
7192
+ default:
7193
+ return true;
7194
+ }
7195
+ };
7196
+ const AxisLabels = ({
7197
+ majorTicks,
7198
+ min,
7199
+ max,
7200
+ labelVisibility,
7201
+ inputTickSet
7202
+ }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
7203
+ const visible = !inputTickSet.has(tick) && isLabelVisible(tick, min, max, labelVisibility);
7204
+ return /* @__PURE__ */ jsxRuntime.jsx(
7205
+ "span",
7206
+ {
7207
+ className: "number-line-tick-label",
7208
+ style: {
7209
+ left: `${toPercent(tick, min, max)}%`,
7210
+ visibility: visible ? "visible" : "hidden"
7211
+ },
7212
+ children: tick
7213
+ },
7214
+ `lbl-${tick}`
7215
+ );
7216
+ }) });
7217
+ const HighlightLabelsRow = ({ highlights, min, max }) => {
7218
+ if (!highlights?.some((h) => h.label)) return null;
7219
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-highlight-labels-row", "aria-hidden": "true", children: highlights.map((h) => {
7220
+ if (!h.label) return null;
7221
+ const centerPct = (toPercent(h.from, min, max) + toPercent(h.to, min, max)) / 2;
7222
+ return /* @__PURE__ */ jsxRuntime.jsx(
7223
+ "span",
7224
+ {
7225
+ className: "number-line-highlight-label",
7226
+ style: { left: `${centerPct}%` },
7227
+ children: h.label
7228
+ },
7229
+ `hl-lbl-${h.id}`
7230
+ );
7231
+ }) });
7232
+ };
7233
+ const CounterControls = ({
7234
+ counter,
7235
+ count,
7236
+ onDecrement,
7237
+ onIncrement
7238
+ }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter", "aria-label": counter.name ?? "Counter", children: [
7239
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-counter-controls", children: [
7240
+ /* @__PURE__ */ jsxRuntime.jsx(
7241
+ "button",
7242
+ {
7243
+ type: "button",
7244
+ className: "number-line-counter-btn",
7245
+ "aria-label": "Decrease",
7246
+ disabled: count <= counter.min,
7247
+ onClick: onDecrement,
7248
+ children: "−"
7249
+ }
7250
+ ),
7251
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-value", "data-testid": "counter-value", children: count }),
7252
+ /* @__PURE__ */ jsxRuntime.jsx(
7253
+ "button",
7254
+ {
7255
+ type: "button",
7256
+ className: "number-line-counter-btn",
7257
+ "aria-label": "Increase",
7258
+ disabled: count >= counter.max,
7259
+ onClick: onIncrement,
7260
+ children: "+"
7261
+ }
7262
+ )
7263
+ ] }),
7264
+ counter.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-label", children: counter.name })
7265
+ ] });
6845
7266
  const NumberLineRenderer = ({
6846
7267
  item,
6847
7268
  widgets,
@@ -6850,27 +7271,59 @@ const NumberLineRenderer = ({
6850
7271
  onChange,
6851
7272
  onInputFocus
6852
7273
  }) => {
6853
- const { min, max, step, minorTicks = 0, style = "arrow-right", positions } = item;
6854
- const majorTicks = React.useMemo(() => getMajorTicks(min, max, step), [min, max, step]);
6855
- const minorValues = React.useMemo(() => getMinorTickValues(majorTicks, minorTicks), [majorTicks, minorTicks]);
6856
- const showArrowRight = style === "arrow-right" || style === "arrow-both";
6857
- const showArrowLeft = style === "arrow-left" || style === "arrow-both";
7274
+ const {
7275
+ min,
7276
+ max,
7277
+ step,
7278
+ minorTicks = 0,
7279
+ style: lineStyle = "arrow-right",
7280
+ labelVisibility,
7281
+ highlights,
7282
+ counter,
7283
+ positions
7284
+ } = item;
7285
+ const [counterCount, setCounterCount] = React.useState(
7286
+ counter !== void 0 ? counter.default ?? counter.min : 0
7287
+ );
7288
+ React.useEffect(() => {
7289
+ setCounterCount(counter !== void 0 ? counter.default ?? counter.min : 0);
7290
+ }, [counter]);
7291
+ const effectiveStep = React.useMemo(() => {
7292
+ if (counter !== void 0 && counterCount > 0) {
7293
+ return cleanFloat((max - min) / counterCount);
7294
+ }
7295
+ return step;
7296
+ }, [counter, counterCount, min, max, step]);
7297
+ const majorTicks = React.useMemo(
7298
+ () => getMajorTicks(min, max, effectiveStep).map(roundTick),
7299
+ [min, max, effectiveStep]
7300
+ );
7301
+ const minorValues = React.useMemo(
7302
+ () => getMinorTickValues(majorTicks, minorTicks),
7303
+ [majorTicks, minorTicks]
7304
+ );
7305
+ const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
7306
+ const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
6858
7307
  const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
6859
7308
  const inputTickSet = React.useMemo(() => {
6860
7309
  if (isDragMode) return /* @__PURE__ */ new Set();
6861
7310
  return new Set(
6862
- positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
7311
+ positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => roundTick(pos.value))
6863
7312
  );
6864
7313
  }, [isDragMode, positions]);
7314
+ const visiblePositions = React.useMemo(
7315
+ () => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
7316
+ [isDragMode, positions]
7317
+ );
6865
7318
  return /* @__PURE__ */ jsxRuntime.jsxs(
6866
7319
  "div",
6867
7320
  {
6868
- className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
7321
+ className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
6869
7322
  style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
6870
- role: "img",
7323
+ role: "group",
6871
7324
  "aria-label": `Number line from ${min} to ${max}`,
6872
7325
  children: [
6873
- !isDragMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
7326
+ visiblePositions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
6874
7327
  "div",
6875
7328
  {
6876
7329
  className: "number-line-position-content",
@@ -6902,50 +7355,43 @@ const NumberLineRenderer = ({
6902
7355
  showArrowRight,
6903
7356
  positions,
6904
7357
  values,
6905
- onChange
7358
+ onChange,
7359
+ highlights,
7360
+ snapKey: counterCount
6906
7361
  }
6907
- ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
6908
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
6909
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
6910
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
6911
- minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
6912
- "div",
6913
- {
6914
- className: "number-line-minor-tick",
6915
- "aria-hidden": "true",
6916
- style: { left: `${toPercent(v, min, max)}%` }
6917
- },
6918
- `minor-${v}`
6919
- )),
6920
- majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
6921
- "div",
6922
- {
6923
- className: "number-line-major-tick",
6924
- style: { left: `${toPercent(tick, min, max)}%` }
6925
- },
6926
- `major-${tick}`
6927
- )),
6928
- positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
6929
- "div",
6930
- {
6931
- className: "number-line-position-dot",
6932
- style: { left: `${toPercent(pos.value, min, max)}%` }
6933
- },
6934
- `dot-${pos.id}`
6935
- ))
6936
- ] }),
6937
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
6938
- const hasInputAtTick = inputTickSet.has(tick);
6939
- return /* @__PURE__ */ jsxRuntime.jsx(
6940
- "span",
6941
- {
6942
- className: "number-line-tick-label",
6943
- style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
6944
- children: tick
6945
- },
6946
- `lbl-${tick}`
6947
- );
6948
- }) })
7362
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
7363
+ AxisTicks,
7364
+ {
7365
+ min,
7366
+ max,
7367
+ majorTicks,
7368
+ minorValues,
7369
+ showArrowLeft,
7370
+ showArrowRight,
7371
+ highlights,
7372
+ positions
7373
+ }
7374
+ ),
7375
+ /* @__PURE__ */ jsxRuntime.jsx(
7376
+ AxisLabels,
7377
+ {
7378
+ majorTicks,
7379
+ min,
7380
+ max,
7381
+ labelVisibility,
7382
+ inputTickSet
7383
+ }
7384
+ ),
7385
+ /* @__PURE__ */ jsxRuntime.jsx(HighlightLabelsRow, { highlights, min, max }),
7386
+ counter !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
7387
+ CounterControls,
7388
+ {
7389
+ counter,
7390
+ count: counterCount,
7391
+ onDecrement: () => setCounterCount((c) => Math.max(counter.min, c - 1)),
7392
+ onIncrement: () => setCounterCount((c) => Math.min(counter.max, c + 1))
7393
+ }
7394
+ )
6949
7395
  ]
6950
7396
  }
6951
7397
  );
@@ -7173,8 +7619,7 @@ const ExerciseRenderer = React.forwardRef(({
7173
7619
  }
7174
7620
  }), [json, values, onSubmit]);
7175
7621
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
7176
- const newValues = { ...values, [widgetId]: value };
7177
- setValues(newValues);
7622
+ setValues((prev) => ({ ...prev, [widgetId]: value }));
7178
7623
  setDroppedSources((prev) => {
7179
7624
  const next = new Map(prev);
7180
7625
  if (fromDrop && value !== "" && sourceOptionId) {
@@ -7184,7 +7629,7 @@ const ExerciseRenderer = React.forwardRef(({
7184
7629
  }
7185
7630
  return next;
7186
7631
  });
7187
- onChange?.(newValues);
7632
+ onChange?.({ ...values, [widgetId]: value });
7188
7633
  };
7189
7634
  const handleSubmit = () => {
7190
7635
  onSubmit?.(values);
@@ -7407,6 +7852,10 @@ exports.LayoutRenderer = LayoutRenderer;
7407
7852
  exports.LexerError = LexerError;
7408
7853
  exports.LineBlockSchema = LineBlockSchema;
7409
7854
  exports.LinebreakSchema = LinebreakSchema;
7855
+ exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
7856
+ exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
7857
+ exports.NumberLineItemSchema = NumberLineItemSchema;
7858
+ exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;
7410
7859
  exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
7411
7860
  exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
7412
7861
  exports.OperatorItemSchema = OperatorItemSchema;
@@ -7480,7 +7929,7 @@ exports.isTextItem = isTextItem$1;
7480
7929
  exports.isToolNode = isToolNode;
7481
7930
  exports.isValidExpression = isValidExpression;
7482
7931
  exports.isValidationNode = isValidationNode;
7483
- exports.isWidgetRef = isWidgetRef$1;
7932
+ exports.isWidgetRef = isWidgetRef;
7484
7933
  exports.parse = parse;
7485
7934
  exports.parseSafe = parseSafe;
7486
7935
  exports.renderExercise = renderExercise;