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