@mathscan/math-exercise-engine 1.1.8 → 1.1.9

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
@@ -58,10 +58,14 @@ const AST_TYPES = {
58
58
  NUMBER_LINE_BLOCK: "NumberLineBlock",
59
59
  /** A position marker on the number line */
60
60
  NUMBER_LINE_POSITION: "NumberLinePosition",
61
+ /** A draggable cursor on the number line */
62
+ NUMBER_LINE_CURSOR: "NumberLineCursor",
61
63
  /** A dynamic subdivision counter embedded in a number line */
62
64
  NUMBER_LINE_COUNTER: "NumberLineCounter",
63
65
  /** A highlighted range on the number line axis */
64
66
  NUMBER_LINE_HIGHLIGHT: "NumberLineHighlight",
67
+ /** An arrow annotation spanning a number line interval */
68
+ NUMBER_LINE_ARROW: "NumberLineArrow",
65
69
  /** Matcher layout block */
66
70
  MATCHER_BLOCK: "MatcherBlock",
67
71
  /** Row within a matcher */
@@ -154,6 +158,10 @@ const JSON_TYPES = {
154
158
  NUMBER_LINE_COUNTER: "number-line-counter",
155
159
  /** Highlighted range within a number line */
156
160
  NUMBER_LINE_HIGHLIGHT: "number-line-highlight",
161
+ /** Draggable cursor within a number line */
162
+ NUMBER_LINE_CURSOR: "number-line-cursor",
163
+ /** Arrow annotation within a number line */
164
+ NUMBER_LINE_ARROW: "number-line-arrow",
157
165
  /** Matcher layout block */
158
166
  MATCHER: "matcher",
159
167
  /** Row within a matcher */
@@ -320,6 +328,14 @@ const POSITION = chevrotain.createToken({
320
328
  name: "POSITION",
321
329
  pattern: /POSITION/
322
330
  });
331
+ const CURSOR = chevrotain.createToken({
332
+ name: "CURSOR",
333
+ pattern: /CURSOR/
334
+ });
335
+ const ARROW = chevrotain.createToken({
336
+ name: "ARROW",
337
+ pattern: /ARROW/
338
+ });
323
339
  const TABLE = chevrotain.createToken({
324
340
  name: "TABLE",
325
341
  pattern: /TABLE/
@@ -517,6 +533,8 @@ const allTokens = [
517
533
  NUMERIC_INPUT,
518
534
  NUMBER_LINE,
519
535
  POSITION,
536
+ CURSOR,
537
+ ARROW,
520
538
  TABLE,
521
539
  ROW,
522
540
  TOP_ROW,
@@ -791,7 +809,9 @@ class MathExerciseParser extends chevrotain.CstParser {
791
809
  this.OR6([
792
810
  { ALT: () => this.SUBRULE(this.position) },
793
811
  { ALT: () => this.SUBRULE(this.counter) },
794
- { ALT: () => this.SUBRULE(this.highlight) }
812
+ { ALT: () => this.SUBRULE(this.highlight) },
813
+ { ALT: () => this.SUBRULE(this.cursor) },
814
+ { ALT: () => this.SUBRULE(this.arrow) }
795
815
  ]);
796
816
  });
797
817
  this.CONSUME(RBrace);
@@ -840,6 +860,38 @@ class MathExerciseParser extends chevrotain.CstParser {
840
860
  this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
841
861
  });
842
862
  });
863
+ this.cursor = this.RULE("cursor", () => {
864
+ this.CONSUME(CURSOR);
865
+ this.CONSUME(LBracket);
866
+ this.SUBRULE(this.parameterList);
867
+ this.CONSUME(RBracket);
868
+ this.MANY2(() => {
869
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
870
+ });
871
+ });
872
+ this.arrow = this.RULE("arrow", () => {
873
+ this.CONSUME(ARROW);
874
+ this.CONSUME(LBracket);
875
+ this.SUBRULE(this.parameterList);
876
+ this.CONSUME(RBracket);
877
+ this.MANY2(() => {
878
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
879
+ });
880
+ this.CONSUME(LBrace);
881
+ this.MANY3(() => {
882
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
883
+ });
884
+ this.MANY4(() => {
885
+ this.OR4([
886
+ { ALT: () => this.SUBRULE(this.inlineElement) },
887
+ { ALT: () => this.CONSUME4(Newline) }
888
+ ]);
889
+ });
890
+ this.CONSUME(RBrace);
891
+ this.MANY5(() => {
892
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
893
+ });
894
+ });
843
895
  this.matcher = this.RULE("matcher", () => {
844
896
  this.MANY(() => {
845
897
  this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
@@ -1867,6 +1919,44 @@ function createNumberLineHighlight(from, to, color, label) {
1867
1919
  ...label !== void 0 && { label }
1868
1920
  });
1869
1921
  }
1922
+ const NumberLineCursorSchema = zod.z.object({
1923
+ type: zod.z.literal(AST_TYPES.NUMBER_LINE_CURSOR),
1924
+ value: zod.z.number().finite({ message: "CURSOR: value must be a finite number" }).default(0),
1925
+ answer: zod.z.number({ required_error: "CURSOR: answer is required" }).finite({ message: "CURSOR: answer must be a finite number" }),
1926
+ color: zod.z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/, {
1927
+ message: 'CURSOR: color must be a hex value (e.g. "#3B82F6" or "#F00")'
1928
+ }).optional(),
1929
+ fillRange: zod.z.boolean().default(false),
1930
+ label: zod.z.string().optional()
1931
+ });
1932
+ function createNumberLineCursor(answer, value, color, fillRange, label) {
1933
+ return NumberLineCursorSchema.parse({
1934
+ type: AST_TYPES.NUMBER_LINE_CURSOR,
1935
+ answer,
1936
+ ...value !== void 0 && { value },
1937
+ ...color !== void 0 && { color },
1938
+ ...fillRange !== void 0 && { fillRange },
1939
+ ...label !== void 0 && { label }
1940
+ });
1941
+ }
1942
+ const NumberLineArrowSchema = zod.z.object({
1943
+ type: zod.z.literal(AST_TYPES.NUMBER_LINE_ARROW),
1944
+ from: zod.z.number().finite({ message: "ARROW: from must be a finite number" }),
1945
+ to: zod.z.number().finite({ message: "ARROW: to must be a finite number" }),
1946
+ color: zod.z.string().optional(),
1947
+ inlines: zod.z.array(InlineNodeSchema)
1948
+ }).refine((d) => d.from < d.to, {
1949
+ message: "ARROW: from must be less than to"
1950
+ });
1951
+ function createNumberLineArrow(from, to, inlines, color) {
1952
+ return NumberLineArrowSchema.parse({
1953
+ type: AST_TYPES.NUMBER_LINE_ARROW,
1954
+ from,
1955
+ to,
1956
+ ...color !== void 0 && { color },
1957
+ inlines
1958
+ });
1959
+ }
1870
1960
  const NumberLineBlockSchema = zod.z.object({
1871
1961
  type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
1872
1962
  min: zod.z.number().finite({ message: "NUMBER_LINE: min must be a finite number" }),
@@ -1885,6 +1975,8 @@ const NumberLineBlockSchema = zod.z.object({
1885
1975
  }).optional(),
1886
1976
  counter: NumberLineCounterSchema.optional(),
1887
1977
  highlights: zod.z.array(NumberLineHighlightSchema).optional(),
1978
+ cursors: zod.z.array(NumberLineCursorSchema).optional(),
1979
+ arrows: zod.z.array(NumberLineArrowSchema).optional(),
1888
1980
  dragMode: zod.z.enum(NUMBER_LINE_DRAG_MODES, {
1889
1981
  errorMap: () => ({
1890
1982
  message: `NUMBER_LINE: dragMode must be one of ${NUMBER_LINE_DRAG_MODES.join(", ")}`
@@ -1918,6 +2010,14 @@ const NumberLineBlockSchema = zod.z.object({
1918
2010
  });
1919
2011
  }
1920
2012
  }
2013
+ for (const arrow of data.arrows ?? []) {
2014
+ if (arrow.from < data.min || arrow.to > data.max) {
2015
+ ctx.addIssue({
2016
+ code: zod.z.ZodIssueCode.custom,
2017
+ message: `NUMBER_LINE: ARROW range [${arrow.from}, ${arrow.to}] must be within [${data.min}, ${data.max}]`
2018
+ });
2019
+ }
2020
+ }
1921
2021
  if (data.counter?.default !== void 0) {
1922
2022
  if (data.counter.default < data.counter.min || data.counter.default > data.counter.max) {
1923
2023
  ctx.addIssue({
@@ -1926,8 +2026,22 @@ const NumberLineBlockSchema = zod.z.object({
1926
2026
  });
1927
2027
  }
1928
2028
  }
2029
+ for (const c of data.cursors ?? []) {
2030
+ if (c.value < data.min || c.value > data.max) {
2031
+ ctx.addIssue({
2032
+ code: zod.z.ZodIssueCode.custom,
2033
+ message: `NUMBER_LINE: CURSOR value (${c.value}) must be within [${data.min}, ${data.max}]`
2034
+ });
2035
+ }
2036
+ if (c.answer < data.min || c.answer > data.max) {
2037
+ ctx.addIssue({
2038
+ code: zod.z.ZodIssueCode.custom,
2039
+ message: `NUMBER_LINE: CURSOR answer (${c.answer}) must be within [${data.min}, ${data.max}]`
2040
+ });
2041
+ }
2042
+ }
1929
2043
  });
1930
- function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, dragMode) {
2044
+ function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, cursors, arrows, dragMode) {
1931
2045
  return NumberLineBlockSchema.parse({
1932
2046
  type: AST_TYPES.NUMBER_LINE_BLOCK,
1933
2047
  min,
@@ -1938,6 +2052,8 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style, lab
1938
2052
  labelVisibility,
1939
2053
  counter,
1940
2054
  highlights,
2055
+ cursors,
2056
+ arrows,
1941
2057
  dragMode,
1942
2058
  positions
1943
2059
  });
@@ -2591,7 +2707,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2591
2707
  }
2592
2708
  const counter = rawCounters ? this.visit(rawCounters) : void 0;
2593
2709
  const highlights = ctx.highlight ? ctx.highlight.map((h) => this.visit(h)) : [];
2594
- return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, dragMode);
2710
+ const cursors = ctx.cursor ? ctx.cursor.map((c) => this.visit(c)) : [];
2711
+ const arrows = ctx.arrow ? ctx.arrow.map((a) => this.visit(a)) : [];
2712
+ return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, cursors.length > 0 ? cursors : void 0, arrows.length > 0 ? arrows : void 0, dragMode);
2595
2713
  }
2596
2714
  position(ctx) {
2597
2715
  const params = this.visit(ctx.parameterList);
@@ -2625,6 +2743,29 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2625
2743
  const label = params.label;
2626
2744
  return createNumberLineHighlight(from, to, color, label);
2627
2745
  }
2746
+ cursor(ctx) {
2747
+ const params = this.visit(ctx.parameterList);
2748
+ if (params.answer === void 0) {
2749
+ throw new Error("CURSOR requires parameter: answer");
2750
+ }
2751
+ const answer = Number(params.answer);
2752
+ const value = params.value !== void 0 ? Number(params.value) : void 0;
2753
+ const color = params.color;
2754
+ const fillRange = params.fillRange !== void 0 ? params.fillRange === true || params.fillRange === "true" : void 0;
2755
+ const label = params.label;
2756
+ return createNumberLineCursor(answer, value, color, fillRange, label);
2757
+ }
2758
+ arrow(ctx) {
2759
+ const params = this.visit(ctx.parameterList);
2760
+ if (params.from === void 0 || params.to === void 0) {
2761
+ throw new Error("ARROW requires parameters: from and to");
2762
+ }
2763
+ const from = Number(params.from);
2764
+ const to = Number(params.to);
2765
+ const color = params.color;
2766
+ const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
2767
+ return createNumberLineArrow(from, to, inlines, color);
2768
+ }
2628
2769
  // ═══════════════════════════════════════════════════════════════════════════
2629
2770
  // Matcher Block
2630
2771
  // ═══════════════════════════════════════════════════════════════════════════
@@ -3279,6 +3420,23 @@ const NumberLineHighlightItemSchema = zod.z.object({
3279
3420
  color: zod.z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
3280
3421
  label: zod.z.string().optional()
3281
3422
  });
3423
+ const NumberLineCursorItemSchema = zod.z.object({
3424
+ id: zod.z.string(),
3425
+ type: zod.z.literal(JSON_TYPES.NUMBER_LINE_CURSOR),
3426
+ value: zod.z.number().finite(),
3427
+ answer: zod.z.number().finite(),
3428
+ color: zod.z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/).optional(),
3429
+ fillRange: zod.z.boolean(),
3430
+ label: zod.z.string().optional()
3431
+ });
3432
+ const NumberLineArrowItemSchema = zod.z.object({
3433
+ id: zod.z.string(),
3434
+ type: zod.z.literal(JSON_TYPES.NUMBER_LINE_ARROW),
3435
+ from: zod.z.number().finite(),
3436
+ to: zod.z.number().finite(),
3437
+ color: zod.z.string().optional(),
3438
+ content: zod.z.array(LayoutItemSchema)
3439
+ });
3282
3440
  const NumberLineItemSchema = zod.z.object({
3283
3441
  id: zod.z.string(),
3284
3442
  type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
@@ -3290,6 +3448,8 @@ const NumberLineItemSchema = zod.z.object({
3290
3448
  labelVisibility: zod.z.enum(NUMBER_LINE_LABELS).optional(),
3291
3449
  counter: NumberLineCounterItemSchema.optional(),
3292
3450
  highlights: zod.z.array(NumberLineHighlightItemSchema).optional(),
3451
+ cursors: zod.z.array(NumberLineCursorItemSchema).optional(),
3452
+ arrows: zod.z.array(NumberLineArrowItemSchema).optional(),
3293
3453
  dragMode: zod.z.enum(NUMBER_LINE_DRAG_MODES).optional(),
3294
3454
  positions: zod.z.array(NumberLinePositionItemSchema)
3295
3455
  });
@@ -3637,6 +3797,9 @@ function toJSON(ast) {
3637
3797
  if (nlItem.counter?.answer !== void 0) {
3638
3798
  answers[nlItem.counter.id] = nlItem.counter.answer;
3639
3799
  }
3800
+ for (const cursor of nlItem.cursors ?? []) {
3801
+ answers[cursor.id] = cursor.answer;
3802
+ }
3640
3803
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
3641
3804
  const matcherBlock = block;
3642
3805
  layout.push({
@@ -3827,9 +3990,13 @@ function collectAllInlines(blocks) {
3827
3990
  }
3828
3991
  }
3829
3992
  } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
3830
- for (const position of block.positions) {
3993
+ const numberLine = block;
3994
+ for (const position of numberLine.positions) {
3831
3995
  result.push(...position.inlines);
3832
3996
  }
3997
+ for (const arrow of numberLine.arrows ?? []) {
3998
+ result.push(...arrow.inlines);
3999
+ }
3833
4000
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
3834
4001
  for (const row of block.rows) {
3835
4002
  for (const item of row.items) {
@@ -3879,6 +4046,29 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
3879
4046
  ...h.label !== void 0 && { label: h.label }
3880
4047
  }));
3881
4048
  }
4049
+ let cursors;
4050
+ if (numberLine.cursors !== void 0 && numberLine.cursors.length > 0) {
4051
+ cursors = numberLine.cursors.map((c) => ({
4052
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_CURSOR),
4053
+ type: JSON_TYPES.NUMBER_LINE_CURSOR,
4054
+ value: c.value,
4055
+ answer: c.answer,
4056
+ fillRange: c.fillRange,
4057
+ ...c.color !== void 0 && { color: c.color },
4058
+ ...c.label !== void 0 && { label: c.label }
4059
+ }));
4060
+ }
4061
+ let arrows;
4062
+ if (numberLine.arrows !== void 0 && numberLine.arrows.length > 0) {
4063
+ arrows = numberLine.arrows.map((a) => ({
4064
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_ARROW),
4065
+ type: JSON_TYPES.NUMBER_LINE_ARROW,
4066
+ from: a.from,
4067
+ to: a.to,
4068
+ ...a.color !== void 0 && { color: a.color },
4069
+ content: a.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
4070
+ }));
4071
+ }
3882
4072
  const item = {
3883
4073
  id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
3884
4074
  type: JSON_TYPES.NUMBER_LINE,
@@ -3890,6 +4080,8 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
3890
4080
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
3891
4081
  ...counterItem !== void 0 && { counter: counterItem },
3892
4082
  ...highlights !== void 0 && { highlights },
4083
+ ...cursors !== void 0 && { cursors },
4084
+ ...arrows !== void 0 && { arrows },
3893
4085
  ...numberLine.dragMode !== void 0 && { dragMode: numberLine.dragMode },
3894
4086
  positions
3895
4087
  };
@@ -4021,7 +4213,7 @@ function transformLine(line, idGenerator, inlineDragIds) {
4021
4213
  }
4022
4214
  function transformInline(inline, idGenerator, inlineDragIds) {
4023
4215
  switch (inline.type) {
4024
- case AST_TYPES.FREE_TEXT:
4216
+ case AST_TYPES.FREE_TEXT: {
4025
4217
  const trimmed = inline.value.trim();
4026
4218
  const operators = ["+", "-", "×", "÷", "=", "<", ">", "?"];
4027
4219
  if (operators.includes(trimmed) && inline.value.trim() === inline.value) {
@@ -4036,6 +4228,7 @@ function transformInline(inline, idGenerator, inlineDragIds) {
4036
4228
  type: JSON_TYPES.TEXT,
4037
4229
  value: inline.value
4038
4230
  };
4231
+ }
4039
4232
  case AST_TYPES.SPACE:
4040
4233
  return {
4041
4234
  id: idGenerator.generate(JSON_TYPES.SPACE),
@@ -8298,7 +8491,7 @@ lodash.exports;
8298
8491
  var tag = baseGetTag(value);
8299
8492
  return tag == errorTag || tag == domExcTag || typeof value.message == "string" && typeof value.name == "string" && !isPlainObject(value);
8300
8493
  }
8301
- function isFinite(value) {
8494
+ function isFinite2(value) {
8302
8495
  return typeof value == "number" && nativeIsFinite(value);
8303
8496
  }
8304
8497
  function isFunction(value) {
@@ -9399,7 +9592,7 @@ lodash.exports;
9399
9592
  lodash2.isEqual = isEqual;
9400
9593
  lodash2.isEqualWith = isEqualWith;
9401
9594
  lodash2.isError = isError;
9402
- lodash2.isFinite = isFinite;
9595
+ lodash2.isFinite = isFinite2;
9403
9596
  lodash2.isFunction = isFunction;
9404
9597
  lodash2.isInteger = isInteger;
9405
9598
  lodash2.isLength = isLength;
@@ -13953,6 +14146,8 @@ const DEFAULT_SOUNDS = {
13953
14146
  "clock-hour-tick": buttonClick,
13954
14147
  "clock-minute-tick": buttonClick,
13955
14148
  "counter-step": buttonClick,
14149
+ "cursor-step": buttonClick,
14150
+ "cursor-release": dragDrop,
13956
14151
  "input-select": inputSelect,
13957
14152
  "answer-correct": answerCorrect,
13958
14153
  "answer-wrong": answerWrong
@@ -16511,6 +16706,182 @@ const HighlightBands = ({ highlights, min, max }) => {
16511
16706
  );
16512
16707
  }) });
16513
16708
  };
16709
+ const DraggableCursor = ({
16710
+ cursor,
16711
+ min,
16712
+ max,
16713
+ snapTicks,
16714
+ audibleTicks,
16715
+ axisRowRef,
16716
+ onChange,
16717
+ currentValue,
16718
+ validationStatus
16719
+ }) => {
16720
+ const [isDragging, setIsDragging] = React.useState(false);
16721
+ const isDraggingRef = React.useRef(false);
16722
+ const currentValueRef = React.useRef(currentValue);
16723
+ currentValueRef.current = currentValue;
16724
+ const dragStartValueRef = React.useRef(currentValue);
16725
+ const lastSoundValueRef = React.useRef(currentValue);
16726
+ const { jiggling, onAnimationEnd } = useJiggle(isDragging);
16727
+ const playSound = useSound();
16728
+ const colorVar = !validationStatus && cursor.color ? { "--nl-cursor-color": cursor.color } : void 0;
16729
+ const initialPct = toPercent(cursor.value, min, max);
16730
+ const ballPct = toPercent(currentValue, min, max);
16731
+ const fillLeft = Math.min(initialPct, ballPct);
16732
+ const fillWidth = Math.abs(ballPct - initialPct);
16733
+ const snap = React.useCallback((clientX) => {
16734
+ const el = axisRowRef.current;
16735
+ if (!el) return currentValueRef.current;
16736
+ const rect = el.getBoundingClientRect();
16737
+ const raw = pixelToValue(clientX, rect.left, rect.width, min, max);
16738
+ return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
16739
+ }, [axisRowRef, min, max, snapTicks]);
16740
+ const handlePointerDown = React.useCallback((e) => {
16741
+ e.currentTarget.setPointerCapture(e.pointerId);
16742
+ e.stopPropagation();
16743
+ isDraggingRef.current = true;
16744
+ dragStartValueRef.current = currentValue;
16745
+ lastSoundValueRef.current = currentValue;
16746
+ setIsDragging(true);
16747
+ }, [currentValue]);
16748
+ const updateCursorValue = React.useCallback((clientX) => {
16749
+ const next = snap(clientX);
16750
+ if (!almostEqual(next, lastSoundValueRef.current)) {
16751
+ const soundTicks = audibleTicks ?? snapTicks;
16752
+ if (soundTicks.some((tick) => almostEqual(tick, next))) playSound("cursor-step");
16753
+ lastSoundValueRef.current = next;
16754
+ }
16755
+ onChange(cursor.id, String(next));
16756
+ return next;
16757
+ }, [snap, audibleTicks, snapTicks, playSound, onChange, cursor.id]);
16758
+ const handlePointerMove = React.useCallback((e) => {
16759
+ if (!isDraggingRef.current) return;
16760
+ e.stopPropagation();
16761
+ updateCursorValue(e.clientX);
16762
+ }, [updateCursorValue]);
16763
+ const handlePointerUp = React.useCallback((e) => {
16764
+ if (!isDraggingRef.current) return;
16765
+ isDraggingRef.current = false;
16766
+ setIsDragging(false);
16767
+ e.currentTarget.releasePointerCapture(e.pointerId);
16768
+ e.stopPropagation();
16769
+ const next = updateCursorValue(e.clientX);
16770
+ if (!almostEqual(next, dragStartValueRef.current)) {
16771
+ playSound("cursor-release");
16772
+ }
16773
+ }, [updateCursorValue, playSound]);
16774
+ const handleKeyDown = React.useCallback((e) => {
16775
+ if (!snapTicks.length) return;
16776
+ const sorted = [...snapTicks].sort((a, b) => a - b);
16777
+ const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
16778
+ const first = sorted[0] ?? currentValue;
16779
+ const last = sorted[sorted.length - 1] ?? currentValue;
16780
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
16781
+ e.preventDefault();
16782
+ const next = sorted[idx + 1] ?? last;
16783
+ const soundTicks = audibleTicks ?? snapTicks;
16784
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16785
+ playSound("cursor-step");
16786
+ }
16787
+ onChange(cursor.id, String(next));
16788
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
16789
+ e.preventDefault();
16790
+ const next = sorted[idx - 1] ?? first;
16791
+ const soundTicks = audibleTicks ?? snapTicks;
16792
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16793
+ playSound("cursor-step");
16794
+ }
16795
+ onChange(cursor.id, String(next));
16796
+ }
16797
+ }, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
16798
+ const ballClass = [
16799
+ "number-line-cursor-ball",
16800
+ isDragging ? "number-line-cursor-ball--dragging" : "",
16801
+ jiggling && !isDragging ? "number-line-cursor-ball--jiggle" : "",
16802
+ validationStatus ? `number-line-cursor-ball--${validationStatus}` : ""
16803
+ ].filter(Boolean).join(" ");
16804
+ const fillClass = [
16805
+ "number-line-cursor-fill",
16806
+ validationStatus ? `number-line-cursor-fill--${validationStatus}` : ""
16807
+ ].filter(Boolean).join(" ");
16808
+ const labelClass = [
16809
+ "number-line-cursor-label",
16810
+ validationStatus ? `number-line-cursor-label--${validationStatus}` : ""
16811
+ ].filter(Boolean).join(" ");
16812
+ const sliderAriaLabel = cursor.label ? `${cursor.label}: cursor position ${currentValue}` : `cursor position ${currentValue}`;
16813
+ return /* @__PURE__ */ jsxRuntime.jsxs(React.Fragment, { children: [
16814
+ cursor.fillRange && fillWidth > 0 && /* @__PURE__ */ jsxRuntime.jsx(
16815
+ "div",
16816
+ {
16817
+ className: fillClass,
16818
+ "aria-hidden": "true",
16819
+ style: { left: `${fillLeft}%`, width: `${fillWidth}%`, ...colorVar }
16820
+ }
16821
+ ),
16822
+ cursor.label && /* @__PURE__ */ jsxRuntime.jsx(
16823
+ "span",
16824
+ {
16825
+ className: labelClass,
16826
+ "aria-hidden": "true",
16827
+ style: { left: `${ballPct}%`, ...colorVar },
16828
+ children: cursor.label
16829
+ }
16830
+ ),
16831
+ /* @__PURE__ */ jsxRuntime.jsx(
16832
+ "div",
16833
+ {
16834
+ className: ballClass,
16835
+ style: { left: `${ballPct}%`, ...colorVar },
16836
+ onPointerDown: handlePointerDown,
16837
+ onPointerMove: handlePointerMove,
16838
+ onPointerUp: handlePointerUp,
16839
+ onKeyDown: handleKeyDown,
16840
+ onAnimationEnd,
16841
+ role: "slider",
16842
+ "aria-valuemin": min,
16843
+ "aria-valuemax": max,
16844
+ "aria-valuenow": currentValue,
16845
+ "aria-label": sliderAriaLabel,
16846
+ tabIndex: 0
16847
+ }
16848
+ )
16849
+ ] });
16850
+ };
16851
+ const CursorBalls = ({
16852
+ cursors,
16853
+ min,
16854
+ max,
16855
+ snapTicks,
16856
+ audibleTicks,
16857
+ axisRowRef,
16858
+ onChange,
16859
+ values,
16860
+ cursorValidationStatuses
16861
+ }) => {
16862
+ if (!cursors?.length) return null;
16863
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: cursors.map((cursor) => {
16864
+ const raw = values[cursor.id];
16865
+ const parsed = Number(raw);
16866
+ const currentValue = raw !== void 0 && raw !== "" && isFinite(parsed) ? parsed : cursor.value;
16867
+ const validationStatus = cursorValidationStatuses?.[cursor.id] ?? null;
16868
+ return /* @__PURE__ */ jsxRuntime.jsx(
16869
+ DraggableCursor,
16870
+ {
16871
+ cursor,
16872
+ min,
16873
+ max,
16874
+ snapTicks,
16875
+ audibleTicks,
16876
+ axisRowRef,
16877
+ onChange,
16878
+ currentValue,
16879
+ validationStatus
16880
+ },
16881
+ cursor.id
16882
+ );
16883
+ }) });
16884
+ };
16514
16885
  const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
16515
16886
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
16516
16887
  id: widgetId,
@@ -16574,6 +16945,8 @@ const FreeDragAxis = ({
16574
16945
  values,
16575
16946
  onChange,
16576
16947
  highlights,
16948
+ cursors,
16949
+ cursorValidationStatuses,
16577
16950
  snapKey
16578
16951
  }) => {
16579
16952
  const axisRef = React.useRef(null);
@@ -16746,6 +17119,20 @@ const FreeDragAxis = ({
16746
17119
  showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16747
17120
  showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16748
17121
  /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
17122
+ /* @__PURE__ */ jsxRuntime.jsx(
17123
+ CursorBalls,
17124
+ {
17125
+ cursors,
17126
+ min,
17127
+ max,
17128
+ snapTicks,
17129
+ audibleTicks: majorTicks,
17130
+ axisRowRef: axisRef,
17131
+ onChange,
17132
+ values,
17133
+ cursorValidationStatuses
17134
+ }
17135
+ ),
16749
17136
  minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
16750
17137
  "div",
16751
17138
  {
@@ -16814,40 +17201,65 @@ const AxisTicks = ({
16814
17201
  showArrowLeft,
16815
17202
  showArrowRight,
16816
17203
  highlights,
16817
- positions
16818
- }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
16819
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
16820
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16821
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16822
- /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
16823
- minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
16824
- "div",
16825
- {
16826
- className: "number-line-minor-tick",
16827
- "aria-hidden": "true",
16828
- style: { left: `${toPercent(v, min, max)}%` }
16829
- },
16830
- `minor-${v}`
16831
- )),
16832
- majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
16833
- "div",
16834
- {
16835
- className: "number-line-major-tick",
16836
- "aria-hidden": "true",
16837
- style: { left: `${toPercent(tick, min, max)}%` }
16838
- },
16839
- `major-${tick}`
16840
- )),
16841
- positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
16842
- "div",
16843
- {
16844
- className: "number-line-position-dot",
16845
- "aria-hidden": "true",
16846
- style: { left: `${toPercent(pos.value, min, max)}%` }
16847
- },
16848
- `dot-${pos.id}`
16849
- ))
16850
- ] });
17204
+ positions,
17205
+ cursors,
17206
+ onChange,
17207
+ values,
17208
+ cursorValidationStatuses
17209
+ }) => {
17210
+ const axisRowRef = React.useRef(null);
17211
+ const snapTicks = React.useMemo(
17212
+ () => [...majorTicks, ...minorValues],
17213
+ [majorTicks, minorValues]
17214
+ );
17215
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: axisRowRef, className: "number-line-axis-row", role: "presentation", children: [
17216
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
17217
+ showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
17218
+ showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
17219
+ /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
17220
+ /* @__PURE__ */ jsxRuntime.jsx(
17221
+ CursorBalls,
17222
+ {
17223
+ cursors,
17224
+ min,
17225
+ max,
17226
+ snapTicks,
17227
+ audibleTicks: majorTicks,
17228
+ axisRowRef,
17229
+ onChange,
17230
+ values,
17231
+ cursorValidationStatuses
17232
+ }
17233
+ ),
17234
+ minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
17235
+ "div",
17236
+ {
17237
+ className: "number-line-minor-tick",
17238
+ "aria-hidden": "true",
17239
+ style: { left: `${toPercent(v, min, max)}%` }
17240
+ },
17241
+ `minor-${v}`
17242
+ )),
17243
+ majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
17244
+ "div",
17245
+ {
17246
+ className: "number-line-major-tick",
17247
+ "aria-hidden": "true",
17248
+ style: { left: `${toPercent(tick, min, max)}%` }
17249
+ },
17250
+ `major-${tick}`
17251
+ )),
17252
+ positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
17253
+ "div",
17254
+ {
17255
+ className: "number-line-position-dot",
17256
+ "aria-hidden": "true",
17257
+ style: { left: `${toPercent(pos.value, min, max)}%` }
17258
+ },
17259
+ `dot-${pos.id}`
17260
+ ))
17261
+ ] });
17262
+ };
16851
17263
  const isLabelVisible = (tick, min, max, labelVisibility) => {
16852
17264
  switch (labelVisibility) {
16853
17265
  case "none":
@@ -16941,6 +17353,207 @@ const CounterControls = ({
16941
17353
  counter.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-label", children: counter.name })
16942
17354
  ] });
16943
17355
  };
17356
+ const ARROW_ARC_LEVEL_HEIGHT = 80;
17357
+ const ARROW_ARC_VIEWBOX_HEIGHT = 64;
17358
+ const ARROW_ARC_VIEWBOX_WIDTH = 100;
17359
+ const ARROW_ARC_BASE_Y = 56;
17360
+ const ARROW_ARC_MIN_LIFT = 28;
17361
+ const ARROW_ARC_MAX_LIFT = 58;
17362
+ const ARROW_ARC_LIFT_RATIO = 1.15;
17363
+ const ARROW_ARC_LABEL_OFFSET = 24;
17364
+ const ARROW_ARC_HEAD_LENGTH = 8;
17365
+ const ARROW_ARC_HEAD_HALF_WIDTH = 3.2;
17366
+ function getArrowArcLift(widthPct) {
17367
+ return Math.min(ARROW_ARC_MAX_LIFT, Math.max(ARROW_ARC_MIN_LIFT, widthPct * ARROW_ARC_LIFT_RATIO));
17368
+ }
17369
+ function fmt(n) {
17370
+ return Number(n.toFixed(2));
17371
+ }
17372
+ function getArrowArcSvgWidth(renderedWidthPx) {
17373
+ return renderedWidthPx > 0 ? renderedWidthPx : ARROW_ARC_VIEWBOX_WIDTH;
17374
+ }
17375
+ function getArrowArcLabelTop(widthPct) {
17376
+ const svgTop = ARROW_ARC_LEVEL_HEIGHT - ARROW_ARC_VIEWBOX_HEIGHT;
17377
+ const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
17378
+ return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
17379
+ }
17380
+ function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
17381
+ const endX = getArrowArcSvgWidth(svgWidthPx);
17382
+ const controlInset = Math.min(svgWidthPx * 0.18, 48);
17383
+ const lift = getArrowArcLift(widthPct);
17384
+ const controlY = fmt(ARROW_ARC_BASE_Y - lift);
17385
+ const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, endX - (endX - controlInset));
17386
+ const tx = Math.cos(tangentAngle);
17387
+ const ty = Math.sin(tangentAngle);
17388
+ const shaftEndX = fmt(endX - tx * ARROW_ARC_HEAD_LENGTH);
17389
+ const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
17390
+ const endControlX = fmt(shaftEndX - tx * controlInset);
17391
+ const endControlY = fmt(shaftEndY - ty * controlInset);
17392
+ const arcPath = `M 0 ${ARROW_ARC_BASE_Y} C ${fmt(controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
17393
+ const nx = -ty;
17394
+ const ny = tx;
17395
+ const baseX = endX - tx * ARROW_ARC_HEAD_LENGTH;
17396
+ const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
17397
+ const headPath = `M ${fmt(endX)} ${ARROW_ARC_BASE_Y} L ${fmt(baseX + nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY + ny * ARROW_ARC_HEAD_HALF_WIDTH)} L ${fmt(baseX - nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY - ny * ARROW_ARC_HEAD_HALF_WIDTH)} Z`;
17398
+ return { arcPath, headPath };
17399
+ }
17400
+ function getArrowArcLayouts(arrows, min, max) {
17401
+ if (!arrows?.length) return [];
17402
+ const sorted = arrows.map((arrow, index) => {
17403
+ const fromPct = toPercent(arrow.from, min, max);
17404
+ const toPct = toPercent(arrow.to, min, max);
17405
+ return {
17406
+ arrow,
17407
+ index,
17408
+ leftPct: fromPct,
17409
+ rightPct: toPct,
17410
+ widthPct: toPct - fromPct,
17411
+ level: 0
17412
+ };
17413
+ }).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
17414
+ const levelRightEdges = [];
17415
+ for (const layout of sorted) {
17416
+ const reusableLevel = levelRightEdges.findIndex((rightPct) => layout.leftPct >= rightPct);
17417
+ const level = reusableLevel === -1 ? levelRightEdges.length : reusableLevel;
17418
+ layout.level = level;
17419
+ levelRightEdges[level] = layout.rightPct;
17420
+ }
17421
+ return sorted.sort((a, b) => a.index - b.index);
17422
+ }
17423
+ function useElementWidth() {
17424
+ const ref = React.useRef(null);
17425
+ const [width, setWidth] = React.useState(0);
17426
+ React.useLayoutEffect(() => {
17427
+ const element = ref.current;
17428
+ if (!element) return;
17429
+ const updateWidth = () => {
17430
+ const nextWidth = element.getBoundingClientRect().width;
17431
+ setWidth((previousWidth) => previousWidth === nextWidth ? previousWidth : nextWidth);
17432
+ };
17433
+ updateWidth();
17434
+ if (typeof ResizeObserver === "undefined") return;
17435
+ const observer = new ResizeObserver(updateWidth);
17436
+ observer.observe(element);
17437
+ return () => observer.disconnect();
17438
+ }, []);
17439
+ return [ref, width];
17440
+ }
17441
+ const ArrowArc = ({
17442
+ layout: { arrow, leftPct, widthPct, level },
17443
+ widgets,
17444
+ values,
17445
+ droppedInputs,
17446
+ validationDetails,
17447
+ onChange,
17448
+ onInputFocus
17449
+ }) => {
17450
+ const [arcRef, renderedWidthPx] = useElementWidth();
17451
+ const hasLabel = arrow.content.length > 0;
17452
+ const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
17453
+ const { arcPath, headPath } = React.useMemo(
17454
+ () => getArrowArcPaths(widthPct, svgWidthPx),
17455
+ [widthPct, svgWidthPx]
17456
+ );
17457
+ return /* @__PURE__ */ jsxRuntime.jsxs(
17458
+ "div",
17459
+ {
17460
+ ref: arcRef,
17461
+ className: "number-line-arrow-arc",
17462
+ "data-level": level,
17463
+ style: {
17464
+ left: `${leftPct}%`,
17465
+ width: `${widthPct}%`,
17466
+ bottom: `${level * ARROW_ARC_LEVEL_HEIGHT}px`,
17467
+ // DSL color overrides the CSS default — only set when the author
17468
+ // explicitly wrote e.g. ARROW[from=0,to=5,color="#2B6DEA"]
17469
+ ...arrow.color !== void 0 && { "--nl-arrow-arc-color": arrow.color }
17470
+ },
17471
+ children: [
17472
+ /* @__PURE__ */ jsxRuntime.jsxs(
17473
+ "svg",
17474
+ {
17475
+ className: "number-line-arrow-arc-svg",
17476
+ viewBox: `0 0 ${svgWidthPx} ${ARROW_ARC_VIEWBOX_HEIGHT}`,
17477
+ preserveAspectRatio: "none",
17478
+ "aria-hidden": "true",
17479
+ focusable: "false",
17480
+ children: [
17481
+ /* @__PURE__ */ jsxRuntime.jsx(
17482
+ "path",
17483
+ {
17484
+ className: "number-line-arrow-arc-path",
17485
+ d: arcPath
17486
+ }
17487
+ ),
17488
+ /* @__PURE__ */ jsxRuntime.jsx(
17489
+ "path",
17490
+ {
17491
+ className: "number-line-arrow-arc-head",
17492
+ d: headPath
17493
+ }
17494
+ )
17495
+ ]
17496
+ }
17497
+ ),
17498
+ hasLabel && /* @__PURE__ */ jsxRuntime.jsx(
17499
+ "div",
17500
+ {
17501
+ className: "number-line-arrow-arc-label",
17502
+ style: { top: `${getArrowArcLabelTop(widthPct)}px` },
17503
+ children: arrow.content.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
17504
+ LayoutRenderer,
17505
+ {
17506
+ item: subItem,
17507
+ widgets,
17508
+ values,
17509
+ droppedInputs,
17510
+ validationDetails,
17511
+ onChange,
17512
+ onInputFocus
17513
+ },
17514
+ subItem.id
17515
+ ))
17516
+ }
17517
+ )
17518
+ ]
17519
+ }
17520
+ );
17521
+ };
17522
+ const ArrowArcsRow = ({
17523
+ arrows,
17524
+ min,
17525
+ max,
17526
+ widgets,
17527
+ values,
17528
+ droppedInputs,
17529
+ validationDetails,
17530
+ onChange,
17531
+ onInputFocus
17532
+ }) => {
17533
+ const layouts = React.useMemo(() => getArrowArcLayouts(arrows, min, max), [arrows, min, max]);
17534
+ if (layouts.length === 0) return null;
17535
+ const levelCount = Math.max(...layouts.map((layout) => layout.level)) + 1;
17536
+ return /* @__PURE__ */ jsxRuntime.jsx(
17537
+ "div",
17538
+ {
17539
+ className: "number-line-arrow-arcs-row",
17540
+ style: { height: `${levelCount * ARROW_ARC_LEVEL_HEIGHT}px` },
17541
+ children: layouts.map((layout) => /* @__PURE__ */ jsxRuntime.jsx(
17542
+ ArrowArc,
17543
+ {
17544
+ layout,
17545
+ widgets,
17546
+ values,
17547
+ droppedInputs,
17548
+ validationDetails,
17549
+ onChange,
17550
+ onInputFocus
17551
+ },
17552
+ layout.arrow.id
17553
+ ))
17554
+ }
17555
+ );
17556
+ };
16944
17557
  const CONTENT_ROW_EXTRA_SPACE = 8;
16945
17558
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
16946
17559
  function useMeasuredPositionContentHeight(positionIds) {
@@ -17054,8 +17667,10 @@ const NumberLineRenderer = ({
17054
17667
  style: lineStyle = "arrow-right",
17055
17668
  labelVisibility,
17056
17669
  highlights,
17670
+ arrows,
17057
17671
  counter,
17058
- positions
17672
+ positions,
17673
+ cursors
17059
17674
  } = item;
17060
17675
  const [counterCount, setCounterCount] = React.useState(
17061
17676
  counter !== void 0 ? counter.default ?? counter.min : 0
@@ -17071,6 +17686,15 @@ const NumberLineRenderer = ({
17071
17686
  onChangeRef.current(counter.id, String(counterCount));
17072
17687
  }
17073
17688
  }, [counterCount, counter?.id]);
17689
+ const cursorKeys = React.useMemo(
17690
+ () => (cursors ?? []).map((c) => `${c.id}:${c.value}`).join(","),
17691
+ [cursors]
17692
+ );
17693
+ React.useEffect(() => {
17694
+ for (const cursor of cursors ?? []) {
17695
+ onChangeRef.current(cursor.id, String(cursor.value));
17696
+ }
17697
+ }, [cursorKeys]);
17074
17698
  const effectiveStep = React.useMemo(() => {
17075
17699
  if (counter !== void 0 && counterCount > 0) {
17076
17700
  return cleanFloat((max - min) / counterCount);
@@ -17111,6 +17735,17 @@ const NumberLineRenderer = ({
17111
17735
  if (counterValidation === void 0) return null;
17112
17736
  return counterValidation.isCorrect ? "correct" : "incorrect";
17113
17737
  }, [validationDetails, counter]);
17738
+ const cursorValidationStatuses = React.useMemo(() => {
17739
+ if (!validationDetails || !cursors?.length) return void 0;
17740
+ const statuses = {};
17741
+ for (const cursor of cursors) {
17742
+ const detail = validationDetails[cursor.id];
17743
+ if (detail !== void 0) {
17744
+ statuses[cursor.id] = detail.isCorrect ? "correct" : "incorrect";
17745
+ }
17746
+ }
17747
+ return statuses;
17748
+ }, [validationDetails, cursors]);
17114
17749
  return /* @__PURE__ */ jsxRuntime.jsxs(
17115
17750
  "div",
17116
17751
  {
@@ -17144,6 +17779,20 @@ const NumberLineRenderer = ({
17144
17779
  },
17145
17780
  pos.id
17146
17781
  )) }),
17782
+ /* @__PURE__ */ jsxRuntime.jsx(
17783
+ ArrowArcsRow,
17784
+ {
17785
+ arrows,
17786
+ min,
17787
+ max,
17788
+ widgets,
17789
+ values,
17790
+ droppedInputs,
17791
+ validationDetails,
17792
+ onChange,
17793
+ onInputFocus
17794
+ }
17795
+ ),
17147
17796
  isAxisDragMode ? /* @__PURE__ */ jsxRuntime.jsx(
17148
17797
  FreeDragAxis,
17149
17798
  {
@@ -17158,6 +17807,8 @@ const NumberLineRenderer = ({
17158
17807
  values,
17159
17808
  onChange,
17160
17809
  highlights,
17810
+ cursors,
17811
+ cursorValidationStatuses,
17161
17812
  snapKey: counterCount
17162
17813
  }
17163
17814
  ) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -17170,7 +17821,11 @@ const NumberLineRenderer = ({
17170
17821
  showArrowLeft,
17171
17822
  showArrowRight,
17172
17823
  highlights,
17173
- positions
17824
+ positions,
17825
+ cursors,
17826
+ onChange,
17827
+ values,
17828
+ cursorValidationStatuses
17174
17829
  }
17175
17830
  ),
17176
17831
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18149,7 +18804,9 @@ exports.MatcherItemSchema = MatcherItemSchema;
18149
18804
  exports.MatcherRowSchema = MatcherRowSchema;
18150
18805
  exports.MatcherWidgetJSONSchema = MatcherWidgetJSONSchema;
18151
18806
  exports.MixedValidationSchema = MixedValidationSchema;
18807
+ exports.NumberLineArrowItemSchema = NumberLineArrowItemSchema;
18152
18808
  exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
18809
+ exports.NumberLineCursorItemSchema = NumberLineCursorItemSchema;
18153
18810
  exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
18154
18811
  exports.NumberLineItemSchema = NumberLineItemSchema;
18155
18812
  exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;