@mathscan/math-exercise-engine 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8069 -3402
- package/dist/index.js +756 -77
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +756 -77
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +253 -45
- 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
|
|
@@ -15048,19 +15243,21 @@ const DragOptions = ({
|
|
|
15048
15243
|
options,
|
|
15049
15244
|
onOptionClick
|
|
15050
15245
|
}) => {
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15246
|
+
const [shuffledOptions] = useState(
|
|
15247
|
+
() => lodashExports.shuffle(options.map((option, idx) => ({
|
|
15248
|
+
optionId: `drag-option-${idx}`,
|
|
15249
|
+
value: String(option)
|
|
15250
|
+
})))
|
|
15251
|
+
);
|
|
15252
|
+
return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: shuffledOptions.map(({ optionId, value }) => /* @__PURE__ */ jsx(
|
|
15253
|
+
SingleDragOption,
|
|
15254
|
+
{
|
|
15255
|
+
optionId,
|
|
15256
|
+
value,
|
|
15257
|
+
onOptionClick
|
|
15258
|
+
},
|
|
15259
|
+
optionId
|
|
15260
|
+
)) }) });
|
|
15064
15261
|
};
|
|
15065
15262
|
const InlineDragOption = ({
|
|
15066
15263
|
widgetId,
|
|
@@ -15644,7 +15841,8 @@ const RadioOptions = ({
|
|
|
15644
15841
|
onChange
|
|
15645
15842
|
}) => {
|
|
15646
15843
|
const playSound = useSound();
|
|
15647
|
-
|
|
15844
|
+
const [shuffledOptions] = useState(() => lodashExports.shuffle(options));
|
|
15845
|
+
return /* @__PURE__ */ jsx("div", { className: "radio-options", children: shuffledOptions.map((option, idx) => {
|
|
15648
15846
|
const isSelected = value === option;
|
|
15649
15847
|
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
15650
15848
|
return /* @__PURE__ */ jsx(
|
|
@@ -16509,6 +16707,182 @@ const HighlightBands = ({ highlights, min, max }) => {
|
|
|
16509
16707
|
);
|
|
16510
16708
|
}) });
|
|
16511
16709
|
};
|
|
16710
|
+
const DraggableCursor = ({
|
|
16711
|
+
cursor,
|
|
16712
|
+
min,
|
|
16713
|
+
max,
|
|
16714
|
+
snapTicks,
|
|
16715
|
+
audibleTicks,
|
|
16716
|
+
axisRowRef,
|
|
16717
|
+
onChange,
|
|
16718
|
+
currentValue,
|
|
16719
|
+
validationStatus
|
|
16720
|
+
}) => {
|
|
16721
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
16722
|
+
const isDraggingRef = useRef(false);
|
|
16723
|
+
const currentValueRef = useRef(currentValue);
|
|
16724
|
+
currentValueRef.current = currentValue;
|
|
16725
|
+
const dragStartValueRef = useRef(currentValue);
|
|
16726
|
+
const lastSoundValueRef = useRef(currentValue);
|
|
16727
|
+
const { jiggling, onAnimationEnd } = useJiggle(isDragging);
|
|
16728
|
+
const playSound = useSound();
|
|
16729
|
+
const colorVar = !validationStatus && cursor.color ? { "--nl-cursor-color": cursor.color } : void 0;
|
|
16730
|
+
const initialPct = toPercent(cursor.value, min, max);
|
|
16731
|
+
const ballPct = toPercent(currentValue, min, max);
|
|
16732
|
+
const fillLeft = Math.min(initialPct, ballPct);
|
|
16733
|
+
const fillWidth = Math.abs(ballPct - initialPct);
|
|
16734
|
+
const snap = useCallback((clientX) => {
|
|
16735
|
+
const el = axisRowRef.current;
|
|
16736
|
+
if (!el) return currentValueRef.current;
|
|
16737
|
+
const rect = el.getBoundingClientRect();
|
|
16738
|
+
const raw = pixelToValue(clientX, rect.left, rect.width, min, max);
|
|
16739
|
+
return snapTicks.length > 0 ? snapToNearest(raw, snapTicks) : cleanFloat(raw);
|
|
16740
|
+
}, [axisRowRef, min, max, snapTicks]);
|
|
16741
|
+
const handlePointerDown = useCallback((e) => {
|
|
16742
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
16743
|
+
e.stopPropagation();
|
|
16744
|
+
isDraggingRef.current = true;
|
|
16745
|
+
dragStartValueRef.current = currentValue;
|
|
16746
|
+
lastSoundValueRef.current = currentValue;
|
|
16747
|
+
setIsDragging(true);
|
|
16748
|
+
}, [currentValue]);
|
|
16749
|
+
const updateCursorValue = useCallback((clientX) => {
|
|
16750
|
+
const next = snap(clientX);
|
|
16751
|
+
if (!almostEqual(next, lastSoundValueRef.current)) {
|
|
16752
|
+
const soundTicks = audibleTicks ?? snapTicks;
|
|
16753
|
+
if (soundTicks.some((tick) => almostEqual(tick, next))) playSound("cursor-step");
|
|
16754
|
+
lastSoundValueRef.current = next;
|
|
16755
|
+
}
|
|
16756
|
+
onChange(cursor.id, String(next));
|
|
16757
|
+
return next;
|
|
16758
|
+
}, [snap, audibleTicks, snapTicks, playSound, onChange, cursor.id]);
|
|
16759
|
+
const handlePointerMove = useCallback((e) => {
|
|
16760
|
+
if (!isDraggingRef.current) return;
|
|
16761
|
+
e.stopPropagation();
|
|
16762
|
+
updateCursorValue(e.clientX);
|
|
16763
|
+
}, [updateCursorValue]);
|
|
16764
|
+
const handlePointerUp = useCallback((e) => {
|
|
16765
|
+
if (!isDraggingRef.current) return;
|
|
16766
|
+
isDraggingRef.current = false;
|
|
16767
|
+
setIsDragging(false);
|
|
16768
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
16769
|
+
e.stopPropagation();
|
|
16770
|
+
const next = updateCursorValue(e.clientX);
|
|
16771
|
+
if (!almostEqual(next, dragStartValueRef.current)) {
|
|
16772
|
+
playSound("cursor-release");
|
|
16773
|
+
}
|
|
16774
|
+
}, [updateCursorValue, playSound]);
|
|
16775
|
+
const handleKeyDown = useCallback((e) => {
|
|
16776
|
+
if (!snapTicks.length) return;
|
|
16777
|
+
const sorted = [...snapTicks].sort((a, b) => a - b);
|
|
16778
|
+
const idx = sorted.findIndex((t) => t >= currentValue - 1e-9);
|
|
16779
|
+
const first = sorted[0] ?? currentValue;
|
|
16780
|
+
const last = sorted[sorted.length - 1] ?? currentValue;
|
|
16781
|
+
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
|
|
16782
|
+
e.preventDefault();
|
|
16783
|
+
const next = sorted[idx + 1] ?? last;
|
|
16784
|
+
const soundTicks = audibleTicks ?? snapTicks;
|
|
16785
|
+
if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
|
|
16786
|
+
playSound("cursor-step");
|
|
16787
|
+
}
|
|
16788
|
+
onChange(cursor.id, String(next));
|
|
16789
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
|
|
16790
|
+
e.preventDefault();
|
|
16791
|
+
const next = sorted[idx - 1] ?? first;
|
|
16792
|
+
const soundTicks = audibleTicks ?? snapTicks;
|
|
16793
|
+
if (!almostEqual(next, currentValue) && soundTicks.some((tick) => almostEqual(tick, next))) {
|
|
16794
|
+
playSound("cursor-step");
|
|
16795
|
+
}
|
|
16796
|
+
onChange(cursor.id, String(next));
|
|
16797
|
+
}
|
|
16798
|
+
}, [snapTicks, audibleTicks, currentValue, playSound, onChange, cursor.id]);
|
|
16799
|
+
const ballClass = [
|
|
16800
|
+
"number-line-cursor-ball",
|
|
16801
|
+
isDragging ? "number-line-cursor-ball--dragging" : "",
|
|
16802
|
+
jiggling && !isDragging ? "number-line-cursor-ball--jiggle" : "",
|
|
16803
|
+
validationStatus ? `number-line-cursor-ball--${validationStatus}` : ""
|
|
16804
|
+
].filter(Boolean).join(" ");
|
|
16805
|
+
const fillClass = [
|
|
16806
|
+
"number-line-cursor-fill",
|
|
16807
|
+
validationStatus ? `number-line-cursor-fill--${validationStatus}` : ""
|
|
16808
|
+
].filter(Boolean).join(" ");
|
|
16809
|
+
const labelClass = [
|
|
16810
|
+
"number-line-cursor-label",
|
|
16811
|
+
validationStatus ? `number-line-cursor-label--${validationStatus}` : ""
|
|
16812
|
+
].filter(Boolean).join(" ");
|
|
16813
|
+
const sliderAriaLabel = cursor.label ? `${cursor.label}: cursor position ${currentValue}` : `cursor position ${currentValue}`;
|
|
16814
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
16815
|
+
cursor.fillRange && fillWidth > 0 && /* @__PURE__ */ jsx(
|
|
16816
|
+
"div",
|
|
16817
|
+
{
|
|
16818
|
+
className: fillClass,
|
|
16819
|
+
"aria-hidden": "true",
|
|
16820
|
+
style: { left: `${fillLeft}%`, width: `${fillWidth}%`, ...colorVar }
|
|
16821
|
+
}
|
|
16822
|
+
),
|
|
16823
|
+
cursor.label && /* @__PURE__ */ jsx(
|
|
16824
|
+
"span",
|
|
16825
|
+
{
|
|
16826
|
+
className: labelClass,
|
|
16827
|
+
"aria-hidden": "true",
|
|
16828
|
+
style: { left: `${ballPct}%`, ...colorVar },
|
|
16829
|
+
children: cursor.label
|
|
16830
|
+
}
|
|
16831
|
+
),
|
|
16832
|
+
/* @__PURE__ */ jsx(
|
|
16833
|
+
"div",
|
|
16834
|
+
{
|
|
16835
|
+
className: ballClass,
|
|
16836
|
+
style: { left: `${ballPct}%`, ...colorVar },
|
|
16837
|
+
onPointerDown: handlePointerDown,
|
|
16838
|
+
onPointerMove: handlePointerMove,
|
|
16839
|
+
onPointerUp: handlePointerUp,
|
|
16840
|
+
onKeyDown: handleKeyDown,
|
|
16841
|
+
onAnimationEnd,
|
|
16842
|
+
role: "slider",
|
|
16843
|
+
"aria-valuemin": min,
|
|
16844
|
+
"aria-valuemax": max,
|
|
16845
|
+
"aria-valuenow": currentValue,
|
|
16846
|
+
"aria-label": sliderAriaLabel,
|
|
16847
|
+
tabIndex: 0
|
|
16848
|
+
}
|
|
16849
|
+
)
|
|
16850
|
+
] });
|
|
16851
|
+
};
|
|
16852
|
+
const CursorBalls = ({
|
|
16853
|
+
cursors,
|
|
16854
|
+
min,
|
|
16855
|
+
max,
|
|
16856
|
+
snapTicks,
|
|
16857
|
+
audibleTicks,
|
|
16858
|
+
axisRowRef,
|
|
16859
|
+
onChange,
|
|
16860
|
+
values,
|
|
16861
|
+
cursorValidationStatuses
|
|
16862
|
+
}) => {
|
|
16863
|
+
if (!cursors?.length) return null;
|
|
16864
|
+
return /* @__PURE__ */ jsx(Fragment, { children: cursors.map((cursor) => {
|
|
16865
|
+
const raw = values[cursor.id];
|
|
16866
|
+
const parsed = Number(raw);
|
|
16867
|
+
const currentValue = raw !== void 0 && raw !== "" && isFinite(parsed) ? parsed : cursor.value;
|
|
16868
|
+
const validationStatus = cursorValidationStatuses?.[cursor.id] ?? null;
|
|
16869
|
+
return /* @__PURE__ */ jsx(
|
|
16870
|
+
DraggableCursor,
|
|
16871
|
+
{
|
|
16872
|
+
cursor,
|
|
16873
|
+
min,
|
|
16874
|
+
max,
|
|
16875
|
+
snapTicks,
|
|
16876
|
+
audibleTicks,
|
|
16877
|
+
axisRowRef,
|
|
16878
|
+
onChange,
|
|
16879
|
+
currentValue,
|
|
16880
|
+
validationStatus
|
|
16881
|
+
},
|
|
16882
|
+
cursor.id
|
|
16883
|
+
);
|
|
16884
|
+
}) });
|
|
16885
|
+
};
|
|
16512
16886
|
const PlacedChip = ({ widgetId, label, leftPercent, isLanding, onRemove }) => {
|
|
16513
16887
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
16514
16888
|
id: widgetId,
|
|
@@ -16572,6 +16946,8 @@ const FreeDragAxis = ({
|
|
|
16572
16946
|
values,
|
|
16573
16947
|
onChange,
|
|
16574
16948
|
highlights,
|
|
16949
|
+
cursors,
|
|
16950
|
+
cursorValidationStatuses,
|
|
16575
16951
|
snapKey
|
|
16576
16952
|
}) => {
|
|
16577
16953
|
const axisRef = useRef(null);
|
|
@@ -16744,6 +17120,20 @@ const FreeDragAxis = ({
|
|
|
16744
17120
|
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
16745
17121
|
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
16746
17122
|
/* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
|
|
17123
|
+
/* @__PURE__ */ jsx(
|
|
17124
|
+
CursorBalls,
|
|
17125
|
+
{
|
|
17126
|
+
cursors,
|
|
17127
|
+
min,
|
|
17128
|
+
max,
|
|
17129
|
+
snapTicks,
|
|
17130
|
+
audibleTicks: majorTicks,
|
|
17131
|
+
axisRowRef: axisRef,
|
|
17132
|
+
onChange,
|
|
17133
|
+
values,
|
|
17134
|
+
cursorValidationStatuses
|
|
17135
|
+
}
|
|
17136
|
+
),
|
|
16747
17137
|
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
16748
17138
|
"div",
|
|
16749
17139
|
{
|
|
@@ -16812,40 +17202,65 @@ const AxisTicks = ({
|
|
|
16812
17202
|
showArrowLeft,
|
|
16813
17203
|
showArrowRight,
|
|
16814
17204
|
highlights,
|
|
16815
|
-
positions
|
|
16816
|
-
|
|
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
|
-
|
|
17205
|
+
positions,
|
|
17206
|
+
cursors,
|
|
17207
|
+
onChange,
|
|
17208
|
+
values,
|
|
17209
|
+
cursorValidationStatuses
|
|
17210
|
+
}) => {
|
|
17211
|
+
const axisRowRef = useRef(null);
|
|
17212
|
+
const snapTicks = useMemo(
|
|
17213
|
+
() => [...majorTicks, ...minorValues],
|
|
17214
|
+
[majorTicks, minorValues]
|
|
17215
|
+
);
|
|
17216
|
+
return /* @__PURE__ */ jsxs("div", { ref: axisRowRef, className: "number-line-axis-row", role: "presentation", children: [
|
|
17217
|
+
/* @__PURE__ */ jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
|
|
17218
|
+
showArrowLeft && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
17219
|
+
showArrowRight && /* @__PURE__ */ jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
17220
|
+
/* @__PURE__ */ jsx(HighlightBands, { highlights, min, max }),
|
|
17221
|
+
/* @__PURE__ */ jsx(
|
|
17222
|
+
CursorBalls,
|
|
17223
|
+
{
|
|
17224
|
+
cursors,
|
|
17225
|
+
min,
|
|
17226
|
+
max,
|
|
17227
|
+
snapTicks,
|
|
17228
|
+
audibleTicks: majorTicks,
|
|
17229
|
+
axisRowRef,
|
|
17230
|
+
onChange,
|
|
17231
|
+
values,
|
|
17232
|
+
cursorValidationStatuses
|
|
17233
|
+
}
|
|
17234
|
+
),
|
|
17235
|
+
minorValues.map((v) => /* @__PURE__ */ jsx(
|
|
17236
|
+
"div",
|
|
17237
|
+
{
|
|
17238
|
+
className: "number-line-minor-tick",
|
|
17239
|
+
"aria-hidden": "true",
|
|
17240
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
17241
|
+
},
|
|
17242
|
+
`minor-${v}`
|
|
17243
|
+
)),
|
|
17244
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsx(
|
|
17245
|
+
"div",
|
|
17246
|
+
{
|
|
17247
|
+
className: "number-line-major-tick",
|
|
17248
|
+
"aria-hidden": "true",
|
|
17249
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
17250
|
+
},
|
|
17251
|
+
`major-${tick}`
|
|
17252
|
+
)),
|
|
17253
|
+
positions.map((pos) => /* @__PURE__ */ jsx(
|
|
17254
|
+
"div",
|
|
17255
|
+
{
|
|
17256
|
+
className: "number-line-position-dot",
|
|
17257
|
+
"aria-hidden": "true",
|
|
17258
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
17259
|
+
},
|
|
17260
|
+
`dot-${pos.id}`
|
|
17261
|
+
))
|
|
17262
|
+
] });
|
|
17263
|
+
};
|
|
16849
17264
|
const isLabelVisible = (tick, min, max, labelVisibility) => {
|
|
16850
17265
|
switch (labelVisibility) {
|
|
16851
17266
|
case "none":
|
|
@@ -16939,6 +17354,209 @@ const CounterControls = ({
|
|
|
16939
17354
|
counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
16940
17355
|
] });
|
|
16941
17356
|
};
|
|
17357
|
+
const ARROW_ARC_LEVEL_HEIGHT = 80;
|
|
17358
|
+
const ARROW_ARC_VIEWBOX_HEIGHT = 64;
|
|
17359
|
+
const ARROW_ARC_VIEWBOX_WIDTH = 100;
|
|
17360
|
+
const ARROW_ARC_BASE_Y = 56;
|
|
17361
|
+
const ARROW_ARC_MIN_LIFT = 28;
|
|
17362
|
+
const ARROW_ARC_MAX_LIFT = 58;
|
|
17363
|
+
const ARROW_ARC_LIFT_RATIO = 1.15;
|
|
17364
|
+
const ARROW_ARC_LABEL_OFFSET = 24;
|
|
17365
|
+
const ARROW_ARC_HEAD_LENGTH = 8;
|
|
17366
|
+
const ARROW_ARC_HEAD_HALF_WIDTH = 3.2;
|
|
17367
|
+
function getArrowArcLift(widthPct) {
|
|
17368
|
+
return Math.min(ARROW_ARC_MAX_LIFT, Math.max(ARROW_ARC_MIN_LIFT, widthPct * ARROW_ARC_LIFT_RATIO));
|
|
17369
|
+
}
|
|
17370
|
+
function fmt(n) {
|
|
17371
|
+
return Number(n.toFixed(2));
|
|
17372
|
+
}
|
|
17373
|
+
function getArrowArcSvgWidth(renderedWidthPx) {
|
|
17374
|
+
return renderedWidthPx > 0 ? renderedWidthPx : ARROW_ARC_VIEWBOX_WIDTH;
|
|
17375
|
+
}
|
|
17376
|
+
function getArrowArcLabelTop(widthPct) {
|
|
17377
|
+
const svgTop = ARROW_ARC_LEVEL_HEIGHT - ARROW_ARC_VIEWBOX_HEIGHT;
|
|
17378
|
+
const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
|
|
17379
|
+
return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
|
|
17380
|
+
}
|
|
17381
|
+
function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
|
|
17382
|
+
const endX = getArrowArcSvgWidth(svgWidthPx);
|
|
17383
|
+
const controlInset = Math.min(svgWidthPx * 0.18, 48);
|
|
17384
|
+
const lift = getArrowArcLift(widthPct);
|
|
17385
|
+
const controlY = fmt(ARROW_ARC_BASE_Y - lift);
|
|
17386
|
+
const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, endX - (endX - controlInset));
|
|
17387
|
+
const tx = Math.cos(tangentAngle);
|
|
17388
|
+
const ty = Math.sin(tangentAngle);
|
|
17389
|
+
const shaftEndX = fmt(endX - tx * ARROW_ARC_HEAD_LENGTH);
|
|
17390
|
+
const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
|
|
17391
|
+
const endControlX = fmt(shaftEndX - tx * controlInset);
|
|
17392
|
+
const endControlY = fmt(shaftEndY - ty * controlInset);
|
|
17393
|
+
const arcPath = `M 0 ${ARROW_ARC_BASE_Y} C ${fmt(controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
|
|
17394
|
+
const nx = -ty;
|
|
17395
|
+
const ny = tx;
|
|
17396
|
+
const baseX = endX - tx * ARROW_ARC_HEAD_LENGTH;
|
|
17397
|
+
const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
|
|
17398
|
+
const headPath = `M ${fmt(endX)} ${ARROW_ARC_BASE_Y} L ${fmt(baseX + nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY + ny * ARROW_ARC_HEAD_HALF_WIDTH)} L ${fmt(baseX - nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY - ny * ARROW_ARC_HEAD_HALF_WIDTH)} Z`;
|
|
17399
|
+
return { arcPath, headPath };
|
|
17400
|
+
}
|
|
17401
|
+
function getArrowArcLayouts(arrows, min, max) {
|
|
17402
|
+
if (!arrows?.length) return [];
|
|
17403
|
+
const sorted = arrows.map((arrow, index) => {
|
|
17404
|
+
const fromPct = toPercent(arrow.from, min, max);
|
|
17405
|
+
const toPct = toPercent(arrow.to, min, max);
|
|
17406
|
+
return {
|
|
17407
|
+
arrow,
|
|
17408
|
+
index,
|
|
17409
|
+
leftPct: fromPct,
|
|
17410
|
+
rightPct: toPct,
|
|
17411
|
+
widthPct: toPct - fromPct,
|
|
17412
|
+
level: 0
|
|
17413
|
+
};
|
|
17414
|
+
}).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
|
|
17415
|
+
const levelRightEdges = [];
|
|
17416
|
+
for (const layout of sorted) {
|
|
17417
|
+
const reusableLevel = levelRightEdges.findIndex((rightPct) => layout.leftPct >= rightPct);
|
|
17418
|
+
const level = reusableLevel === -1 ? levelRightEdges.length : reusableLevel;
|
|
17419
|
+
layout.level = level;
|
|
17420
|
+
levelRightEdges[level] = layout.rightPct;
|
|
17421
|
+
}
|
|
17422
|
+
return sorted.sort((a, b) => a.index - b.index);
|
|
17423
|
+
}
|
|
17424
|
+
function getArrowArcsRowHeight(layouts) {
|
|
17425
|
+
if (layouts.length === 0) return 0;
|
|
17426
|
+
const levelCount = Math.max(...layouts.map((layout) => layout.level)) + 1;
|
|
17427
|
+
return levelCount * ARROW_ARC_LEVEL_HEIGHT;
|
|
17428
|
+
}
|
|
17429
|
+
function useElementWidth() {
|
|
17430
|
+
const ref = useRef(null);
|
|
17431
|
+
const [width, setWidth] = useState(0);
|
|
17432
|
+
useLayoutEffect(() => {
|
|
17433
|
+
const element = ref.current;
|
|
17434
|
+
if (!element) return;
|
|
17435
|
+
const updateWidth = () => {
|
|
17436
|
+
const nextWidth = element.getBoundingClientRect().width;
|
|
17437
|
+
setWidth((previousWidth) => previousWidth === nextWidth ? previousWidth : nextWidth);
|
|
17438
|
+
};
|
|
17439
|
+
updateWidth();
|
|
17440
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
17441
|
+
const observer = new ResizeObserver(updateWidth);
|
|
17442
|
+
observer.observe(element);
|
|
17443
|
+
return () => observer.disconnect();
|
|
17444
|
+
}, []);
|
|
17445
|
+
return [ref, width];
|
|
17446
|
+
}
|
|
17447
|
+
const ArrowArc = ({
|
|
17448
|
+
layout: { arrow, leftPct, widthPct, level },
|
|
17449
|
+
widgets,
|
|
17450
|
+
values,
|
|
17451
|
+
droppedInputs,
|
|
17452
|
+
validationDetails,
|
|
17453
|
+
onChange,
|
|
17454
|
+
onInputFocus
|
|
17455
|
+
}) => {
|
|
17456
|
+
const [arcRef, renderedWidthPx] = useElementWidth();
|
|
17457
|
+
const hasLabel = arrow.content.length > 0;
|
|
17458
|
+
const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
|
|
17459
|
+
const { arcPath, headPath } = useMemo(
|
|
17460
|
+
() => getArrowArcPaths(widthPct, svgWidthPx),
|
|
17461
|
+
[widthPct, svgWidthPx]
|
|
17462
|
+
);
|
|
17463
|
+
return /* @__PURE__ */ jsxs(
|
|
17464
|
+
"div",
|
|
17465
|
+
{
|
|
17466
|
+
ref: arcRef,
|
|
17467
|
+
className: "number-line-arrow-arc",
|
|
17468
|
+
"data-level": level,
|
|
17469
|
+
style: {
|
|
17470
|
+
left: `${leftPct}%`,
|
|
17471
|
+
width: `${widthPct}%`,
|
|
17472
|
+
bottom: `${level * ARROW_ARC_LEVEL_HEIGHT}px`,
|
|
17473
|
+
// DSL color overrides the CSS default — only set when the author
|
|
17474
|
+
// explicitly wrote e.g. ARROW[from=0,to=5,color="#2B6DEA"]
|
|
17475
|
+
...arrow.color !== void 0 && { "--nl-arrow-arc-color": arrow.color }
|
|
17476
|
+
},
|
|
17477
|
+
children: [
|
|
17478
|
+
/* @__PURE__ */ jsxs(
|
|
17479
|
+
"svg",
|
|
17480
|
+
{
|
|
17481
|
+
className: "number-line-arrow-arc-svg",
|
|
17482
|
+
viewBox: `0 0 ${svgWidthPx} ${ARROW_ARC_VIEWBOX_HEIGHT}`,
|
|
17483
|
+
preserveAspectRatio: "none",
|
|
17484
|
+
"aria-hidden": "true",
|
|
17485
|
+
focusable: "false",
|
|
17486
|
+
children: [
|
|
17487
|
+
/* @__PURE__ */ jsx(
|
|
17488
|
+
"path",
|
|
17489
|
+
{
|
|
17490
|
+
className: "number-line-arrow-arc-path",
|
|
17491
|
+
d: arcPath
|
|
17492
|
+
}
|
|
17493
|
+
),
|
|
17494
|
+
/* @__PURE__ */ jsx(
|
|
17495
|
+
"path",
|
|
17496
|
+
{
|
|
17497
|
+
className: "number-line-arrow-arc-head",
|
|
17498
|
+
d: headPath
|
|
17499
|
+
}
|
|
17500
|
+
)
|
|
17501
|
+
]
|
|
17502
|
+
}
|
|
17503
|
+
),
|
|
17504
|
+
hasLabel && /* @__PURE__ */ jsx(
|
|
17505
|
+
"div",
|
|
17506
|
+
{
|
|
17507
|
+
className: "number-line-arrow-arc-label",
|
|
17508
|
+
style: { top: `${getArrowArcLabelTop(widthPct)}px` },
|
|
17509
|
+
children: arrow.content.map((subItem) => /* @__PURE__ */ jsx(
|
|
17510
|
+
LayoutRenderer,
|
|
17511
|
+
{
|
|
17512
|
+
item: subItem,
|
|
17513
|
+
widgets,
|
|
17514
|
+
values,
|
|
17515
|
+
droppedInputs,
|
|
17516
|
+
validationDetails,
|
|
17517
|
+
onChange,
|
|
17518
|
+
onInputFocus
|
|
17519
|
+
},
|
|
17520
|
+
subItem.id
|
|
17521
|
+
))
|
|
17522
|
+
}
|
|
17523
|
+
)
|
|
17524
|
+
]
|
|
17525
|
+
}
|
|
17526
|
+
);
|
|
17527
|
+
};
|
|
17528
|
+
const ArrowArcsRow = ({
|
|
17529
|
+
layouts,
|
|
17530
|
+
rowHeight,
|
|
17531
|
+
widgets,
|
|
17532
|
+
values,
|
|
17533
|
+
droppedInputs,
|
|
17534
|
+
validationDetails,
|
|
17535
|
+
onChange,
|
|
17536
|
+
onInputFocus
|
|
17537
|
+
}) => {
|
|
17538
|
+
if (layouts.length === 0) return null;
|
|
17539
|
+
return /* @__PURE__ */ jsx(
|
|
17540
|
+
"div",
|
|
17541
|
+
{
|
|
17542
|
+
className: "number-line-arrow-arcs-row",
|
|
17543
|
+
style: { height: `${rowHeight}px` },
|
|
17544
|
+
children: layouts.map((layout) => /* @__PURE__ */ jsx(
|
|
17545
|
+
ArrowArc,
|
|
17546
|
+
{
|
|
17547
|
+
layout,
|
|
17548
|
+
widgets,
|
|
17549
|
+
values,
|
|
17550
|
+
droppedInputs,
|
|
17551
|
+
validationDetails,
|
|
17552
|
+
onChange,
|
|
17553
|
+
onInputFocus
|
|
17554
|
+
},
|
|
17555
|
+
layout.arrow.id
|
|
17556
|
+
))
|
|
17557
|
+
}
|
|
17558
|
+
);
|
|
17559
|
+
};
|
|
16942
17560
|
const CONTENT_ROW_EXTRA_SPACE = 8;
|
|
16943
17561
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
16944
17562
|
function useMeasuredPositionContentHeight(positionIds) {
|
|
@@ -17052,8 +17670,10 @@ const NumberLineRenderer = ({
|
|
|
17052
17670
|
style: lineStyle = "arrow-right",
|
|
17053
17671
|
labelVisibility,
|
|
17054
17672
|
highlights,
|
|
17673
|
+
arrows,
|
|
17055
17674
|
counter,
|
|
17056
|
-
positions
|
|
17675
|
+
positions,
|
|
17676
|
+
cursors
|
|
17057
17677
|
} = item;
|
|
17058
17678
|
const [counterCount, setCounterCount] = useState(
|
|
17059
17679
|
counter !== void 0 ? counter.default ?? counter.min : 0
|
|
@@ -17069,6 +17689,15 @@ const NumberLineRenderer = ({
|
|
|
17069
17689
|
onChangeRef.current(counter.id, String(counterCount));
|
|
17070
17690
|
}
|
|
17071
17691
|
}, [counterCount, counter?.id]);
|
|
17692
|
+
const cursorKeys = useMemo(
|
|
17693
|
+
() => (cursors ?? []).map((c) => `${c.id}:${c.value}`).join(","),
|
|
17694
|
+
[cursors]
|
|
17695
|
+
);
|
|
17696
|
+
useEffect(() => {
|
|
17697
|
+
for (const cursor of cursors ?? []) {
|
|
17698
|
+
onChangeRef.current(cursor.id, String(cursor.value));
|
|
17699
|
+
}
|
|
17700
|
+
}, [cursorKeys]);
|
|
17072
17701
|
const effectiveStep = useMemo(() => {
|
|
17073
17702
|
if (counter !== void 0 && counterCount > 0) {
|
|
17074
17703
|
return cleanFloat((max - min) / counterCount);
|
|
@@ -17102,6 +17731,15 @@ const NumberLineRenderer = ({
|
|
|
17102
17731
|
[visiblePositions]
|
|
17103
17732
|
);
|
|
17104
17733
|
const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
|
|
17734
|
+
const arrowArcLayouts = useMemo(
|
|
17735
|
+
() => getArrowArcLayouts(arrows, min, max),
|
|
17736
|
+
[arrows, min, max]
|
|
17737
|
+
);
|
|
17738
|
+
const arrowArcsRowHeight = useMemo(
|
|
17739
|
+
() => getArrowArcsRowHeight(arrowArcLayouts),
|
|
17740
|
+
[arrowArcLayouts]
|
|
17741
|
+
);
|
|
17742
|
+
const hasOverlayRow = arrowArcsRowHeight > 0 || visiblePositions.length > 0;
|
|
17105
17743
|
const nlValidationStatus = useMemo(() => {
|
|
17106
17744
|
if (!validationDetails || counter === void 0) return null;
|
|
17107
17745
|
if (counter.answer === void 0) return null;
|
|
@@ -17109,6 +17747,17 @@ const NumberLineRenderer = ({
|
|
|
17109
17747
|
if (counterValidation === void 0) return null;
|
|
17110
17748
|
return counterValidation.isCorrect ? "correct" : "incorrect";
|
|
17111
17749
|
}, [validationDetails, counter]);
|
|
17750
|
+
const cursorValidationStatuses = useMemo(() => {
|
|
17751
|
+
if (!validationDetails || !cursors?.length) return void 0;
|
|
17752
|
+
const statuses = {};
|
|
17753
|
+
for (const cursor of cursors) {
|
|
17754
|
+
const detail = validationDetails[cursor.id];
|
|
17755
|
+
if (detail !== void 0) {
|
|
17756
|
+
statuses[cursor.id] = detail.isCorrect ? "correct" : "incorrect";
|
|
17757
|
+
}
|
|
17758
|
+
}
|
|
17759
|
+
return statuses;
|
|
17760
|
+
}, [validationDetails, cursors]);
|
|
17112
17761
|
return /* @__PURE__ */ jsxs(
|
|
17113
17762
|
"div",
|
|
17114
17763
|
{
|
|
@@ -17120,28 +17769,50 @@ const NumberLineRenderer = ({
|
|
|
17120
17769
|
role: "group",
|
|
17121
17770
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
17122
17771
|
children: [
|
|
17123
|
-
|
|
17772
|
+
hasOverlayRow && /* @__PURE__ */ jsxs(
|
|
17124
17773
|
"div",
|
|
17125
17774
|
{
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
|
|
17775
|
+
className: "number-line-overlays-row",
|
|
17776
|
+
style: { "--nl-arrow-arcs-row-height": `${arrowArcsRowHeight}px` },
|
|
17777
|
+
children: [
|
|
17778
|
+
/* @__PURE__ */ jsx(
|
|
17779
|
+
ArrowArcsRow,
|
|
17780
|
+
{
|
|
17781
|
+
layouts: arrowArcLayouts,
|
|
17782
|
+
rowHeight: arrowArcsRowHeight,
|
|
17783
|
+
widgets,
|
|
17784
|
+
values,
|
|
17785
|
+
droppedInputs,
|
|
17786
|
+
validationDetails,
|
|
17787
|
+
onChange,
|
|
17788
|
+
onInputFocus
|
|
17789
|
+
}
|
|
17790
|
+
),
|
|
17791
|
+
visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
|
|
17792
|
+
"div",
|
|
17793
|
+
{
|
|
17794
|
+
ref: getPositionContentRef(pos.id),
|
|
17795
|
+
className: "number-line-position-content",
|
|
17796
|
+
style: { left: `${toPercent(pos.value, min, max)}%` },
|
|
17797
|
+
children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
|
|
17798
|
+
LayoutRenderer,
|
|
17799
|
+
{
|
|
17800
|
+
item: subItem,
|
|
17801
|
+
widgets,
|
|
17802
|
+
values,
|
|
17803
|
+
droppedInputs,
|
|
17804
|
+
validationDetails,
|
|
17805
|
+
onChange,
|
|
17806
|
+
onInputFocus
|
|
17807
|
+
},
|
|
17808
|
+
subItem.id
|
|
17809
|
+
))
|
|
17810
|
+
},
|
|
17811
|
+
pos.id
|
|
17812
|
+
)) })
|
|
17813
|
+
]
|
|
17814
|
+
}
|
|
17815
|
+
),
|
|
17145
17816
|
isAxisDragMode ? /* @__PURE__ */ jsx(
|
|
17146
17817
|
FreeDragAxis,
|
|
17147
17818
|
{
|
|
@@ -17156,6 +17827,8 @@ const NumberLineRenderer = ({
|
|
|
17156
17827
|
values,
|
|
17157
17828
|
onChange,
|
|
17158
17829
|
highlights,
|
|
17830
|
+
cursors,
|
|
17831
|
+
cursorValidationStatuses,
|
|
17159
17832
|
snapKey: counterCount
|
|
17160
17833
|
}
|
|
17161
17834
|
) : /* @__PURE__ */ jsx(
|
|
@@ -17168,7 +17841,11 @@ const NumberLineRenderer = ({
|
|
|
17168
17841
|
showArrowLeft,
|
|
17169
17842
|
showArrowRight,
|
|
17170
17843
|
highlights,
|
|
17171
|
-
positions
|
|
17844
|
+
positions,
|
|
17845
|
+
cursors,
|
|
17846
|
+
onChange,
|
|
17847
|
+
values,
|
|
17848
|
+
cursorValidationStatuses
|
|
17172
17849
|
}
|
|
17173
17850
|
),
|
|
17174
17851
|
/* @__PURE__ */ jsx(
|
|
@@ -18148,7 +18825,9 @@ export {
|
|
|
18148
18825
|
MatcherRowSchema,
|
|
18149
18826
|
MatcherWidgetJSONSchema,
|
|
18150
18827
|
MixedValidationSchema,
|
|
18828
|
+
NumberLineArrowItemSchema,
|
|
18151
18829
|
NumberLineCounterItemSchema,
|
|
18830
|
+
NumberLineCursorItemSchema,
|
|
18152
18831
|
NumberLineHighlightItemSchema,
|
|
18153
18832
|
NumberLineItemSchema,
|
|
18154
18833
|
NumberLinePositionItemSchema,
|