@mathscan/math-exercise-engine 1.0.10 → 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,16 +453,14 @@ 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);
441
460
  });
442
- this.MANY2(() => {
443
- this.SUBRULE(this.contentBlock);
461
+ this.MANY2({
462
+ GATE: this.BACKTRACK(this.contentBlock),
463
+ DEF: () => this.SUBRULE(this.contentBlock)
444
464
  });
445
465
  this.MANY3(() => {
446
466
  this.SUBRULE(this.tool);
@@ -449,10 +469,7 @@ class MathExerciseParser extends chevrotain.CstParser {
449
469
  this.SUBRULE(this.validation);
450
470
  });
451
471
  this.MANY4(() => {
452
- this.OR2([
453
- { ALT: () => this.CONSUME2(Newline) },
454
- { ALT: () => this.CONSUME2(Space) }
455
- ]);
472
+ this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
456
473
  });
457
474
  });
458
475
  this.question = this.RULE("question", () => {
@@ -461,10 +478,7 @@ class MathExerciseParser extends chevrotain.CstParser {
461
478
  this.CONSUME(StringLiteral, { LABEL: "value" });
462
479
  this.CONSUME(RBracket);
463
480
  this.MANY(() => {
464
- this.OR([
465
- { ALT: () => this.CONSUME(Newline) },
466
- { ALT: () => this.CONSUME(Space) }
467
- ]);
481
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
468
482
  });
469
483
  });
470
484
  this.contentBlock = this.RULE("contentBlock", () => {
@@ -486,77 +500,50 @@ class MathExerciseParser extends chevrotain.CstParser {
486
500
  });
487
501
  this.table = this.RULE("table", () => {
488
502
  this.MANY(() => {
489
- this.OR2([
490
- { ALT: () => this.CONSUME(Space) },
491
- { ALT: () => this.CONSUME(Newline) }
492
- ]);
503
+ this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
493
504
  });
494
505
  this.CONSUME(TABLE);
495
506
  this.CONSUME(LBracket);
496
507
  this.SUBRULE(this.parameterList);
497
508
  this.CONSUME(RBracket);
498
509
  this.MANY2(() => {
499
- this.OR3([
500
- { ALT: () => this.CONSUME2(Space) },
501
- { ALT: () => this.CONSUME2(Newline) }
502
- ]);
510
+ this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
503
511
  });
504
512
  this.CONSUME(LBrace);
505
513
  this.MANY3(() => {
506
- this.OR4([
507
- { ALT: () => this.CONSUME3(Space) },
508
- { ALT: () => this.CONSUME3(Newline) }
509
- ]);
514
+ this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
510
515
  });
511
516
  this.AT_LEAST_ONE(() => {
512
517
  this.SUBRULE(this.row);
513
518
  });
514
519
  this.CONSUME(RBrace);
515
520
  this.MANY4(() => {
516
- this.OR5([
517
- { ALT: () => this.CONSUME4(Newline) },
518
- { ALT: () => this.CONSUME4(Space) }
519
- ]);
521
+ this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
520
522
  });
521
523
  });
522
524
  this.row = this.RULE("row", () => {
523
525
  this.MANY(() => {
524
- this.OR([
525
- { ALT: () => this.CONSUME(Space) },
526
- { ALT: () => this.CONSUME(Newline) }
527
- ]);
526
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
528
527
  });
529
528
  this.CONSUME(ROW);
530
529
  this.MANY2(() => {
531
- this.OR2([
532
- { ALT: () => this.CONSUME2(Space) },
533
- { ALT: () => this.CONSUME2(Newline) }
534
- ]);
530
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
535
531
  });
536
532
  this.CONSUME(LBrace);
537
533
  this.MANY3(() => {
538
- this.OR3([
539
- { ALT: () => this.CONSUME3(Space) },
540
- { ALT: () => this.CONSUME3(Newline) }
541
- ]);
534
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
542
535
  });
543
536
  this.AT_LEAST_ONE(() => {
544
537
  this.SUBRULE(this.cell);
545
538
  });
546
539
  this.CONSUME(RBrace);
547
540
  this.MANY4(() => {
548
- this.OR4([
549
- { ALT: () => this.CONSUME4(Space) },
550
- { ALT: () => this.CONSUME4(Newline) }
551
- ]);
541
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
552
542
  });
553
543
  });
554
544
  this.cell = this.RULE("cell", () => {
555
545
  this.MANY(() => {
556
- this.OR([
557
- { ALT: () => this.CONSUME(Space) },
558
- { ALT: () => this.CONSUME(Newline) }
559
- ]);
546
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
560
547
  });
561
548
  this.CONSUME(CELL);
562
549
  this.OPTION(() => {
@@ -565,10 +552,7 @@ class MathExerciseParser extends chevrotain.CstParser {
565
552
  this.CONSUME(RBracket);
566
553
  });
567
554
  this.MANY2(() => {
568
- this.OR2([
569
- { ALT: () => this.CONSUME2(Space) },
570
- { ALT: () => this.CONSUME2(Newline) }
571
- ]);
555
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
572
556
  });
573
557
  this.CONSUME(LBrace);
574
558
  this.MANY3(() => {
@@ -579,70 +563,47 @@ class MathExerciseParser extends chevrotain.CstParser {
579
563
  });
580
564
  this.CONSUME(RBrace);
581
565
  this.MANY4(() => {
582
- this.OR4([
583
- { ALT: () => this.CONSUME4(Space) },
584
- { ALT: () => this.CONSUME4(Newline) }
585
- ]);
566
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
586
567
  });
587
568
  });
588
569
  this.numberLine = this.RULE("numberLine", () => {
589
570
  this.MANY(() => {
590
- this.OR2([
591
- { ALT: () => this.CONSUME(Space) },
592
- { ALT: () => this.CONSUME(Newline) }
593
- ]);
571
+ this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
594
572
  });
595
573
  this.CONSUME(NUMBER_LINE);
596
574
  this.CONSUME(LBracket);
597
575
  this.SUBRULE(this.parameterList);
598
576
  this.CONSUME(RBracket);
599
577
  this.MANY2(() => {
600
- this.OR3([
601
- { ALT: () => this.CONSUME2(Space) },
602
- { ALT: () => this.CONSUME2(Newline) }
603
- ]);
578
+ this.OR3([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
604
579
  });
605
580
  this.CONSUME(LBrace);
606
581
  this.MANY3(() => {
607
- this.OR4([
608
- { ALT: () => this.CONSUME3(Space) },
609
- { ALT: () => this.CONSUME3(Newline) }
610
- ]);
582
+ this.OR4([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
611
583
  });
612
584
  this.MANY4(() => {
613
- 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
+ ]);
614
590
  });
615
591
  this.CONSUME(RBrace);
616
592
  this.MANY5(() => {
617
- this.OR5([
618
- { ALT: () => this.CONSUME4(Newline) },
619
- { ALT: () => this.CONSUME4(Space) }
620
- ]);
593
+ this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
621
594
  });
622
595
  });
623
596
  this.position = this.RULE("position", () => {
624
- this.MANY(() => {
625
- this.OR([
626
- { ALT: () => this.CONSUME(Space) },
627
- { ALT: () => this.CONSUME(Newline) }
628
- ]);
629
- });
630
597
  this.CONSUME(POSITION);
631
598
  this.CONSUME(LBracket);
632
599
  this.SUBRULE(this.parameterList);
633
600
  this.CONSUME(RBracket);
634
601
  this.MANY2(() => {
635
- this.OR2([
636
- { ALT: () => this.CONSUME2(Space) },
637
- { ALT: () => this.CONSUME2(Newline) }
638
- ]);
602
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
639
603
  });
640
604
  this.CONSUME(LBrace);
641
605
  this.MANY3(() => {
642
- this.OR3([
643
- { ALT: () => this.CONSUME3(Space) },
644
- { ALT: () => this.CONSUME3(Newline) }
645
- ]);
606
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
646
607
  });
647
608
  this.MANY4(() => {
648
609
  this.OR4([
@@ -652,10 +613,25 @@ class MathExerciseParser extends chevrotain.CstParser {
652
613
  });
653
614
  this.CONSUME(RBrace);
654
615
  this.MANY5(() => {
655
- this.OR5([
656
- { ALT: () => this.CONSUME5(Space) },
657
- { ALT: () => this.CONSUME5(Newline) }
658
- ]);
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) }]);
659
635
  });
660
636
  });
661
637
  this.line = this.RULE("line", () => {
@@ -668,7 +644,7 @@ class MathExerciseParser extends chevrotain.CstParser {
668
644
  this.MANY2(() => {
669
645
  this.CONSUME2(Space);
670
646
  });
671
- this.AT_LEAST_ONE2(() => {
647
+ this.MANY3(() => {
672
648
  this.CONSUME(Newline);
673
649
  });
674
650
  });
@@ -752,28 +728,19 @@ class MathExerciseParser extends chevrotain.CstParser {
752
728
  this.SUBRULE(this.stringList);
753
729
  this.CONSUME(RBracket);
754
730
  this.MANY(() => {
755
- this.OR([
756
- { ALT: () => this.CONSUME(Newline) },
757
- { ALT: () => this.CONSUME(Space) }
758
- ]);
731
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
759
732
  });
760
733
  });
761
734
  this.radioButtons = this.RULE("radioButtons", () => {
762
735
  this.MANY(() => {
763
- this.OR([
764
- { ALT: () => this.CONSUME(Newline) },
765
- { ALT: () => this.CONSUME(Space) }
766
- ]);
736
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
767
737
  });
768
738
  this.CONSUME(RADIO_BUTTONS_OPTIONS);
769
739
  this.CONSUME(LBracket);
770
740
  this.SUBRULE(this.parameterList);
771
741
  this.CONSUME(RBracket);
772
742
  this.MANY2(() => {
773
- this.OR2([
774
- { ALT: () => this.CONSUME2(Newline) },
775
- { ALT: () => this.CONSUME2(Space) }
776
- ]);
743
+ this.OR2([{ ALT: () => this.CONSUME2(Newline) }, { ALT: () => this.CONSUME2(Space) }]);
777
744
  });
778
745
  });
779
746
  this.numericKeypad = this.RULE("numericKeypad", () => {
@@ -784,10 +751,7 @@ class MathExerciseParser extends chevrotain.CstParser {
784
751
  });
785
752
  this.CONSUME(RBracket);
786
753
  this.MANY(() => {
787
- this.OR([
788
- { ALT: () => this.CONSUME(Newline) },
789
- { ALT: () => this.CONSUME(Space) }
790
- ]);
754
+ this.OR([{ ALT: () => this.CONSUME(Newline) }, { ALT: () => this.CONSUME(Space) }]);
791
755
  });
792
756
  });
793
757
  this.validation = this.RULE("validation", () => {
@@ -1022,7 +986,7 @@ function isLineBlock(value) {
1022
986
  }
1023
987
  const NumberLinePositionSchema = zod.z.object({
1024
988
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_POSITION),
1025
- value: zod.z.number(),
989
+ value: zod.z.number().finite({ message: "POSITION: value must be a finite number" }),
1026
990
  inlines: zod.z.array(InlineNodeSchema).min(1)
1027
991
  });
1028
992
  function createNumberLinePosition(value, inlines) {
@@ -1042,10 +1006,50 @@ function createNumberLinePosition(value, inlines) {
1042
1006
  inlines
1043
1007
  });
1044
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
+ }
1045
1049
  const NumberLineBlockSchema = zod.z.object({
1046
1050
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
1047
- min: zod.z.number(),
1048
- 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" }),
1049
1053
  step: zod.z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
1050
1054
  minorTicks: zod.z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
1051
1055
  style: zod.z.enum(NUMBER_LINE_STYLES, {
@@ -1053,6 +1057,13 @@ const NumberLineBlockSchema = zod.z.object({
1053
1057
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
1054
1058
  })
1055
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(),
1056
1067
  positions: zod.z.array(NumberLinePositionSchema)
1057
1068
  }).refine((data) => data.min < data.max, {
1058
1069
  message: "NUMBER_LINE: min must be less than max"
@@ -1073,8 +1084,24 @@ const NumberLineBlockSchema = zod.z.object({
1073
1084
  }
1074
1085
  seen.add(pos.value);
1075
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
+ }
1076
1103
  });
1077
- function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
1104
+ function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights) {
1078
1105
  return NumberLineBlockSchema.parse({
1079
1106
  type: AST_TYPES.NUMBER_LINE_BLOCK,
1080
1107
  min,
@@ -1082,6 +1109,9 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
1082
1109
  step,
1083
1110
  minorTicks,
1084
1111
  style,
1112
+ labelVisibility,
1113
+ counter,
1114
+ highlights,
1085
1115
  positions
1086
1116
  });
1087
1117
  }
@@ -1292,8 +1322,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1292
1322
  const step = params.step !== void 0 ? Number(params.step) : void 0;
1293
1323
  const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
1294
1324
  const style = params.style;
1325
+ const labelVisibility = params.labelVisibility;
1295
1326
  const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
1296
- 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);
1297
1334
  }
1298
1335
  position(ctx) {
1299
1336
  const params = this.visit(ctx.parameterList);
@@ -1304,6 +1341,28 @@ class NaturalDSLVisitor extends BaseCstVisitor {
1304
1341
  const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
1305
1342
  return createNumberLinePosition(value, inlines);
1306
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
+ }
1307
1366
  // ═══════════════════════════════════════════════════════════════════════════
1308
1367
  // Line Block
1309
1368
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1634,17 +1693,36 @@ const TableItemSchema = zod.z.object({
1634
1693
  const NumberLinePositionItemSchema = zod.z.object({
1635
1694
  id: zod.z.string(),
1636
1695
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
1637
- value: zod.z.number(),
1638
- 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()
1639
1714
  });
1640
1715
  const NumberLineItemSchema = zod.z.object({
1641
1716
  id: zod.z.string(),
1642
1717
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
1643
- min: zod.z.number(),
1644
- max: zod.z.number(),
1718
+ min: zod.z.number().finite(),
1719
+ max: zod.z.number().finite(),
1645
1720
  step: zod.z.number().positive(),
1646
1721
  minorTicks: zod.z.number().int().nonnegative().optional(),
1647
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(),
1648
1726
  positions: zod.z.array(NumberLinePositionItemSchema)
1649
1727
  });
1650
1728
  const StringInputJSONSchema = zod.z.object({
@@ -1718,7 +1796,7 @@ function isQuestionText(item) {
1718
1796
  function isTextItem$1(item) {
1719
1797
  return item.type === JSON_TYPES.TEXT;
1720
1798
  }
1721
- function isWidgetRef$1(item) {
1799
+ function isWidgetRef(item) {
1722
1800
  return item.type === JSON_TYPES.WIDGET_REF;
1723
1801
  }
1724
1802
  function isHighlightedText(item) {
@@ -1891,16 +1969,42 @@ function transformNumberLine(numberLine, idGenerator) {
1891
1969
  content
1892
1970
  };
1893
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
+ }
1894
1995
  const item = {
1895
1996
  id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
1896
1997
  type: JSON_TYPES.NUMBER_LINE,
1897
1998
  min: numberLine.min,
1898
1999
  max: numberLine.max,
1899
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 },
1900
2006
  positions
1901
2007
  };
1902
- if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
1903
- if (numberLine.style !== void 0) item.style = numberLine.style;
1904
2008
  return item;
1905
2009
  }
1906
2010
  function transformTable(table, idGenerator) {
@@ -2023,6 +2127,49 @@ function extractVariables$1(expression, validVariables) {
2023
2127
  }
2024
2128
  return uniqueVars;
2025
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
+ }
2026
2173
  function validateSimple(exercise, userInputs) {
2027
2174
  const details = {};
2028
2175
  let allCorrect = true;
@@ -2036,10 +2183,11 @@ function validateSimple(exercise, userInputs) {
2036
2183
  score: 0
2037
2184
  };
2038
2185
  }
2186
+ const positionMap = buildPositionMap(exercise);
2039
2187
  for (const [widgetId, expected] of Object.entries(answers)) {
2040
2188
  totalCount++;
2041
2189
  const userValue = userInputs[widgetId];
2042
- const isCorrect = areValuesEqual(userValue, expected);
2190
+ const isCorrect = areValuesEqual(userValue, expected, positionMap[widgetId]);
2043
2191
  details[widgetId] = {
2044
2192
  widgetId,
2045
2193
  userValue,
@@ -2067,19 +2215,13 @@ function validateSimple(exercise, userInputs) {
2067
2215
  score: totalCount > 0 ? correctCount / totalCount : 0
2068
2216
  };
2069
2217
  }
2070
- function areValuesEqual(userValue, expected) {
2071
- if (userValue === void 0 || userValue === null) {
2072
- return false;
2073
- }
2074
- const normalizedUser = normalize(userValue);
2075
- const normalizedExpected = normalize(expected);
2076
- return normalizedUser === normalizedExpected;
2077
- }
2078
- function normalize(value) {
2079
- if (typeof value === "number") {
2080
- return String(Math.round(value * 1e10) / 1e10);
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);
2081
2223
  }
2082
- return String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
2224
+ return normalize(userValue) === normalize(expected);
2083
2225
  }
2084
2226
  const math = mathjs.create(mathjs.all);
2085
2227
  const BLOCKED_FUNCTIONS = [
@@ -6621,7 +6763,10 @@ const WidgetRenderer = ({
6621
6763
  }
6622
6764
  return null;
6623
6765
  };
6624
- 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;
6625
6770
  const toPercent = (value, min, max) => {
6626
6771
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
6627
6772
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -6637,19 +6782,24 @@ const getMajorTicks = (min, max, step) => {
6637
6782
  const count = Math.floor((max - min) / step);
6638
6783
  const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
6639
6784
  const lastTick = ticks[ticks.length - 1];
6640
- 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
+ }
6641
6790
  return ticks;
6642
6791
  };
6643
6792
  const getMinorTickValues = (majorTicks, minorTicks) => {
6644
- if (minorTicks <= 0) return [];
6793
+ if (minorTicks <= 0 || majorTicks.length < 2) return [];
6645
6794
  const result = [];
6646
- majorTicks.slice(0, -1).forEach((tick, idx) => {
6647
- const next = majorTicks[idx + 1];
6648
- 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;
6649
6799
  for (let j = 1; j <= minorTicks; j++) {
6650
- result.push(cleanFloat(tick + j / (minorTicks + 1) * interval));
6800
+ result.push(cleanFloat(from + j / (minorTicks + 1) * interval));
6651
6801
  }
6652
- });
6802
+ }
6653
6803
  return result;
6654
6804
  };
6655
6805
  const getSnapTicks = (majorTicks, minorTicks) => [
@@ -6658,19 +6808,41 @@ const getSnapTicks = (majorTicks, minorTicks) => [
6658
6808
  ];
6659
6809
  const snapToNearest = (raw, ticks) => {
6660
6810
  if (ticks.length === 0) return cleanFloat(raw);
6661
- const snapped = ticks.reduce(
6662
- (best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
6663
- );
6664
- return cleanFloat(snapped);
6811
+ return ticks.reduce((best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best);
6665
6812
  };
6666
- const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
6667
6813
  const getPositionWidgetId = (content) => {
6668
6814
  for (const item of content) {
6669
6815
  if (isWidgetRef(item)) return item.widgetId;
6670
6816
  }
6671
6817
  return null;
6672
6818
  };
6673
- 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 }) => {
6674
6846
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
6675
6847
  id: widgetId,
6676
6848
  data: { type: "placed-chip", widgetId, label }
@@ -6678,23 +6850,49 @@ const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
6678
6850
  return /* @__PURE__ */ jsxRuntime.jsx(
6679
6851
  "div",
6680
6852
  {
6681
- className: "number-line-placed-chip",
6682
- style: { left: `${leftPercent}%`, opacity: isDragging ? 0.3 : 1 },
6683
- children: /* @__PURE__ */ jsxRuntime.jsx(
6684
- "button",
6685
- {
6686
- ref: setNodeRef,
6687
- className: "number-line-placed-chip-btn",
6688
- title: "Drag to move · Click to remove",
6689
- onClick: onRemove,
6690
- ...listeners,
6691
- ...attributes,
6692
- children: label
6693
- }
6694
- )
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
+ ] })
6695
6878
  }
6696
6879
  );
6697
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
+ }
6698
6896
  const FreeDragAxis = ({
6699
6897
  axisDropId,
6700
6898
  min,
@@ -6705,17 +6903,51 @@ const FreeDragAxis = ({
6705
6903
  showArrowRight,
6706
6904
  positions,
6707
6905
  values,
6708
- onChange
6906
+ onChange,
6907
+ highlights,
6908
+ snapKey
6709
6909
  }) => {
6710
6910
  const axisRef = React.useRef(null);
6711
6911
  const snapTicks = React.useMemo(
6712
6912
  () => getSnapTicks(majorTicks, minorTicks),
6713
6913
  [majorTicks, minorTicks]
6714
6914
  );
6715
- const [dropLabels, setDropLabels] = React.useState(/* @__PURE__ */ new Map());
6716
- 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]);
6929
+ React.useEffect(() => {
6930
+ placedChipsRef.current = placedChips;
6931
+ }, [placedChips]);
6932
+ const isFirstRender = React.useRef(true);
6717
6933
  React.useEffect(() => {
6718
- setDropLabels((prev) => {
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) => {
6719
6951
  let changed = false;
6720
6952
  const next = new Map(prev);
6721
6953
  for (const [wid] of next) {
@@ -6724,21 +6956,13 @@ const FreeDragAxis = ({
6724
6956
  changed = true;
6725
6957
  }
6726
6958
  }
6727
- return changed ? next : prev;
6728
- });
6729
- setDropSources((prev) => {
6730
- let changed = false;
6731
- const next = new Map(prev);
6732
- for (const [wid] of next) {
6733
- if (!values[wid]) {
6734
- next.delete(wid);
6735
- changed = true;
6736
- }
6959
+ if (changed) {
6960
+ placedChipsRef.current = next;
6737
6961
  }
6738
6962
  return changed ? next : prev;
6739
6963
  });
6740
6964
  }, [values]);
6741
- const { setNodeRef, isOver } = useDroppable({ id: axisDropId });
6965
+ const { setNodeRef } = useDroppable({ id: axisDropId });
6742
6966
  const mergedRef = React.useCallback((node) => {
6743
6967
  axisRef.current = node;
6744
6968
  setNodeRef(node);
@@ -6746,91 +6970,163 @@ const FreeDragAxis = ({
6746
6970
  const snapFromEvent = React.useCallback((translated) => {
6747
6971
  if (!axisRef.current) return null;
6748
6972
  const axisRect = axisRef.current.getBoundingClientRect();
6973
+ if (axisRect.width === 0) return null;
6749
6974
  const chipCenterX = translated.left + translated.width / 2;
6750
6975
  const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
6751
6976
  return snapToNearest(raw, snapTicks);
6752
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
+ }, []);
6753
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
+ },
6754
7014
  onDragEnd(event) {
7015
+ setHoverSnap(null);
6755
7016
  if (!event.over || event.over.id !== axisDropId) return;
6756
7017
  if (!axisRef.current) return;
6757
7018
  const translated = event.active.rect.current.translated;
6758
7019
  if (!translated) return;
6759
- const placed = snapFromEvent(translated);
6760
- 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);
6761
7028
  const data = event.active.data.current;
6762
- if (data?.type === "placed-chip") {
6763
- const widgetId = data.widgetId;
7029
+ if (!data) return;
7030
+ if (data.type === "placed-chip") {
7031
+ const oldWidgetId = data.widgetId;
6764
7032
  const label = data.label;
6765
- const originalSourceId = dropSources.get(widgetId) ?? widgetId;
6766
- onChange(widgetId, String(placed), true, originalSourceId);
6767
- 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);
6768
7051
  return;
6769
7052
  }
6770
- const draggedItem = event.active.data.current;
6771
- if (draggedItem?.type !== "drag-option") return;
6772
- const targetPos = positions.find((pos) => {
6773
- const wid = getPositionWidgetId(pos.content);
6774
- return wid && !values[wid];
6775
- });
7053
+ if (data.type !== "drag-option") return;
7054
+ if (isTickOccupied(placed)) return;
7055
+ const targetPos = findNearestFreeWidgetSlotCb(placed);
6776
7056
  if (!targetPos) return;
6777
7057
  const targetWidgetId = getPositionWidgetId(targetPos.content);
6778
7058
  if (!targetWidgetId) return;
6779
- const chipLabel = draggedItem.data.value;
6780
- const sourceOptId = String(event.active.id);
6781
- onChange(targetWidgetId, String(placed), true, sourceOptId);
6782
- setDropLabels((prev) => new Map(prev).set(targetWidgetId, chipLabel));
6783
- 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);
6784
7068
  }
6785
7069
  });
6786
7070
  return /* @__PURE__ */ jsxRuntime.jsxs(
6787
7071
  "div",
6788
7072
  {
6789
7073
  ref: mergedRef,
6790
- className: `number-line-axis-row nl-axis-drag-mode${isOver ? " nl-axis-drag-over" : ""}`,
7074
+ className: "number-line-axis-row nl-axis-drag-mode",
6791
7075
  children: [
6792
7076
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track" }),
6793
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
6794
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
6795
- 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(
6796
7081
  "div",
6797
7082
  {
6798
- className: "number-line-minor-tick",
7083
+ className: `number-line-minor-tick${hoverSnap === v ? " nl-tick-hover" : ""}`,
6799
7084
  style: { left: `${toPercent(v, min, max)}%` }
6800
7085
  },
6801
- `minor-${v}`
7086
+ `minor-${cleanFloat(v)}`
6802
7087
  )),
6803
7088
  majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
6804
7089
  "div",
6805
7090
  {
6806
- className: "number-line-major-tick",
7091
+ className: `number-line-major-tick${hoverSnap === tick ? " nl-tick-hover" : ""}`,
6807
7092
  style: { left: `${toPercent(tick, min, max)}%` }
6808
7093
  },
6809
- `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}`
6810
7111
  )),
6811
7112
  positions.map((pos) => {
6812
7113
  const wid = getPositionWidgetId(pos.content);
6813
- const dropPos = wid ? parseFloat(values[wid] ?? "") : NaN;
6814
- const label = wid ? dropLabels.get(wid) : void 0;
6815
- if (!wid || isNaN(dropPos) || !label) return null;
7114
+ const chip = wid ? placedChips.get(wid) : void 0;
7115
+ if (!wid || !chip) return null;
6816
7116
  return /* @__PURE__ */ jsxRuntime.jsx(
6817
7117
  PlacedChip,
6818
7118
  {
6819
7119
  widgetId: wid,
6820
- label,
6821
- leftPercent: toPercent(dropPos, min, max),
7120
+ label: chip.label,
7121
+ leftPercent: toPercent(chip.position, min, max),
7122
+ isLanding: landingChips.has(wid),
6822
7123
  onRemove: () => {
6823
- const sourceId = dropSources.get(wid);
6824
- onChange(wid, "", false, sourceId);
6825
- setDropLabels((prev) => {
6826
- const m = new Map(prev);
6827
- m.delete(wid);
6828
- return m;
6829
- });
6830
- setDropSources((prev) => {
6831
- const m = new Map(prev);
6832
- m.delete(wid);
6833
- 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;
6834
7130
  });
6835
7131
  }
6836
7132
  },
@@ -6841,6 +7137,132 @@ const FreeDragAxis = ({
6841
7137
  }
6842
7138
  );
6843
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
+ ] });
6844
7266
  const NumberLineRenderer = ({
6845
7267
  item,
6846
7268
  widgets,
@@ -6849,27 +7271,59 @@ const NumberLineRenderer = ({
6849
7271
  onChange,
6850
7272
  onInputFocus
6851
7273
  }) => {
6852
- const { min, max, step, minorTicks = 0, style = "arrow-right", positions } = item;
6853
- const majorTicks = React.useMemo(() => getMajorTicks(min, max, step), [min, max, step]);
6854
- const minorValues = React.useMemo(() => getMinorTickValues(majorTicks, minorTicks), [majorTicks, minorTicks]);
6855
- const showArrowRight = style === "arrow-right" || style === "arrow-both";
6856
- 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";
6857
7307
  const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
6858
7308
  const inputTickSet = React.useMemo(() => {
6859
7309
  if (isDragMode) return /* @__PURE__ */ new Set();
6860
7310
  return new Set(
6861
- positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
7311
+ positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => roundTick(pos.value))
6862
7312
  );
6863
7313
  }, [isDragMode, positions]);
7314
+ const visiblePositions = React.useMemo(
7315
+ () => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
7316
+ [isDragMode, positions]
7317
+ );
6864
7318
  return /* @__PURE__ */ jsxRuntime.jsxs(
6865
7319
  "div",
6866
7320
  {
6867
- className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
7321
+ className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
6868
7322
  style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
6869
- role: "img",
7323
+ role: "group",
6870
7324
  "aria-label": `Number line from ${min} to ${max}`,
6871
7325
  children: [
6872
- !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(
6873
7327
  "div",
6874
7328
  {
6875
7329
  className: "number-line-position-content",
@@ -6901,50 +7355,43 @@ const NumberLineRenderer = ({
6901
7355
  showArrowRight,
6902
7356
  positions,
6903
7357
  values,
6904
- onChange
7358
+ onChange,
7359
+ highlights,
7360
+ snapKey: counterCount
6905
7361
  }
6906
- ) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
6907
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
6908
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
6909
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
6910
- minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
6911
- "div",
6912
- {
6913
- className: "number-line-minor-tick",
6914
- "aria-hidden": "true",
6915
- style: { left: `${toPercent(v, min, max)}%` }
6916
- },
6917
- `minor-${v}`
6918
- )),
6919
- majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
6920
- "div",
6921
- {
6922
- className: "number-line-major-tick",
6923
- style: { left: `${toPercent(tick, min, max)}%` }
6924
- },
6925
- `major-${tick}`
6926
- )),
6927
- positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
6928
- "div",
6929
- {
6930
- className: "number-line-position-dot",
6931
- style: { left: `${toPercent(pos.value, min, max)}%` }
6932
- },
6933
- `dot-${pos.id}`
6934
- ))
6935
- ] }),
6936
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
6937
- const hasInputAtTick = inputTickSet.has(tick);
6938
- return /* @__PURE__ */ jsxRuntime.jsx(
6939
- "span",
6940
- {
6941
- className: "number-line-tick-label",
6942
- style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
6943
- children: tick
6944
- },
6945
- `lbl-${tick}`
6946
- );
6947
- }) })
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
+ )
6948
7395
  ]
6949
7396
  }
6950
7397
  );
@@ -7172,8 +7619,7 @@ const ExerciseRenderer = React.forwardRef(({
7172
7619
  }
7173
7620
  }), [json, values, onSubmit]);
7174
7621
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
7175
- const newValues = { ...values, [widgetId]: value };
7176
- setValues(newValues);
7622
+ setValues((prev) => ({ ...prev, [widgetId]: value }));
7177
7623
  setDroppedSources((prev) => {
7178
7624
  const next = new Map(prev);
7179
7625
  if (fromDrop && value !== "" && sourceOptionId) {
@@ -7183,7 +7629,7 @@ const ExerciseRenderer = React.forwardRef(({
7183
7629
  }
7184
7630
  return next;
7185
7631
  });
7186
- onChange?.(newValues);
7632
+ onChange?.({ ...values, [widgetId]: value });
7187
7633
  };
7188
7634
  const handleSubmit = () => {
7189
7635
  onSubmit?.(values);
@@ -7406,6 +7852,10 @@ exports.LayoutRenderer = LayoutRenderer;
7406
7852
  exports.LexerError = LexerError;
7407
7853
  exports.LineBlockSchema = LineBlockSchema;
7408
7854
  exports.LinebreakSchema = LinebreakSchema;
7855
+ exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
7856
+ exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
7857
+ exports.NumberLineItemSchema = NumberLineItemSchema;
7858
+ exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;
7409
7859
  exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
7410
7860
  exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
7411
7861
  exports.OperatorItemSchema = OperatorItemSchema;
@@ -7479,7 +7929,7 @@ exports.isTextItem = isTextItem$1;
7479
7929
  exports.isToolNode = isToolNode;
7480
7930
  exports.isValidExpression = isValidExpression;
7481
7931
  exports.isValidationNode = isValidationNode;
7482
- exports.isWidgetRef = isWidgetRef$1;
7932
+ exports.isWidgetRef = isWidgetRef;
7483
7933
  exports.parse = parse;
7484
7934
  exports.parseSafe = parseSafe;
7485
7935
  exports.renderExercise = renderExercise;