@mathscan/math-exercise-engine 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -56,10 +56,14 @@ const AST_TYPES = {
56
56
  NUMBER_LINE_BLOCK: "NumberLineBlock",
57
57
  /** A position marker on the number line */
58
58
  NUMBER_LINE_POSITION: "NumberLinePosition",
59
+ /** A draggable cursor on the number line */
60
+ NUMBER_LINE_CURSOR: "NumberLineCursor",
59
61
  /** A dynamic subdivision counter embedded in a number line */
60
62
  NUMBER_LINE_COUNTER: "NumberLineCounter",
61
63
  /** A highlighted range on the number line axis */
62
64
  NUMBER_LINE_HIGHLIGHT: "NumberLineHighlight",
65
+ /** An arrow annotation spanning a number line interval */
66
+ NUMBER_LINE_ARROW: "NumberLineArrow",
63
67
  /** Matcher layout block */
64
68
  MATCHER_BLOCK: "MatcherBlock",
65
69
  /** Row within a matcher */
@@ -152,6 +156,10 @@ const JSON_TYPES = {
152
156
  NUMBER_LINE_COUNTER: "number-line-counter",
153
157
  /** Highlighted range within a number line */
154
158
  NUMBER_LINE_HIGHLIGHT: "number-line-highlight",
159
+ /** Draggable cursor within a number line */
160
+ NUMBER_LINE_CURSOR: "number-line-cursor",
161
+ /** Arrow annotation within a number line */
162
+ NUMBER_LINE_ARROW: "number-line-arrow",
155
163
  /** Matcher layout block */
156
164
  MATCHER: "matcher",
157
165
  /** Row within a matcher */
@@ -318,6 +326,14 @@ const POSITION = createToken({
318
326
  name: "POSITION",
319
327
  pattern: /POSITION/
320
328
  });
329
+ const CURSOR = createToken({
330
+ name: "CURSOR",
331
+ pattern: /CURSOR/
332
+ });
333
+ const ARROW = createToken({
334
+ name: "ARROW",
335
+ pattern: /ARROW/
336
+ });
321
337
  const TABLE = createToken({
322
338
  name: "TABLE",
323
339
  pattern: /TABLE/
@@ -515,6 +531,8 @@ const allTokens = [
515
531
  NUMERIC_INPUT,
516
532
  NUMBER_LINE,
517
533
  POSITION,
534
+ CURSOR,
535
+ ARROW,
518
536
  TABLE,
519
537
  ROW,
520
538
  TOP_ROW,
@@ -789,7 +807,9 @@ class MathExerciseParser extends CstParser {
789
807
  this.OR6([
790
808
  { ALT: () => this.SUBRULE(this.position) },
791
809
  { ALT: () => this.SUBRULE(this.counter) },
792
- { ALT: () => this.SUBRULE(this.highlight) }
810
+ { ALT: () => this.SUBRULE(this.highlight) },
811
+ { ALT: () => this.SUBRULE(this.cursor) },
812
+ { ALT: () => this.SUBRULE(this.arrow) }
793
813
  ]);
794
814
  });
795
815
  this.CONSUME(RBrace);
@@ -838,6 +858,38 @@ class MathExerciseParser extends CstParser {
838
858
  this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
839
859
  });
840
860
  });
861
+ this.cursor = this.RULE("cursor", () => {
862
+ this.CONSUME(CURSOR);
863
+ this.CONSUME(LBracket);
864
+ this.SUBRULE(this.parameterList);
865
+ this.CONSUME(RBracket);
866
+ this.MANY2(() => {
867
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
868
+ });
869
+ });
870
+ this.arrow = this.RULE("arrow", () => {
871
+ this.CONSUME(ARROW);
872
+ this.CONSUME(LBracket);
873
+ this.SUBRULE(this.parameterList);
874
+ this.CONSUME(RBracket);
875
+ this.MANY2(() => {
876
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
877
+ });
878
+ this.CONSUME(LBrace);
879
+ this.MANY3(() => {
880
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
881
+ });
882
+ this.MANY4(() => {
883
+ this.OR4([
884
+ { ALT: () => this.SUBRULE(this.inlineElement) },
885
+ { ALT: () => this.CONSUME4(Newline) }
886
+ ]);
887
+ });
888
+ this.CONSUME(RBrace);
889
+ this.MANY5(() => {
890
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
891
+ });
892
+ });
841
893
  this.matcher = this.RULE("matcher", () => {
842
894
  this.MANY(() => {
843
895
  this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
@@ -1865,6 +1917,44 @@ function createNumberLineHighlight(from, to, color, label) {
1865
1917
  ...label !== void 0 && { label }
1866
1918
  });
1867
1919
  }
1920
+ const NumberLineCursorSchema = z.object({
1921
+ type: z.literal(AST_TYPES.NUMBER_LINE_CURSOR),
1922
+ value: z.number().finite({ message: "CURSOR: value must be a finite number" }).default(0),
1923
+ answer: z.number({ required_error: "CURSOR: answer is required" }).finite({ message: "CURSOR: answer must be a finite number" }),
1924
+ color: z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/, {
1925
+ message: 'CURSOR: color must be a hex value (e.g. "#3B82F6" or "#F00")'
1926
+ }).optional(),
1927
+ fillRange: z.boolean().default(false),
1928
+ label: z.string().optional()
1929
+ });
1930
+ function createNumberLineCursor(answer, value, color, fillRange, label) {
1931
+ return NumberLineCursorSchema.parse({
1932
+ type: AST_TYPES.NUMBER_LINE_CURSOR,
1933
+ answer,
1934
+ ...value !== void 0 && { value },
1935
+ ...color !== void 0 && { color },
1936
+ ...fillRange !== void 0 && { fillRange },
1937
+ ...label !== void 0 && { label }
1938
+ });
1939
+ }
1940
+ const NumberLineArrowSchema = z.object({
1941
+ type: z.literal(AST_TYPES.NUMBER_LINE_ARROW),
1942
+ from: z.number().finite({ message: "ARROW: from must be a finite number" }),
1943
+ to: z.number().finite({ message: "ARROW: to must be a finite number" }),
1944
+ color: z.string().optional(),
1945
+ inlines: z.array(InlineNodeSchema)
1946
+ }).refine((d) => d.from < d.to, {
1947
+ message: "ARROW: from must be less than to"
1948
+ });
1949
+ function createNumberLineArrow(from, to, inlines, color) {
1950
+ return NumberLineArrowSchema.parse({
1951
+ type: AST_TYPES.NUMBER_LINE_ARROW,
1952
+ from,
1953
+ to,
1954
+ ...color !== void 0 && { color },
1955
+ inlines
1956
+ });
1957
+ }
1868
1958
  const NumberLineBlockSchema = z.object({
1869
1959
  type: z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
1870
1960
  min: z.number().finite({ message: "NUMBER_LINE: min must be a finite number" }),
@@ -1883,6 +1973,8 @@ const NumberLineBlockSchema = z.object({
1883
1973
  }).optional(),
1884
1974
  counter: NumberLineCounterSchema.optional(),
1885
1975
  highlights: z.array(NumberLineHighlightSchema).optional(),
1976
+ cursors: z.array(NumberLineCursorSchema).optional(),
1977
+ arrows: z.array(NumberLineArrowSchema).optional(),
1886
1978
  dragMode: z.enum(NUMBER_LINE_DRAG_MODES, {
1887
1979
  errorMap: () => ({
1888
1980
  message: `NUMBER_LINE: dragMode must be one of ${NUMBER_LINE_DRAG_MODES.join(", ")}`
@@ -1916,6 +2008,14 @@ const NumberLineBlockSchema = z.object({
1916
2008
  });
1917
2009
  }
1918
2010
  }
2011
+ for (const arrow of data.arrows ?? []) {
2012
+ if (arrow.from < data.min || arrow.to > data.max) {
2013
+ ctx.addIssue({
2014
+ code: z.ZodIssueCode.custom,
2015
+ message: `NUMBER_LINE: ARROW range [${arrow.from}, ${arrow.to}] must be within [${data.min}, ${data.max}]`
2016
+ });
2017
+ }
2018
+ }
1919
2019
  if (data.counter?.default !== void 0) {
1920
2020
  if (data.counter.default < data.counter.min || data.counter.default > data.counter.max) {
1921
2021
  ctx.addIssue({
@@ -1924,8 +2024,22 @@ const NumberLineBlockSchema = z.object({
1924
2024
  });
1925
2025
  }
1926
2026
  }
2027
+ for (const c of data.cursors ?? []) {
2028
+ if (c.value < data.min || c.value > data.max) {
2029
+ ctx.addIssue({
2030
+ code: z.ZodIssueCode.custom,
2031
+ message: `NUMBER_LINE: CURSOR value (${c.value}) must be within [${data.min}, ${data.max}]`
2032
+ });
2033
+ }
2034
+ if (c.answer < data.min || c.answer > data.max) {
2035
+ ctx.addIssue({
2036
+ code: z.ZodIssueCode.custom,
2037
+ message: `NUMBER_LINE: CURSOR answer (${c.answer}) must be within [${data.min}, ${data.max}]`
2038
+ });
2039
+ }
2040
+ }
1927
2041
  });
1928
- function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, dragMode) {
2042
+ function createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, cursors, arrows, dragMode) {
1929
2043
  return NumberLineBlockSchema.parse({
1930
2044
  type: AST_TYPES.NUMBER_LINE_BLOCK,
1931
2045
  min,
@@ -1936,6 +2050,8 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style, lab
1936
2050
  labelVisibility,
1937
2051
  counter,
1938
2052
  highlights,
2053
+ cursors,
2054
+ arrows,
1939
2055
  dragMode,
1940
2056
  positions
1941
2057
  });
@@ -2589,7 +2705,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2589
2705
  }
2590
2706
  const counter = rawCounters ? this.visit(rawCounters) : void 0;
2591
2707
  const highlights = ctx.highlight ? ctx.highlight.map((h) => this.visit(h)) : [];
2592
- return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, dragMode);
2708
+ const cursors = ctx.cursor ? ctx.cursor.map((c) => this.visit(c)) : [];
2709
+ const arrows = ctx.arrow ? ctx.arrow.map((a) => this.visit(a)) : [];
2710
+ return createNumberLineBlock(min, max, positions, step, minorTicks, style, labelVisibility, counter, highlights, cursors.length > 0 ? cursors : void 0, arrows.length > 0 ? arrows : void 0, dragMode);
2593
2711
  }
2594
2712
  position(ctx) {
2595
2713
  const params = this.visit(ctx.parameterList);
@@ -2623,6 +2741,29 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2623
2741
  const label = params.label;
2624
2742
  return createNumberLineHighlight(from, to, color, label);
2625
2743
  }
2744
+ cursor(ctx) {
2745
+ const params = this.visit(ctx.parameterList);
2746
+ if (params.answer === void 0) {
2747
+ throw new Error("CURSOR requires parameter: answer");
2748
+ }
2749
+ const answer = Number(params.answer);
2750
+ const value = params.value !== void 0 ? Number(params.value) : void 0;
2751
+ const color = params.color;
2752
+ const fillRange = params.fillRange !== void 0 ? params.fillRange === true || params.fillRange === "true" : void 0;
2753
+ const label = params.label;
2754
+ return createNumberLineCursor(answer, value, color, fillRange, label);
2755
+ }
2756
+ arrow(ctx) {
2757
+ const params = this.visit(ctx.parameterList);
2758
+ if (params.from === void 0 || params.to === void 0) {
2759
+ throw new Error("ARROW requires parameters: from and to");
2760
+ }
2761
+ const from = Number(params.from);
2762
+ const to = Number(params.to);
2763
+ const color = params.color;
2764
+ const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
2765
+ return createNumberLineArrow(from, to, inlines, color);
2766
+ }
2626
2767
  // ═══════════════════════════════════════════════════════════════════════════
2627
2768
  // Matcher Block
2628
2769
  // ═══════════════════════════════════════════════════════════════════════════
@@ -3277,6 +3418,23 @@ const NumberLineHighlightItemSchema = z.object({
3277
3418
  color: z.enum(NUMBER_LINE_HIGHLIGHT_COLORS).optional(),
3278
3419
  label: z.string().optional()
3279
3420
  });
3421
+ const NumberLineCursorItemSchema = z.object({
3422
+ id: z.string(),
3423
+ type: z.literal(JSON_TYPES.NUMBER_LINE_CURSOR),
3424
+ value: z.number().finite(),
3425
+ answer: z.number().finite(),
3426
+ color: z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/).optional(),
3427
+ fillRange: z.boolean(),
3428
+ label: z.string().optional()
3429
+ });
3430
+ const NumberLineArrowItemSchema = z.object({
3431
+ id: z.string(),
3432
+ type: z.literal(JSON_TYPES.NUMBER_LINE_ARROW),
3433
+ from: z.number().finite(),
3434
+ to: z.number().finite(),
3435
+ color: z.string().optional(),
3436
+ content: z.array(LayoutItemSchema)
3437
+ });
3280
3438
  const NumberLineItemSchema = z.object({
3281
3439
  id: z.string(),
3282
3440
  type: z.literal(JSON_TYPES.NUMBER_LINE),
@@ -3288,6 +3446,8 @@ const NumberLineItemSchema = z.object({
3288
3446
  labelVisibility: z.enum(NUMBER_LINE_LABELS).optional(),
3289
3447
  counter: NumberLineCounterItemSchema.optional(),
3290
3448
  highlights: z.array(NumberLineHighlightItemSchema).optional(),
3449
+ cursors: z.array(NumberLineCursorItemSchema).optional(),
3450
+ arrows: z.array(NumberLineArrowItemSchema).optional(),
3291
3451
  dragMode: z.enum(NUMBER_LINE_DRAG_MODES).optional(),
3292
3452
  positions: z.array(NumberLinePositionItemSchema)
3293
3453
  });
@@ -3635,6 +3795,9 @@ function toJSON(ast) {
3635
3795
  if (nlItem.counter?.answer !== void 0) {
3636
3796
  answers[nlItem.counter.id] = nlItem.counter.answer;
3637
3797
  }
3798
+ for (const cursor of nlItem.cursors ?? []) {
3799
+ answers[cursor.id] = cursor.answer;
3800
+ }
3638
3801
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
3639
3802
  const matcherBlock = block;
3640
3803
  layout.push({
@@ -3825,9 +3988,13 @@ function collectAllInlines(blocks) {
3825
3988
  }
3826
3989
  }
3827
3990
  } else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
3828
- for (const position of block.positions) {
3991
+ const numberLine = block;
3992
+ for (const position of numberLine.positions) {
3829
3993
  result.push(...position.inlines);
3830
3994
  }
3995
+ for (const arrow of numberLine.arrows ?? []) {
3996
+ result.push(...arrow.inlines);
3997
+ }
3831
3998
  } else if (block.type === AST_TYPES.MATCHER_BLOCK) {
3832
3999
  for (const row of block.rows) {
3833
4000
  for (const item of row.items) {
@@ -3877,6 +4044,29 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
3877
4044
  ...h.label !== void 0 && { label: h.label }
3878
4045
  }));
3879
4046
  }
4047
+ let cursors;
4048
+ if (numberLine.cursors !== void 0 && numberLine.cursors.length > 0) {
4049
+ cursors = numberLine.cursors.map((c) => ({
4050
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_CURSOR),
4051
+ type: JSON_TYPES.NUMBER_LINE_CURSOR,
4052
+ value: c.value,
4053
+ answer: c.answer,
4054
+ fillRange: c.fillRange,
4055
+ ...c.color !== void 0 && { color: c.color },
4056
+ ...c.label !== void 0 && { label: c.label }
4057
+ }));
4058
+ }
4059
+ let arrows;
4060
+ if (numberLine.arrows !== void 0 && numberLine.arrows.length > 0) {
4061
+ arrows = numberLine.arrows.map((a) => ({
4062
+ id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_ARROW),
4063
+ type: JSON_TYPES.NUMBER_LINE_ARROW,
4064
+ from: a.from,
4065
+ to: a.to,
4066
+ ...a.color !== void 0 && { color: a.color },
4067
+ content: a.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
4068
+ }));
4069
+ }
3880
4070
  const item = {
3881
4071
  id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
3882
4072
  type: JSON_TYPES.NUMBER_LINE,
@@ -3888,6 +4078,8 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
3888
4078
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
3889
4079
  ...counterItem !== void 0 && { counter: counterItem },
3890
4080
  ...highlights !== void 0 && { highlights },
4081
+ ...cursors !== void 0 && { cursors },
4082
+ ...arrows !== void 0 && { arrows },
3891
4083
  ...numberLine.dragMode !== void 0 && { dragMode: numberLine.dragMode },
3892
4084
  positions
3893
4085
  };
@@ -4019,7 +4211,7 @@ function transformLine(line, idGenerator, inlineDragIds) {
4019
4211
  }
4020
4212
  function transformInline(inline, idGenerator, inlineDragIds) {
4021
4213
  switch (inline.type) {
4022
- case AST_TYPES.FREE_TEXT:
4214
+ case AST_TYPES.FREE_TEXT: {
4023
4215
  const trimmed = inline.value.trim();
4024
4216
  const operators = ["+", "-", "×", "÷", "=", "<", ">", "?"];
4025
4217
  if (operators.includes(trimmed) && inline.value.trim() === inline.value) {
@@ -4034,6 +4226,7 @@ function transformInline(inline, idGenerator, inlineDragIds) {
4034
4226
  type: JSON_TYPES.TEXT,
4035
4227
  value: inline.value
4036
4228
  };
4229
+ }
4037
4230
  case AST_TYPES.SPACE:
4038
4231
  return {
4039
4232
  id: idGenerator.generate(JSON_TYPES.SPACE),
@@ -8296,7 +8489,7 @@ lodash.exports;
8296
8489
  var tag = baseGetTag(value);
8297
8490
  return tag == errorTag || tag == domExcTag || typeof value.message == "string" && typeof value.name == "string" && !isPlainObject(value);
8298
8491
  }
8299
- function isFinite(value) {
8492
+ function isFinite2(value) {
8300
8493
  return typeof value == "number" && nativeIsFinite(value);
8301
8494
  }
8302
8495
  function isFunction(value) {
@@ -9397,7 +9590,7 @@ lodash.exports;
9397
9590
  lodash2.isEqual = isEqual;
9398
9591
  lodash2.isEqualWith = isEqualWith;
9399
9592
  lodash2.isError = isError;
9400
- lodash2.isFinite = isFinite;
9593
+ lodash2.isFinite = isFinite2;
9401
9594
  lodash2.isFunction = isFunction;
9402
9595
  lodash2.isInteger = isInteger;
9403
9596
  lodash2.isLength = isLength;
@@ -13951,6 +14144,8 @@ const DEFAULT_SOUNDS = {
13951
14144
  "clock-hour-tick": buttonClick,
13952
14145
  "clock-minute-tick": buttonClick,
13953
14146
  "counter-step": buttonClick,
14147
+ "cursor-step": buttonClick,
14148
+ "cursor-release": dragDrop,
13954
14149
  "input-select": inputSelect,
13955
14150
  "answer-correct": answerCorrect,
13956
14151
  "answer-wrong": answerWrong
@@ -15048,19 +15243,21 @@ const DragOptions = ({
15048
15243
  options,
15049
15244
  onOptionClick
15050
15245
  }) => {
15051
- return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
15052
- const optionId = `drag-option-${idx}`;
15053
- const optionValue = String(option);
15054
- return /* @__PURE__ */ jsx(
15055
- SingleDragOption,
15056
- {
15057
- optionId,
15058
- value: optionValue,
15059
- onOptionClick
15060
- },
15061
- optionId
15062
- );
15063
- }) }) });
15246
+ const [shuffledOptions] = useState(
15247
+ () => lodashExports.shuffle(options.map((option, idx) => ({
15248
+ optionId: `drag-option-${idx}`,
15249
+ value: String(option)
15250
+ })))
15251
+ );
15252
+ return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: shuffledOptions.map(({ optionId, value }) => /* @__PURE__ */ jsx(
15253
+ SingleDragOption,
15254
+ {
15255
+ optionId,
15256
+ value,
15257
+ onOptionClick
15258
+ },
15259
+ optionId
15260
+ )) }) });
15064
15261
  };
15065
15262
  const InlineDragOption = ({
15066
15263
  widgetId,
@@ -15644,7 +15841,8 @@ const RadioOptions = ({
15644
15841
  onChange
15645
15842
  }) => {
15646
15843
  const playSound = useSound();
15647
- return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
15844
+ const [shuffledOptions] = useState(() => lodashExports.shuffle(options));
15845
+ return /* @__PURE__ */ jsx("div", { className: "radio-options", children: shuffledOptions.map((option, idx) => {
15648
15846
  const isSelected = value === option;
15649
15847
  const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
15650
15848
  return /* @__PURE__ */ jsx(
@@ -16509,6 +16707,182 @@ const HighlightBands = ({ highlights, min, max }) => {
16509
16707
  );
16510
16708
  }) });
16511
16709
  };
16710
+ const DraggableCursor = ({
16711
+ cursor,
16712
+ min,
16713
+ max,
16714
+ snapTicks,
16715
+ audibleTicks,
16716
+ axisRowRef,
16717
+ onChange,
16718
+ currentValue,
16719
+ validationStatus
16720
+ }) => {
16721
+ const [isDragging, setIsDragging] = useState(false);
16722
+ const isDraggingRef = useRef(false);
16723
+ const currentValueRef = useRef(currentValue);
16724
+ currentValueRef.current = currentValue;
16725
+ const dragStartValueRef = useRef(currentValue);
16726
+ const lastSoundValueRef = useRef(currentValue);
16727
+ const { jiggling, onAnimationEnd } = useJiggle(isDragging);
16728
+ const playSound = useSound();
16729
+ const colorVar = !validationStatus && cursor.color ? { "--nl-cursor-color": cursor.color } : void 0;
16730
+ const initialPct = toPercent(cursor.value, min, max);
16731
+ const ballPct = toPercent(currentValue, min, max);
16732
+ const fillLeft = Math.min(initialPct, ballPct);
16733
+ const fillWidth = Math.abs(ballPct - initialPct);
16734
+ const snap = useCallback((clientX) => {
16735
+ const el = axisRowRef.current;
16736
+ if (!el) return currentValueRef.current;
16737
+ const rect = el.getBoundingClientRect();
16738
+ const raw = pixelToValue(clientX, rect.left, rect.width, min, max);
16739
+ return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
16740
+ }, [axisRowRef, min, max, snapTicks]);
16741
+ const handlePointerDown = useCallback((e) => {
16742
+ e.currentTarget.setPointerCapture(e.pointerId);
16743
+ e.stopPropagation();
16744
+ isDraggingRef.current = true;
16745
+ dragStartValueRef.current = currentValue;
16746
+ lastSoundValueRef.current = currentValue;
16747
+ setIsDragging(true);
16748
+ }, [currentValue]);
16749
+ const updateCursorValue = useCallback((clientX) => {
16750
+ const next = snap(clientX);
16751
+ if (!almostEqual(next, lastSoundValueRef.current)) {
16752
+ const soundTicks = audibleTicks ?? snapTicks;
16753
+ if (soundTicks.some((tick) => almostEqual(tick, next))) playSound("cursor-step");
16754
+ lastSoundValueRef.current = next;
16755
+ }
16756
+ onChange(cursor.id, String(next));
16757
+ return next;
16758
+ }, [snap, audibleTicks, snapTicks, playSound, onChange, cursor.id]);
16759
+ const handlePointerMove = useCallback((e) => {
16760
+ if (!isDraggingRef.current) return;
16761
+ e.stopPropagation();
16762
+ updateCursorValue(e.clientX);
16763
+ }, [updateCursorValue]);
16764
+ const handlePointerUp = useCallback((e) => {
16765
+ if (!isDraggingRef.current) return;
16766
+ isDraggingRef.current = false;
16767
+ setIsDragging(false);
16768
+ e.currentTarget.releasePointerCapture(e.pointerId);
16769
+ e.stopPropagation();
16770
+ const next = updateCursorValue(e.clientX);
16771
+ if (!almostEqual(next, dragStartValueRef.current)) {
16772
+ playSound("cursor-release");
16773
+ }
16774
+ }, [updateCursorValue, playSound]);
16775
+ const handleKeyDown = useCallback((e) => {
16776
+ if (!snapTicks.length) return;
16777
+ const sorted = [...snapTicks].sort((a, b) => a - b);
16778
+ const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
16779
+ const first = sorted[0] ?? currentValue;
16780
+ const last = sorted[sorted.length - 1] ?? currentValue;
16781
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
16782
+ e.preventDefault();
16783
+ const next = sorted[idx + 1] ?? last;
16784
+ const soundTicks = audibleTicks ?? snapTicks;
16785
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16786
+ playSound("cursor-step");
16787
+ }
16788
+ onChange(cursor.id, String(next));
16789
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
16790
+ e.preventDefault();
16791
+ const next = sorted[idx - 1] ?? first;
16792
+ const soundTicks = audibleTicks ?? snapTicks;
16793
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16794
+ playSound("cursor-step");
16795
+ }
16796
+ onChange(cursor.id, String(next));
16797
+ }
16798
+ }, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
16799
+ const ballClass = [
16800
+ "number-line-cursor-ball",
16801
+ isDragging ? "number-line-cursor-ball--dragging" : "",
16802
+ jiggling && !isDragging ? "number-line-cursor-ball--jiggle" : "",
16803
+ validationStatus ? `number-line-cursor-ball--${validationStatus}` : ""
16804
+ ].filter(Boolean).join(" ");
16805
+ const fillClass = [
16806
+ "number-line-cursor-fill",
16807
+ validationStatus ? `number-line-cursor-fill--${validationStatus}` : ""
16808
+ ].filter(Boolean).join(" ");
16809
+ const labelClass = [
16810
+ "number-line-cursor-label",
16811
+ validationStatus ? `number-line-cursor-label--${validationStatus}` : ""
16812
+ ].filter(Boolean).join(" ");
16813
+ const sliderAriaLabel = cursor.label ? `${cursor.label}: cursor position ${currentValue}` : `cursor position ${currentValue}`;
16814
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
16815
+ cursor.fillRange && fillWidth > 0 && /* @__PURE__ */ jsx(
16816
+ "div",
16817
+ {
16818
+ className: fillClass,
16819
+ "aria-hidden": "true",
16820
+ style: { left: `${fillLeft}%`, width: `${fillWidth}%`, ...colorVar }
16821
+ }
16822
+ ),
16823
+ cursor.label && /* @__PURE__ */ jsx(
16824
+ "span",
16825
+ {
16826
+ className: labelClass,
16827
+ "aria-hidden": "true",
16828
+ style: { left: `${ballPct}%`, ...colorVar },
16829
+ children: cursor.label
16830
+ }
16831
+ ),
16832
+ /* @__PURE__ */ jsx(
16833
+ "div",
16834
+ {
16835
+ className: ballClass,
16836
+ style: { left: `${ballPct}%`, ...colorVar },
16837
+ onPointerDown: handlePointerDown,
16838
+ onPointerMove: handlePointerMove,
16839
+ onPointerUp: handlePointerUp,
16840
+ onKeyDown: handleKeyDown,
16841
+ onAnimationEnd,
16842
+ role: "slider",
16843
+ "aria-valuemin": min,
16844
+ "aria-valuemax": max,
16845
+ "aria-valuenow": currentValue,
16846
+ "aria-label": sliderAriaLabel,
16847
+ tabIndex: 0
16848
+ }
16849
+ )
16850
+ ] });
16851
+ };
16852
+ const CursorBalls = ({
16853
+ cursors,
16854
+ min,
16855
+ max,
16856
+ snapTicks,
16857
+ audibleTicks,
16858
+ axisRowRef,
16859
+ onChange,
16860
+ values,
16861
+ cursorValidationStatuses
16862
+ }) => {
16863
+ if (!cursors?.length) return null;
16864
+ return /* @__PURE__ */ jsx(Fragment, { children: cursors.map((cursor) => {
16865
+ const raw = values[cursor.id];
16866
+ const parsed = Number(raw);
16867
+ const currentValue = raw !== void 0 && raw !== "" && isFinite(parsed) ? parsed : cursor.value;
16868
+ const validationStatus = cursorValidationStatuses?.[cursor.id] ?? null;
16869
+ return /* @__PURE__ */ jsx(
16870
+ DraggableCursor,
16871
+ {
16872
+ cursor,
16873
+ min,
16874
+ max,
16875
+ snapTicks,
16876
+ audibleTicks,
16877
+ axisRowRef,
16878
+ onChange,
16879
+ currentValue,
16880
+ validationStatus
16881
+ },
16882
+ cursor.id
16883
+ );
16884
+ }) });
16885
+ };
16512
16886
  const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
16513
16887
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
16514
16888
  id: widgetId,
@@ -16572,6 +16946,8 @@ const FreeDragAxis = ({
16572
16946
  values,
16573
16947
  onChange,
16574
16948
  highlights,
16949
+ cursors,
16950
+ cursorValidationStatuses,
16575
16951
  snapKey
16576
16952
  }) => {
16577
16953
  const axisRef = useRef(null);
@@ -16744,6 +17120,20 @@ const FreeDragAxis = ({
16744
17120
  showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16745
17121
  showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16746
17122
  /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
17123
+ /* @__PURE__ */ jsx(
17124
+ CursorBalls,
17125
+ {
17126
+ cursors,
17127
+ min,
17128
+ max,
17129
+ snapTicks,
17130
+ audibleTicks: majorTicks,
17131
+ axisRowRef: axisRef,
17132
+ onChange,
17133
+ values,
17134
+ cursorValidationStatuses
17135
+ }
17136
+ ),
16747
17137
  minorValues.map((v) => /* @__PURE__ */ jsx(
16748
17138
  "div",
16749
17139
  {
@@ -16812,40 +17202,65 @@ const AxisTicks = ({
16812
17202
  showArrowLeft,
16813
17203
  showArrowRight,
16814
17204
  highlights,
16815
- positions
16816
- }) => /* @__PURE__ */ jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
16817
- /* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
16818
- showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16819
- showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16820
- /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
16821
- minorValues.map((v) => /* @__PURE__ */ jsx(
16822
- "div",
16823
- {
16824
- className: "number-line-minor-tick",
16825
- "aria-hidden": "true",
16826
- style: { left: `${toPercent(v, min, max)}%` }
16827
- },
16828
- `minor-${v}`
16829
- )),
16830
- majorTicks.map((tick) => /* @__PURE__ */ jsx(
16831
- "div",
16832
- {
16833
- className: "number-line-major-tick",
16834
- "aria-hidden": "true",
16835
- style: { left: `${toPercent(tick, min, max)}%` }
16836
- },
16837
- `major-${tick}`
16838
- )),
16839
- positions.map((pos) => /* @__PURE__ */ jsx(
16840
- "div",
16841
- {
16842
- className: "number-line-position-dot",
16843
- "aria-hidden": "true",
16844
- style: { left: `${toPercent(pos.value, min, max)}%` }
16845
- },
16846
- `dot-${pos.id}`
16847
- ))
16848
- ] });
17205
+ positions,
17206
+ cursors,
17207
+ onChange,
17208
+ values,
17209
+ cursorValidationStatuses
17210
+ }) => {
17211
+ const axisRowRef = useRef(null);
17212
+ const snapTicks = useMemo(
17213
+ () => [...majorTicks, ...minorValues],
17214
+ [majorTicks, minorValues]
17215
+ );
17216
+ return /* @__PURE__ */ jsxs("div", { ref: axisRowRef, className: "number-line-axis-row", role: "presentation", children: [
17217
+ /* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
17218
+ showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
17219
+ showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
17220
+ /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
17221
+ /* @__PURE__ */ jsx(
17222
+ CursorBalls,
17223
+ {
17224
+ cursors,
17225
+ min,
17226
+ max,
17227
+ snapTicks,
17228
+ audibleTicks: majorTicks,
17229
+ axisRowRef,
17230
+ onChange,
17231
+ values,
17232
+ cursorValidationStatuses
17233
+ }
17234
+ ),
17235
+ minorValues.map((v) => /* @__PURE__ */ jsx(
17236
+ "div",
17237
+ {
17238
+ className: "number-line-minor-tick",
17239
+ "aria-hidden": "true",
17240
+ style: { left: `${toPercent(v, min, max)}%` }
17241
+ },
17242
+ `minor-${v}`
17243
+ )),
17244
+ majorTicks.map((tick) => /* @__PURE__ */ jsx(
17245
+ "div",
17246
+ {
17247
+ className: "number-line-major-tick",
17248
+ "aria-hidden": "true",
17249
+ style: { left: `${toPercent(tick, min, max)}%` }
17250
+ },
17251
+ `major-${tick}`
17252
+ )),
17253
+ positions.map((pos) => /* @__PURE__ */ jsx(
17254
+ "div",
17255
+ {
17256
+ className: "number-line-position-dot",
17257
+ "aria-hidden": "true",
17258
+ style: { left: `${toPercent(pos.value, min, max)}%` }
17259
+ },
17260
+ `dot-${pos.id}`
17261
+ ))
17262
+ ] });
17263
+ };
16849
17264
  const isLabelVisible = (tick, min, max, labelVisibility) => {
16850
17265
  switch (labelVisibility) {
16851
17266
  case "none":
@@ -16939,6 +17354,209 @@ const CounterControls = ({
16939
17354
  counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
16940
17355
  ] });
16941
17356
  };
17357
+ const ARROW_ARC_LEVEL_HEIGHT = 80;
17358
+ const ARROW_ARC_VIEWBOX_HEIGHT = 64;
17359
+ const ARROW_ARC_VIEWBOX_WIDTH = 100;
17360
+ const ARROW_ARC_BASE_Y = 56;
17361
+ const ARROW_ARC_MIN_LIFT = 28;
17362
+ const ARROW_ARC_MAX_LIFT = 58;
17363
+ const ARROW_ARC_LIFT_RATIO = 1.15;
17364
+ const ARROW_ARC_LABEL_OFFSET = 24;
17365
+ const ARROW_ARC_HEAD_LENGTH = 8;
17366
+ const ARROW_ARC_HEAD_HALF_WIDTH = 3.2;
17367
+ function getArrowArcLift(widthPct) {
17368
+ return Math.min(ARROW_ARC_MAX_LIFT, Math.max(ARROW_ARC_MIN_LIFT, widthPct * ARROW_ARC_LIFT_RATIO));
17369
+ }
17370
+ function fmt(n) {
17371
+ return Number(n.toFixed(2));
17372
+ }
17373
+ function getArrowArcSvgWidth(renderedWidthPx) {
17374
+ return renderedWidthPx > 0 ? renderedWidthPx : ARROW_ARC_VIEWBOX_WIDTH;
17375
+ }
17376
+ function getArrowArcLabelTop(widthPct) {
17377
+ const svgTop = ARROW_ARC_LEVEL_HEIGHT - ARROW_ARC_VIEWBOX_HEIGHT;
17378
+ const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
17379
+ return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
17380
+ }
17381
+ function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
17382
+ const endX = getArrowArcSvgWidth(svgWidthPx);
17383
+ const controlInset = Math.min(svgWidthPx * 0.18, 48);
17384
+ const lift = getArrowArcLift(widthPct);
17385
+ const controlY = fmt(ARROW_ARC_BASE_Y - lift);
17386
+ const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, endX - (endX - controlInset));
17387
+ const tx = Math.cos(tangentAngle);
17388
+ const ty = Math.sin(tangentAngle);
17389
+ const shaftEndX = fmt(endX - tx * ARROW_ARC_HEAD_LENGTH);
17390
+ const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
17391
+ const endControlX = fmt(shaftEndX - tx * controlInset);
17392
+ const endControlY = fmt(shaftEndY - ty * controlInset);
17393
+ const arcPath = `M 0 ${ARROW_ARC_BASE_Y} C ${fmt(controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
17394
+ const nx = -ty;
17395
+ const ny = tx;
17396
+ const baseX = endX - tx * ARROW_ARC_HEAD_LENGTH;
17397
+ const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
17398
+ 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`;
17399
+ return { arcPath, headPath };
17400
+ }
17401
+ function getArrowArcLayouts(arrows, min, max) {
17402
+ if (!arrows?.length) return [];
17403
+ const sorted = arrows.map((arrow, index) => {
17404
+ const fromPct = toPercent(arrow.from, min, max);
17405
+ const toPct = toPercent(arrow.to, min, max);
17406
+ return {
17407
+ arrow,
17408
+ index,
17409
+ leftPct: fromPct,
17410
+ rightPct: toPct,
17411
+ widthPct: toPct - fromPct,
17412
+ level: 0
17413
+ };
17414
+ }).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
17415
+ const levelRightEdges = [];
17416
+ for (const layout of sorted) {
17417
+ const reusableLevel = levelRightEdges.findIndex((rightPct) => layout.leftPct >= rightPct);
17418
+ const level = reusableLevel === -1 ? levelRightEdges.length : reusableLevel;
17419
+ layout.level = level;
17420
+ levelRightEdges[level] = layout.rightPct;
17421
+ }
17422
+ return sorted.sort((a, b) => a.index - b.index);
17423
+ }
17424
+ function getArrowArcsRowHeight(layouts) {
17425
+ if (layouts.length === 0) return 0;
17426
+ const levelCount = Math.max(...layouts.map((layout) => layout.level)) + 1;
17427
+ return levelCount * ARROW_ARC_LEVEL_HEIGHT;
17428
+ }
17429
+ function useElementWidth() {
17430
+ const ref = useRef(null);
17431
+ const [width, setWidth] = useState(0);
17432
+ useLayoutEffect(() => {
17433
+ const element = ref.current;
17434
+ if (!element) return;
17435
+ const updateWidth = () => {
17436
+ const nextWidth = element.getBoundingClientRect().width;
17437
+ setWidth((previousWidth) => previousWidth === nextWidth ? previousWidth : nextWidth);
17438
+ };
17439
+ updateWidth();
17440
+ if (typeof ResizeObserver === "undefined") return;
17441
+ const observer = new ResizeObserver(updateWidth);
17442
+ observer.observe(element);
17443
+ return () => observer.disconnect();
17444
+ }, []);
17445
+ return [ref, width];
17446
+ }
17447
+ const ArrowArc = ({
17448
+ layout: { arrow, leftPct, widthPct, level },
17449
+ widgets,
17450
+ values,
17451
+ droppedInputs,
17452
+ validationDetails,
17453
+ onChange,
17454
+ onInputFocus
17455
+ }) => {
17456
+ const [arcRef, renderedWidthPx] = useElementWidth();
17457
+ const hasLabel = arrow.content.length > 0;
17458
+ const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
17459
+ const { arcPath, headPath } = useMemo(
17460
+ () => getArrowArcPaths(widthPct, svgWidthPx),
17461
+ [widthPct, svgWidthPx]
17462
+ );
17463
+ return /* @__PURE__ */ jsxs(
17464
+ "div",
17465
+ {
17466
+ ref: arcRef,
17467
+ className: "number-line-arrow-arc",
17468
+ "data-level": level,
17469
+ style: {
17470
+ left: `${leftPct}%`,
17471
+ width: `${widthPct}%`,
17472
+ bottom: `${level * ARROW_ARC_LEVEL_HEIGHT}px`,
17473
+ // DSL color overrides the CSS default — only set when the author
17474
+ // explicitly wrote e.g. ARROW[from=0,to=5,color="#2B6DEA"]
17475
+ ...arrow.color !== void 0 && { "--nl-arrow-arc-color": arrow.color }
17476
+ },
17477
+ children: [
17478
+ /* @__PURE__ */ jsxs(
17479
+ "svg",
17480
+ {
17481
+ className: "number-line-arrow-arc-svg",
17482
+ viewBox: `0 0 ${svgWidthPx} ${ARROW_ARC_VIEWBOX_HEIGHT}`,
17483
+ preserveAspectRatio: "none",
17484
+ "aria-hidden": "true",
17485
+ focusable: "false",
17486
+ children: [
17487
+ /* @__PURE__ */ jsx(
17488
+ "path",
17489
+ {
17490
+ className: "number-line-arrow-arc-path",
17491
+ d: arcPath
17492
+ }
17493
+ ),
17494
+ /* @__PURE__ */ jsx(
17495
+ "path",
17496
+ {
17497
+ className: "number-line-arrow-arc-head",
17498
+ d: headPath
17499
+ }
17500
+ )
17501
+ ]
17502
+ }
17503
+ ),
17504
+ hasLabel && /* @__PURE__ */ jsx(
17505
+ "div",
17506
+ {
17507
+ className: "number-line-arrow-arc-label",
17508
+ style: { top: `${getArrowArcLabelTop(widthPct)}px` },
17509
+ children: arrow.content.map((subItem) => /* @__PURE__ */ jsx(
17510
+ LayoutRenderer,
17511
+ {
17512
+ item: subItem,
17513
+ widgets,
17514
+ values,
17515
+ droppedInputs,
17516
+ validationDetails,
17517
+ onChange,
17518
+ onInputFocus
17519
+ },
17520
+ subItem.id
17521
+ ))
17522
+ }
17523
+ )
17524
+ ]
17525
+ }
17526
+ );
17527
+ };
17528
+ const ArrowArcsRow = ({
17529
+ layouts,
17530
+ rowHeight,
17531
+ widgets,
17532
+ values,
17533
+ droppedInputs,
17534
+ validationDetails,
17535
+ onChange,
17536
+ onInputFocus
17537
+ }) => {
17538
+ if (layouts.length === 0) return null;
17539
+ return /* @__PURE__ */ jsx(
17540
+ "div",
17541
+ {
17542
+ className: "number-line-arrow-arcs-row",
17543
+ style: { height: `${rowHeight}px` },
17544
+ children: layouts.map((layout) => /* @__PURE__ */ jsx(
17545
+ ArrowArc,
17546
+ {
17547
+ layout,
17548
+ widgets,
17549
+ values,
17550
+ droppedInputs,
17551
+ validationDetails,
17552
+ onChange,
17553
+ onInputFocus
17554
+ },
17555
+ layout.arrow.id
17556
+ ))
17557
+ }
17558
+ );
17559
+ };
16942
17560
  const CONTENT_ROW_EXTRA_SPACE = 8;
16943
17561
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
16944
17562
  function useMeasuredPositionContentHeight(positionIds) {
@@ -17052,8 +17670,10 @@ const NumberLineRenderer = ({
17052
17670
  style: lineStyle = "arrow-right",
17053
17671
  labelVisibility,
17054
17672
  highlights,
17673
+ arrows,
17055
17674
  counter,
17056
- positions
17675
+ positions,
17676
+ cursors
17057
17677
  } = item;
17058
17678
  const [counterCount, setCounterCount] = useState(
17059
17679
  counter !== void 0 ? counter.default ?? counter.min : 0
@@ -17069,6 +17689,15 @@ const NumberLineRenderer = ({
17069
17689
  onChangeRef.current(counter.id, String(counterCount));
17070
17690
  }
17071
17691
  }, [counterCount, counter?.id]);
17692
+ const cursorKeys = useMemo(
17693
+ () => (cursors ?? []).map((c) => `${c.id}:${c.value}`).join(","),
17694
+ [cursors]
17695
+ );
17696
+ useEffect(() => {
17697
+ for (const cursor of cursors ?? []) {
17698
+ onChangeRef.current(cursor.id, String(cursor.value));
17699
+ }
17700
+ }, [cursorKeys]);
17072
17701
  const effectiveStep = useMemo(() => {
17073
17702
  if (counter !== void 0 && counterCount > 0) {
17074
17703
  return cleanFloat((max - min) / counterCount);
@@ -17102,6 +17731,15 @@ const NumberLineRenderer = ({
17102
17731
  [visiblePositions]
17103
17732
  );
17104
17733
  const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
17734
+ const arrowArcLayouts = useMemo(
17735
+ () => getArrowArcLayouts(arrows, min, max),
17736
+ [arrows, min, max]
17737
+ );
17738
+ const arrowArcsRowHeight = useMemo(
17739
+ () => getArrowArcsRowHeight(arrowArcLayouts),
17740
+ [arrowArcLayouts]
17741
+ );
17742
+ const hasOverlayRow = arrowArcsRowHeight > 0 || visiblePositions.length > 0;
17105
17743
  const nlValidationStatus = useMemo(() => {
17106
17744
  if (!validationDetails || counter === void 0) return null;
17107
17745
  if (counter.answer === void 0) return null;
@@ -17109,6 +17747,17 @@ const NumberLineRenderer = ({
17109
17747
  if (counterValidation === void 0) return null;
17110
17748
  return counterValidation.isCorrect ? "correct" : "incorrect";
17111
17749
  }, [validationDetails, counter]);
17750
+ const cursorValidationStatuses = useMemo(() => {
17751
+ if (!validationDetails || !cursors?.length) return void 0;
17752
+ const statuses = {};
17753
+ for (const cursor of cursors) {
17754
+ const detail = validationDetails[cursor.id];
17755
+ if (detail !== void 0) {
17756
+ statuses[cursor.id] = detail.isCorrect ? "correct" : "incorrect";
17757
+ }
17758
+ }
17759
+ return statuses;
17760
+ }, [validationDetails, cursors]);
17112
17761
  return /* @__PURE__ */ jsxs(
17113
17762
  "div",
17114
17763
  {
@@ -17120,28 +17769,50 @@ const NumberLineRenderer = ({
17120
17769
  role: "group",
17121
17770
  "aria-label": `Number line from ${min} to ${max}`,
17122
17771
  children: [
17123
- visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
17772
+ hasOverlayRow && /* @__PURE__ */ jsxs(
17124
17773
  "div",
17125
17774
  {
17126
- ref: getPositionContentRef(pos.id),
17127
- className: "number-line-position-content",
17128
- style: { left: `${toPercent(pos.value, min, max)}%` },
17129
- children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
17130
- LayoutRenderer,
17131
- {
17132
- item: subItem,
17133
- widgets,
17134
- values,
17135
- droppedInputs,
17136
- validationDetails,
17137
- onChange,
17138
- onInputFocus
17139
- },
17140
- subItem.id
17141
- ))
17142
- },
17143
- pos.id
17144
- )) }),
17775
+ className: "number-line-overlays-row",
17776
+ style: { "--nl-arrow-arcs-row-height": `${arrowArcsRowHeight}px` },
17777
+ children: [
17778
+ /* @__PURE__ */ jsx(
17779
+ ArrowArcsRow,
17780
+ {
17781
+ layouts: arrowArcLayouts,
17782
+ rowHeight: arrowArcsRowHeight,
17783
+ widgets,
17784
+ values,
17785
+ droppedInputs,
17786
+ validationDetails,
17787
+ onChange,
17788
+ onInputFocus
17789
+ }
17790
+ ),
17791
+ visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
17792
+ "div",
17793
+ {
17794
+ ref: getPositionContentRef(pos.id),
17795
+ className: "number-line-position-content",
17796
+ style: { left: `${toPercent(pos.value, min, max)}%` },
17797
+ children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
17798
+ LayoutRenderer,
17799
+ {
17800
+ item: subItem,
17801
+ widgets,
17802
+ values,
17803
+ droppedInputs,
17804
+ validationDetails,
17805
+ onChange,
17806
+ onInputFocus
17807
+ },
17808
+ subItem.id
17809
+ ))
17810
+ },
17811
+ pos.id
17812
+ )) })
17813
+ ]
17814
+ }
17815
+ ),
17145
17816
  isAxisDragMode ? /* @__PURE__ */ jsx(
17146
17817
  FreeDragAxis,
17147
17818
  {
@@ -17156,6 +17827,8 @@ const NumberLineRenderer = ({
17156
17827
  values,
17157
17828
  onChange,
17158
17829
  highlights,
17830
+ cursors,
17831
+ cursorValidationStatuses,
17159
17832
  snapKey: counterCount
17160
17833
  }
17161
17834
  ) : /* @__PURE__ */ jsx(
@@ -17168,7 +17841,11 @@ const NumberLineRenderer = ({
17168
17841
  showArrowLeft,
17169
17842
  showArrowRight,
17170
17843
  highlights,
17171
- positions
17844
+ positions,
17845
+ cursors,
17846
+ onChange,
17847
+ values,
17848
+ cursorValidationStatuses
17172
17849
  }
17173
17850
  ),
17174
17851
  /* @__PURE__ */ jsx(
@@ -18148,7 +18825,9 @@ export {
18148
18825
  MatcherRowSchema,
18149
18826
  MatcherWidgetJSONSchema,
18150
18827
  MixedValidationSchema,
18828
+ NumberLineArrowItemSchema,
18151
18829
  NumberLineCounterItemSchema,
18830
+ NumberLineCursorItemSchema,
18152
18831
  NumberLineHighlightItemSchema,
18153
18832
  NumberLineItemSchema,
18154
18833
  NumberLinePositionItemSchema,