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