@mathscan/math-exercise-engine 1.0.15 → 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 +2803 -81
- package/dist/index.js +972 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +972 -78
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +236 -63
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -54,6 +54,8 @@ const AST_TYPES = {
|
|
|
54
54
|
MATCHER_ROW: "MatcherRow",
|
|
55
55
|
/** Item within a matcher row */
|
|
56
56
|
MATCHER_ITEM: "MatcherItem",
|
|
57
|
+
/** Visual fraction inline widget */
|
|
58
|
+
VISUAL_FRACTION: "VisualFraction",
|
|
57
59
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
60
|
// Tools
|
|
59
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -134,7 +136,9 @@ const JSON_TYPES = {
|
|
|
134
136
|
/** Numeric keypad widget */
|
|
135
137
|
NUMERIC_KEYPAD: "numeric-keypad",
|
|
136
138
|
/** Inline fraction widget */
|
|
137
|
-
FRACTION: "fraction"
|
|
139
|
+
FRACTION: "fraction",
|
|
140
|
+
/** Visual fraction widget */
|
|
141
|
+
VISUAL_FRACTION: "visual-fraction"
|
|
138
142
|
};
|
|
139
143
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
140
144
|
const VALIDATION_MODES = {
|
|
@@ -268,6 +272,10 @@ const HIGHLIGHT = chevrotain.createToken({
|
|
|
268
272
|
name: "HIGHLIGHT",
|
|
269
273
|
pattern: /HIGHLIGHT(?!_TEXT)/
|
|
270
274
|
});
|
|
275
|
+
const VISUAL_FRACTION = chevrotain.createToken({
|
|
276
|
+
name: "VISUAL_FRACTION",
|
|
277
|
+
pattern: /VISUAL_FRACTION/
|
|
278
|
+
});
|
|
271
279
|
const FRACTION = chevrotain.createToken({
|
|
272
280
|
name: "FRACTION",
|
|
273
281
|
pattern: /FRACTION/
|
|
@@ -397,6 +405,7 @@ const allTokens = [
|
|
|
397
405
|
CELL,
|
|
398
406
|
COUNTER,
|
|
399
407
|
HIGHLIGHT,
|
|
408
|
+
VISUAL_FRACTION,
|
|
400
409
|
FRACTION,
|
|
401
410
|
MATCHER,
|
|
402
411
|
ITEM,
|
|
@@ -862,6 +871,8 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
862
871
|
{ ALT: () => this.SUBRULE(this.fraction) },
|
|
863
872
|
// IMAGE (inline image element)
|
|
864
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) },
|
|
865
876
|
// W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
|
|
866
877
|
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
|
|
867
878
|
]);
|
|
@@ -899,6 +910,41 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
899
910
|
this.SUBRULE(this.parameterList);
|
|
900
911
|
this.CONSUME(RBracket);
|
|
901
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
|
+
});
|
|
902
948
|
this.inlineInput = this.RULE("inlineInput", () => {
|
|
903
949
|
this.OR([
|
|
904
950
|
{ ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
|
|
@@ -986,7 +1032,9 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
986
1032
|
// Tuple value: (start, end)
|
|
987
1033
|
{ ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
|
|
988
1034
|
// Curly-brace string list: {"a", "b", "c"}
|
|
989
|
-
{ 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" }) }
|
|
990
1038
|
]);
|
|
991
1039
|
});
|
|
992
1040
|
this.tuple = this.RULE("tuple", () => {
|
|
@@ -1042,6 +1090,25 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
1042
1090
|
}
|
|
1043
1091
|
}
|
|
1044
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;
|
|
1045
1112
|
const FreeTextInlineSchema = zod.z.object({
|
|
1046
1113
|
type: zod.z.literal(AST_TYPES.FREE_TEXT),
|
|
1047
1114
|
value: zod.z.string()
|
|
@@ -1071,6 +1138,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
1071
1138
|
name: zod.z.string(),
|
|
1072
1139
|
inputType: zod.z.literal(INPUT_TYPES.STRING),
|
|
1073
1140
|
length: zod.z.number().int().positive(),
|
|
1141
|
+
placeholder: zod.z.string().max(32).optional(),
|
|
1074
1142
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
1075
1143
|
}),
|
|
1076
1144
|
zod.z.object({
|
|
@@ -1078,6 +1146,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
1078
1146
|
name: zod.z.string(),
|
|
1079
1147
|
inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
|
|
1080
1148
|
digits: zod.z.number().int().positive(),
|
|
1149
|
+
placeholder: zod.z.string().max(32).optional(),
|
|
1081
1150
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
1082
1151
|
})
|
|
1083
1152
|
]);
|
|
@@ -1100,6 +1169,60 @@ const ImageInlineSchema = zod.z.object({
|
|
|
1100
1169
|
width: zod.z.number().positive().optional(),
|
|
1101
1170
|
height: zod.z.number().positive().optional()
|
|
1102
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
|
+
}
|
|
1103
1226
|
const InlineNodeSchema = zod.z.union([
|
|
1104
1227
|
FreeTextInlineSchema,
|
|
1105
1228
|
SpaceInlineSchema,
|
|
@@ -1108,7 +1231,8 @@ const InlineNodeSchema = zod.z.union([
|
|
|
1108
1231
|
BoldTextInlineSchema,
|
|
1109
1232
|
InlineInputWidgetSchema,
|
|
1110
1233
|
FractionInlineSchema,
|
|
1111
|
-
ImageInlineSchema
|
|
1234
|
+
ImageInlineSchema,
|
|
1235
|
+
VisualFractionInlineSchema
|
|
1112
1236
|
]);
|
|
1113
1237
|
function createFreeText(value) {
|
|
1114
1238
|
return FreeTextInlineSchema.parse({
|
|
@@ -1143,23 +1267,25 @@ function createBoldText(value, highlight) {
|
|
|
1143
1267
|
highlight
|
|
1144
1268
|
});
|
|
1145
1269
|
}
|
|
1146
|
-
function createStringInput(name, length, answer) {
|
|
1147
|
-
return InlineInputWidgetSchema
|
|
1270
|
+
function createStringInput(name, length, answer, placeholder) {
|
|
1271
|
+
return parseOrThrow(InlineInputWidgetSchema, {
|
|
1148
1272
|
type: AST_TYPES.INLINE_INPUT,
|
|
1149
1273
|
name,
|
|
1150
1274
|
inputType: INPUT_TYPES.STRING,
|
|
1151
1275
|
length,
|
|
1276
|
+
placeholder,
|
|
1152
1277
|
answer
|
|
1153
|
-
});
|
|
1278
|
+
}, "STRING_INPUT");
|
|
1154
1279
|
}
|
|
1155
|
-
function createNumericInput(name, digits, answer) {
|
|
1156
|
-
return InlineInputWidgetSchema
|
|
1280
|
+
function createNumericInput(name, digits, answer, placeholder) {
|
|
1281
|
+
return parseOrThrow(InlineInputWidgetSchema, {
|
|
1157
1282
|
type: AST_TYPES.INLINE_INPUT,
|
|
1158
1283
|
name,
|
|
1159
1284
|
inputType: INPUT_TYPES.NUMERIC,
|
|
1160
1285
|
digits,
|
|
1286
|
+
placeholder,
|
|
1161
1287
|
answer
|
|
1162
|
-
});
|
|
1288
|
+
}, "NUMERIC_INPUT");
|
|
1163
1289
|
}
|
|
1164
1290
|
function createImage(name, src, alt, options = {}) {
|
|
1165
1291
|
return ImageInlineSchema.parse({
|
|
@@ -1177,6 +1303,14 @@ function createFraction(name, options = {}) {
|
|
|
1177
1303
|
...options
|
|
1178
1304
|
});
|
|
1179
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
|
+
}
|
|
1180
1314
|
function isFreeText(value) {
|
|
1181
1315
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
1182
1316
|
}
|
|
@@ -1195,6 +1329,9 @@ function isImage(value) {
|
|
|
1195
1329
|
function isInlineNode(value) {
|
|
1196
1330
|
return InlineNodeSchema.safeParse(value).success;
|
|
1197
1331
|
}
|
|
1332
|
+
function isVisualFractionInline(value) {
|
|
1333
|
+
return VisualFractionInlineSchema.safeParse(value).success;
|
|
1334
|
+
}
|
|
1198
1335
|
const TextBlockSchema = zod.z.object({
|
|
1199
1336
|
type: zod.z.literal(AST_TYPES.TEXT_BLOCK),
|
|
1200
1337
|
value: zod.z.string()
|
|
@@ -1390,6 +1527,10 @@ const DisplayFractionInlineSchema = FractionInlineSchema.refine(
|
|
|
1390
1527
|
(f) => f.numerator !== void 0 && f.denominator !== void 0,
|
|
1391
1528
|
{ message: "FRACTION inside MATCHER ITEM must be in display mode (both numerator and denominator required)" }
|
|
1392
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
|
+
);
|
|
1393
1534
|
const DisplayInlineNodeSchema = zod.z.union([
|
|
1394
1535
|
FreeTextInlineSchema,
|
|
1395
1536
|
SpaceInlineSchema,
|
|
@@ -1397,7 +1538,8 @@ const DisplayInlineNodeSchema = zod.z.union([
|
|
|
1397
1538
|
ColorTextInlineSchema,
|
|
1398
1539
|
BoldTextInlineSchema,
|
|
1399
1540
|
DisplayFractionInlineSchema,
|
|
1400
|
-
ImageInlineSchema
|
|
1541
|
+
ImageInlineSchema,
|
|
1542
|
+
DisplayVisualFractionInlineSchema
|
|
1401
1543
|
]);
|
|
1402
1544
|
const MatcherItemSchema = zod.z.object({
|
|
1403
1545
|
type: zod.z.literal(AST_TYPES.MATCHER_ITEM),
|
|
@@ -1493,7 +1635,8 @@ const DragOptionsToolSchema = zod.z.object({
|
|
|
1493
1635
|
const NumericKeypadToolSchema = zod.z.object({
|
|
1494
1636
|
type: zod.z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
1495
1637
|
includeOperators: zod.z.boolean().optional(),
|
|
1496
|
-
includeDecimal: zod.z.boolean().optional()
|
|
1638
|
+
includeDecimal: zod.z.boolean().optional(),
|
|
1639
|
+
style: zod.z.enum(["diamond", "classic"]).optional()
|
|
1497
1640
|
});
|
|
1498
1641
|
const ToolNodeSchema = zod.z.discriminatedUnion("type", [
|
|
1499
1642
|
DragOptionsToolSchema,
|
|
@@ -1505,11 +1648,12 @@ function createDragOptions(options) {
|
|
|
1505
1648
|
options
|
|
1506
1649
|
});
|
|
1507
1650
|
}
|
|
1508
|
-
function createNumericKeypad(includeOperators, includeDecimal) {
|
|
1651
|
+
function createNumericKeypad(includeOperators, includeDecimal, style) {
|
|
1509
1652
|
return NumericKeypadToolSchema.parse({
|
|
1510
1653
|
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
1511
1654
|
includeOperators,
|
|
1512
|
-
includeDecimal
|
|
1655
|
+
includeDecimal,
|
|
1656
|
+
style
|
|
1513
1657
|
});
|
|
1514
1658
|
}
|
|
1515
1659
|
function isDragOptions(value) {
|
|
@@ -1863,6 +2007,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1863
2007
|
if (ctx.image) {
|
|
1864
2008
|
return this.visit(ctx.image);
|
|
1865
2009
|
}
|
|
2010
|
+
if (ctx.visualFraction) {
|
|
2011
|
+
return this.visit(ctx.visualFraction);
|
|
2012
|
+
}
|
|
1866
2013
|
if (ctx.inlineInput) {
|
|
1867
2014
|
return this.visit(ctx.inlineInput);
|
|
1868
2015
|
}
|
|
@@ -1914,16 +2061,32 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1914
2061
|
const height = params.height;
|
|
1915
2062
|
return createImage(name, src, alt, { width, height });
|
|
1916
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
|
+
}
|
|
1917
2079
|
inlineInput(ctx) {
|
|
1918
2080
|
const params = this.visit(ctx.parameterList);
|
|
1919
2081
|
const name = params.name;
|
|
1920
2082
|
const answer = params.answer;
|
|
2083
|
+
const placeholder = params.placeholder;
|
|
1921
2084
|
if (ctx.stringInput) {
|
|
1922
2085
|
const length = params.length;
|
|
1923
|
-
return createStringInput(name, length, answer);
|
|
2086
|
+
return createStringInput(name, length, answer, placeholder);
|
|
1924
2087
|
}
|
|
1925
2088
|
const digits = params.digits;
|
|
1926
|
-
return createNumericInput(name, digits, answer);
|
|
2089
|
+
return createNumericInput(name, digits, answer, placeholder);
|
|
1927
2090
|
}
|
|
1928
2091
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1929
2092
|
// Tools
|
|
@@ -1945,7 +2108,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1945
2108
|
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
1946
2109
|
const includeOperators = params.includeOperators === true || params.includeOperators === "true";
|
|
1947
2110
|
const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
|
|
1948
|
-
|
|
2111
|
+
const style = params.style;
|
|
2112
|
+
return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0, style);
|
|
1949
2113
|
}
|
|
1950
2114
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1951
2115
|
// Validation
|
|
@@ -2188,12 +2352,14 @@ const MatcherRowItemSchema = zod.z.object({
|
|
|
2188
2352
|
const StringInputJSONSchema = zod.z.object({
|
|
2189
2353
|
type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
|
|
2190
2354
|
inputType: zod.z.literal(INPUT_TYPES.STRING),
|
|
2191
|
-
length: zod.z.number().int().positive()
|
|
2355
|
+
length: zod.z.number().int().positive(),
|
|
2356
|
+
placeholder: zod.z.string().max(32).optional()
|
|
2192
2357
|
});
|
|
2193
2358
|
const NumericInputJSONSchema = zod.z.object({
|
|
2194
2359
|
type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
|
|
2195
2360
|
inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
|
|
2196
|
-
digits: zod.z.number().int().positive()
|
|
2361
|
+
digits: zod.z.number().int().positive(),
|
|
2362
|
+
placeholder: zod.z.string().max(32).optional()
|
|
2197
2363
|
});
|
|
2198
2364
|
const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
|
|
2199
2365
|
StringInputJSONSchema,
|
|
@@ -2210,7 +2376,8 @@ const RadioOptionsJSONSchema = zod.z.object({
|
|
|
2210
2376
|
const NumericKeypadJSONSchema = zod.z.object({
|
|
2211
2377
|
type: zod.z.literal(JSON_TYPES.NUMERIC_KEYPAD),
|
|
2212
2378
|
includeOperators: zod.z.boolean().optional(),
|
|
2213
|
-
includeDecimal: zod.z.boolean().optional()
|
|
2379
|
+
includeDecimal: zod.z.boolean().optional(),
|
|
2380
|
+
style: zod.z.enum(["diamond", "classic"]).optional()
|
|
2214
2381
|
});
|
|
2215
2382
|
const FractionJSONSchema = zod.z.object({
|
|
2216
2383
|
type: zod.z.literal(JSON_TYPES.FRACTION),
|
|
@@ -2226,24 +2393,44 @@ const MatcherWidgetJSONSchema = zod.z.object({
|
|
|
2226
2393
|
/** Number of answer groups — equals answers.length, stored here to avoid prop-drilling. */
|
|
2227
2394
|
groupCount: zod.z.number().int().min(1)
|
|
2228
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);
|
|
2229
2413
|
const WidgetJSONSchema = zod.z.union([
|
|
2230
2414
|
InlineInputJSONSchema,
|
|
2231
2415
|
DragOptionsJSONSchema,
|
|
2232
2416
|
RadioOptionsJSONSchema,
|
|
2233
2417
|
NumericKeypadJSONSchema,
|
|
2234
2418
|
FractionJSONSchema,
|
|
2235
|
-
MatcherWidgetJSONSchema
|
|
2419
|
+
MatcherWidgetJSONSchema,
|
|
2420
|
+
VisualFractionJSONSchema
|
|
2236
2421
|
]);
|
|
2237
2422
|
const ValidationJSONSchema = zod.z.object({
|
|
2238
2423
|
mode: zod.z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
2239
2424
|
// Simple answers: string|number per widget id.
|
|
2240
2425
|
// Matcher answers: string[][] keyed by matcher name. Outer/inner min(1)
|
|
2241
2426
|
// mirrors MatcherBlockSchema invariants in src/ast/matcher.ts.
|
|
2427
|
+
// VisualFraction answers: number[] — one count per color group.
|
|
2242
2428
|
answers: zod.z.record(
|
|
2243
2429
|
zod.z.union([
|
|
2244
2430
|
zod.z.string(),
|
|
2245
2431
|
zod.z.number(),
|
|
2246
|
-
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())
|
|
2247
2434
|
])
|
|
2248
2435
|
).optional(),
|
|
2249
2436
|
expression: zod.z.string().optional(),
|
|
@@ -2394,13 +2581,15 @@ function toJSON(ast) {
|
|
|
2394
2581
|
widgets[inline.name] = {
|
|
2395
2582
|
type: JSON_TYPES.INLINE_INPUT,
|
|
2396
2583
|
inputType: INPUT_TYPES.STRING,
|
|
2397
|
-
length: inline.length
|
|
2584
|
+
length: inline.length,
|
|
2585
|
+
placeholder: inline.placeholder
|
|
2398
2586
|
};
|
|
2399
2587
|
} else {
|
|
2400
2588
|
widgets[inline.name] = {
|
|
2401
2589
|
type: JSON_TYPES.INLINE_INPUT,
|
|
2402
2590
|
inputType: INPUT_TYPES.NUMERIC,
|
|
2403
|
-
digits: inline.digits
|
|
2591
|
+
digits: inline.digits,
|
|
2592
|
+
placeholder: inline.placeholder
|
|
2404
2593
|
};
|
|
2405
2594
|
}
|
|
2406
2595
|
if (inline.answer !== void 0) {
|
|
@@ -2418,6 +2607,22 @@ function toJSON(ast) {
|
|
|
2418
2607
|
answers[inline.name] = inline.answer;
|
|
2419
2608
|
}
|
|
2420
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
|
+
}
|
|
2421
2626
|
}
|
|
2422
2627
|
for (const block of ast.lines) {
|
|
2423
2628
|
if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
@@ -2444,7 +2649,8 @@ function toJSON(ast) {
|
|
|
2444
2649
|
widgets["_numeric_keypad"] = {
|
|
2445
2650
|
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
2446
2651
|
includeOperators: tool.includeOperators,
|
|
2447
|
-
includeDecimal: tool.includeDecimal
|
|
2652
|
+
includeDecimal: tool.includeDecimal,
|
|
2653
|
+
style: tool.style
|
|
2448
2654
|
};
|
|
2449
2655
|
}
|
|
2450
2656
|
}
|
|
@@ -2658,6 +2864,12 @@ function transformInline(inline, idGenerator) {
|
|
|
2658
2864
|
...inline.width !== void 0 && { width: inline.width },
|
|
2659
2865
|
...inline.height !== void 0 && { height: inline.height }
|
|
2660
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
|
+
};
|
|
2661
2873
|
default:
|
|
2662
2874
|
throw new Error(`Unknown inline type: ${inline.type}`);
|
|
2663
2875
|
}
|
|
@@ -8200,6 +8412,11 @@ function areMatcherEqual(expected, result) {
|
|
|
8200
8412
|
const normalize2 = (groups) => lodashExports.sortBy(groups.map((g) => lodashExports.sortBy(g)), (g) => g.join("|"));
|
|
8201
8413
|
return lodashExports.isEqual(normalize2(expected), normalize2(result));
|
|
8202
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
|
+
}
|
|
8203
8420
|
const math$1 = mathjs.create(mathjs.all);
|
|
8204
8421
|
function collectNumberLines(items) {
|
|
8205
8422
|
const result = [];
|
|
@@ -8248,6 +8465,15 @@ function validateSimple(exercise, userInputs) {
|
|
|
8248
8465
|
else allCorrect = false;
|
|
8249
8466
|
continue;
|
|
8250
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
|
+
}
|
|
8251
8477
|
totalCount++;
|
|
8252
8478
|
const userValue = userInputs[widgetId];
|
|
8253
8479
|
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
@@ -8754,10 +8980,10 @@ function getOwnerDocument(target) {
|
|
|
8754
8980
|
}
|
|
8755
8981
|
return document;
|
|
8756
8982
|
}
|
|
8757
|
-
const useIsomorphicLayoutEffect = canUseDOM ? React.useLayoutEffect : React.useEffect;
|
|
8983
|
+
const useIsomorphicLayoutEffect$1 = canUseDOM ? React.useLayoutEffect : React.useEffect;
|
|
8758
8984
|
function useEvent(handler) {
|
|
8759
8985
|
const handlerRef = React.useRef(handler);
|
|
8760
|
-
useIsomorphicLayoutEffect(() => {
|
|
8986
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
8761
8987
|
handlerRef.current = handler;
|
|
8762
8988
|
});
|
|
8763
8989
|
return React.useCallback(function() {
|
|
@@ -8785,7 +9011,7 @@ function useLatestValue(value, dependencies) {
|
|
|
8785
9011
|
dependencies = [value];
|
|
8786
9012
|
}
|
|
8787
9013
|
const valueRef = React.useRef(value);
|
|
8788
|
-
useIsomorphicLayoutEffect(() => {
|
|
9014
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
8789
9015
|
if (valueRef.current !== value) {
|
|
8790
9016
|
valueRef.current = value;
|
|
8791
9017
|
}
|
|
@@ -10653,9 +10879,9 @@ function useResizeObserver(_ref) {
|
|
|
10653
10879
|
return void 0;
|
|
10654
10880
|
}
|
|
10655
10881
|
const {
|
|
10656
|
-
ResizeObserver
|
|
10882
|
+
ResizeObserver: ResizeObserver2
|
|
10657
10883
|
} = window;
|
|
10658
|
-
return new
|
|
10884
|
+
return new ResizeObserver2(handleResize);
|
|
10659
10885
|
},
|
|
10660
10886
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
10661
10887
|
[disabled]
|
|
@@ -10709,7 +10935,7 @@ function useRect(element, measure, fallbackRect) {
|
|
|
10709
10935
|
const resizeObserver = useResizeObserver({
|
|
10710
10936
|
callback: measureRect
|
|
10711
10937
|
});
|
|
10712
|
-
useIsomorphicLayoutEffect(() => {
|
|
10938
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
10713
10939
|
measureRect();
|
|
10714
10940
|
if (element) {
|
|
10715
10941
|
resizeObserver == null ? void 0 : resizeObserver.observe(element);
|
|
@@ -10883,7 +11109,7 @@ function useRects(elements, measure) {
|
|
|
10883
11109
|
const resizeObserver = useResizeObserver({
|
|
10884
11110
|
callback: measureRects
|
|
10885
11111
|
});
|
|
10886
|
-
useIsomorphicLayoutEffect(() => {
|
|
11112
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
10887
11113
|
resizeObserver == null ? void 0 : resizeObserver.disconnect();
|
|
10888
11114
|
measureRects();
|
|
10889
11115
|
elements.forEach((element) => resizeObserver == null ? void 0 : resizeObserver.observe(element));
|
|
@@ -11245,7 +11471,7 @@ function useLayoutShiftScrollCompensation(_ref) {
|
|
|
11245
11471
|
x: config,
|
|
11246
11472
|
y: config
|
|
11247
11473
|
} : config;
|
|
11248
|
-
useIsomorphicLayoutEffect(() => {
|
|
11474
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11249
11475
|
const disabled = !x && !y;
|
|
11250
11476
|
if (disabled || !activeNode) {
|
|
11251
11477
|
initialized.current = false;
|
|
@@ -11606,7 +11832,7 @@ const DndContext = /* @__PURE__ */ React.memo(function DndContext2(_ref) {
|
|
|
11606
11832
|
}, [draggableNodes, instantiateSensor]);
|
|
11607
11833
|
const activators = useCombineActivators(sensors, bindActivatorToSensorInstantiator);
|
|
11608
11834
|
useSensorSetup(sensors);
|
|
11609
|
-
useIsomorphicLayoutEffect(() => {
|
|
11835
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11610
11836
|
if (activeNodeRect && status === Status.Initializing) {
|
|
11611
11837
|
setStatus(Status.Initialized);
|
|
11612
11838
|
}
|
|
@@ -11690,7 +11916,7 @@ const DndContext = /* @__PURE__ */ React.memo(function DndContext2(_ref) {
|
|
|
11690
11916
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
11691
11917
|
[overId]
|
|
11692
11918
|
);
|
|
11693
|
-
useIsomorphicLayoutEffect(() => {
|
|
11919
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11694
11920
|
sensorContext.current = {
|
|
11695
11921
|
activatorEvent,
|
|
11696
11922
|
active,
|
|
@@ -11817,7 +12043,7 @@ function useDraggable(_ref) {
|
|
|
11817
12043
|
const [activatorNode, setActivatorNodeRef] = useNodeRef();
|
|
11818
12044
|
const listeners = useSyntheticListeners(activators, id);
|
|
11819
12045
|
const dataRef = useLatestValue(data);
|
|
11820
|
-
useIsomorphicLayoutEffect(
|
|
12046
|
+
useIsomorphicLayoutEffect$1(
|
|
11821
12047
|
() => {
|
|
11822
12048
|
draggableNodes.set(id, {
|
|
11823
12049
|
id,
|
|
@@ -11990,7 +12216,7 @@ function AnimationManager(_ref) {
|
|
|
11990
12216
|
if (!children && !clonedChildren && previousChildren) {
|
|
11991
12217
|
setClonedChildren(previousChildren);
|
|
11992
12218
|
}
|
|
11993
|
-
useIsomorphicLayoutEffect(() => {
|
|
12219
|
+
useIsomorphicLayoutEffect$1(() => {
|
|
11994
12220
|
if (!element) {
|
|
11995
12221
|
return;
|
|
11996
12222
|
}
|
|
@@ -12672,6 +12898,7 @@ const InlineInput = ({
|
|
|
12672
12898
|
inputType,
|
|
12673
12899
|
length,
|
|
12674
12900
|
digits,
|
|
12901
|
+
placeholder,
|
|
12675
12902
|
onChange,
|
|
12676
12903
|
onFocus
|
|
12677
12904
|
}) => {
|
|
@@ -12717,19 +12944,88 @@ const InlineInput = ({
|
|
|
12717
12944
|
onChange: (e) => handleChange(e.target.value, false),
|
|
12718
12945
|
onFocus: () => onFocus?.(widgetId),
|
|
12719
12946
|
maxLength: expectedLength,
|
|
12720
|
-
style: { width: dynamicWidth }
|
|
12947
|
+
style: { width: dynamicWidth },
|
|
12948
|
+
placeholder
|
|
12721
12949
|
}
|
|
12722
12950
|
)
|
|
12723
12951
|
)
|
|
12724
12952
|
}
|
|
12725
12953
|
);
|
|
12726
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
|
+
};
|
|
12727
13023
|
const DragOptions = ({
|
|
12728
13024
|
options,
|
|
12729
13025
|
onOptionClick
|
|
12730
13026
|
}) => {
|
|
12731
13027
|
const { usedOptionIds } = useExerciseDnd();
|
|
12732
|
-
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) => {
|
|
12733
13029
|
const optionId = `drag-option-${idx}`;
|
|
12734
13030
|
const optionValue = String(option);
|
|
12735
13031
|
const isUsed = usedOptionIds.has(optionId);
|
|
@@ -12756,7 +13052,7 @@ const DragOptions = ({
|
|
|
12756
13052
|
}
|
|
12757
13053
|
)
|
|
12758
13054
|
) }, optionId);
|
|
12759
|
-
}) });
|
|
13055
|
+
}) }) });
|
|
12760
13056
|
};
|
|
12761
13057
|
const NumericKeypad = ({
|
|
12762
13058
|
value,
|
|
@@ -12852,28 +13148,6 @@ const FractionInput = ({
|
|
|
12852
13148
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator") })
|
|
12853
13149
|
] });
|
|
12854
13150
|
};
|
|
12855
|
-
const RadioOptions = ({
|
|
12856
|
-
name,
|
|
12857
|
-
options,
|
|
12858
|
-
value,
|
|
12859
|
-
validationStatus = null,
|
|
12860
|
-
onChange
|
|
12861
|
-
}) => {
|
|
12862
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "radio-options", children: options.map((option, idx) => {
|
|
12863
|
-
const isSelected = value === option;
|
|
12864
|
-
const statusClass = isSelected && validationStatus ? ` widget--${validationStatus}` : "";
|
|
12865
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
12866
|
-
"button",
|
|
12867
|
-
{
|
|
12868
|
-
className: `radio-option${isSelected ? " selected" : ""}${statusClass}`,
|
|
12869
|
-
onClick: () => onChange(name, option),
|
|
12870
|
-
type: "button",
|
|
12871
|
-
children: option
|
|
12872
|
-
},
|
|
12873
|
-
`${name}-radio-${idx}`
|
|
12874
|
-
);
|
|
12875
|
-
}) });
|
|
12876
|
-
};
|
|
12877
13151
|
function isLightColor(hex) {
|
|
12878
13152
|
const clean = hex.replace("#", "");
|
|
12879
13153
|
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
@@ -12888,7 +13162,8 @@ function isLightColor(hex) {
|
|
|
12888
13162
|
return 1.05 / (L + 0.05) < 4.5;
|
|
12889
13163
|
}
|
|
12890
13164
|
function getTextColorForBackground(color) {
|
|
12891
|
-
|
|
13165
|
+
const hslLightness = parseHslLightness(color);
|
|
13166
|
+
if (hslLightness !== null) return hslLightness >= 60 ? "#1a1a1a" : "#ffffff";
|
|
12892
13167
|
return isLightColor(color) ? "#1a1a1a" : "#ffffff";
|
|
12893
13168
|
}
|
|
12894
13169
|
function generateHslPalette(n) {
|
|
@@ -12897,7 +13172,7 @@ function generateHslPalette(n) {
|
|
|
12897
13172
|
return `hsl(${hue}, 60%, 72%)`;
|
|
12898
13173
|
});
|
|
12899
13174
|
}
|
|
12900
|
-
function
|
|
13175
|
+
function resolveGroupPalette(userColors, groupCount) {
|
|
12901
13176
|
if (groupCount <= 0) return [];
|
|
12902
13177
|
if (!userColors || userColors.length === 0) {
|
|
12903
13178
|
return generateHslPalette(groupCount);
|
|
@@ -12909,6 +13184,478 @@ function resolveMatcherPalette(userColors, groupCount) {
|
|
|
12909
13184
|
const extras = generateHslPalette(needed);
|
|
12910
13185
|
return [...userColors, ...extras];
|
|
12911
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
|
+
};
|
|
12912
13659
|
function collectItemIds(rows) {
|
|
12913
13660
|
return rows.flatMap((row) => row.items.map((item) => item.itemId));
|
|
12914
13661
|
}
|
|
@@ -12950,7 +13697,7 @@ const MatcherWidget = ({
|
|
|
12950
13697
|
const { rows, colors: userColors, groupCount } = widget;
|
|
12951
13698
|
const widgetsMap = widgets ?? EMPTY_WIDGETS;
|
|
12952
13699
|
const palette = React.useMemo(
|
|
12953
|
-
() =>
|
|
13700
|
+
() => resolveGroupPalette(userColors, groupCount),
|
|
12954
13701
|
[userColors, groupCount]
|
|
12955
13702
|
);
|
|
12956
13703
|
const textColors = React.useMemo(
|
|
@@ -12966,13 +13713,21 @@ const MatcherWidget = ({
|
|
|
12966
13713
|
setAssignments((prev) => {
|
|
12967
13714
|
const current = prev[itemId] ?? null;
|
|
12968
13715
|
const next = current === null ? 0 : current + 1 >= groupCount ? null : current + 1;
|
|
12969
|
-
|
|
12970
|
-
onChange(widgetId, buildGroups(updated));
|
|
12971
|
-
return updated;
|
|
13716
|
+
return { ...prev, [itemId]: next };
|
|
12972
13717
|
});
|
|
12973
13718
|
},
|
|
12974
|
-
[groupCount
|
|
13719
|
+
[groupCount]
|
|
12975
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]);
|
|
12976
13731
|
const maxCols = React.useMemo(
|
|
12977
13732
|
() => rows.length === 0 ? 1 : Math.max(...rows.map((r) => r.items.length)),
|
|
12978
13733
|
[rows]
|
|
@@ -12993,9 +13748,9 @@ const MatcherWidget = ({
|
|
|
12993
13748
|
{
|
|
12994
13749
|
type: "button",
|
|
12995
13750
|
className: `matcher-card${isSelected ? " matcher-card--selected" : ""}`,
|
|
12996
|
-
style: color ? {
|
|
12997
|
-
"--matcher-card-color": color,
|
|
12998
|
-
"--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]})`
|
|
12999
13754
|
} : void 0,
|
|
13000
13755
|
onClick: () => handleCardClick(item.itemId),
|
|
13001
13756
|
"aria-pressed": isSelected,
|
|
@@ -13037,6 +13792,9 @@ function isFractionWidget(widget) {
|
|
|
13037
13792
|
function isMatcherWidget(widget) {
|
|
13038
13793
|
return widget.type === "matcher";
|
|
13039
13794
|
}
|
|
13795
|
+
function isVisualFractionWidget(widget) {
|
|
13796
|
+
return widget.type === "visual-fraction";
|
|
13797
|
+
}
|
|
13040
13798
|
const WidgetRenderer = ({
|
|
13041
13799
|
widget,
|
|
13042
13800
|
widgetId,
|
|
@@ -13062,6 +13820,7 @@ const WidgetRenderer = ({
|
|
|
13062
13820
|
inputType: widget.inputType,
|
|
13063
13821
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
13064
13822
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
13823
|
+
placeholder: widget.placeholder,
|
|
13065
13824
|
onChange,
|
|
13066
13825
|
onFocus: onInputFocus
|
|
13067
13826
|
}
|
|
@@ -13090,14 +13849,24 @@ const WidgetRenderer = ({
|
|
|
13090
13849
|
);
|
|
13091
13850
|
}
|
|
13092
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
|
+
}
|
|
13093
13865
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13094
|
-
|
|
13866
|
+
DiamondKeypad,
|
|
13095
13867
|
{
|
|
13096
13868
|
value: strValue,
|
|
13097
|
-
|
|
13098
|
-
includeDecimal: widget.includeDecimal,
|
|
13099
|
-
onChange: onKeypadChange || (() => {
|
|
13100
|
-
})
|
|
13869
|
+
onChange: keypadOnChange
|
|
13101
13870
|
}
|
|
13102
13871
|
);
|
|
13103
13872
|
}
|
|
@@ -13129,6 +13898,18 @@ const WidgetRenderer = ({
|
|
|
13129
13898
|
}
|
|
13130
13899
|
);
|
|
13131
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
|
+
}
|
|
13132
13913
|
return null;
|
|
13133
13914
|
};
|
|
13134
13915
|
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
@@ -13631,6 +14412,102 @@ const CounterControls = ({
|
|
|
13631
14412
|
] }),
|
|
13632
14413
|
counter.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "number-line-counter-label", children: counter.name })
|
|
13633
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
|
+
}
|
|
13634
14511
|
const NumberLineRenderer = ({
|
|
13635
14512
|
item,
|
|
13636
14513
|
widgets,
|
|
@@ -13685,17 +14562,26 @@ const NumberLineRenderer = ({
|
|
|
13685
14562
|
() => positions.filter((pos) => !isDragMode || getPositionWidgetId(pos.content) === null),
|
|
13686
14563
|
[isDragMode, positions]
|
|
13687
14564
|
);
|
|
14565
|
+
const visiblePositionIds = React.useMemo(
|
|
14566
|
+
() => visiblePositions.map((pos) => pos.id),
|
|
14567
|
+
[visiblePositions]
|
|
14568
|
+
);
|
|
14569
|
+
const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
|
|
13688
14570
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
13689
14571
|
"div",
|
|
13690
14572
|
{
|
|
13691
14573
|
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
13692
|
-
style: {
|
|
14574
|
+
style: {
|
|
14575
|
+
[CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
|
|
14576
|
+
...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
|
|
14577
|
+
},
|
|
13693
14578
|
role: "group",
|
|
13694
14579
|
"aria-label": `Number line from ${min} to ${max}`,
|
|
13695
14580
|
children: [
|
|
13696
14581
|
visiblePositions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: visiblePositions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
13697
14582
|
"div",
|
|
13698
14583
|
{
|
|
14584
|
+
ref: getPositionContentRef(pos.id),
|
|
13699
14585
|
className: "number-line-position-content",
|
|
13700
14586
|
style: { left: `${toPercent(pos.value, min, max)}%` },
|
|
13701
14587
|
children: pos.content.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -13898,7 +14784,7 @@ const LayoutRenderer = ({
|
|
|
13898
14784
|
if (!widget) return null;
|
|
13899
14785
|
const isCorrect = validationDetails?.[item.widgetId];
|
|
13900
14786
|
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
13901
|
-
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;
|
|
13902
14788
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
13903
14789
|
WidgetRenderer,
|
|
13904
14790
|
{
|
|
@@ -14302,9 +15188,14 @@ exports.TextBlockSchema = TextBlockSchema;
|
|
|
14302
15188
|
exports.TextItemSchema = TextItemSchema;
|
|
14303
15189
|
exports.ToolNodeSchema = ToolNodeSchema;
|
|
14304
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;
|
|
14305
15195
|
exports.ValidationError = ValidationError;
|
|
14306
15196
|
exports.ValidationJSONSchema = ValidationJSONSchema;
|
|
14307
15197
|
exports.ValidationNodeSchema = ValidationNodeSchema;
|
|
15198
|
+
exports.VisualFractionInlineSchema = VisualFractionInlineSchema;
|
|
14308
15199
|
exports.WidgetJSONSchema = WidgetJSONSchema;
|
|
14309
15200
|
exports.WidgetRefSchema = WidgetRefSchema;
|
|
14310
15201
|
exports.WidgetRenderer = WidgetRenderer;
|
|
@@ -14329,6 +15220,7 @@ exports.createSimpleValidation = createSimpleValidation;
|
|
|
14329
15220
|
exports.createSpace = createSpace;
|
|
14330
15221
|
exports.createStringInput = createStringInput;
|
|
14331
15222
|
exports.createTextBlock = createTextBlock;
|
|
15223
|
+
exports.createVisualFraction = createVisualFraction;
|
|
14332
15224
|
exports.evaluate = evaluate;
|
|
14333
15225
|
exports.extractExpressionVariables = extractVariables;
|
|
14334
15226
|
exports.findValidCombination = findValidCombination;
|
|
@@ -14372,6 +15264,7 @@ exports.isTextItem = isTextItem$1;
|
|
|
14372
15264
|
exports.isToolNode = isToolNode;
|
|
14373
15265
|
exports.isValidExpression = isValidExpression;
|
|
14374
15266
|
exports.isValidationNode = isValidationNode;
|
|
15267
|
+
exports.isVisualFractionInline = isVisualFractionInline;
|
|
14375
15268
|
exports.isWidgetRef = isWidgetRef;
|
|
14376
15269
|
exports.parse = parse;
|
|
14377
15270
|
exports.parseFractionKey = parseFractionKey;
|
|
@@ -14388,6 +15281,7 @@ exports.validateExerciseJSONSafe = validateExerciseJSONSafe;
|
|
|
14388
15281
|
exports.validateExpression = validateExpression;
|
|
14389
15282
|
exports.validateInputFormats = validateInputFormats;
|
|
14390
15283
|
exports.validateSimple = validateSimple;
|
|
15284
|
+
exports.validateVisualFractionCapacity = validateVisualFractionCapacity;
|
|
14391
15285
|
exports.validateWidgetJSON = validateWidgetJSON;
|
|
14392
15286
|
exports.validateWidgetJSONSafe = validateWidgetJSONSafe;
|
|
14393
15287
|
//# sourceMappingURL=index.js.map
|