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