@mathscan/math-exercise-engine 1.0.14 → 1.0.16
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 +4453 -84
- package/dist/index.js +1113 -77
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1113 -77
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +250 -63
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -23,6 +23,8 @@ const AST_TYPES = {
|
|
|
23
23
|
INLINE_INPUT: "InlineInput",
|
|
24
24
|
/** Inline fraction widget (numerator/denominator with optional inputs) */
|
|
25
25
|
FRACTION: "Fraction",
|
|
26
|
+
/** Inline image element */
|
|
27
|
+
IMAGE: "Image",
|
|
26
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
29
|
// Blocks
|
|
28
30
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -50,6 +52,8 @@ const AST_TYPES = {
|
|
|
50
52
|
MATCHER_ROW: "MatcherRow",
|
|
51
53
|
/** Item within a matcher row */
|
|
52
54
|
MATCHER_ITEM: "MatcherItem",
|
|
55
|
+
/** Visual fraction inline widget */
|
|
56
|
+
VISUAL_FRACTION: "VisualFraction",
|
|
53
57
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
54
58
|
// Tools
|
|
55
59
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -88,6 +92,8 @@ const JSON_TYPES = {
|
|
|
88
92
|
COLOR_TEXT: "color-text",
|
|
89
93
|
/** Bold text */
|
|
90
94
|
BOLD_TEXT: "bold-text",
|
|
95
|
+
/** Inline image */
|
|
96
|
+
IMAGE: "image",
|
|
91
97
|
/** Reference to a widget */
|
|
92
98
|
WIDGET_REF: "widget-ref",
|
|
93
99
|
/** Line break element */
|
|
@@ -128,7 +134,9 @@ const JSON_TYPES = {
|
|
|
128
134
|
/** Numeric keypad widget */
|
|
129
135
|
NUMERIC_KEYPAD: "numeric-keypad",
|
|
130
136
|
/** Inline fraction widget */
|
|
131
|
-
FRACTION: "fraction"
|
|
137
|
+
FRACTION: "fraction",
|
|
138
|
+
/** Visual fraction widget */
|
|
139
|
+
VISUAL_FRACTION: "visual-fraction"
|
|
132
140
|
};
|
|
133
141
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
134
142
|
const VALIDATION_MODES = {
|
|
@@ -262,6 +270,10 @@ const HIGHLIGHT = createToken({
|
|
|
262
270
|
name: "HIGHLIGHT",
|
|
263
271
|
pattern: /HIGHLIGHT(?!_TEXT)/
|
|
264
272
|
});
|
|
273
|
+
const VISUAL_FRACTION = createToken({
|
|
274
|
+
name: "VISUAL_FRACTION",
|
|
275
|
+
pattern: /VISUAL_FRACTION/
|
|
276
|
+
});
|
|
265
277
|
const FRACTION = createToken({
|
|
266
278
|
name: "FRACTION",
|
|
267
279
|
pattern: /FRACTION/
|
|
@@ -278,6 +290,10 @@ const ANSWERS = createToken({
|
|
|
278
290
|
name: "ANSWERS",
|
|
279
291
|
pattern: /ANSWERS/
|
|
280
292
|
});
|
|
293
|
+
const IMAGE = createToken({
|
|
294
|
+
name: "IMAGE",
|
|
295
|
+
pattern: /IMAGE/
|
|
296
|
+
});
|
|
281
297
|
const NumberLiteral = createToken({
|
|
282
298
|
name: "NumberLiteral",
|
|
283
299
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -387,10 +403,12 @@ const allTokens = [
|
|
|
387
403
|
CELL,
|
|
388
404
|
COUNTER,
|
|
389
405
|
HIGHLIGHT,
|
|
406
|
+
VISUAL_FRACTION,
|
|
390
407
|
FRACTION,
|
|
391
408
|
MATCHER,
|
|
392
409
|
ITEM,
|
|
393
410
|
ANSWERS,
|
|
411
|
+
IMAGE,
|
|
394
412
|
// Literals
|
|
395
413
|
NumberLiteral,
|
|
396
414
|
StringLiteral,
|
|
@@ -849,6 +867,10 @@ class MathExerciseParser extends CstParser {
|
|
|
849
867
|
{ ALT: () => this.SUBRULE(this.boldText) },
|
|
850
868
|
// FRACTION (inline fraction widget)
|
|
851
869
|
{ ALT: () => this.SUBRULE(this.fraction) },
|
|
870
|
+
// IMAGE (inline image element)
|
|
871
|
+
{ ALT: () => this.SUBRULE(this.image) },
|
|
872
|
+
// VISUAL_FRACTION — always allowed; display-only when answer=[] or absence of answer parameter, interactive when answer=[...] with values.
|
|
873
|
+
{ ALT: () => this.SUBRULE(this.visualFraction) },
|
|
852
874
|
// W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
|
|
853
875
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
|
|
854
876
|
]);
|
|
@@ -880,6 +902,47 @@ class MathExerciseParser extends CstParser {
|
|
|
880
902
|
this.SUBRULE(this.parameterList);
|
|
881
903
|
this.CONSUME(RBracket);
|
|
882
904
|
});
|
|
905
|
+
this.image = this.RULE("image", () => {
|
|
906
|
+
this.CONSUME(IMAGE);
|
|
907
|
+
this.CONSUME(LBracket);
|
|
908
|
+
this.SUBRULE(this.parameterList);
|
|
909
|
+
this.CONSUME(RBracket);
|
|
910
|
+
});
|
|
911
|
+
this.visualFraction = this.RULE("visualFraction", () => {
|
|
912
|
+
this.CONSUME(VISUAL_FRACTION);
|
|
913
|
+
this.CONSUME(LBracket);
|
|
914
|
+
this.SUBRULE(this.parameterList);
|
|
915
|
+
this.CONSUME(RBracket);
|
|
916
|
+
});
|
|
917
|
+
this.numberArray = this.RULE("numberArray", () => {
|
|
918
|
+
this.CONSUME(LBracket);
|
|
919
|
+
this.MANY(() => {
|
|
920
|
+
this.CONSUME(Space);
|
|
921
|
+
});
|
|
922
|
+
this.OPTION(() => {
|
|
923
|
+
this.CONSUME(NumberLiteral, { LABEL: "items" });
|
|
924
|
+
this.MANY2(() => {
|
|
925
|
+
this.MANY3(() => {
|
|
926
|
+
this.CONSUME2(Space);
|
|
927
|
+
});
|
|
928
|
+
this.CONSUME(Comma);
|
|
929
|
+
this.MANY4(() => {
|
|
930
|
+
this.CONSUME3(Space);
|
|
931
|
+
});
|
|
932
|
+
this.CONSUME2(NumberLiteral, { LABEL: "items" });
|
|
933
|
+
});
|
|
934
|
+
this.OPTION2(() => {
|
|
935
|
+
this.MANY5(() => {
|
|
936
|
+
this.CONSUME4(Space);
|
|
937
|
+
});
|
|
938
|
+
this.CONSUME2(Comma);
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
this.MANY6(() => {
|
|
942
|
+
this.CONSUME5(Space);
|
|
943
|
+
});
|
|
944
|
+
this.CONSUME(RBracket);
|
|
945
|
+
});
|
|
883
946
|
this.inlineInput = this.RULE("inlineInput", () => {
|
|
884
947
|
this.OR([
|
|
885
948
|
{ ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
|
|
@@ -967,7 +1030,9 @@ class MathExerciseParser extends CstParser {
|
|
|
967
1030
|
// Tuple value: (start, end)
|
|
968
1031
|
{ ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
|
|
969
1032
|
// Curly-brace string list: {"a", "b", "c"}
|
|
970
|
-
{ ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) }
|
|
1033
|
+
{ ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) },
|
|
1034
|
+
// Square-bracket number array: [1, 2, 3] — used by VISUAL_FRACTION filled/answer
|
|
1035
|
+
{ ALT: () => this.SUBRULE(this.numberArray, { LABEL: "value" }) }
|
|
971
1036
|
]);
|
|
972
1037
|
});
|
|
973
1038
|
this.tuple = this.RULE("tuple", () => {
|
|
@@ -1023,6 +1088,25 @@ class MathExerciseParser extends CstParser {
|
|
|
1023
1088
|
}
|
|
1024
1089
|
}
|
|
1025
1090
|
const parser = new MathExerciseParser();
|
|
1091
|
+
function parseOrThrow(schema, data, label) {
|
|
1092
|
+
const result = schema.safeParse(data);
|
|
1093
|
+
if (!result.success) {
|
|
1094
|
+
const issue = result.error.issues[0];
|
|
1095
|
+
if (!issue) throw new Error(`${label}: validation failed`);
|
|
1096
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
1097
|
+
throw new Error(`${label}: ${path} ${issue.message}`);
|
|
1098
|
+
}
|
|
1099
|
+
return result.data;
|
|
1100
|
+
}
|
|
1101
|
+
const VISUAL_FRACTION_SHAPES = ["bar", "circle", "triangle", "grid"];
|
|
1102
|
+
const VISUAL_FRACTION_SHAPE = {
|
|
1103
|
+
BAR: "bar",
|
|
1104
|
+
CIRCLE: "circle",
|
|
1105
|
+
TRIANGLE: "triangle",
|
|
1106
|
+
GRID: "grid"
|
|
1107
|
+
};
|
|
1108
|
+
const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
|
|
1109
|
+
const VISUAL_FRACTION_MAX_DIVISIONS = 100;
|
|
1026
1110
|
const FreeTextInlineSchema = z.object({
|
|
1027
1111
|
type: z.literal(AST_TYPES.FREE_TEXT),
|
|
1028
1112
|
value: z.string()
|
|
@@ -1052,6 +1136,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
|
1052
1136
|
name: z.string(),
|
|
1053
1137
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
1054
1138
|
length: z.number().int().positive(),
|
|
1139
|
+
placeholder: z.string().max(32).optional(),
|
|
1055
1140
|
answer: z.union([z.string(), z.number()]).optional()
|
|
1056
1141
|
}),
|
|
1057
1142
|
z.object({
|
|
@@ -1059,6 +1144,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
|
1059
1144
|
name: z.string(),
|
|
1060
1145
|
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
1061
1146
|
digits: z.number().int().positive(),
|
|
1147
|
+
placeholder: z.string().max(32).optional(),
|
|
1062
1148
|
answer: z.union([z.string(), z.number()]).optional()
|
|
1063
1149
|
})
|
|
1064
1150
|
]);
|
|
@@ -1070,6 +1156,71 @@ const FractionInlineSchema = z.object({
|
|
|
1070
1156
|
answer: z.string().optional(),
|
|
1071
1157
|
simplified: z.boolean().default(true)
|
|
1072
1158
|
});
|
|
1159
|
+
const IMAGE_SRC_SCHEME_RE$1 = /^(https?:\/\/|data:image\/)/i;
|
|
1160
|
+
const ImageInlineSchema = z.object({
|
|
1161
|
+
type: z.literal(AST_TYPES.IMAGE),
|
|
1162
|
+
name: z.string().min(1),
|
|
1163
|
+
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE$1.test(s), {
|
|
1164
|
+
message: "src must use http(s) or data:image scheme"
|
|
1165
|
+
}),
|
|
1166
|
+
alt: z.string(),
|
|
1167
|
+
width: z.number().positive().optional(),
|
|
1168
|
+
height: z.number().positive().optional()
|
|
1169
|
+
});
|
|
1170
|
+
const VisualFractionInlineSchema = z.object({
|
|
1171
|
+
type: z.literal(AST_TYPES.VISUAL_FRACTION),
|
|
1172
|
+
name: z.string(),
|
|
1173
|
+
shape: z.enum(VISUAL_FRACTION_SHAPES).default("bar"),
|
|
1174
|
+
divisions: z.number().int().positive().max(VISUAL_FRACTION_MAX_DIVISIONS, {
|
|
1175
|
+
message: `divisions cannot exceed ${VISUAL_FRACTION_MAX_DIVISIONS}`
|
|
1176
|
+
}),
|
|
1177
|
+
filled: z.array(z.number().int().nonnegative()).default([]),
|
|
1178
|
+
answer: z.array(z.number().int().nonnegative()).default([]),
|
|
1179
|
+
orientation: z.enum(["horizontal", "vertical"]).optional(),
|
|
1180
|
+
label: z.string().optional(),
|
|
1181
|
+
/** Unified author size.
|
|
1182
|
+
* - bar / grid: overrides `--visual-fraction-part-size` (cell size).
|
|
1183
|
+
* - circle / triangle: maps to `--visual-fraction-size` with the renderer scale factor.
|
|
1184
|
+
* Omit to use CSS defaults. */
|
|
1185
|
+
size: z.number().positive().optional()
|
|
1186
|
+
}).superRefine(validateVisualFractionCapacity);
|
|
1187
|
+
function validateVisualFractionCapacity(data, ctx) {
|
|
1188
|
+
const filledTotal = data.filled.reduce((sum, count) => sum + count, 0);
|
|
1189
|
+
const answerTotal = data.answer.reduce((sum, count) => sum + count, 0);
|
|
1190
|
+
if (filledTotal > data.divisions) {
|
|
1191
|
+
ctx.addIssue({
|
|
1192
|
+
code: z.ZodIssueCode.custom,
|
|
1193
|
+
message: `filled total (${filledTotal}) cannot exceed divisions (${data.divisions})`,
|
|
1194
|
+
path: ["filled"]
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
if (filledTotal + answerTotal > data.divisions) {
|
|
1198
|
+
ctx.addIssue({
|
|
1199
|
+
code: z.ZodIssueCode.custom,
|
|
1200
|
+
message: `filled + answer total (${filledTotal + answerTotal}) cannot exceed divisions (${data.divisions})`,
|
|
1201
|
+
path: ["answer"]
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
const shape = data.shape;
|
|
1205
|
+
if (shape === "triangle" && !isPerfectSquare(data.divisions)) {
|
|
1206
|
+
ctx.addIssue({
|
|
1207
|
+
code: z.ZodIssueCode.custom,
|
|
1208
|
+
message: `triangle divisions (${data.divisions}) must be a perfect square to render equal triangle parts`,
|
|
1209
|
+
path: ["divisions"]
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
if (shape === "grid" && !isPerfectSquare(data.divisions)) {
|
|
1213
|
+
ctx.addIssue({
|
|
1214
|
+
code: z.ZodIssueCode.custom,
|
|
1215
|
+
message: `grid divisions (${data.divisions}) must be a perfect square to render equal rows and columns`,
|
|
1216
|
+
path: ["divisions"]
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function isPerfectSquare(value) {
|
|
1221
|
+
const root = Math.sqrt(value);
|
|
1222
|
+
return Number.isInteger(root);
|
|
1223
|
+
}
|
|
1073
1224
|
const InlineNodeSchema = z.union([
|
|
1074
1225
|
FreeTextInlineSchema,
|
|
1075
1226
|
SpaceInlineSchema,
|
|
@@ -1077,7 +1228,9 @@ const InlineNodeSchema = z.union([
|
|
|
1077
1228
|
ColorTextInlineSchema,
|
|
1078
1229
|
BoldTextInlineSchema,
|
|
1079
1230
|
InlineInputWidgetSchema,
|
|
1080
|
-
FractionInlineSchema
|
|
1231
|
+
FractionInlineSchema,
|
|
1232
|
+
ImageInlineSchema,
|
|
1233
|
+
VisualFractionInlineSchema
|
|
1081
1234
|
]);
|
|
1082
1235
|
function createFreeText(value) {
|
|
1083
1236
|
return FreeTextInlineSchema.parse({
|
|
@@ -1112,22 +1265,33 @@ function createBoldText(value, highlight) {
|
|
|
1112
1265
|
highlight
|
|
1113
1266
|
});
|
|
1114
1267
|
}
|
|
1115
|
-
function createStringInput(name, length, answer) {
|
|
1116
|
-
return InlineInputWidgetSchema
|
|
1268
|
+
function createStringInput(name, length, answer, placeholder) {
|
|
1269
|
+
return parseOrThrow(InlineInputWidgetSchema, {
|
|
1117
1270
|
type: AST_TYPES.INLINE_INPUT,
|
|
1118
1271
|
name,
|
|
1119
1272
|
inputType: INPUT_TYPES.STRING,
|
|
1120
1273
|
length,
|
|
1274
|
+
placeholder,
|
|
1121
1275
|
answer
|
|
1122
|
-
});
|
|
1276
|
+
}, "STRING_INPUT");
|
|
1123
1277
|
}
|
|
1124
|
-
function createNumericInput(name, digits, answer) {
|
|
1125
|
-
return InlineInputWidgetSchema
|
|
1278
|
+
function createNumericInput(name, digits, answer, placeholder) {
|
|
1279
|
+
return parseOrThrow(InlineInputWidgetSchema, {
|
|
1126
1280
|
type: AST_TYPES.INLINE_INPUT,
|
|
1127
1281
|
name,
|
|
1128
1282
|
inputType: INPUT_TYPES.NUMERIC,
|
|
1129
1283
|
digits,
|
|
1284
|
+
placeholder,
|
|
1130
1285
|
answer
|
|
1286
|
+
}, "NUMERIC_INPUT");
|
|
1287
|
+
}
|
|
1288
|
+
function createImage(name, src, alt, options = {}) {
|
|
1289
|
+
return ImageInlineSchema.parse({
|
|
1290
|
+
type: AST_TYPES.IMAGE,
|
|
1291
|
+
name,
|
|
1292
|
+
src,
|
|
1293
|
+
alt,
|
|
1294
|
+
...options
|
|
1131
1295
|
});
|
|
1132
1296
|
}
|
|
1133
1297
|
function createFraction(name, options = {}) {
|
|
@@ -1137,6 +1301,14 @@ function createFraction(name, options = {}) {
|
|
|
1137
1301
|
...options
|
|
1138
1302
|
});
|
|
1139
1303
|
}
|
|
1304
|
+
function createVisualFraction(name, divisions, options = {}) {
|
|
1305
|
+
return VisualFractionInlineSchema.parse({
|
|
1306
|
+
type: AST_TYPES.VISUAL_FRACTION,
|
|
1307
|
+
name,
|
|
1308
|
+
divisions,
|
|
1309
|
+
...options
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1140
1312
|
function isFreeText(value) {
|
|
1141
1313
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
1142
1314
|
}
|
|
@@ -1149,9 +1321,15 @@ function isInlineInput(value) {
|
|
|
1149
1321
|
function isFraction(value) {
|
|
1150
1322
|
return FractionInlineSchema.safeParse(value).success;
|
|
1151
1323
|
}
|
|
1324
|
+
function isImage(value) {
|
|
1325
|
+
return ImageInlineSchema.safeParse(value).success;
|
|
1326
|
+
}
|
|
1152
1327
|
function isInlineNode(value) {
|
|
1153
1328
|
return InlineNodeSchema.safeParse(value).success;
|
|
1154
1329
|
}
|
|
1330
|
+
function isVisualFractionInline(value) {
|
|
1331
|
+
return VisualFractionInlineSchema.safeParse(value).success;
|
|
1332
|
+
}
|
|
1155
1333
|
const TextBlockSchema = z.object({
|
|
1156
1334
|
type: z.literal(AST_TYPES.TEXT_BLOCK),
|
|
1157
1335
|
value: z.string()
|
|
@@ -1347,13 +1525,19 @@ const DisplayFractionInlineSchema = FractionInlineSchema.refine(
|
|
|
1347
1525
|
(f) => f.numerator !== void 0 && f.denominator !== void 0,
|
|
1348
1526
|
{ message: "FRACTION inside MATCHER ITEM must be in display mode (both numerator and denominator required)" }
|
|
1349
1527
|
);
|
|
1528
|
+
const DisplayVisualFractionInlineSchema = VisualFractionInlineSchema.refine(
|
|
1529
|
+
(vf) => vf.answer.length === 0,
|
|
1530
|
+
{ message: "VISUAL_FRACTION inside MATCHER ITEM must be display-only (answer must be empty: answer=[])" }
|
|
1531
|
+
);
|
|
1350
1532
|
const DisplayInlineNodeSchema = z.union([
|
|
1351
1533
|
FreeTextInlineSchema,
|
|
1352
1534
|
SpaceInlineSchema,
|
|
1353
1535
|
HighlightedTextInlineSchema,
|
|
1354
1536
|
ColorTextInlineSchema,
|
|
1355
1537
|
BoldTextInlineSchema,
|
|
1356
|
-
DisplayFractionInlineSchema
|
|
1538
|
+
DisplayFractionInlineSchema,
|
|
1539
|
+
ImageInlineSchema,
|
|
1540
|
+
DisplayVisualFractionInlineSchema
|
|
1357
1541
|
]);
|
|
1358
1542
|
const MatcherItemSchema = z.object({
|
|
1359
1543
|
type: z.literal(AST_TYPES.MATCHER_ITEM),
|
|
@@ -1449,7 +1633,8 @@ const DragOptionsToolSchema = z.object({
|
|
|
1449
1633
|
const NumericKeypadToolSchema = z.object({
|
|
1450
1634
|
type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
1451
1635
|
includeOperators: z.boolean().optional(),
|
|
1452
|
-
includeDecimal: z.boolean().optional()
|
|
1636
|
+
includeDecimal: z.boolean().optional(),
|
|
1637
|
+
style: z.enum(["diamond", "classic"]).optional()
|
|
1453
1638
|
});
|
|
1454
1639
|
const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
1455
1640
|
DragOptionsToolSchema,
|
|
@@ -1461,11 +1646,12 @@ function createDragOptions(options) {
|
|
|
1461
1646
|
options
|
|
1462
1647
|
});
|
|
1463
1648
|
}
|
|
1464
|
-
function createNumericKeypad(includeOperators, includeDecimal) {
|
|
1649
|
+
function createNumericKeypad(includeOperators, includeDecimal, style) {
|
|
1465
1650
|
return NumericKeypadToolSchema.parse({
|
|
1466
1651
|
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
1467
1652
|
includeOperators,
|
|
1468
|
-
includeDecimal
|
|
1653
|
+
includeDecimal,
|
|
1654
|
+
style
|
|
1469
1655
|
});
|
|
1470
1656
|
}
|
|
1471
1657
|
function isDragOptions(value) {
|
|
@@ -1529,6 +1715,51 @@ const ExerciseDocumentSchema = z.object({
|
|
|
1529
1715
|
lines: z.array(ContentBlockSchema),
|
|
1530
1716
|
tools: z.array(ToolNodeSchema).optional(),
|
|
1531
1717
|
validation: ValidationNodeSchema.optional()
|
|
1718
|
+
}).superRefine((doc, ctx) => {
|
|
1719
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1720
|
+
const record = (name, path) => {
|
|
1721
|
+
if (seen.has(name)) {
|
|
1722
|
+
ctx.addIssue({
|
|
1723
|
+
code: z.ZodIssueCode.custom,
|
|
1724
|
+
message: `Duplicate name "${name}" — widget/image names must be unique across the document`,
|
|
1725
|
+
path
|
|
1726
|
+
});
|
|
1727
|
+
} else {
|
|
1728
|
+
seen.add(name);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
const visitInlines = (inlines, basePath) => {
|
|
1732
|
+
inlines.forEach((inline, i) => {
|
|
1733
|
+
if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE) {
|
|
1734
|
+
record(inline.name, [...basePath, i, "name"]);
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
};
|
|
1738
|
+
doc.lines.forEach((block, bi) => {
|
|
1739
|
+
if (block.type === AST_TYPES.LINE_BLOCK) {
|
|
1740
|
+
visitInlines(block.inlines, ["lines", bi, "inlines"]);
|
|
1741
|
+
} else if (block.type === AST_TYPES.TABLE_BLOCK) {
|
|
1742
|
+
block.rows.forEach((row, ri) => {
|
|
1743
|
+
row.cells.forEach((cell, ci) => {
|
|
1744
|
+
visitInlines(cell.inlines, ["lines", bi, "rows", ri, "cells", ci, "inlines"]);
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
1748
|
+
block.positions.forEach((pos, pi) => {
|
|
1749
|
+
visitInlines(pos.inlines, ["lines", bi, "positions", pi, "inlines"]);
|
|
1750
|
+
});
|
|
1751
|
+
} else if (block.type === AST_TYPES.MATCHER_BLOCK) {
|
|
1752
|
+
const m = block;
|
|
1753
|
+
record(m.name, ["lines", bi, "name"]);
|
|
1754
|
+
m.rows.forEach((row, ri) => {
|
|
1755
|
+
row.items.forEach((item, ii) => {
|
|
1756
|
+
visitInlines(item.inlines, ["lines", bi, "rows", ri, "items", ii, "inlines"]);
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
1760
|
+
record(block.name, ["lines", bi, "name"]);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1532
1763
|
});
|
|
1533
1764
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
1534
1765
|
return ExerciseDocumentSchema.parse({
|
|
@@ -1771,6 +2002,12 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1771
2002
|
if (ctx.fraction) {
|
|
1772
2003
|
return this.visit(ctx.fraction);
|
|
1773
2004
|
}
|
|
2005
|
+
if (ctx.image) {
|
|
2006
|
+
return this.visit(ctx.image);
|
|
2007
|
+
}
|
|
2008
|
+
if (ctx.visualFraction) {
|
|
2009
|
+
return this.visit(ctx.visualFraction);
|
|
2010
|
+
}
|
|
1774
2011
|
if (ctx.inlineInput) {
|
|
1775
2012
|
return this.visit(ctx.inlineInput);
|
|
1776
2013
|
}
|
|
@@ -1813,16 +2050,41 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1813
2050
|
const simplified = params.simplified !== void 0 ? params.simplified : void 0;
|
|
1814
2051
|
return createFraction(name, { numerator, denominator, answer, simplified });
|
|
1815
2052
|
}
|
|
2053
|
+
image(ctx) {
|
|
2054
|
+
const params = this.visit(ctx.parameterList);
|
|
2055
|
+
const name = params.name;
|
|
2056
|
+
const src = params.src;
|
|
2057
|
+
const alt = params.alt;
|
|
2058
|
+
const width = params.width;
|
|
2059
|
+
const height = params.height;
|
|
2060
|
+
return createImage(name, src, alt, { width, height });
|
|
2061
|
+
}
|
|
2062
|
+
visualFraction(ctx) {
|
|
2063
|
+
const params = this.visit(ctx.parameterList);
|
|
2064
|
+
const name = params.name;
|
|
2065
|
+
const divisions = params.divisions;
|
|
2066
|
+
const shape = params.shape;
|
|
2067
|
+
const filled = params.filled ?? [];
|
|
2068
|
+
const answer = params.answer ?? [];
|
|
2069
|
+
const orientation = params.orientation;
|
|
2070
|
+
const label = params.label !== void 0 ? params.label : void 0;
|
|
2071
|
+
const size = params.size !== void 0 ? params.size : void 0;
|
|
2072
|
+
return createVisualFraction(name, divisions, { shape, filled, answer, orientation, label, size });
|
|
2073
|
+
}
|
|
2074
|
+
numberArray(ctx) {
|
|
2075
|
+
return ctx.items ? ctx.items.map((t) => parseFloat(t.image)) : [];
|
|
2076
|
+
}
|
|
1816
2077
|
inlineInput(ctx) {
|
|
1817
2078
|
const params = this.visit(ctx.parameterList);
|
|
1818
2079
|
const name = params.name;
|
|
1819
2080
|
const answer = params.answer;
|
|
2081
|
+
const placeholder = params.placeholder;
|
|
1820
2082
|
if (ctx.stringInput) {
|
|
1821
2083
|
const length = params.length;
|
|
1822
|
-
return createStringInput(name, length, answer);
|
|
2084
|
+
return createStringInput(name, length, answer, placeholder);
|
|
1823
2085
|
}
|
|
1824
2086
|
const digits = params.digits;
|
|
1825
|
-
return createNumericInput(name, digits, answer);
|
|
2087
|
+
return createNumericInput(name, digits, answer, placeholder);
|
|
1826
2088
|
}
|
|
1827
2089
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1828
2090
|
// Tools
|
|
@@ -1844,7 +2106,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1844
2106
|
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
1845
2107
|
const includeOperators = params.includeOperators === true || params.includeOperators === "true";
|
|
1846
2108
|
const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
|
|
1847
|
-
|
|
2109
|
+
const style = params.style;
|
|
2110
|
+
return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0, style);
|
|
1848
2111
|
}
|
|
1849
2112
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1850
2113
|
// Validation
|
|
@@ -1971,6 +2234,18 @@ const BoldTextSchema = z.object({
|
|
|
1971
2234
|
value: z.string(),
|
|
1972
2235
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
1973
2236
|
});
|
|
2237
|
+
const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
|
|
2238
|
+
const ImageItemSchema = z.object({
|
|
2239
|
+
id: z.string(),
|
|
2240
|
+
type: z.literal(JSON_TYPES.IMAGE),
|
|
2241
|
+
name: z.string().min(1),
|
|
2242
|
+
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE.test(s), {
|
|
2243
|
+
message: "src must use http(s) or data:image scheme"
|
|
2244
|
+
}),
|
|
2245
|
+
alt: z.string(),
|
|
2246
|
+
width: z.number().positive().optional(),
|
|
2247
|
+
height: z.number().positive().optional()
|
|
2248
|
+
});
|
|
1974
2249
|
const SpaceItemSchema = z.object({
|
|
1975
2250
|
id: z.string(),
|
|
1976
2251
|
type: z.literal(JSON_TYPES.SPACE)
|
|
@@ -1993,6 +2268,7 @@ const LayoutItemSchema = z.lazy(
|
|
|
1993
2268
|
HighlightedTextSchema,
|
|
1994
2269
|
ColorTextSchema,
|
|
1995
2270
|
BoldTextSchema,
|
|
2271
|
+
ImageItemSchema,
|
|
1996
2272
|
SpaceItemSchema,
|
|
1997
2273
|
LinebreakSchema,
|
|
1998
2274
|
GroupItemSchema,
|
|
@@ -2074,12 +2350,14 @@ const MatcherRowItemSchema = z.object({
|
|
|
2074
2350
|
const StringInputJSONSchema = z.object({
|
|
2075
2351
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
2076
2352
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
2077
|
-
length: z.number().int().positive()
|
|
2353
|
+
length: z.number().int().positive(),
|
|
2354
|
+
placeholder: z.string().max(32).optional()
|
|
2078
2355
|
});
|
|
2079
2356
|
const NumericInputJSONSchema = z.object({
|
|
2080
2357
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
2081
2358
|
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
2082
|
-
digits: z.number().int().positive()
|
|
2359
|
+
digits: z.number().int().positive(),
|
|
2360
|
+
placeholder: z.string().max(32).optional()
|
|
2083
2361
|
});
|
|
2084
2362
|
const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
2085
2363
|
StringInputJSONSchema,
|
|
@@ -2096,7 +2374,8 @@ const RadioOptionsJSONSchema = z.object({
|
|
|
2096
2374
|
const NumericKeypadJSONSchema = z.object({
|
|
2097
2375
|
type: z.literal(JSON_TYPES.NUMERIC_KEYPAD),
|
|
2098
2376
|
includeOperators: z.boolean().optional(),
|
|
2099
|
-
includeDecimal: z.boolean().optional()
|
|
2377
|
+
includeDecimal: z.boolean().optional(),
|
|
2378
|
+
style: z.enum(["diamond", "classic"]).optional()
|
|
2100
2379
|
});
|
|
2101
2380
|
const FractionJSONSchema = z.object({
|
|
2102
2381
|
type: z.literal(JSON_TYPES.FRACTION),
|
|
@@ -2112,24 +2391,44 @@ const MatcherWidgetJSONSchema = z.object({
|
|
|
2112
2391
|
/** Number of answer groups — equals answers.length, stored here to avoid prop-drilling. */
|
|
2113
2392
|
groupCount: z.number().int().min(1)
|
|
2114
2393
|
});
|
|
2394
|
+
const VisualFractionJSONSchema = z.object({
|
|
2395
|
+
type: z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
2396
|
+
shape: z.enum(["bar", "circle", "triangle", "grid"]).default("bar"),
|
|
2397
|
+
divisions: z.number().int().positive().max(VISUAL_FRACTION_MAX_DIVISIONS, {
|
|
2398
|
+
message: `divisions cannot exceed ${VISUAL_FRACTION_MAX_DIVISIONS}`
|
|
2399
|
+
}),
|
|
2400
|
+
filled: z.array(z.number().int().nonnegative()),
|
|
2401
|
+
answer: z.array(z.number().int().nonnegative()),
|
|
2402
|
+
orientation: z.enum(["horizontal", "vertical"]).optional(),
|
|
2403
|
+
/** Optional label text rendered near the shape — replaces the old showLabel boolean */
|
|
2404
|
+
label: z.string().optional(),
|
|
2405
|
+
/** Overall shape size in unified author units.
|
|
2406
|
+
* - bar / grid: overrides `--visual-fraction-part-size` CSS custom property (cell size in rem).
|
|
2407
|
+
* - circle / triangle: overrides `--visual-fraction-size` through the renderer scale factor.
|
|
2408
|
+
*/
|
|
2409
|
+
size: z.number().positive().optional()
|
|
2410
|
+
}).superRefine(validateVisualFractionCapacity);
|
|
2115
2411
|
const WidgetJSONSchema = z.union([
|
|
2116
2412
|
InlineInputJSONSchema,
|
|
2117
2413
|
DragOptionsJSONSchema,
|
|
2118
2414
|
RadioOptionsJSONSchema,
|
|
2119
2415
|
NumericKeypadJSONSchema,
|
|
2120
2416
|
FractionJSONSchema,
|
|
2121
|
-
MatcherWidgetJSONSchema
|
|
2417
|
+
MatcherWidgetJSONSchema,
|
|
2418
|
+
VisualFractionJSONSchema
|
|
2122
2419
|
]);
|
|
2123
2420
|
const ValidationJSONSchema = z.object({
|
|
2124
2421
|
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
2125
2422
|
// Simple answers: string|number per widget id.
|
|
2126
2423
|
// Matcher answers: string[][] keyed by matcher name. Outer/inner min(1)
|
|
2127
2424
|
// mirrors MatcherBlockSchema invariants in src/ast/matcher.ts.
|
|
2425
|
+
// VisualFraction answers: number[] — one count per color group.
|
|
2128
2426
|
answers: z.record(
|
|
2129
2427
|
z.union([
|
|
2130
2428
|
z.string(),
|
|
2131
2429
|
z.number(),
|
|
2132
|
-
z.array(z.array(z.string().min(1)).min(1)).min(1)
|
|
2430
|
+
z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
2431
|
+
z.array(z.number().int().nonnegative())
|
|
2133
2432
|
])
|
|
2134
2433
|
).optional(),
|
|
2135
2434
|
expression: z.string().optional(),
|
|
@@ -2179,6 +2478,9 @@ function isColorText(item) {
|
|
|
2179
2478
|
function isBoldText(item) {
|
|
2180
2479
|
return item.type === JSON_TYPES.BOLD_TEXT;
|
|
2181
2480
|
}
|
|
2481
|
+
function isImageItem(item) {
|
|
2482
|
+
return item.type === JSON_TYPES.IMAGE;
|
|
2483
|
+
}
|
|
2182
2484
|
function isSpaceItem(item) {
|
|
2183
2485
|
return item.type === JSON_TYPES.SPACE;
|
|
2184
2486
|
}
|
|
@@ -2277,13 +2579,15 @@ function toJSON(ast) {
|
|
|
2277
2579
|
widgets[inline.name] = {
|
|
2278
2580
|
type: JSON_TYPES.INLINE_INPUT,
|
|
2279
2581
|
inputType: INPUT_TYPES.STRING,
|
|
2280
|
-
length: inline.length
|
|
2582
|
+
length: inline.length,
|
|
2583
|
+
placeholder: inline.placeholder
|
|
2281
2584
|
};
|
|
2282
2585
|
} else {
|
|
2283
2586
|
widgets[inline.name] = {
|
|
2284
2587
|
type: JSON_TYPES.INLINE_INPUT,
|
|
2285
2588
|
inputType: INPUT_TYPES.NUMERIC,
|
|
2286
|
-
digits: inline.digits
|
|
2589
|
+
digits: inline.digits,
|
|
2590
|
+
placeholder: inline.placeholder
|
|
2287
2591
|
};
|
|
2288
2592
|
}
|
|
2289
2593
|
if (inline.answer !== void 0) {
|
|
@@ -2301,6 +2605,22 @@ function toJSON(ast) {
|
|
|
2301
2605
|
answers[inline.name] = inline.answer;
|
|
2302
2606
|
}
|
|
2303
2607
|
}
|
|
2608
|
+
if (inline.type === AST_TYPES.VISUAL_FRACTION) {
|
|
2609
|
+
const answerArr = inline.answer ?? [];
|
|
2610
|
+
widgets[inline.name] = {
|
|
2611
|
+
type: JSON_TYPES.VISUAL_FRACTION,
|
|
2612
|
+
shape: inline.shape,
|
|
2613
|
+
divisions: inline.divisions,
|
|
2614
|
+
filled: inline.filled ?? [],
|
|
2615
|
+
answer: answerArr,
|
|
2616
|
+
...inline.orientation !== void 0 && { orientation: inline.orientation },
|
|
2617
|
+
...inline.label !== void 0 && { label: inline.label },
|
|
2618
|
+
...inline.size !== void 0 && { size: inline.size }
|
|
2619
|
+
};
|
|
2620
|
+
if (answerArr.length > 0) {
|
|
2621
|
+
answers[inline.name] = answerArr;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2304
2624
|
}
|
|
2305
2625
|
for (const block of ast.lines) {
|
|
2306
2626
|
if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
@@ -2327,7 +2647,8 @@ function toJSON(ast) {
|
|
|
2327
2647
|
widgets["_numeric_keypad"] = {
|
|
2328
2648
|
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
2329
2649
|
includeOperators: tool.includeOperators,
|
|
2330
|
-
includeDecimal: tool.includeDecimal
|
|
2650
|
+
includeDecimal: tool.includeDecimal,
|
|
2651
|
+
style: tool.style
|
|
2331
2652
|
};
|
|
2332
2653
|
}
|
|
2333
2654
|
}
|
|
@@ -2531,6 +2852,22 @@ function transformInline(inline, idGenerator) {
|
|
|
2531
2852
|
type: JSON_TYPES.WIDGET_REF,
|
|
2532
2853
|
widgetId: inline.name
|
|
2533
2854
|
};
|
|
2855
|
+
case AST_TYPES.IMAGE:
|
|
2856
|
+
return {
|
|
2857
|
+
id: idGenerator.generate(JSON_TYPES.IMAGE),
|
|
2858
|
+
type: JSON_TYPES.IMAGE,
|
|
2859
|
+
name: inline.name,
|
|
2860
|
+
src: inline.src,
|
|
2861
|
+
alt: inline.alt,
|
|
2862
|
+
...inline.width !== void 0 && { width: inline.width },
|
|
2863
|
+
...inline.height !== void 0 && { height: inline.height }
|
|
2864
|
+
};
|
|
2865
|
+
case AST_TYPES.VISUAL_FRACTION:
|
|
2866
|
+
return {
|
|
2867
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
2868
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
2869
|
+
widgetId: inline.name
|
|
2870
|
+
};
|
|
2534
2871
|
default:
|
|
2535
2872
|
throw new Error(`Unknown inline type: ${inline.type}`);
|
|
2536
2873
|
}
|
|
@@ -8073,6 +8410,11 @@ function areMatcherEqual(expected, result) {
|
|
|
8073
8410
|
const normalize2 = (groups) => lodashExports.sortBy(groups.map((g) => lodashExports.sortBy(g)), (g) => g.join("|"));
|
|
8074
8411
|
return lodashExports.isEqual(normalize2(expected), normalize2(result));
|
|
8075
8412
|
}
|
|
8413
|
+
function areVisualFractionCountsEqual(expected, result) {
|
|
8414
|
+
if (!Array.isArray(result)) return false;
|
|
8415
|
+
if (expected.length !== result.length) return false;
|
|
8416
|
+
return lodashExports.isEqual(lodashExports.sortBy(expected), lodashExports.sortBy(result));
|
|
8417
|
+
}
|
|
8076
8418
|
const math$1 = create(all);
|
|
8077
8419
|
function collectNumberLines(items) {
|
|
8078
8420
|
const result = [];
|
|
@@ -8121,6 +8463,15 @@ function validateSimple(exercise, userInputs) {
|
|
|
8121
8463
|
else allCorrect = false;
|
|
8122
8464
|
continue;
|
|
8123
8465
|
}
|
|
8466
|
+
if (widget?.type === JSON_TYPES.VISUAL_FRACTION) {
|
|
8467
|
+
const userValue2 = userInputs[widgetId];
|
|
8468
|
+
const isCorrect2 = areVisualFractionCountsEqual(expected, userValue2);
|
|
8469
|
+
details[widgetId] = { widgetId, userValue: userValue2, isCorrect: isCorrect2 };
|
|
8470
|
+
totalCount++;
|
|
8471
|
+
if (isCorrect2) correctCount++;
|
|
8472
|
+
else allCorrect = false;
|
|
8473
|
+
continue;
|
|
8474
|
+
}
|
|
8124
8475
|
totalCount++;
|
|
8125
8476
|
const userValue = userInputs[widgetId];
|
|
8126
8477
|
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
@@ -8627,10 +8978,10 @@ function getOwnerDocument(target) {
|
|
|
8627
8978
|
}
|
|
8628
8979
|
return document;
|
|
8629
8980
|
}
|
|
8630
|
-
const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;
|
|
8981
|
+
const useIsomorphicLayoutEffect$1 = canUseDOM ? useLayoutEffect : useEffect;
|
|
8631
8982
|
function useEvent(handler) {
|
|
8632
8983
|
const handlerRef = useRef(handler);
|
|
8633
|
-
useIsomorphicLayoutEffect(() => {
|
|
8984
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
8634
8985
|
handlerRef.current = handler;
|
|
8635
8986
|
});
|
|
8636
8987
|
return useCallback(function() {
|
|
@@ -8658,7 +9009,7 @@ function useLatestValue(value, dependencies) {
|
|
|
8658
9009
|
dependencies = [value];
|
|
8659
9010
|
}
|
|
8660
9011
|
const valueRef = useRef(value);
|
|
8661
|
-
useIsomorphicLayoutEffect(() => {
|
|
9012
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
8662
9013
|
if (valueRef.current !== value) {
|
|
8663
9014
|
valueRef.current = value;
|
|
8664
9015
|
}
|
|
@@ -10526,9 +10877,9 @@ function useResizeObserver(_ref) {
|
|
|
10526
10877
|
return void 0;
|
|
10527
10878
|
}
|
|
10528
10879
|
const {
|
|
10529
|
-
ResizeObserver
|
|
10880
|
+
ResizeObserver: ResizeObserver2
|
|
10530
10881
|
} = window;
|
|
10531
|
-
return new
|
|
10882
|
+
return new ResizeObserver2(handleResize);
|
|
10532
10883
|
},
|
|
10533
10884
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
10534
10885
|
[disabled]
|
|
@@ -10582,7 +10933,7 @@ function useRect(element, measure, fallbackRect) {
|
|
|
10582
10933
|
const resizeObserver = useResizeObserver({
|
|
10583
10934
|
callback: measureRect
|
|
10584
10935
|
});
|
|
10585
|
-
useIsomorphicLayoutEffect(() => {
|
|
10936
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
10586
10937
|
measureRect();
|
|
10587
10938
|
if (element) {
|
|
10588
10939
|
resizeObserver == null ? void 0 : resizeObserver.observe(element);
|
|
@@ -10756,7 +11107,7 @@ function useRects(elements, measure) {
|
|
|
10756
11107
|
const resizeObserver = useResizeObserver({
|
|
10757
11108
|
callback: measureRects
|
|
10758
11109
|
});
|
|
10759
|
-
useIsomorphicLayoutEffect(() => {
|
|
11110
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
10760
11111
|
resizeObserver == null ? void 0 : resizeObserver.disconnect();
|
|
10761
11112
|
measureRects();
|
|
10762
11113
|
elements.forEach((element) => resizeObserver == null ? void 0 : resizeObserver.observe(element));
|
|
@@ -11118,7 +11469,7 @@ function useLayoutShiftScrollCompensation(_ref) {
|
|
|
11118
11469
|
x: config,
|
|
11119
11470
|
y: config
|
|
11120
11471
|
} : config;
|
|
11121
|
-
useIsomorphicLayoutEffect(() => {
|
|
11472
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11122
11473
|
const disabled = !x && !y;
|
|
11123
11474
|
if (disabled || !activeNode) {
|
|
11124
11475
|
initialized.current = false;
|
|
@@ -11479,7 +11830,7 @@ const DndContext = /* @__PURE__ */ memo(function DndContext2(_ref) {
|
|
|
11479
11830
|
}, [draggableNodes, instantiateSensor]);
|
|
11480
11831
|
const activators = useCombineActivators(sensors, bindActivatorToSensorInstantiator);
|
|
11481
11832
|
useSensorSetup(sensors);
|
|
11482
|
-
useIsomorphicLayoutEffect(() => {
|
|
11833
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11483
11834
|
if (activeNodeRect && status === Status.Initializing) {
|
|
11484
11835
|
setStatus(Status.Initialized);
|
|
11485
11836
|
}
|
|
@@ -11563,7 +11914,7 @@ const DndContext = /* @__PURE__ */ memo(function DndContext2(_ref) {
|
|
|
11563
11914
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
11564
11915
|
[overId]
|
|
11565
11916
|
);
|
|
11566
|
-
useIsomorphicLayoutEffect(() => {
|
|
11917
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11567
11918
|
sensorContext.current = {
|
|
11568
11919
|
activatorEvent,
|
|
11569
11920
|
active,
|
|
@@ -11690,7 +12041,7 @@ function useDraggable(_ref) {
|
|
|
11690
12041
|
const [activatorNode, setActivatorNodeRef] = useNodeRef();
|
|
11691
12042
|
const listeners = useSyntheticListeners(activators, id);
|
|
11692
12043
|
const dataRef = useLatestValue(data);
|
|
11693
|
-
useIsomorphicLayoutEffect(
|
|
12044
|
+
useIsomorphicLayoutEffect$1(
|
|
11694
12045
|
() => {
|
|
11695
12046
|
draggableNodes.set(id, {
|
|
11696
12047
|
id,
|
|
@@ -11863,7 +12214,7 @@ function AnimationManager(_ref) {
|
|
|
11863
12214
|
if (!children && !clonedChildren && previousChildren) {
|
|
11864
12215
|
setClonedChildren(previousChildren);
|
|
11865
12216
|
}
|
|
11866
|
-
useIsomorphicLayoutEffect(() => {
|
|
12217
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11867
12218
|
if (!element) {
|
|
11868
12219
|
return;
|
|
11869
12220
|
}
|
|
@@ -12545,6 +12896,7 @@ const InlineInput = ({
|
|
|
12545
12896
|
inputType,
|
|
12546
12897
|
length,
|
|
12547
12898
|
digits,
|
|
12899
|
+
placeholder,
|
|
12548
12900
|
onChange,
|
|
12549
12901
|
onFocus
|
|
12550
12902
|
}) => {
|
|
@@ -12590,19 +12942,88 @@ const InlineInput = ({
|
|
|
12590
12942
|
onChange: (e) => handleChange(e.target.value, false),
|
|
12591
12943
|
onFocus: () => onFocus?.(widgetId),
|
|
12592
12944
|
maxLength: expectedLength,
|
|
12593
|
-
style: { width: dynamicWidth }
|
|
12945
|
+
style: { width: dynamicWidth },
|
|
12946
|
+
placeholder
|
|
12594
12947
|
}
|
|
12595
12948
|
)
|
|
12596
12949
|
)
|
|
12597
12950
|
}
|
|
12598
12951
|
);
|
|
12599
12952
|
};
|
|
12953
|
+
const PillBackground = ({
|
|
12954
|
+
width = "100%",
|
|
12955
|
+
minHeight = "72px",
|
|
12956
|
+
fill = "#FFE7B4",
|
|
12957
|
+
children,
|
|
12958
|
+
className = "",
|
|
12959
|
+
style,
|
|
12960
|
+
...rest
|
|
12961
|
+
}) => {
|
|
12962
|
+
return /* @__PURE__ */ jsxs(
|
|
12963
|
+
"div",
|
|
12964
|
+
{
|
|
12965
|
+
className: `pill-bg-wrapper ${className}`.trim(),
|
|
12966
|
+
style: {
|
|
12967
|
+
position: "relative",
|
|
12968
|
+
display: "inline-flex",
|
|
12969
|
+
alignItems: "center",
|
|
12970
|
+
justifyContent: "center",
|
|
12971
|
+
width,
|
|
12972
|
+
minHeight,
|
|
12973
|
+
...style
|
|
12974
|
+
},
|
|
12975
|
+
...rest,
|
|
12976
|
+
children: [
|
|
12977
|
+
/* @__PURE__ */ jsx(
|
|
12978
|
+
"svg",
|
|
12979
|
+
{
|
|
12980
|
+
viewBox: "0 0 510 108",
|
|
12981
|
+
preserveAspectRatio: "none",
|
|
12982
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
12983
|
+
"aria-hidden": "true",
|
|
12984
|
+
style: {
|
|
12985
|
+
position: "absolute",
|
|
12986
|
+
inset: 0,
|
|
12987
|
+
width: "100%",
|
|
12988
|
+
height: "100%",
|
|
12989
|
+
display: "block"
|
|
12990
|
+
},
|
|
12991
|
+
children: /* @__PURE__ */ jsx(
|
|
12992
|
+
"path",
|
|
12993
|
+
{
|
|
12994
|
+
d: "M439.707 0C457.071 0 472.52 8.196 482.398 20.9297C498.608 26.3308 510 39.1028 510 54C510 68.8966 498.608 81.667 482.399 87.0684C472.521 99.803 457.072 108 439.707 108H70.8975C53.653 108 38.2956 99.9172 28.4092 87.333C11.7719 82.0918 0.00025108 69.1432 0 54C0 38.8562 11.7721 25.906 28.4102 20.665C38.2966 8.08186 53.6537 0 70.8975 0H439.707Z",
|
|
12995
|
+
fill
|
|
12996
|
+
}
|
|
12997
|
+
)
|
|
12998
|
+
}
|
|
12999
|
+
),
|
|
13000
|
+
/* @__PURE__ */ jsx(
|
|
13001
|
+
"div",
|
|
13002
|
+
{
|
|
13003
|
+
style: {
|
|
13004
|
+
position: "relative",
|
|
13005
|
+
zIndex: 1,
|
|
13006
|
+
display: "flex",
|
|
13007
|
+
alignItems: "center",
|
|
13008
|
+
justifyContent: "center",
|
|
13009
|
+
width: "100%",
|
|
13010
|
+
height: "100%",
|
|
13011
|
+
padding: "0 2rem",
|
|
13012
|
+
boxSizing: "border-box"
|
|
13013
|
+
},
|
|
13014
|
+
children
|
|
13015
|
+
}
|
|
13016
|
+
)
|
|
13017
|
+
]
|
|
13018
|
+
}
|
|
13019
|
+
);
|
|
13020
|
+
};
|
|
12600
13021
|
const DragOptions = ({
|
|
12601
13022
|
options,
|
|
12602
13023
|
onOptionClick
|
|
12603
13024
|
}) => {
|
|
12604
13025
|
const { usedOptionIds } = useExerciseDnd();
|
|
12605
|
-
return /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
|
|
13026
|
+
return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
|
|
12606
13027
|
const optionId = `drag-option-${idx}`;
|
|
12607
13028
|
const optionValue = String(option);
|
|
12608
13029
|
const isUsed = usedOptionIds.has(optionId);
|
|
@@ -12629,7 +13050,7 @@ const DragOptions = ({
|
|
|
12629
13050
|
}
|
|
12630
13051
|
)
|
|
12631
13052
|
) }, optionId);
|
|
12632
|
-
}) });
|
|
13053
|
+
}) }) });
|
|
12633
13054
|
};
|
|
12634
13055
|
const NumericKeypad = ({
|
|
12635
13056
|
value,
|
|
@@ -12725,28 +13146,6 @@ const FractionInput = ({
|
|
|
12725
13146
|
/* @__PURE__ */ jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator") })
|
|
12726
13147
|
] });
|
|
12727
13148
|
};
|
|
12728
|
-
const RadioOptions = ({
|
|
12729
|
-
name,
|
|
12730
|
-
options,
|
|
12731
|
-
value,
|
|
12732
|
-
validationStatus = null,
|
|
12733
|
-
onChange
|
|
12734
|
-
}) => {
|
|
12735
|
-
return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
12736
|
-
const isSelected = value === option;
|
|
12737
|
-
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
12738
|
-
return /* @__PURE__ */ jsx(
|
|
12739
|
-
"button",
|
|
12740
|
-
{
|
|
12741
|
-
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
12742
|
-
onClick: () => onChange(name, option),
|
|
12743
|
-
type: "button",
|
|
12744
|
-
children: option
|
|
12745
|
-
},
|
|
12746
|
-
`${name}-radio-${idx}`
|
|
12747
|
-
);
|
|
12748
|
-
}) });
|
|
12749
|
-
};
|
|
12750
13149
|
function isLightColor(hex) {
|
|
12751
13150
|
const clean = hex.replace("#", "");
|
|
12752
13151
|
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
@@ -12761,7 +13160,8 @@ function isLightColor(hex) {
|
|
|
12761
13160
|
return 1.05 / (L + 0.05) < 4.5;
|
|
12762
13161
|
}
|
|
12763
13162
|
function getTextColorForBackground(color) {
|
|
12764
|
-
|
|
13163
|
+
const hslLightness = parseHslLightness(color);
|
|
13164
|
+
if (hslLightness !== null) return hslLightness >= 60 ? "#1a1a1a" : "#ffffff";
|
|
12765
13165
|
return isLightColor(color) ? "#1a1a1a" : "#ffffff";
|
|
12766
13166
|
}
|
|
12767
13167
|
function generateHslPalette(n) {
|
|
@@ -12770,7 +13170,7 @@ function generateHslPalette(n) {
|
|
|
12770
13170
|
return `hsl(${hue}, 60%, 72%)`;
|
|
12771
13171
|
});
|
|
12772
13172
|
}
|
|
12773
|
-
function
|
|
13173
|
+
function resolveGroupPalette(userColors, groupCount) {
|
|
12774
13174
|
if (groupCount <= 0) return [];
|
|
12775
13175
|
if (!userColors || userColors.length === 0) {
|
|
12776
13176
|
return generateHslPalette(groupCount);
|
|
@@ -12782,6 +13182,478 @@ function resolveMatcherPalette(userColors, groupCount) {
|
|
|
12782
13182
|
const extras = generateHslPalette(needed);
|
|
12783
13183
|
return [...userColors, ...extras];
|
|
12784
13184
|
}
|
|
13185
|
+
function resolveDistinctGroupPalette(groupCount, options = {}) {
|
|
13186
|
+
if (groupCount <= 0) return [];
|
|
13187
|
+
const used = new Set((options.excludeColors ?? []).map(normalizeColorToken));
|
|
13188
|
+
const palette = [];
|
|
13189
|
+
for (const color of options.seedColors ?? []) {
|
|
13190
|
+
const token = normalizeColorToken(color);
|
|
13191
|
+
if (used.has(token)) continue;
|
|
13192
|
+
palette.push(color);
|
|
13193
|
+
used.add(token);
|
|
13194
|
+
if (palette.length === groupCount) return palette;
|
|
13195
|
+
}
|
|
13196
|
+
const addCandidates = (candidates) => {
|
|
13197
|
+
for (const color of candidates) {
|
|
13198
|
+
const token = normalizeColorToken(color);
|
|
13199
|
+
if (used.has(token)) continue;
|
|
13200
|
+
palette.push(color);
|
|
13201
|
+
used.add(token);
|
|
13202
|
+
if (palette.length === groupCount) return;
|
|
13203
|
+
}
|
|
13204
|
+
};
|
|
13205
|
+
addCandidates(generateHslPalette(groupCount));
|
|
13206
|
+
let candidateCount = groupCount + 1;
|
|
13207
|
+
while (palette.length < groupCount) {
|
|
13208
|
+
addCandidates(generateHslPalette(candidateCount++));
|
|
13209
|
+
}
|
|
13210
|
+
return palette;
|
|
13211
|
+
}
|
|
13212
|
+
function getGroupClassName(groupIndex) {
|
|
13213
|
+
return `widget-group-${groupIndex + 1}`;
|
|
13214
|
+
}
|
|
13215
|
+
function normalizeColorToken(color) {
|
|
13216
|
+
return color.replace(/\s+/g, "").toLowerCase();
|
|
13217
|
+
}
|
|
13218
|
+
function parseHslLightness(color) {
|
|
13219
|
+
const match = color.match(/^hsl\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*([\d.]+)%\s*\)$/i);
|
|
13220
|
+
return match ? Number(match[1]) : null;
|
|
13221
|
+
}
|
|
13222
|
+
function sumCounts(counts) {
|
|
13223
|
+
return counts.reduce((sum, count) => sum + count, 0);
|
|
13224
|
+
}
|
|
13225
|
+
function countsToPartGroups(counts) {
|
|
13226
|
+
return counts.flatMap(
|
|
13227
|
+
(count, groupIndex) => Array.from({ length: count }, () => groupIndex)
|
|
13228
|
+
);
|
|
13229
|
+
}
|
|
13230
|
+
function normalizeCounts(value, expected) {
|
|
13231
|
+
if (!Array.isArray(value)) return expected.map(() => 0);
|
|
13232
|
+
return expected.map((max, i) => Math.max(0, Math.min(max, value[i] ?? 0)));
|
|
13233
|
+
}
|
|
13234
|
+
function serializeCounts(value) {
|
|
13235
|
+
return Array.isArray(value) ? value.join("|") : null;
|
|
13236
|
+
}
|
|
13237
|
+
function countsToAssignments(counts, divisions, filledCount) {
|
|
13238
|
+
const assignments = Array.from({ length: divisions }, () => null);
|
|
13239
|
+
let index = filledCount;
|
|
13240
|
+
counts.forEach((count, groupIndex) => {
|
|
13241
|
+
for (let i = 0; i < count && index < divisions; i++) {
|
|
13242
|
+
assignments[index] = groupIndex;
|
|
13243
|
+
index++;
|
|
13244
|
+
}
|
|
13245
|
+
});
|
|
13246
|
+
return assignments;
|
|
13247
|
+
}
|
|
13248
|
+
function getNextAssignments(assignments, partIndex, groupCount) {
|
|
13249
|
+
if (groupCount === 0) return [...assignments];
|
|
13250
|
+
const current = assignments[partIndex] ?? null;
|
|
13251
|
+
const next = current === null ? 0 : current + 1 >= groupCount ? null : current + 1;
|
|
13252
|
+
const updated = [...assignments];
|
|
13253
|
+
updated[partIndex] = next;
|
|
13254
|
+
return updated;
|
|
13255
|
+
}
|
|
13256
|
+
function assignmentsToCounts(assignments, groupCount) {
|
|
13257
|
+
const counts = Array.from({ length: groupCount }, () => 0);
|
|
13258
|
+
for (const groupIndex of assignments) {
|
|
13259
|
+
if (groupIndex !== null && groupIndex >= 0 && groupIndex < groupCount) {
|
|
13260
|
+
counts[groupIndex] = (counts[groupIndex] ?? 0) + 1;
|
|
13261
|
+
}
|
|
13262
|
+
}
|
|
13263
|
+
return counts;
|
|
13264
|
+
}
|
|
13265
|
+
function useVisualFractionState({
|
|
13266
|
+
widgetId,
|
|
13267
|
+
divisions,
|
|
13268
|
+
filledCount,
|
|
13269
|
+
answer,
|
|
13270
|
+
value,
|
|
13271
|
+
onChange
|
|
13272
|
+
}) {
|
|
13273
|
+
const [partAssignments, setPartAssignments] = useState(
|
|
13274
|
+
() => countsToAssignments(normalizeCounts(value, answer), divisions, filledCount)
|
|
13275
|
+
);
|
|
13276
|
+
const lastInternalValueRef = useRef(null);
|
|
13277
|
+
const valueKey = serializeCounts(value);
|
|
13278
|
+
useEffect(() => {
|
|
13279
|
+
if (valueKey !== null && valueKey === lastInternalValueRef.current) return;
|
|
13280
|
+
lastInternalValueRef.current = null;
|
|
13281
|
+
setPartAssignments(
|
|
13282
|
+
countsToAssignments(normalizeCounts(value, answer), divisions, filledCount)
|
|
13283
|
+
);
|
|
13284
|
+
}, [answer, divisions, filledCount, valueKey]);
|
|
13285
|
+
const handlePartClick = (partIndex) => {
|
|
13286
|
+
if (partIndex < filledCount) return;
|
|
13287
|
+
const updated = getNextAssignments(partAssignments, partIndex, answer.length);
|
|
13288
|
+
const nextCounts = assignmentsToCounts(updated, answer.length);
|
|
13289
|
+
lastInternalValueRef.current = serializeCounts(nextCounts);
|
|
13290
|
+
setPartAssignments(updated);
|
|
13291
|
+
onChange(widgetId, nextCounts);
|
|
13292
|
+
};
|
|
13293
|
+
return { partAssignments, handlePartClick };
|
|
13294
|
+
}
|
|
13295
|
+
const VISUAL_FRACTION_DISPLAY_COLOR = "#AE9CFF";
|
|
13296
|
+
const VISUAL_FRACTION_BORDER_COLOR = "#705DC0";
|
|
13297
|
+
const VISUAL_FRACTION_SVG_SIZE_SCALE = 8;
|
|
13298
|
+
function getCircleSectorProps(index, divisions) {
|
|
13299
|
+
if (divisions <= 1) return { kind: "circle" };
|
|
13300
|
+
const angle = 360 / divisions;
|
|
13301
|
+
const startAngle = -90 + index * angle;
|
|
13302
|
+
const endAngle = startAngle + angle;
|
|
13303
|
+
const start = polarToCartesian(50, 50, 48, endAngle);
|
|
13304
|
+
const end = polarToCartesian(50, 50, 48, startAngle);
|
|
13305
|
+
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
13306
|
+
return {
|
|
13307
|
+
kind: "path",
|
|
13308
|
+
d: [
|
|
13309
|
+
"M",
|
|
13310
|
+
50,
|
|
13311
|
+
50,
|
|
13312
|
+
"L",
|
|
13313
|
+
start.x,
|
|
13314
|
+
start.y,
|
|
13315
|
+
"A",
|
|
13316
|
+
48,
|
|
13317
|
+
48,
|
|
13318
|
+
0,
|
|
13319
|
+
largeArcFlag,
|
|
13320
|
+
0,
|
|
13321
|
+
end.x,
|
|
13322
|
+
end.y,
|
|
13323
|
+
"Z"
|
|
13324
|
+
].join(" ")
|
|
13325
|
+
};
|
|
13326
|
+
}
|
|
13327
|
+
function polarToCartesian(cx, cy, r, angleInDegrees) {
|
|
13328
|
+
const rad = angleInDegrees * Math.PI / 180;
|
|
13329
|
+
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
|
13330
|
+
}
|
|
13331
|
+
function getEqualTrianglePartPath(index, divisions) {
|
|
13332
|
+
const side = Math.sqrt(divisions);
|
|
13333
|
+
if (!Number.isInteger(side)) {
|
|
13334
|
+
throw new Error(`getEqualTrianglePartPath: divisions (${divisions}) must be a perfect square.`);
|
|
13335
|
+
}
|
|
13336
|
+
const smallSide = 96 / side;
|
|
13337
|
+
const smallHeight = smallSide * Math.sqrt(3) / 2;
|
|
13338
|
+
const totalHeight = side * smallHeight;
|
|
13339
|
+
const offsetY = (100 - totalHeight) / 2;
|
|
13340
|
+
let cursor = 0;
|
|
13341
|
+
for (let row = 0; row < side; row++) {
|
|
13342
|
+
const y = offsetY + row * smallHeight;
|
|
13343
|
+
const rowLeft = 50 - (row + 1) * smallSide / 2;
|
|
13344
|
+
for (let col = 0; col <= row; col++) {
|
|
13345
|
+
if (cursor === index) {
|
|
13346
|
+
const x = rowLeft + col * smallSide;
|
|
13347
|
+
return ["M", x + smallSide / 2, y, "L", x + smallSide, y + smallHeight, "L", x, y + smallHeight, "Z"].join(" ");
|
|
13348
|
+
}
|
|
13349
|
+
cursor++;
|
|
13350
|
+
}
|
|
13351
|
+
for (let col = 0; col < row; col++) {
|
|
13352
|
+
if (cursor === index) {
|
|
13353
|
+
const x = rowLeft + col * smallSide;
|
|
13354
|
+
return ["M", x + smallSide, y + smallHeight, "L", x + smallSide / 2, y, "L", x + smallSide * 1.5, y, "Z"].join(" ");
|
|
13355
|
+
}
|
|
13356
|
+
cursor++;
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
return "";
|
|
13360
|
+
}
|
|
13361
|
+
function getGridColumnCount(divisions) {
|
|
13362
|
+
return Math.ceil(Math.sqrt(divisions));
|
|
13363
|
+
}
|
|
13364
|
+
function getInteractiveProps(part, onPartClick) {
|
|
13365
|
+
if (!part.isInteractiveSlot) return {};
|
|
13366
|
+
return {
|
|
13367
|
+
onClick: () => onPartClick(part.index),
|
|
13368
|
+
role: "button",
|
|
13369
|
+
"aria-pressed": part.isSelected,
|
|
13370
|
+
"aria-label": getPartAriaLabel(part),
|
|
13371
|
+
tabIndex: 0,
|
|
13372
|
+
onKeyDown: (event) => {
|
|
13373
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
13374
|
+
event.preventDefault();
|
|
13375
|
+
onPartClick(part.index);
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
};
|
|
13379
|
+
}
|
|
13380
|
+
function getPartAriaLabel(part) {
|
|
13381
|
+
return part.groupIndex === null ? `Part ${part.index + 1}, empty` : `Part ${part.index + 1}, group ${part.groupIndex + 1}`;
|
|
13382
|
+
}
|
|
13383
|
+
const BarShape = ({
|
|
13384
|
+
parts,
|
|
13385
|
+
divisions,
|
|
13386
|
+
orientation,
|
|
13387
|
+
onPartClick
|
|
13388
|
+
}) => {
|
|
13389
|
+
const renderedParts = orientation === "vertical" ? [...parts].reverse() : parts;
|
|
13390
|
+
return /* @__PURE__ */ jsx(
|
|
13391
|
+
"div",
|
|
13392
|
+
{
|
|
13393
|
+
className: `visual-fraction-widget__parts${orientation === "vertical" ? " visual-fraction-widget__parts--vertical" : " visual-fraction-widget__parts--horizontal"}`,
|
|
13394
|
+
style: { "--visual-fraction-divisions": divisions },
|
|
13395
|
+
children: renderedParts.map((part) => /* @__PURE__ */ jsx(
|
|
13396
|
+
"div",
|
|
13397
|
+
{
|
|
13398
|
+
className: part.className,
|
|
13399
|
+
style: part.style,
|
|
13400
|
+
...getInteractiveProps(part, onPartClick)
|
|
13401
|
+
},
|
|
13402
|
+
part.index
|
|
13403
|
+
))
|
|
13404
|
+
}
|
|
13405
|
+
);
|
|
13406
|
+
};
|
|
13407
|
+
BarShape.displayName = "BarShape";
|
|
13408
|
+
const GridShape = ({
|
|
13409
|
+
parts,
|
|
13410
|
+
divisions,
|
|
13411
|
+
onPartClick
|
|
13412
|
+
}) => /* @__PURE__ */ jsx(
|
|
13413
|
+
"div",
|
|
13414
|
+
{
|
|
13415
|
+
className: "visual-fraction-widget__parts visual-fraction-widget__parts--horizontal",
|
|
13416
|
+
style: {
|
|
13417
|
+
"--visual-fraction-divisions": divisions,
|
|
13418
|
+
"--visual-fraction-grid-cols": getGridColumnCount(divisions)
|
|
13419
|
+
},
|
|
13420
|
+
children: parts.map((part) => /* @__PURE__ */ jsx(
|
|
13421
|
+
"div",
|
|
13422
|
+
{
|
|
13423
|
+
className: part.className,
|
|
13424
|
+
style: part.style,
|
|
13425
|
+
...getInteractiveProps(part, onPartClick)
|
|
13426
|
+
},
|
|
13427
|
+
part.index
|
|
13428
|
+
))
|
|
13429
|
+
}
|
|
13430
|
+
);
|
|
13431
|
+
GridShape.displayName = "GridShape";
|
|
13432
|
+
const CircleShape = ({
|
|
13433
|
+
parts,
|
|
13434
|
+
divisions,
|
|
13435
|
+
onPartClick
|
|
13436
|
+
}) => /* @__PURE__ */ jsx(
|
|
13437
|
+
"svg",
|
|
13438
|
+
{
|
|
13439
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__circle",
|
|
13440
|
+
viewBox: "0 0 100 100",
|
|
13441
|
+
children: parts.map((part) => {
|
|
13442
|
+
const sectorProps = getCircleSectorProps(part.index, divisions);
|
|
13443
|
+
const className = `${part.className} visual-fraction-widget__sector`;
|
|
13444
|
+
const interactiveProps = getInteractiveProps(part, onPartClick);
|
|
13445
|
+
return sectorProps.kind === "circle" ? /* @__PURE__ */ jsx(
|
|
13446
|
+
"circle",
|
|
13447
|
+
{
|
|
13448
|
+
className,
|
|
13449
|
+
style: part.style,
|
|
13450
|
+
cx: "50",
|
|
13451
|
+
cy: "50",
|
|
13452
|
+
r: "48",
|
|
13453
|
+
...interactiveProps
|
|
13454
|
+
},
|
|
13455
|
+
part.index
|
|
13456
|
+
) : /* @__PURE__ */ jsx(
|
|
13457
|
+
"path",
|
|
13458
|
+
{
|
|
13459
|
+
className,
|
|
13460
|
+
style: part.style,
|
|
13461
|
+
d: sectorProps.d,
|
|
13462
|
+
...interactiveProps
|
|
13463
|
+
},
|
|
13464
|
+
part.index
|
|
13465
|
+
);
|
|
13466
|
+
})
|
|
13467
|
+
}
|
|
13468
|
+
);
|
|
13469
|
+
CircleShape.displayName = "CircleShape";
|
|
13470
|
+
const TriangleShape = ({
|
|
13471
|
+
parts,
|
|
13472
|
+
divisions,
|
|
13473
|
+
onPartClick
|
|
13474
|
+
}) => /* @__PURE__ */ jsx(
|
|
13475
|
+
"svg",
|
|
13476
|
+
{
|
|
13477
|
+
className: "visual-fraction-widget__shape visual-fraction-widget__triangle",
|
|
13478
|
+
viewBox: "0 0 100 100",
|
|
13479
|
+
children: parts.map((part) => /* @__PURE__ */ jsx(
|
|
13480
|
+
"path",
|
|
13481
|
+
{
|
|
13482
|
+
className: `${part.className} visual-fraction-widget__sector`,
|
|
13483
|
+
style: part.style,
|
|
13484
|
+
d: getEqualTrianglePartPath(part.index, divisions),
|
|
13485
|
+
...getInteractiveProps(part, onPartClick)
|
|
13486
|
+
},
|
|
13487
|
+
part.index
|
|
13488
|
+
))
|
|
13489
|
+
}
|
|
13490
|
+
);
|
|
13491
|
+
TriangleShape.displayName = "TriangleShape";
|
|
13492
|
+
const ShapeBar = ({ parts, divisions, orientation, onPartClick }) => /* @__PURE__ */ jsx(BarShape, { parts, divisions, orientation, onPartClick });
|
|
13493
|
+
const ShapeCircle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(CircleShape, { parts, divisions, onPartClick });
|
|
13494
|
+
const ShapeTriangle = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(TriangleShape, { parts, divisions, onPartClick });
|
|
13495
|
+
const ShapeGrid = ({ parts, divisions, onPartClick }) => /* @__PURE__ */ jsx(GridShape, { parts, divisions, onPartClick });
|
|
13496
|
+
const SHAPE_COMPONENTS = {
|
|
13497
|
+
[VISUAL_FRACTION_SHAPE.BAR]: ShapeBar,
|
|
13498
|
+
[VISUAL_FRACTION_SHAPE.CIRCLE]: ShapeCircle,
|
|
13499
|
+
[VISUAL_FRACTION_SHAPE.TRIANGLE]: ShapeTriangle,
|
|
13500
|
+
[VISUAL_FRACTION_SHAPE.GRID]: ShapeGrid
|
|
13501
|
+
};
|
|
13502
|
+
const VisualFraction = ({
|
|
13503
|
+
widgetId,
|
|
13504
|
+
widget,
|
|
13505
|
+
value,
|
|
13506
|
+
validationStatus = null,
|
|
13507
|
+
onChange
|
|
13508
|
+
}) => {
|
|
13509
|
+
const { shape, divisions, filled, answer, orientation, label } = widget;
|
|
13510
|
+
const filledCount = sumCounts(filled);
|
|
13511
|
+
const filledPartGroups = useMemo(() => countsToPartGroups(filled), [filled]);
|
|
13512
|
+
const filledPalette = useMemo(() => {
|
|
13513
|
+
if (filled.length === 0) return [];
|
|
13514
|
+
return resolveDistinctGroupPalette(filled.length, {
|
|
13515
|
+
seedColors: [VISUAL_FRACTION_DISPLAY_COLOR],
|
|
13516
|
+
excludeColors: [VISUAL_FRACTION_BORDER_COLOR]
|
|
13517
|
+
});
|
|
13518
|
+
}, [filled]);
|
|
13519
|
+
const answerPalette = useMemo(() => {
|
|
13520
|
+
if (answer.length === 0) return [];
|
|
13521
|
+
return resolveDistinctGroupPalette(answer.length, {
|
|
13522
|
+
excludeColors: [VISUAL_FRACTION_BORDER_COLOR, ...filledPalette]
|
|
13523
|
+
});
|
|
13524
|
+
}, [answer, filledPalette]);
|
|
13525
|
+
const { partAssignments, handlePartClick } = useVisualFractionState({
|
|
13526
|
+
widgetId,
|
|
13527
|
+
divisions,
|
|
13528
|
+
filledCount,
|
|
13529
|
+
answer,
|
|
13530
|
+
value,
|
|
13531
|
+
onChange
|
|
13532
|
+
});
|
|
13533
|
+
const parts = useMemo(
|
|
13534
|
+
() => Array.from({ length: divisions }, (_, i) => {
|
|
13535
|
+
let partClass = "visual-fraction-widget__part visual-fraction-widget__part--empty";
|
|
13536
|
+
let groupIdx = null;
|
|
13537
|
+
if (i < filledCount) {
|
|
13538
|
+
groupIdx = filledPartGroups[i] ?? 0;
|
|
13539
|
+
partClass = "visual-fraction-widget__part visual-fraction-widget__part--filled";
|
|
13540
|
+
} else if (partAssignments[i] != null) {
|
|
13541
|
+
groupIdx = partAssignments[i];
|
|
13542
|
+
partClass = "visual-fraction-widget__part visual-fraction-widget__part--answer";
|
|
13543
|
+
}
|
|
13544
|
+
if (groupIdx !== null) {
|
|
13545
|
+
partClass += ` visual-fraction-widget__part--group-${groupIdx + 1} ${getGroupClassName(groupIdx)}`;
|
|
13546
|
+
}
|
|
13547
|
+
const colorKind = i < filledCount ? "filled" : "answer";
|
|
13548
|
+
const palette = colorKind === "filled" ? filledPalette : answerPalette;
|
|
13549
|
+
const fallbackColor = palette[groupIdx ?? 0];
|
|
13550
|
+
const style = groupIdx !== null ? {
|
|
13551
|
+
"--visual-fraction-part-color": `var(--visual-fraction-${colorKind}-group-${groupIdx + 1}-color, ${fallbackColor})`,
|
|
13552
|
+
"--visual-fraction-part-text-color": `var(--visual-fraction-${colorKind}-group-${groupIdx + 1}-text-color, ${getTextColorForBackground(fallbackColor)})`
|
|
13553
|
+
} : void 0;
|
|
13554
|
+
return {
|
|
13555
|
+
index: i,
|
|
13556
|
+
className: partClass,
|
|
13557
|
+
style,
|
|
13558
|
+
// A slot is interactive when there are answer groups AND it falls in the non-filled range.
|
|
13559
|
+
// answer.length counts groups (not total slots); the state hook handles per-slot cycling.
|
|
13560
|
+
isInteractiveSlot: answer.length > 0 && i >= filledCount,
|
|
13561
|
+
isSelected: groupIdx !== null,
|
|
13562
|
+
groupIndex: groupIdx
|
|
13563
|
+
};
|
|
13564
|
+
}),
|
|
13565
|
+
[divisions, filledCount, filledPartGroups, partAssignments, answer, filledPalette, answerPalette]
|
|
13566
|
+
);
|
|
13567
|
+
const statusClass = validationStatus ? ` visual-fraction-widget--${validationStatus} widget--${validationStatus}` : "";
|
|
13568
|
+
const rootStyle = useMemo(() => getSizeStyle(widget.size, shape), [widget.size, shape]);
|
|
13569
|
+
const ShapeComponent = SHAPE_COMPONENTS[shape];
|
|
13570
|
+
return /* @__PURE__ */ jsxs(
|
|
13571
|
+
"div",
|
|
13572
|
+
{
|
|
13573
|
+
className: `visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
|
|
13574
|
+
style: rootStyle,
|
|
13575
|
+
role: "group",
|
|
13576
|
+
"aria-label": `${divisions} part ${shape} fraction`,
|
|
13577
|
+
children: [
|
|
13578
|
+
/* @__PURE__ */ jsx(
|
|
13579
|
+
ShapeComponent,
|
|
13580
|
+
{
|
|
13581
|
+
parts,
|
|
13582
|
+
divisions,
|
|
13583
|
+
orientation: orientation ?? "horizontal",
|
|
13584
|
+
onPartClick: handlePartClick
|
|
13585
|
+
}
|
|
13586
|
+
),
|
|
13587
|
+
label && /* @__PURE__ */ jsx("span", { className: "visual-fraction-widget__label", children: label })
|
|
13588
|
+
]
|
|
13589
|
+
}
|
|
13590
|
+
);
|
|
13591
|
+
};
|
|
13592
|
+
VisualFraction.displayName = "VisualFraction";
|
|
13593
|
+
function getSizeStyle(size, shape) {
|
|
13594
|
+
if (size === void 0) return void 0;
|
|
13595
|
+
if (shape === "circle" || shape === "triangle") {
|
|
13596
|
+
return {
|
|
13597
|
+
"--visual-fraction-size": `${size * VISUAL_FRACTION_SVG_SIZE_SCALE}rem`
|
|
13598
|
+
};
|
|
13599
|
+
}
|
|
13600
|
+
return {
|
|
13601
|
+
"--visual-fraction-part-size": `${size}rem`
|
|
13602
|
+
};
|
|
13603
|
+
}
|
|
13604
|
+
const RadioOptions = ({
|
|
13605
|
+
name,
|
|
13606
|
+
options,
|
|
13607
|
+
value,
|
|
13608
|
+
validationStatus = null,
|
|
13609
|
+
onChange
|
|
13610
|
+
}) => {
|
|
13611
|
+
return /* @__PURE__ */ jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
13612
|
+
const isSelected = value === option;
|
|
13613
|
+
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
13614
|
+
return /* @__PURE__ */ jsx(
|
|
13615
|
+
"button",
|
|
13616
|
+
{
|
|
13617
|
+
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
13618
|
+
onClick: () => onChange(name, option),
|
|
13619
|
+
type: "button",
|
|
13620
|
+
children: option
|
|
13621
|
+
},
|
|
13622
|
+
`${name}-radio-${idx}`
|
|
13623
|
+
);
|
|
13624
|
+
}) });
|
|
13625
|
+
};
|
|
13626
|
+
const Btn = ({
|
|
13627
|
+
label,
|
|
13628
|
+
extraClass = "keypad-btn-digit",
|
|
13629
|
+
onClick
|
|
13630
|
+
}) => /* @__PURE__ */ jsx("button", { className: `keypad-btn ${extraClass}`, onClick, children: /* @__PURE__ */ jsx("span", { children: label }) });
|
|
13631
|
+
const DiamondKeypad = ({ value, onChange }) => {
|
|
13632
|
+
const append = (ch) => onChange(value + ch);
|
|
13633
|
+
const backspace = () => onChange(value.slice(0, -1));
|
|
13634
|
+
return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", children: [
|
|
13635
|
+
/* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
|
|
13636
|
+
/* @__PURE__ */ jsx(Btn, { label: "1", onClick: () => append("1") }),
|
|
13637
|
+
/* @__PURE__ */ jsx(Btn, { label: "2", onClick: () => append("2") }),
|
|
13638
|
+
/* @__PURE__ */ jsx(Btn, { label: "3", onClick: () => append("3") })
|
|
13639
|
+
] }),
|
|
13640
|
+
/* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
|
|
13641
|
+
/* @__PURE__ */ jsx(Btn, { label: "4", onClick: () => append("4") }),
|
|
13642
|
+
/* @__PURE__ */ jsx(Btn, { label: "5", onClick: () => append("5") }),
|
|
13643
|
+
/* @__PURE__ */ jsx(Btn, { label: "6", onClick: () => append("6") }),
|
|
13644
|
+
/* @__PURE__ */ jsx(Btn, { label: "7", onClick: () => append("7") })
|
|
13645
|
+
] }),
|
|
13646
|
+
/* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
|
|
13647
|
+
/* @__PURE__ */ jsx(Btn, { label: "8", onClick: () => append("8") }),
|
|
13648
|
+
/* @__PURE__ */ jsx(Btn, { label: "9", onClick: () => append("9") }),
|
|
13649
|
+
/* @__PURE__ */ jsx(Btn, { label: "0", onClick: () => append("0") })
|
|
13650
|
+
] }),
|
|
13651
|
+
/* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
|
|
13652
|
+
/* @__PURE__ */ jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
|
|
13653
|
+
/* @__PURE__ */ jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
|
|
13654
|
+
] })
|
|
13655
|
+
] });
|
|
13656
|
+
};
|
|
12785
13657
|
function collectItemIds(rows) {
|
|
12786
13658
|
return rows.flatMap((row) => row.items.map((item) => item.itemId));
|
|
12787
13659
|
}
|
|
@@ -12823,7 +13695,7 @@ const MatcherWidget = ({
|
|
|
12823
13695
|
const { rows, colors: userColors, groupCount } = widget;
|
|
12824
13696
|
const widgetsMap = widgets ?? EMPTY_WIDGETS;
|
|
12825
13697
|
const palette = useMemo(
|
|
12826
|
-
() =>
|
|
13698
|
+
() => resolveGroupPalette(userColors, groupCount),
|
|
12827
13699
|
[userColors, groupCount]
|
|
12828
13700
|
);
|
|
12829
13701
|
const textColors = useMemo(
|
|
@@ -12839,13 +13711,21 @@ const MatcherWidget = ({
|
|
|
12839
13711
|
setAssignments((prev) => {
|
|
12840
13712
|
const current = prev[itemId] ?? null;
|
|
12841
13713
|
const next = current === null ? 0 : current + 1 >= groupCount ? null : current + 1;
|
|
12842
|
-
|
|
12843
|
-
onChange(widgetId, buildGroups(updated));
|
|
12844
|
-
return updated;
|
|
13714
|
+
return { ...prev, [itemId]: next };
|
|
12845
13715
|
});
|
|
12846
13716
|
},
|
|
12847
|
-
[groupCount
|
|
13717
|
+
[groupCount]
|
|
12848
13718
|
);
|
|
13719
|
+
const onChangeRef = useRef(onChange);
|
|
13720
|
+
onChangeRef.current = onChange;
|
|
13721
|
+
const isMounted = useRef(false);
|
|
13722
|
+
useEffect(() => {
|
|
13723
|
+
if (!isMounted.current) {
|
|
13724
|
+
isMounted.current = true;
|
|
13725
|
+
return;
|
|
13726
|
+
}
|
|
13727
|
+
onChangeRef.current(widgetId, buildGroups(assignments));
|
|
13728
|
+
}, [assignments, widgetId]);
|
|
12849
13729
|
const maxCols = useMemo(
|
|
12850
13730
|
() => rows.length === 0 ? 1 : Math.max(...rows.map((r) => r.items.length)),
|
|
12851
13731
|
[rows]
|
|
@@ -12866,9 +13746,9 @@ const MatcherWidget = ({
|
|
|
12866
13746
|
{
|
|
12867
13747
|
type: "button",
|
|
12868
13748
|
className: `matcher-card${isSelected ? " matcher-card--selected" : ""}`,
|
|
12869
|
-
style: color ? {
|
|
12870
|
-
"--matcher-card-color": color,
|
|
12871
|
-
"--matcher-card-text-color": textColors[colorIdx]
|
|
13749
|
+
style: colorIdx !== null && color ? {
|
|
13750
|
+
"--matcher-card-color": `var(--matcher-group-${colorIdx + 1}-color, ${color})`,
|
|
13751
|
+
"--matcher-card-text-color": `var(--matcher-group-${colorIdx + 1}-text-color, ${textColors[colorIdx]})`
|
|
12872
13752
|
} : void 0,
|
|
12873
13753
|
onClick: () => handleCardClick(item.itemId),
|
|
12874
13754
|
"aria-pressed": isSelected,
|
|
@@ -12910,6 +13790,9 @@ function isFractionWidget(widget) {
|
|
|
12910
13790
|
function isMatcherWidget(widget) {
|
|
12911
13791
|
return widget.type === "matcher";
|
|
12912
13792
|
}
|
|
13793
|
+
function isVisualFractionWidget(widget) {
|
|
13794
|
+
return widget.type === "visual-fraction";
|
|
13795
|
+
}
|
|
12913
13796
|
const WidgetRenderer = ({
|
|
12914
13797
|
widget,
|
|
12915
13798
|
widgetId,
|
|
@@ -12935,6 +13818,7 @@ const WidgetRenderer = ({
|
|
|
12935
13818
|
inputType: widget.inputType,
|
|
12936
13819
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
12937
13820
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
13821
|
+
placeholder: widget.placeholder,
|
|
12938
13822
|
onChange,
|
|
12939
13823
|
onFocus: onInputFocus
|
|
12940
13824
|
}
|
|
@@ -12963,14 +13847,24 @@ const WidgetRenderer = ({
|
|
|
12963
13847
|
);
|
|
12964
13848
|
}
|
|
12965
13849
|
if (isNumericKeypadWidget(widget)) {
|
|
13850
|
+
const keypadOnChange = onKeypadChange || (() => {
|
|
13851
|
+
});
|
|
13852
|
+
if (widget.style === "classic") {
|
|
13853
|
+
return /* @__PURE__ */ jsx(
|
|
13854
|
+
NumericKeypad,
|
|
13855
|
+
{
|
|
13856
|
+
value: strValue,
|
|
13857
|
+
includeOperators: widget.includeOperators,
|
|
13858
|
+
includeDecimal: widget.includeDecimal,
|
|
13859
|
+
onChange: keypadOnChange
|
|
13860
|
+
}
|
|
13861
|
+
);
|
|
13862
|
+
}
|
|
12966
13863
|
return /* @__PURE__ */ jsx(
|
|
12967
|
-
|
|
13864
|
+
DiamondKeypad,
|
|
12968
13865
|
{
|
|
12969
13866
|
value: strValue,
|
|
12970
|
-
|
|
12971
|
-
includeDecimal: widget.includeDecimal,
|
|
12972
|
-
onChange: onKeypadChange || (() => {
|
|
12973
|
-
})
|
|
13867
|
+
onChange: keypadOnChange
|
|
12974
13868
|
}
|
|
12975
13869
|
);
|
|
12976
13870
|
}
|
|
@@ -13002,6 +13896,18 @@ const WidgetRenderer = ({
|
|
|
13002
13896
|
}
|
|
13003
13897
|
);
|
|
13004
13898
|
}
|
|
13899
|
+
if (isVisualFractionWidget(widget)) {
|
|
13900
|
+
return /* @__PURE__ */ jsx(
|
|
13901
|
+
VisualFraction,
|
|
13902
|
+
{
|
|
13903
|
+
widgetId,
|
|
13904
|
+
widget,
|
|
13905
|
+
value: Array.isArray(value) && value.every((v) => typeof v === "number") ? value : void 0,
|
|
13906
|
+
validationStatus,
|
|
13907
|
+
onChange: (id, nextValue) => onChange(id, nextValue)
|
|
13908
|
+
}
|
|
13909
|
+
);
|
|
13910
|
+
}
|
|
13005
13911
|
return null;
|
|
13006
13912
|
};
|
|
13007
13913
|
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
@@ -13504,6 +14410,102 @@ const CounterControls = ({
|
|
|
13504
14410
|
] }),
|
|
13505
14411
|
counter.name && /* @__PURE__ */ jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
13506
14412
|
] });
|
|
14413
|
+
const CONTENT_ROW_EXTRA_SPACE = 8;
|
|
14414
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
14415
|
+
function useMeasuredPositionContentHeight(positionIds) {
|
|
14416
|
+
const [heightPx, setHeightPx] = useState(null);
|
|
14417
|
+
const elementsRef = useRef(/* @__PURE__ */ new Map());
|
|
14418
|
+
const refCallbacksRef = useRef(/* @__PURE__ */ new Map());
|
|
14419
|
+
const observerRef = useRef(null);
|
|
14420
|
+
const measureFrameRef = useRef(null);
|
|
14421
|
+
const idsKey = positionIds.join("|");
|
|
14422
|
+
const runMeasure = useCallback(() => {
|
|
14423
|
+
let maxHeight = 0;
|
|
14424
|
+
for (const element of elementsRef.current.values()) {
|
|
14425
|
+
const rectHeight = element.getBoundingClientRect().height;
|
|
14426
|
+
const measuredHeight = Math.max(rectHeight, element.offsetHeight);
|
|
14427
|
+
maxHeight = Math.max(maxHeight, measuredHeight);
|
|
14428
|
+
}
|
|
14429
|
+
const nextHeight = maxHeight > 0 ? Math.ceil(maxHeight + CONTENT_ROW_EXTRA_SPACE) : null;
|
|
14430
|
+
setHeightPx((current) => current === nextHeight ? current : nextHeight);
|
|
14431
|
+
}, []);
|
|
14432
|
+
const measure = useCallback(() => {
|
|
14433
|
+
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
|
|
14434
|
+
runMeasure();
|
|
14435
|
+
return;
|
|
14436
|
+
}
|
|
14437
|
+
if (measureFrameRef.current !== null) return;
|
|
14438
|
+
measureFrameRef.current = window.requestAnimationFrame(() => {
|
|
14439
|
+
measureFrameRef.current = null;
|
|
14440
|
+
runMeasure();
|
|
14441
|
+
});
|
|
14442
|
+
}, [runMeasure]);
|
|
14443
|
+
useEffect(
|
|
14444
|
+
() => () => {
|
|
14445
|
+
if (measureFrameRef.current !== null && typeof window !== "undefined") {
|
|
14446
|
+
window.cancelAnimationFrame(measureFrameRef.current);
|
|
14447
|
+
measureFrameRef.current = null;
|
|
14448
|
+
}
|
|
14449
|
+
},
|
|
14450
|
+
[]
|
|
14451
|
+
);
|
|
14452
|
+
const getPositionContentRef = useCallback(
|
|
14453
|
+
(id) => {
|
|
14454
|
+
const cachedRef = refCallbacksRef.current.get(id);
|
|
14455
|
+
if (cachedRef) return cachedRef;
|
|
14456
|
+
const refCallback = (node) => {
|
|
14457
|
+
const current = elementsRef.current.get(id);
|
|
14458
|
+
if (current && observerRef.current) {
|
|
14459
|
+
observerRef.current.unobserve(current);
|
|
14460
|
+
}
|
|
14461
|
+
if (node) {
|
|
14462
|
+
elementsRef.current.set(id, node);
|
|
14463
|
+
observerRef.current?.observe(node);
|
|
14464
|
+
} else {
|
|
14465
|
+
elementsRef.current.delete(id);
|
|
14466
|
+
}
|
|
14467
|
+
measure();
|
|
14468
|
+
};
|
|
14469
|
+
refCallbacksRef.current.set(id, refCallback);
|
|
14470
|
+
return refCallback;
|
|
14471
|
+
},
|
|
14472
|
+
[measure]
|
|
14473
|
+
);
|
|
14474
|
+
useEffect(() => {
|
|
14475
|
+
const liveIds = new Set(positionIds);
|
|
14476
|
+
for (const id of elementsRef.current.keys()) {
|
|
14477
|
+
if (!liveIds.has(id)) {
|
|
14478
|
+
elementsRef.current.delete(id);
|
|
14479
|
+
}
|
|
14480
|
+
}
|
|
14481
|
+
for (const id of refCallbacksRef.current.keys()) {
|
|
14482
|
+
if (!liveIds.has(id)) {
|
|
14483
|
+
refCallbacksRef.current.delete(id);
|
|
14484
|
+
}
|
|
14485
|
+
}
|
|
14486
|
+
measure();
|
|
14487
|
+
}, [idsKey, measure]);
|
|
14488
|
+
useIsomorphicLayoutEffect(() => {
|
|
14489
|
+
if (typeof ResizeObserver === "undefined") {
|
|
14490
|
+
measure();
|
|
14491
|
+
return void 0;
|
|
14492
|
+
}
|
|
14493
|
+
const observer = new ResizeObserver(measure);
|
|
14494
|
+
observerRef.current = observer;
|
|
14495
|
+
for (const element of elementsRef.current.values()) {
|
|
14496
|
+
observer.observe(element);
|
|
14497
|
+
}
|
|
14498
|
+
measure();
|
|
14499
|
+
return () => {
|
|
14500
|
+
observer.disconnect();
|
|
14501
|
+
observerRef.current = null;
|
|
14502
|
+
};
|
|
14503
|
+
}, [idsKey, measure]);
|
|
14504
|
+
return {
|
|
14505
|
+
positionContentHeight: heightPx === null ? void 0 : `${heightPx}px`,
|
|
14506
|
+
getPositionContentRef
|
|
14507
|
+
};
|
|
14508
|
+
}
|
|
13507
14509
|
const NumberLineRenderer = ({
|
|
13508
14510
|
item,
|
|
13509
14511
|
widgets,
|
|
@@ -13558,17 +14560,26 @@ const NumberLineRenderer = ({
|
|
|
13558
14560
|
() => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
|
|
13559
14561
|
[isDragMode, positions]
|
|
13560
14562
|
);
|
|
14563
|
+
const visiblePositionIds = useMemo(
|
|
14564
|
+
() => visiblePositions.map((pos) => pos.id),
|
|
14565
|
+
[visiblePositions]
|
|
14566
|
+
);
|
|
14567
|
+
const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
|
|
13561
14568
|
return /* @__PURE__ */ jsxs(
|
|
13562
14569
|
"div",
|
|
13563
14570
|
{
|
|
13564
14571
|
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
13565
|
-
style: {
|
|
14572
|
+
style: {
|
|
14573
|
+
[CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
|
|
14574
|
+
...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
|
|
14575
|
+
},
|
|
13566
14576
|
role: "group",
|
|
13567
14577
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
13568
14578
|
children: [
|
|
13569
14579
|
visiblePositions.length > 0 && /* @__PURE__ */ jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsx(
|
|
13570
14580
|
"div",
|
|
13571
14581
|
{
|
|
14582
|
+
ref: getPositionContentRef(pos.id),
|
|
13572
14583
|
className: "number-line-position-content",
|
|
13573
14584
|
style: { left: `${toPercent(pos.value, min, max)}%` },
|
|
13574
14585
|
children: pos.content.map((subItem) => /* @__PURE__ */ jsx(
|
|
@@ -13754,12 +14765,24 @@ const LayoutRenderer = ({
|
|
|
13754
14765
|
}
|
|
13755
14766
|
return /* @__PURE__ */ jsx("strong", { className: "bold-text text-item", dir: dirAttr, children: item.value });
|
|
13756
14767
|
}
|
|
14768
|
+
case JSON_TYPES.IMAGE:
|
|
14769
|
+
return /* @__PURE__ */ jsx(
|
|
14770
|
+
"img",
|
|
14771
|
+
{
|
|
14772
|
+
className: "image-item text-item",
|
|
14773
|
+
src: item.src,
|
|
14774
|
+
alt: item.alt,
|
|
14775
|
+
...item.width !== void 0 && { width: item.width },
|
|
14776
|
+
...item.height !== void 0 && { height: item.height },
|
|
14777
|
+
loading: "lazy"
|
|
14778
|
+
}
|
|
14779
|
+
);
|
|
13757
14780
|
case JSON_TYPES.WIDGET_REF: {
|
|
13758
14781
|
const widget = widgets[item.widgetId];
|
|
13759
14782
|
if (!widget) return null;
|
|
13760
14783
|
const isCorrect = validationDetails?.[item.widgetId];
|
|
13761
14784
|
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
13762
|
-
const widgetKey = widget.type === JSON_TYPES.MATCHER ? `${item.widgetId}-${resetKey}` : item.widgetId;
|
|
14785
|
+
const widgetKey = widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.VISUAL_FRACTION ? `${item.widgetId}-${resetKey}` : item.widgetId;
|
|
13763
14786
|
return /* @__PURE__ */ jsx(
|
|
13764
14787
|
WidgetRenderer,
|
|
13765
14788
|
{
|
|
@@ -14129,6 +15152,8 @@ export {
|
|
|
14129
15152
|
GroupItemSchema,
|
|
14130
15153
|
HighlightedTextInlineSchema,
|
|
14131
15154
|
HighlightedTextSchema,
|
|
15155
|
+
ImageInlineSchema,
|
|
15156
|
+
ImageItemSchema,
|
|
14132
15157
|
InlineInputJSONSchema,
|
|
14133
15158
|
InlineInputWidgetSchema,
|
|
14134
15159
|
InlineNodeSchema,
|
|
@@ -14162,9 +15187,14 @@ export {
|
|
|
14162
15187
|
TextItemSchema,
|
|
14163
15188
|
ToolNodeSchema,
|
|
14164
15189
|
TransformError,
|
|
15190
|
+
VISUAL_FRACTION_DEFAULT_SIZE,
|
|
15191
|
+
VISUAL_FRACTION_MAX_DIVISIONS,
|
|
15192
|
+
VISUAL_FRACTION_SHAPE,
|
|
15193
|
+
VISUAL_FRACTION_SHAPES,
|
|
14165
15194
|
ValidationError,
|
|
14166
15195
|
ValidationJSONSchema,
|
|
14167
15196
|
ValidationNodeSchema,
|
|
15197
|
+
VisualFractionInlineSchema,
|
|
14168
15198
|
WidgetJSONSchema,
|
|
14169
15199
|
WidgetRefSchema,
|
|
14170
15200
|
WidgetRenderer,
|
|
@@ -14178,6 +15208,7 @@ export {
|
|
|
14178
15208
|
createFraction,
|
|
14179
15209
|
createFreeText,
|
|
14180
15210
|
createHighlightedText,
|
|
15211
|
+
createImage,
|
|
14181
15212
|
createLineBlock,
|
|
14182
15213
|
createMatcherBlock,
|
|
14183
15214
|
createMatcherItem,
|
|
@@ -14188,6 +15219,7 @@ export {
|
|
|
14188
15219
|
createSpace,
|
|
14189
15220
|
createStringInput,
|
|
14190
15221
|
createTextBlock,
|
|
15222
|
+
createVisualFraction,
|
|
14191
15223
|
evaluate,
|
|
14192
15224
|
extractVariables as extractExpressionVariables,
|
|
14193
15225
|
findValidCombination,
|
|
@@ -14205,6 +15237,8 @@ export {
|
|
|
14205
15237
|
isFreeText,
|
|
14206
15238
|
isGroupItem,
|
|
14207
15239
|
isHighlightedText,
|
|
15240
|
+
isImage,
|
|
15241
|
+
isImageItem,
|
|
14208
15242
|
isInlineInput,
|
|
14209
15243
|
isInlineInputJSON,
|
|
14210
15244
|
isInlineNode,
|
|
@@ -14229,6 +15263,7 @@ export {
|
|
|
14229
15263
|
isToolNode,
|
|
14230
15264
|
isValidExpression,
|
|
14231
15265
|
isValidationNode,
|
|
15266
|
+
isVisualFractionInline,
|
|
14232
15267
|
isWidgetRef,
|
|
14233
15268
|
parse,
|
|
14234
15269
|
parseFractionKey,
|
|
@@ -14245,6 +15280,7 @@ export {
|
|
|
14245
15280
|
validateExpression,
|
|
14246
15281
|
validateInputFormats,
|
|
14247
15282
|
validateSimple,
|
|
15283
|
+
validateVisualFractionCapacity,
|
|
14248
15284
|
validateWidgetJSON,
|
|
14249
15285
|
validateWidgetJSONSafe
|
|
14250
15286
|
};
|