@mathscan/math-exercise-engine 1.1.8 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8069 -3402
- package/dist/index.js +700 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +700 -43
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +220 -44
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
@@ -16509,6 +16704,182 @@ const HighlightBands = ({ highlights, min, max }) => {
|
|
|
16509
16704
|
);
|
|
16510
16705
|
}) });
|
|
16511
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
|
+
};
|
|
16512
16883
|
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
16513
16884
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
16514
16885
|
id: widgetId,
|
|
@@ -16572,6 +16943,8 @@ const FreeDragAxis = ({
|
|
|
16572
16943
|
values,
|
|
16573
16944
|
onChange,
|
|
16574
16945
|
highlights,
|
|
16946
|
+
cursors,
|
|
16947
|
+
cursorValidationStatuses,
|
|
16575
16948
|
snapKey
|
|
16576
16949
|
}) => {
|
|
16577
16950
|
const axisRef = useRef(null);
|
|
@@ -16744,6 +17117,20 @@ const FreeDragAxis = ({
|
|
|
16744
17117
|
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
16745
17118
|
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
16746
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
|
+
),
|
|
16747
17134
|
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
16748
17135
|
"div",
|
|
16749
17136
|
{
|
|
@@ -16812,40 +17199,65 @@ const AxisTicks = ({
|
|
|
16812
17199
|
showArrowLeft,
|
|
16813
17200
|
showArrowRight,
|
|
16814
17201
|
highlights,
|
|
16815
|
-
positions
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
},
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
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
|
+
};
|
|
16849
17261
|
const isLabelVisible = (tick, min, max, labelVisibility) => {
|
|
16850
17262
|
switch (labelVisibility) {
|
|
16851
17263
|
case "none":
|
|
@@ -16939,6 +17351,207 @@ const CounterControls = ({
|
|
|
16939
17351
|
counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
16940
17352
|
] });
|
|
16941
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
|
+
};
|
|
16942
17555
|
const CONTENT_ROW_EXTRA_SPACE = 8;
|
|
16943
17556
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
16944
17557
|
function useMeasuredPositionContentHeight(positionIds) {
|
|
@@ -17052,8 +17665,10 @@ const NumberLineRenderer = ({
|
|
|
17052
17665
|
style: lineStyle = "arrow-right",
|
|
17053
17666
|
labelVisibility,
|
|
17054
17667
|
highlights,
|
|
17668
|
+
arrows,
|
|
17055
17669
|
counter,
|
|
17056
|
-
positions
|
|
17670
|
+
positions,
|
|
17671
|
+
cursors
|
|
17057
17672
|
} = item;
|
|
17058
17673
|
const [counterCount, setCounterCount] = useState(
|
|
17059
17674
|
counter !== void 0 ? counter.default ?? counter.min : 0
|
|
@@ -17069,6 +17684,15 @@ const NumberLineRenderer = ({
|
|
|
17069
17684
|
onChangeRef.current(counter.id, String(counterCount));
|
|
17070
17685
|
}
|
|
17071
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]);
|
|
17072
17696
|
const effectiveStep = useMemo(() => {
|
|
17073
17697
|
if (counter !== void 0 && counterCount > 0) {
|
|
17074
17698
|
return cleanFloat((max - min) / counterCount);
|
|
@@ -17109,6 +17733,17 @@ const NumberLineRenderer = ({
|
|
|
17109
17733
|
if (counterValidation === void 0) return null;
|
|
17110
17734
|
return counterValidation.isCorrect ? "correct" : "incorrect";
|
|
17111
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]);
|
|
17112
17747
|
return /* @__PURE__ */ jsxs(
|
|
17113
17748
|
"div",
|
|
17114
17749
|
{
|
|
@@ -17142,6 +17777,20 @@ const NumberLineRenderer = ({
|
|
|
17142
17777
|
},
|
|
17143
17778
|
pos.id
|
|
17144
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
|
+
),
|
|
17145
17794
|
isAxisDragMode ? /* @__PURE__ */ jsx(
|
|
17146
17795
|
FreeDragAxis,
|
|
17147
17796
|
{
|
|
@@ -17156,6 +17805,8 @@ const NumberLineRenderer = ({
|
|
|
17156
17805
|
values,
|
|
17157
17806
|
onChange,
|
|
17158
17807
|
highlights,
|
|
17808
|
+
cursors,
|
|
17809
|
+
cursorValidationStatuses,
|
|
17159
17810
|
snapKey: counterCount
|
|
17160
17811
|
}
|
|
17161
17812
|
) : /* @__PURE__ */ jsx(
|
|
@@ -17168,7 +17819,11 @@ const NumberLineRenderer = ({
|
|
|
17168
17819
|
showArrowLeft,
|
|
17169
17820
|
showArrowRight,
|
|
17170
17821
|
highlights,
|
|
17171
|
-
positions
|
|
17822
|
+
positions,
|
|
17823
|
+
cursors,
|
|
17824
|
+
onChange,
|
|
17825
|
+
values,
|
|
17826
|
+
cursorValidationStatuses
|
|
17172
17827
|
}
|
|
17173
17828
|
),
|
|
17174
17829
|
/* @__PURE__ */ jsx(
|
|
@@ -18148,7 +18803,9 @@ export {
|
|
|
18148
18803
|
MatcherRowSchema,
|
|
18149
18804
|
MatcherWidgetJSONSchema,
|
|
18150
18805
|
MixedValidationSchema,
|
|
18806
|
+
NumberLineArrowItemSchema,
|
|
18151
18807
|
NumberLineCounterItemSchema,
|
|
18808
|
+
NumberLineCursorItemSchema,
|
|
18152
18809
|
NumberLineHighlightItemSchema,
|
|
18153
18810
|
NumberLineItemSchema,
|
|
18154
18811
|
NumberLinePositionItemSchema,
|