@mathscan/math-exercise-engine 1.1.7 → 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.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
@@ -14237,6 +14432,29 @@ function useRotate(options) {
14237
14432
  const [rotation, setRotation] = useState(initialRotation);
14238
14433
  return { rotation, setRotation };
14239
14434
  }
14435
+ const REPEAT_DELAY_MS = 5e3;
14436
+ function useJiggle(isDragging) {
14437
+ const [jiggling, setJiggling] = useState(true);
14438
+ const [discovered, setDiscovered] = useState(false);
14439
+ const timerRef = useRef(null);
14440
+ useEffect(() => {
14441
+ if (isDragging && !discovered) {
14442
+ setDiscovered(true);
14443
+ if (timerRef.current) clearTimeout(timerRef.current);
14444
+ setJiggling(false);
14445
+ }
14446
+ }, [isDragging, discovered]);
14447
+ useEffect(() => () => {
14448
+ if (timerRef.current) clearTimeout(timerRef.current);
14449
+ }, []);
14450
+ const onAnimationEnd = useCallback(() => {
14451
+ setJiggling(false);
14452
+ if (!discovered) {
14453
+ timerRef.current = setTimeout(() => setJiggling(true), REPEAT_DELAY_MS);
14454
+ }
14455
+ }, [discovered]);
14456
+ return { jiggling, onAnimationEnd };
14457
+ }
14240
14458
  const VW$2 = 320;
14241
14459
  const VH$2 = 44;
14242
14460
  const RULER_ASPECT_RATIO = VH$2 / VW$2;
@@ -14626,6 +14844,7 @@ const DraggableTool = ({
14626
14844
  const [isFlipped, setIsFlipped] = useState(false);
14627
14845
  const drag = useDrag({ initialPosition });
14628
14846
  const rotate = useRotate({ initialRotation });
14847
+ const { jiggling, onAnimationEnd } = useJiggle(drag.isDragging);
14629
14848
  const dragRef = useRef(drag);
14630
14849
  const rotateRef = useRef(rotate);
14631
14850
  dragRef.current = drag;
@@ -14758,7 +14977,15 @@ const DraggableTool = ({
14758
14977
  onMouseMove: handleMouseMove,
14759
14978
  onMouseLeave: handleMouseLeave,
14760
14979
  children: [
14761
- /* @__PURE__ */ jsx("div", { style: { pointerEvents: "none", display: "block" }, children: /* @__PURE__ */ jsx(ToolContent, { type, width, imageSrc, flipped: isFlipped }) }),
14980
+ /* @__PURE__ */ jsx(
14981
+ "div",
14982
+ {
14983
+ className: jiggling ? "tool-content-jiggle" : void 0,
14984
+ onAnimationEnd,
14985
+ style: { pointerEvents: "none", display: "block" },
14986
+ children: /* @__PURE__ */ jsx(ToolContent, { type, width, imageSrc, flipped: isFlipped })
14987
+ }
14988
+ ),
14762
14989
  type === "set-square" && /* @__PURE__ */ jsx(
14763
14990
  "button",
14764
14991
  {
@@ -16477,6 +16704,182 @@ const HighlightBands = ({ highlights, min, max }) => {
16477
16704
  );
16478
16705
  }) });
16479
16706
  };
16707
+ const DraggableCursor = ({
16708
+ cursor,
16709
+ min,
16710
+ max,
16711
+ snapTicks,
16712
+ audibleTicks,
16713
+ axisRowRef,
16714
+ onChange,
16715
+ currentValue,
16716
+ validationStatus
16717
+ }) => {
16718
+ const [isDragging, setIsDragging] = useState(false);
16719
+ const isDraggingRef = useRef(false);
16720
+ const currentValueRef = useRef(currentValue);
16721
+ currentValueRef.current = currentValue;
16722
+ const dragStartValueRef = useRef(currentValue);
16723
+ const lastSoundValueRef = useRef(currentValue);
16724
+ const { jiggling, onAnimationEnd } = useJiggle(isDragging);
16725
+ const playSound = useSound();
16726
+ const colorVar = !validationStatus && cursor.color ? { "--nl-cursor-color": cursor.color } : void 0;
16727
+ const initialPct = toPercent(cursor.value, min, max);
16728
+ const ballPct = toPercent(currentValue, min, max);
16729
+ const fillLeft = Math.min(initialPct, ballPct);
16730
+ const fillWidth = Math.abs(ballPct - initialPct);
16731
+ const snap = useCallback((clientX) => {
16732
+ const el = axisRowRef.current;
16733
+ if (!el) return currentValueRef.current;
16734
+ const rect = el.getBoundingClientRect();
16735
+ const raw = pixelToValue(clientX, rect.left, rect.width, min, max);
16736
+ return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
16737
+ }, [axisRowRef, min, max, snapTicks]);
16738
+ const handlePointerDown = useCallback((e) => {
16739
+ e.currentTarget.setPointerCapture(e.pointerId);
16740
+ e.stopPropagation();
16741
+ isDraggingRef.current = true;
16742
+ dragStartValueRef.current = currentValue;
16743
+ lastSoundValueRef.current = currentValue;
16744
+ setIsDragging(true);
16745
+ }, [currentValue]);
16746
+ const updateCursorValue = useCallback((clientX) => {
16747
+ const next = snap(clientX);
16748
+ if (!almostEqual(next, lastSoundValueRef.current)) {
16749
+ const soundTicks = audibleTicks ?? snapTicks;
16750
+ if (soundTicks.some((tick) => almostEqual(tick, next))) playSound("cursor-step");
16751
+ lastSoundValueRef.current = next;
16752
+ }
16753
+ onChange(cursor.id, String(next));
16754
+ return next;
16755
+ }, [snap, audibleTicks, snapTicks, playSound, onChange, cursor.id]);
16756
+ const handlePointerMove = useCallback((e) => {
16757
+ if (!isDraggingRef.current) return;
16758
+ e.stopPropagation();
16759
+ updateCursorValue(e.clientX);
16760
+ }, [updateCursorValue]);
16761
+ const handlePointerUp = useCallback((e) => {
16762
+ if (!isDraggingRef.current) return;
16763
+ isDraggingRef.current = false;
16764
+ setIsDragging(false);
16765
+ e.currentTarget.releasePointerCapture(e.pointerId);
16766
+ e.stopPropagation();
16767
+ const next = updateCursorValue(e.clientX);
16768
+ if (!almostEqual(next, dragStartValueRef.current)) {
16769
+ playSound("cursor-release");
16770
+ }
16771
+ }, [updateCursorValue, playSound]);
16772
+ const handleKeyDown = useCallback((e) => {
16773
+ if (!snapTicks.length) return;
16774
+ const sorted = [...snapTicks].sort((a, b) => a - b);
16775
+ const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
16776
+ const first = sorted[0] ?? currentValue;
16777
+ const last = sorted[sorted.length - 1] ?? currentValue;
16778
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
16779
+ e.preventDefault();
16780
+ const next = sorted[idx + 1] ?? last;
16781
+ const soundTicks = audibleTicks ?? snapTicks;
16782
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16783
+ playSound("cursor-step");
16784
+ }
16785
+ onChange(cursor.id, String(next));
16786
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
16787
+ e.preventDefault();
16788
+ const next = sorted[idx - 1] ?? first;
16789
+ const soundTicks = audibleTicks ?? snapTicks;
16790
+ if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
16791
+ playSound("cursor-step");
16792
+ }
16793
+ onChange(cursor.id, String(next));
16794
+ }
16795
+ }, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
16796
+ const ballClass = [
16797
+ "number-line-cursor-ball",
16798
+ isDragging ? "number-line-cursor-ball--dragging" : "",
16799
+ jiggling && !isDragging ? "number-line-cursor-ball--jiggle" : "",
16800
+ validationStatus ? `number-line-cursor-ball--${validationStatus}` : ""
16801
+ ].filter(Boolean).join(" ");
16802
+ const fillClass = [
16803
+ "number-line-cursor-fill",
16804
+ validationStatus ? `number-line-cursor-fill--${validationStatus}` : ""
16805
+ ].filter(Boolean).join(" ");
16806
+ const labelClass = [
16807
+ "number-line-cursor-label",
16808
+ validationStatus ? `number-line-cursor-label--${validationStatus}` : ""
16809
+ ].filter(Boolean).join(" ");
16810
+ const sliderAriaLabel = cursor.label ? `${cursor.label}: cursor position ${currentValue}` : `cursor position ${currentValue}`;
16811
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
16812
+ cursor.fillRange && fillWidth > 0 && /* @__PURE__ */ jsx(
16813
+ "div",
16814
+ {
16815
+ className: fillClass,
16816
+ "aria-hidden": "true",
16817
+ style: { left: `${fillLeft}%`, width: `${fillWidth}%`, ...colorVar }
16818
+ }
16819
+ ),
16820
+ cursor.label && /* @__PURE__ */ jsx(
16821
+ "span",
16822
+ {
16823
+ className: labelClass,
16824
+ "aria-hidden": "true",
16825
+ style: { left: `${ballPct}%`, ...colorVar },
16826
+ children: cursor.label
16827
+ }
16828
+ ),
16829
+ /* @__PURE__ */ jsx(
16830
+ "div",
16831
+ {
16832
+ className: ballClass,
16833
+ style: { left: `${ballPct}%`, ...colorVar },
16834
+ onPointerDown: handlePointerDown,
16835
+ onPointerMove: handlePointerMove,
16836
+ onPointerUp: handlePointerUp,
16837
+ onKeyDown: handleKeyDown,
16838
+ onAnimationEnd,
16839
+ role: "slider",
16840
+ "aria-valuemin": min,
16841
+ "aria-valuemax": max,
16842
+ "aria-valuenow": currentValue,
16843
+ "aria-label": sliderAriaLabel,
16844
+ tabIndex: 0
16845
+ }
16846
+ )
16847
+ ] });
16848
+ };
16849
+ const CursorBalls = ({
16850
+ cursors,
16851
+ min,
16852
+ max,
16853
+ snapTicks,
16854
+ audibleTicks,
16855
+ axisRowRef,
16856
+ onChange,
16857
+ values,
16858
+ cursorValidationStatuses
16859
+ }) => {
16860
+ if (!cursors?.length) return null;
16861
+ return /* @__PURE__ */ jsx(Fragment, { children: cursors.map((cursor) => {
16862
+ const raw = values[cursor.id];
16863
+ const parsed = Number(raw);
16864
+ const currentValue = raw !== void 0 && raw !== "" && isFinite(parsed) ? parsed : cursor.value;
16865
+ const validationStatus = cursorValidationStatuses?.[cursor.id] ?? null;
16866
+ return /* @__PURE__ */ jsx(
16867
+ DraggableCursor,
16868
+ {
16869
+ cursor,
16870
+ min,
16871
+ max,
16872
+ snapTicks,
16873
+ audibleTicks,
16874
+ axisRowRef,
16875
+ onChange,
16876
+ currentValue,
16877
+ validationStatus
16878
+ },
16879
+ cursor.id
16880
+ );
16881
+ }) });
16882
+ };
16480
16883
  const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
16481
16884
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
16482
16885
  id: widgetId,
@@ -16540,6 +16943,8 @@ const FreeDragAxis = ({
16540
16943
  values,
16541
16944
  onChange,
16542
16945
  highlights,
16946
+ cursors,
16947
+ cursorValidationStatuses,
16543
16948
  snapKey
16544
16949
  }) => {
16545
16950
  const axisRef = useRef(null);
@@ -16712,6 +17117,20 @@ const FreeDragAxis = ({
16712
17117
  showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16713
17118
  showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16714
17119
  /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
17120
+ /* @__PURE__ */ jsx(
17121
+ CursorBalls,
17122
+ {
17123
+ cursors,
17124
+ min,
17125
+ max,
17126
+ snapTicks,
17127
+ audibleTicks: majorTicks,
17128
+ axisRowRef: axisRef,
17129
+ onChange,
17130
+ values,
17131
+ cursorValidationStatuses
17132
+ }
17133
+ ),
16715
17134
  minorValues.map((v) => /* @__PURE__ */ jsx(
16716
17135
  "div",
16717
17136
  {
@@ -16780,40 +17199,65 @@ const AxisTicks = ({
16780
17199
  showArrowLeft,
16781
17200
  showArrowRight,
16782
17201
  highlights,
16783
- positions
16784
- }) => /* @__PURE__ */ jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
16785
- /* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
16786
- showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16787
- showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16788
- /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
16789
- minorValues.map((v) => /* @__PURE__ */ jsx(
16790
- "div",
16791
- {
16792
- className: "number-line-minor-tick",
16793
- "aria-hidden": "true",
16794
- style: { left: `${toPercent(v, min, max)}%` }
16795
- },
16796
- `minor-${v}`
16797
- )),
16798
- majorTicks.map((tick) => /* @__PURE__ */ jsx(
16799
- "div",
16800
- {
16801
- className: "number-line-major-tick",
16802
- "aria-hidden": "true",
16803
- style: { left: `${toPercent(tick, min, max)}%` }
16804
- },
16805
- `major-${tick}`
16806
- )),
16807
- positions.map((pos) => /* @__PURE__ */ jsx(
16808
- "div",
16809
- {
16810
- className: "number-line-position-dot",
16811
- "aria-hidden": "true",
16812
- style: { left: `${toPercent(pos.value, min, max)}%` }
16813
- },
16814
- `dot-${pos.id}`
16815
- ))
16816
- ] });
17202
+ positions,
17203
+ cursors,
17204
+ onChange,
17205
+ values,
17206
+ cursorValidationStatuses
17207
+ }) => {
17208
+ const axisRowRef = useRef(null);
17209
+ const snapTicks = useMemo(
17210
+ () => [...majorTicks, ...minorValues],
17211
+ [majorTicks, minorValues]
17212
+ );
17213
+ return /* @__PURE__ */ jsxs("div", { ref: axisRowRef, className: "number-line-axis-row", role: "presentation", children: [
17214
+ /* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
17215
+ showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
17216
+ showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
17217
+ /* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
17218
+ /* @__PURE__ */ jsx(
17219
+ CursorBalls,
17220
+ {
17221
+ cursors,
17222
+ min,
17223
+ max,
17224
+ snapTicks,
17225
+ audibleTicks: majorTicks,
17226
+ axisRowRef,
17227
+ onChange,
17228
+ values,
17229
+ cursorValidationStatuses
17230
+ }
17231
+ ),
17232
+ minorValues.map((v) => /* @__PURE__ */ jsx(
17233
+ "div",
17234
+ {
17235
+ className: "number-line-minor-tick",
17236
+ "aria-hidden": "true",
17237
+ style: { left: `${toPercent(v, min, max)}%` }
17238
+ },
17239
+ `minor-${v}`
17240
+ )),
17241
+ majorTicks.map((tick) => /* @__PURE__ */ jsx(
17242
+ "div",
17243
+ {
17244
+ className: "number-line-major-tick",
17245
+ "aria-hidden": "true",
17246
+ style: { left: `${toPercent(tick, min, max)}%` }
17247
+ },
17248
+ `major-${tick}`
17249
+ )),
17250
+ positions.map((pos) => /* @__PURE__ */ jsx(
17251
+ "div",
17252
+ {
17253
+ className: "number-line-position-dot",
17254
+ "aria-hidden": "true",
17255
+ style: { left: `${toPercent(pos.value, min, max)}%` }
17256
+ },
17257
+ `dot-${pos.id}`
17258
+ ))
17259
+ ] });
17260
+ };
16817
17261
  const isLabelVisible = (tick, min, max, labelVisibility) => {
16818
17262
  switch (labelVisibility) {
16819
17263
  case "none":
@@ -16907,6 +17351,207 @@ const CounterControls = ({
16907
17351
  counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
16908
17352
  ] });
16909
17353
  };
17354
+ const ARROW_ARC_LEVEL_HEIGHT = 80;
17355
+ const ARROW_ARC_VIEWBOX_HEIGHT = 64;
17356
+ const ARROW_ARC_VIEWBOX_WIDTH = 100;
17357
+ const ARROW_ARC_BASE_Y = 56;
17358
+ const ARROW_ARC_MIN_LIFT = 28;
17359
+ const ARROW_ARC_MAX_LIFT = 58;
17360
+ const ARROW_ARC_LIFT_RATIO = 1.15;
17361
+ const ARROW_ARC_LABEL_OFFSET = 24;
17362
+ const ARROW_ARC_HEAD_LENGTH = 8;
17363
+ const ARROW_ARC_HEAD_HALF_WIDTH = 3.2;
17364
+ function getArrowArcLift(widthPct) {
17365
+ return Math.min(ARROW_ARC_MAX_LIFT, Math.max(ARROW_ARC_MIN_LIFT, widthPct * ARROW_ARC_LIFT_RATIO));
17366
+ }
17367
+ function fmt(n) {
17368
+ return Number(n.toFixed(2));
17369
+ }
17370
+ function getArrowArcSvgWidth(renderedWidthPx) {
17371
+ return renderedWidthPx > 0 ? renderedWidthPx : ARROW_ARC_VIEWBOX_WIDTH;
17372
+ }
17373
+ function getArrowArcLabelTop(widthPct) {
17374
+ const svgTop = ARROW_ARC_LEVEL_HEIGHT - ARROW_ARC_VIEWBOX_HEIGHT;
17375
+ const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
17376
+ return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
17377
+ }
17378
+ function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
17379
+ const endX = getArrowArcSvgWidth(svgWidthPx);
17380
+ const controlInset = Math.min(svgWidthPx * 0.18, 48);
17381
+ const lift = getArrowArcLift(widthPct);
17382
+ const controlY = fmt(ARROW_ARC_BASE_Y - lift);
17383
+ const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, endX - (endX - controlInset));
17384
+ const tx = Math.cos(tangentAngle);
17385
+ const ty = Math.sin(tangentAngle);
17386
+ const shaftEndX = fmt(endX - tx * ARROW_ARC_HEAD_LENGTH);
17387
+ const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
17388
+ const endControlX = fmt(shaftEndX - tx * controlInset);
17389
+ const endControlY = fmt(shaftEndY - ty * controlInset);
17390
+ const arcPath = `M 0 ${ARROW_ARC_BASE_Y} C ${fmt(controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
17391
+ const nx = -ty;
17392
+ const ny = tx;
17393
+ const baseX = endX - tx * ARROW_ARC_HEAD_LENGTH;
17394
+ const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
17395
+ 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`;
17396
+ return { arcPath, headPath };
17397
+ }
17398
+ function getArrowArcLayouts(arrows, min, max) {
17399
+ if (!arrows?.length) return [];
17400
+ const sorted = arrows.map((arrow, index) => {
17401
+ const fromPct = toPercent(arrow.from, min, max);
17402
+ const toPct = toPercent(arrow.to, min, max);
17403
+ return {
17404
+ arrow,
17405
+ index,
17406
+ leftPct: fromPct,
17407
+ rightPct: toPct,
17408
+ widthPct: toPct - fromPct,
17409
+ level: 0
17410
+ };
17411
+ }).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
17412
+ const levelRightEdges = [];
17413
+ for (const layout of sorted) {
17414
+ const reusableLevel = levelRightEdges.findIndex((rightPct) => layout.leftPct >= rightPct);
17415
+ const level = reusableLevel === -1 ? levelRightEdges.length : reusableLevel;
17416
+ layout.level = level;
17417
+ levelRightEdges[level] = layout.rightPct;
17418
+ }
17419
+ return sorted.sort((a, b) => a.index - b.index);
17420
+ }
17421
+ function useElementWidth() {
17422
+ const ref = useRef(null);
17423
+ const [width, setWidth] = useState(0);
17424
+ useLayoutEffect(() => {
17425
+ const element = ref.current;
17426
+ if (!element) return;
17427
+ const updateWidth = () => {
17428
+ const nextWidth = element.getBoundingClientRect().width;
17429
+ setWidth((previousWidth) => previousWidth === nextWidth ? previousWidth : nextWidth);
17430
+ };
17431
+ updateWidth();
17432
+ if (typeof ResizeObserver === "undefined") return;
17433
+ const observer = new ResizeObserver(updateWidth);
17434
+ observer.observe(element);
17435
+ return () => observer.disconnect();
17436
+ }, []);
17437
+ return [ref, width];
17438
+ }
17439
+ const ArrowArc = ({
17440
+ layout: { arrow, leftPct, widthPct, level },
17441
+ widgets,
17442
+ values,
17443
+ droppedInputs,
17444
+ validationDetails,
17445
+ onChange,
17446
+ onInputFocus
17447
+ }) => {
17448
+ const [arcRef, renderedWidthPx] = useElementWidth();
17449
+ const hasLabel = arrow.content.length > 0;
17450
+ const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
17451
+ const { arcPath, headPath } = useMemo(
17452
+ () => getArrowArcPaths(widthPct, svgWidthPx),
17453
+ [widthPct, svgWidthPx]
17454
+ );
17455
+ return /* @__PURE__ */ jsxs(
17456
+ "div",
17457
+ {
17458
+ ref: arcRef,
17459
+ className: "number-line-arrow-arc",
17460
+ "data-level": level,
17461
+ style: {
17462
+ left: `${leftPct}%`,
17463
+ width: `${widthPct}%`,
17464
+ bottom: `${level * ARROW_ARC_LEVEL_HEIGHT}px`,
17465
+ // DSL color overrides the CSS default — only set when the author
17466
+ // explicitly wrote e.g. ARROW[from=0,to=5,color="#2B6DEA"]
17467
+ ...arrow.color !== void 0 && { "--nl-arrow-arc-color": arrow.color }
17468
+ },
17469
+ children: [
17470
+ /* @__PURE__ */ jsxs(
17471
+ "svg",
17472
+ {
17473
+ className: "number-line-arrow-arc-svg",
17474
+ viewBox: `0 0 ${svgWidthPx} ${ARROW_ARC_VIEWBOX_HEIGHT}`,
17475
+ preserveAspectRatio: "none",
17476
+ "aria-hidden": "true",
17477
+ focusable: "false",
17478
+ children: [
17479
+ /* @__PURE__ */ jsx(
17480
+ "path",
17481
+ {
17482
+ className: "number-line-arrow-arc-path",
17483
+ d: arcPath
17484
+ }
17485
+ ),
17486
+ /* @__PURE__ */ jsx(
17487
+ "path",
17488
+ {
17489
+ className: "number-line-arrow-arc-head",
17490
+ d: headPath
17491
+ }
17492
+ )
17493
+ ]
17494
+ }
17495
+ ),
17496
+ hasLabel && /* @__PURE__ */ jsx(
17497
+ "div",
17498
+ {
17499
+ className: "number-line-arrow-arc-label",
17500
+ style: { top: `${getArrowArcLabelTop(widthPct)}px` },
17501
+ children: arrow.content.map((subItem) => /* @__PURE__ */ jsx(
17502
+ LayoutRenderer,
17503
+ {
17504
+ item: subItem,
17505
+ widgets,
17506
+ values,
17507
+ droppedInputs,
17508
+ validationDetails,
17509
+ onChange,
17510
+ onInputFocus
17511
+ },
17512
+ subItem.id
17513
+ ))
17514
+ }
17515
+ )
17516
+ ]
17517
+ }
17518
+ );
17519
+ };
17520
+ const ArrowArcsRow = ({
17521
+ arrows,
17522
+ min,
17523
+ max,
17524
+ widgets,
17525
+ values,
17526
+ droppedInputs,
17527
+ validationDetails,
17528
+ onChange,
17529
+ onInputFocus
17530
+ }) => {
17531
+ const layouts = useMemo(() => getArrowArcLayouts(arrows, min, max), [arrows, min, max]);
17532
+ if (layouts.length === 0) return null;
17533
+ const levelCount = Math.max(...layouts.map((layout) => layout.level)) + 1;
17534
+ return /* @__PURE__ */ jsx(
17535
+ "div",
17536
+ {
17537
+ className: "number-line-arrow-arcs-row",
17538
+ style: { height: `${levelCount * ARROW_ARC_LEVEL_HEIGHT}px` },
17539
+ children: layouts.map((layout) => /* @__PURE__ */ jsx(
17540
+ ArrowArc,
17541
+ {
17542
+ layout,
17543
+ widgets,
17544
+ values,
17545
+ droppedInputs,
17546
+ validationDetails,
17547
+ onChange,
17548
+ onInputFocus
17549
+ },
17550
+ layout.arrow.id
17551
+ ))
17552
+ }
17553
+ );
17554
+ };
16910
17555
  const CONTENT_ROW_EXTRA_SPACE = 8;
16911
17556
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
16912
17557
  function useMeasuredPositionContentHeight(positionIds) {
@@ -17020,8 +17665,10 @@ const NumberLineRenderer = ({
17020
17665
  style: lineStyle = "arrow-right",
17021
17666
  labelVisibility,
17022
17667
  highlights,
17668
+ arrows,
17023
17669
  counter,
17024
- positions
17670
+ positions,
17671
+ cursors
17025
17672
  } = item;
17026
17673
  const [counterCount, setCounterCount] = useState(
17027
17674
  counter !== void 0 ? counter.default ?? counter.min : 0
@@ -17037,6 +17684,15 @@ const NumberLineRenderer = ({
17037
17684
  onChangeRef.current(counter.id, String(counterCount));
17038
17685
  }
17039
17686
  }, [counterCount, counter?.id]);
17687
+ const cursorKeys = useMemo(
17688
+ () => (cursors ?? []).map((c) => `${c.id}:${c.value}`).join(","),
17689
+ [cursors]
17690
+ );
17691
+ useEffect(() => {
17692
+ for (const cursor of cursors ?? []) {
17693
+ onChangeRef.current(cursor.id, String(cursor.value));
17694
+ }
17695
+ }, [cursorKeys]);
17040
17696
  const effectiveStep = useMemo(() => {
17041
17697
  if (counter !== void 0 && counterCount > 0) {
17042
17698
  return cleanFloat((max - min) / counterCount);
@@ -17077,6 +17733,17 @@ const NumberLineRenderer = ({
17077
17733
  if (counterValidation === void 0) return null;
17078
17734
  return counterValidation.isCorrect ? "correct" : "incorrect";
17079
17735
  }, [validationDetails, counter]);
17736
+ const cursorValidationStatuses = useMemo(() => {
17737
+ if (!validationDetails || !cursors?.length) return void 0;
17738
+ const statuses = {};
17739
+ for (const cursor of cursors) {
17740
+ const detail = validationDetails[cursor.id];
17741
+ if (detail !== void 0) {
17742
+ statuses[cursor.id] = detail.isCorrect ? "correct" : "incorrect";
17743
+ }
17744
+ }
17745
+ return statuses;
17746
+ }, [validationDetails, cursors]);
17080
17747
  return /* @__PURE__ */ jsxs(
17081
17748
  "div",
17082
17749
  {
@@ -17110,6 +17777,20 @@ const NumberLineRenderer = ({
17110
17777
  },
17111
17778
  pos.id
17112
17779
  )) }),
17780
+ /* @__PURE__ */ jsx(
17781
+ ArrowArcsRow,
17782
+ {
17783
+ arrows,
17784
+ min,
17785
+ max,
17786
+ widgets,
17787
+ values,
17788
+ droppedInputs,
17789
+ validationDetails,
17790
+ onChange,
17791
+ onInputFocus
17792
+ }
17793
+ ),
17113
17794
  isAxisDragMode ? /* @__PURE__ */ jsx(
17114
17795
  FreeDragAxis,
17115
17796
  {
@@ -17124,6 +17805,8 @@ const NumberLineRenderer = ({
17124
17805
  values,
17125
17806
  onChange,
17126
17807
  highlights,
17808
+ cursors,
17809
+ cursorValidationStatuses,
17127
17810
  snapKey: counterCount
17128
17811
  }
17129
17812
  ) : /* @__PURE__ */ jsx(
@@ -17136,7 +17819,11 @@ const NumberLineRenderer = ({
17136
17819
  showArrowLeft,
17137
17820
  showArrowRight,
17138
17821
  highlights,
17139
- positions
17822
+ positions,
17823
+ cursors,
17824
+ onChange,
17825
+ values,
17826
+ cursorValidationStatuses
17140
17827
  }
17141
17828
  ),
17142
17829
  /* @__PURE__ */ jsx(
@@ -18116,7 +18803,9 @@ export {
18116
18803
  MatcherRowSchema,
18117
18804
  MatcherWidgetJSONSchema,
18118
18805
  MixedValidationSchema,
18806
+ NumberLineArrowItemSchema,
18119
18807
  NumberLineCounterItemSchema,
18808
+ NumberLineCursorItemSchema,
18120
18809
  NumberLineHighlightItemSchema,
18121
18810
  NumberLineItemSchema,
18122
18811
  NumberLinePositionItemSchema,