@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.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
@@ -14239,6 +14434,29 @@ function useRotate(options) {
14239
14434
  const [rotation, setRotation] = React.useState(initialRotation);
14240
14435
  return { rotation, setRotation };
14241
14436
  }
14437
+ const REPEAT_DELAY_MS = 5e3;
14438
+ function useJiggle(isDragging) {
14439
+ const [jiggling, setJiggling] = React.useState(true);
14440
+ const [discovered, setDiscovered] = React.useState(false);
14441
+ const timerRef = React.useRef(null);
14442
+ React.useEffect(() => {
14443
+ if (isDragging && !discovered) {
14444
+ setDiscovered(true);
14445
+ if (timerRef.current) clearTimeout(timerRef.current);
14446
+ setJiggling(false);
14447
+ }
14448
+ }, [isDragging, discovered]);
14449
+ React.useEffect(() => () => {
14450
+ if (timerRef.current) clearTimeout(timerRef.current);
14451
+ }, []);
14452
+ const onAnimationEnd = React.useCallback(() => {
14453
+ setJiggling(false);
14454
+ if (!discovered) {
14455
+ timerRef.current = setTimeout(() => setJiggling(true), REPEAT_DELAY_MS);
14456
+ }
14457
+ }, [discovered]);
14458
+ return { jiggling, onAnimationEnd };
14459
+ }
14242
14460
  const VW$2 = 320;
14243
14461
  const VH$2 = 44;
14244
14462
  const RULER_ASPECT_RATIO = VH$2 / VW$2;
@@ -14628,6 +14846,7 @@ const DraggableTool = ({
14628
14846
  const [isFlipped, setIsFlipped] = React.useState(false);
14629
14847
  const drag = useDrag({ initialPosition });
14630
14848
  const rotate = useRotate({ initialRotation });
14849
+ const { jiggling, onAnimationEnd } = useJiggle(drag.isDragging);
14631
14850
  const dragRef = React.useRef(drag);
14632
14851
  const rotateRef = React.useRef(rotate);
14633
14852
  dragRef.current = drag;
@@ -14760,7 +14979,15 @@ const DraggableTool = ({
14760
14979
  onMouseMove: handleMouseMove,
14761
14980
  onMouseLeave: handleMouseLeave,
14762
14981
  children: [
14763
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { pointerEvents: "none", display: "block" }, children: /* @__PURE__ */ jsxRuntime.jsx(ToolContent, { type, width, imageSrc, flipped: isFlipped }) }),
14982
+ /* @__PURE__ */ jsxRuntime.jsx(
14983
+ "div",
14984
+ {
14985
+ className: jiggling ? "tool-content-jiggle" : void 0,
14986
+ onAnimationEnd,
14987
+ style: { pointerEvents: "none", display: "block" },
14988
+ children: /* @__PURE__ */ jsxRuntime.jsx(ToolContent, { type, width, imageSrc, flipped: isFlipped })
14989
+ }
14990
+ ),
14764
14991
  type === "set-square" && /* @__PURE__ */ jsxRuntime.jsx(
14765
14992
  "button",
14766
14993
  {
@@ -16479,6 +16706,182 @@ const HighlightBands = ({ highlights, min, max }) => {
16479
16706
  );
16480
16707
  }) });
16481
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
+ };
16482
16885
  const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
16483
16886
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
16484
16887
  id: widgetId,
@@ -16542,6 +16945,8 @@ const FreeDragAxis = ({
16542
16945
  values,
16543
16946
  onChange,
16544
16947
  highlights,
16948
+ cursors,
16949
+ cursorValidationStatuses,
16545
16950
  snapKey
16546
16951
  }) => {
16547
16952
  const axisRef = React.useRef(null);
@@ -16714,6 +17119,20 @@ const FreeDragAxis = ({
16714
17119
  showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16715
17120
  showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16716
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
+ ),
16717
17136
  minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
16718
17137
  "div",
16719
17138
  {
@@ -16782,40 +17201,65 @@ const AxisTicks = ({
16782
17201
  showArrowLeft,
16783
17202
  showArrowRight,
16784
17203
  highlights,
16785
- positions
16786
- }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
16787
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
16788
- showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
16789
- showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
16790
- /* @__PURE__ */ jsxRuntime.jsx(HighlightBands, { highlights, min, max }),
16791
- minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
16792
- "div",
16793
- {
16794
- className: "number-line-minor-tick",
16795
- "aria-hidden": "true",
16796
- style: { left: `${toPercent(v, min, max)}%` }
16797
- },
16798
- `minor-${v}`
16799
- )),
16800
- majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
16801
- "div",
16802
- {
16803
- className: "number-line-major-tick",
16804
- "aria-hidden": "true",
16805
- style: { left: `${toPercent(tick, min, max)}%` }
16806
- },
16807
- `major-${tick}`
16808
- )),
16809
- positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
16810
- "div",
16811
- {
16812
- className: "number-line-position-dot",
16813
- "aria-hidden": "true",
16814
- style: { left: `${toPercent(pos.value, min, max)}%` }
16815
- },
16816
- `dot-${pos.id}`
16817
- ))
16818
- ] });
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
+ };
16819
17263
  const isLabelVisible = (tick, min, max, labelVisibility) => {
16820
17264
  switch (labelVisibility) {
16821
17265
  case "none":
@@ -16909,6 +17353,207 @@ const CounterControls = ({
16909
17353
  counter.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-label", children: counter.name })
16910
17354
  ] });
16911
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
+ };
16912
17557
  const CONTENT_ROW_EXTRA_SPACE = 8;
16913
17558
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
16914
17559
  function useMeasuredPositionContentHeight(positionIds) {
@@ -17022,8 +17667,10 @@ const NumberLineRenderer = ({
17022
17667
  style: lineStyle = "arrow-right",
17023
17668
  labelVisibility,
17024
17669
  highlights,
17670
+ arrows,
17025
17671
  counter,
17026
- positions
17672
+ positions,
17673
+ cursors
17027
17674
  } = item;
17028
17675
  const [counterCount, setCounterCount] = React.useState(
17029
17676
  counter !== void 0 ? counter.default ?? counter.min : 0
@@ -17039,6 +17686,15 @@ const NumberLineRenderer = ({
17039
17686
  onChangeRef.current(counter.id, String(counterCount));
17040
17687
  }
17041
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]);
17042
17698
  const effectiveStep = React.useMemo(() => {
17043
17699
  if (counter !== void 0 && counterCount > 0) {
17044
17700
  return cleanFloat((max - min) / counterCount);
@@ -17079,6 +17735,17 @@ const NumberLineRenderer = ({
17079
17735
  if (counterValidation === void 0) return null;
17080
17736
  return counterValidation.isCorrect ? "correct" : "incorrect";
17081
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]);
17082
17749
  return /* @__PURE__ */ jsxRuntime.jsxs(
17083
17750
  "div",
17084
17751
  {
@@ -17112,6 +17779,20 @@ const NumberLineRenderer = ({
17112
17779
  },
17113
17780
  pos.id
17114
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
+ ),
17115
17796
  isAxisDragMode ? /* @__PURE__ */ jsxRuntime.jsx(
17116
17797
  FreeDragAxis,
17117
17798
  {
@@ -17126,6 +17807,8 @@ const NumberLineRenderer = ({
17126
17807
  values,
17127
17808
  onChange,
17128
17809
  highlights,
17810
+ cursors,
17811
+ cursorValidationStatuses,
17129
17812
  snapKey: counterCount
17130
17813
  }
17131
17814
  ) : /* @__PURE__ */ jsxRuntime.jsx(
@@ -17138,7 +17821,11 @@ const NumberLineRenderer = ({
17138
17821
  showArrowLeft,
17139
17822
  showArrowRight,
17140
17823
  highlights,
17141
- positions
17824
+ positions,
17825
+ cursors,
17826
+ onChange,
17827
+ values,
17828
+ cursorValidationStatuses
17142
17829
  }
17143
17830
  ),
17144
17831
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18117,7 +18804,9 @@ exports.MatcherItemSchema = MatcherItemSchema;
18117
18804
  exports.MatcherRowSchema = MatcherRowSchema;
18118
18805
  exports.MatcherWidgetJSONSchema = MatcherWidgetJSONSchema;
18119
18806
  exports.MixedValidationSchema = MixedValidationSchema;
18807
+ exports.NumberLineArrowItemSchema = NumberLineArrowItemSchema;
18120
18808
  exports.NumberLineCounterItemSchema = NumberLineCounterItemSchema;
18809
+ exports.NumberLineCursorItemSchema = NumberLineCursorItemSchema;
18121
18810
  exports.NumberLineHighlightItemSchema = NumberLineHighlightItemSchema;
18122
18811
  exports.NumberLineItemSchema = NumberLineItemSchema;
18123
18812
  exports.NumberLinePositionItemSchema = NumberLinePositionItemSchema;