@mathscan/math-exercise-engine 1.2.13 → 1.2.14

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.
@@ -2091,6 +2091,8 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2091
2091
  width: InlineInputWidthSchema,
2092
2092
  placeholder: z.string().max(32).optional(),
2093
2093
  disabled: z.boolean().optional(),
2094
+ /** Starting value the student can edit (shown read-only when disabled). */
2095
+ prefill: z.string().optional(),
2094
2096
  answer: z.union([z.string(), z.number()]).optional()
2095
2097
  }),
2096
2098
  z.object({
@@ -2101,6 +2103,8 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2101
2103
  width: InlineInputWidthSchema,
2102
2104
  placeholder: z.string().max(32).optional(),
2103
2105
  disabled: z.boolean().optional(),
2106
+ /** Starting value the student can edit (shown read-only when disabled). */
2107
+ prefill: z.string().optional(),
2104
2108
  answer: z.union([z.string(), z.number()]).optional()
2105
2109
  })
2106
2110
  ]);
@@ -2256,7 +2260,7 @@ function createBoldText(value, highlight) {
2256
2260
  highlight
2257
2261
  });
2258
2262
  }
2259
- function createStringInput(name, length, answer, placeholder, disabled, width) {
2263
+ function createStringInput(name, length, answer, placeholder, disabled, width, prefill) {
2260
2264
  return parseOrThrow(
2261
2265
  InlineInputWidgetSchema,
2262
2266
  {
@@ -2267,13 +2271,14 @@ function createStringInput(name, length, answer, placeholder, disabled, width) {
2267
2271
  width,
2268
2272
  placeholder,
2269
2273
  disabled,
2274
+ prefill,
2270
2275
  // A disabled input is never graded, so its answer is dropped.
2271
2276
  answer: disabled ? void 0 : answer
2272
2277
  },
2273
2278
  "STRING_INPUT"
2274
2279
  );
2275
2280
  }
2276
- function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2281
+ function createNumericInput(name, digits, answer, placeholder, disabled, width, prefill) {
2277
2282
  return parseOrThrow(
2278
2283
  InlineInputWidgetSchema,
2279
2284
  {
@@ -2284,6 +2289,7 @@ function createNumericInput(name, digits, answer, placeholder, disabled, width)
2284
2289
  width,
2285
2290
  placeholder,
2286
2291
  disabled,
2292
+ prefill,
2287
2293
  // A disabled input is never graded, so its answer is dropped.
2288
2294
  answer: disabled ? void 0 : answer
2289
2295
  },
@@ -4323,6 +4329,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4323
4329
  const name = params.name;
4324
4330
  const answer = params.answer;
4325
4331
  const placeholder = params.placeholder;
4332
+ const prefill = params.prefill === void 0 ? void 0 : String(params.prefill);
4326
4333
  let disabled;
4327
4334
  if (params.disabled === true || params.disabled === "true") disabled = true;
4328
4335
  else if (params.disabled === false || params.disabled === "false") disabled = false;
@@ -4340,10 +4347,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4340
4347
  }
4341
4348
  if (ctx.stringInput) {
4342
4349
  const length = params.length;
4343
- return createStringInput(name, length, answer, placeholder, disabled, width);
4350
+ return createStringInput(name, length, answer, placeholder, disabled, width, prefill);
4344
4351
  }
4345
4352
  const digits = params.digits;
4346
- return createNumericInput(name, digits, answer, placeholder, disabled, width);
4353
+ return createNumericInput(name, digits, answer, placeholder, disabled, width, prefill);
4347
4354
  }
4348
4355
  inlineDragOption(ctx) {
4349
4356
  const valueToken = ctx.StringLiteral[0];
@@ -5087,7 +5094,9 @@ const StringInputJSONSchema = z.object({
5087
5094
  width: InlineInputWidthSchema,
5088
5095
  placeholder: z.string().max(32).optional(),
5089
5096
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5090
- disabled: z.boolean().optional()
5097
+ disabled: z.boolean().optional(),
5098
+ /** Starting value the student can edit (shown read-only when disabled). */
5099
+ prefill: z.string().optional()
5091
5100
  });
5092
5101
  const NumericInputJSONSchema = z.object({
5093
5102
  type: z.literal(JSON_TYPES.INLINE_INPUT),
@@ -5097,7 +5106,9 @@ const NumericInputJSONSchema = z.object({
5097
5106
  width: InlineInputWidthSchema,
5098
5107
  placeholder: z.string().max(32).optional(),
5099
5108
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5100
- disabled: z.boolean().optional()
5109
+ disabled: z.boolean().optional(),
5110
+ /** Starting value the student can edit (shown read-only when disabled). */
5111
+ prefill: z.string().optional()
5101
5112
  });
5102
5113
  const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
5103
5114
  StringInputJSONSchema,
@@ -10992,8 +11003,10 @@ function toJSON(ast) {
10992
11003
  layout.push(...lineItems);
10993
11004
  }
10994
11005
  }
11006
+ const hasDragBank = ast.tools?.some((tool) => tool.type === AST_TYPES.DRAG_OPTIONS) ?? false;
10995
11007
  for (const inline of allInlines) {
10996
11008
  if (inline.type === AST_TYPES.INLINE_INPUT) {
11009
+ const disabled = inline.disabled || hasDragBank && inline.prefill !== void 0;
10997
11010
  if (inline.inputType === INPUT_TYPES.STRING) {
10998
11011
  widgets[inline.name] = {
10999
11012
  type: JSON_TYPES.INLINE_INPUT,
@@ -11001,7 +11014,8 @@ function toJSON(ast) {
11001
11014
  length: inline.length,
11002
11015
  ...inline.width !== void 0 && { width: inline.width },
11003
11016
  placeholder: inline.placeholder,
11004
- ...inline.disabled && { disabled: true }
11017
+ ...disabled && { disabled: true },
11018
+ ...inline.prefill !== void 0 && { prefill: inline.prefill }
11005
11019
  };
11006
11020
  } else {
11007
11021
  widgets[inline.name] = {
@@ -11010,10 +11024,11 @@ function toJSON(ast) {
11010
11024
  digits: inline.digits,
11011
11025
  ...inline.width !== void 0 && { width: inline.width },
11012
11026
  placeholder: inline.placeholder,
11013
- ...inline.disabled && { disabled: true }
11027
+ ...disabled && { disabled: true },
11028
+ ...inline.prefill !== void 0 && { prefill: inline.prefill }
11014
11029
  };
11015
11030
  }
11016
- if (inline.answer !== void 0) {
11031
+ if (inline.answer !== void 0 && !disabled) {
11017
11032
  answers[inline.name] = inline.answer;
11018
11033
  }
11019
11034
  } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
@@ -12239,7 +12254,20 @@ function revealGeometryWidget(json) {
12239
12254
  if (!revealAnswer(store, loaded.data).success) return null;
12240
12255
  return serializeGeometryState(store.getState().geometry);
12241
12256
  }
12257
+ function withDisabledPrefilledInputs(exercise) {
12258
+ if (exercise.widgets[DRAG_OPTIONS_WIDGET_ID]?.type !== JSON_TYPES.DRAG_OPTIONS) return exercise;
12259
+ const widgets = { ...exercise.widgets };
12260
+ let changed = false;
12261
+ for (const [id, widget] of Object.entries(widgets)) {
12262
+ if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill !== void 0 && !widget.disabled) {
12263
+ widgets[id] = { ...widget, disabled: true };
12264
+ changed = true;
12265
+ }
12266
+ }
12267
+ return changed ? { ...exercise, widgets } : exercise;
12268
+ }
12242
12269
  function withoutDisabledWidgets(exercise) {
12270
+ exercise = withDisabledPrefilledInputs(exercise);
12243
12271
  const validation = exercise.validation;
12244
12272
  if (!validation) return exercise;
12245
12273
  const disabled = new Set(
@@ -12262,6 +12290,7 @@ function withoutDisabledWidgets(exercise) {
12262
12290
  };
12263
12291
  }
12264
12292
  function findDisabledInExpression(exercise) {
12293
+ exercise = withDisabledPrefilledInputs(exercise);
12265
12294
  const expression = exercise.validation?.expression;
12266
12295
  if (!expression) return [];
12267
12296
  return extractVariables(expression, Object.keys(exercise.widgets)).filter((name) => exercise.widgets[name] && isDisabledInputJSON(exercise.widgets[name]));
@@ -16342,7 +16371,6 @@ const Draggable = ({
16342
16371
  attributes,
16343
16372
  listeners,
16344
16373
  setNodeRef,
16345
- transform,
16346
16374
  isDragging
16347
16375
  } = useDraggable({
16348
16376
  id,
@@ -16350,7 +16378,6 @@ const Draggable = ({
16350
16378
  disabled
16351
16379
  });
16352
16380
  const style = {
16353
- transform: CSS$1.Translate.toString(transform),
16354
16381
  cursor: disabled ? "default" : "grab",
16355
16382
  touchAction: "none"
16356
16383
  };
@@ -17101,6 +17128,16 @@ const SlotSizer = ({
17101
17128
  style: { ...style, visibility: "hidden" }
17102
17129
  }
17103
17130
  );
17131
+ const FixedInputChip = ({ value }) => /* @__PURE__ */ jsx(
17132
+ "button",
17133
+ {
17134
+ type: "button",
17135
+ className: "inline-input-chip",
17136
+ disabled: true,
17137
+ style: OVERLAY_STYLE,
17138
+ children: value
17139
+ }
17140
+ );
17104
17141
  const PlacedInputChip = ({
17105
17142
  widgetId,
17106
17143
  value,
@@ -17170,6 +17207,8 @@ function findNextInput(container, currentKey) {
17170
17207
  }
17171
17208
  const KeypadContext = createContext(false);
17172
17209
  const useHasKeypad = () => useContext(KeypadContext);
17210
+ const DragBankContext = createContext(false);
17211
+ const useHasDragBank = () => useContext(DragBankContext);
17173
17212
  const InlineInput = ({
17174
17213
  widgetId,
17175
17214
  value,
@@ -17187,6 +17226,7 @@ const InlineInput = ({
17187
17226
  const playSound = useSound();
17188
17227
  const locked = useExerciseLocked();
17189
17228
  const hasKeypad = useHasKeypad();
17229
+ const hasDragBank = useHasDragBank();
17190
17230
  const handleChange = useCallback((newValue, fromDrop = false, sourceOptionId) => {
17191
17231
  onChange(widgetId, newValue, fromDrop, sourceOptionId);
17192
17232
  }, [onChange, widgetId]);
@@ -17197,6 +17237,7 @@ const InlineInput = ({
17197
17237
  const dynamicWidth = `${(width ?? expectedLength ?? 5) * 0.8 + 1}em`;
17198
17238
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
17199
17239
  const disabledClass = disabled ? " inline-input--disabled" : "";
17240
+ const isFixedChip = hasDragBank && disabled && value !== "";
17200
17241
  return /* @__PURE__ */ jsx(
17201
17242
  Droppable,
17202
17243
  {
@@ -17205,7 +17246,10 @@ const InlineInput = ({
17205
17246
  onDrop: handleDrop,
17206
17247
  disabled: locked || disabled,
17207
17248
  className: "inline-widget inline-input-droppable",
17208
- children: isDropped && value !== "" ? (
17249
+ children: isFixedChip ? /* @__PURE__ */ jsxs(Fragment, { children: [
17250
+ /* @__PURE__ */ jsx(SlotSizer, { className: "inline-input", style: { width: dynamicWidth }, placeholder }),
17251
+ /* @__PURE__ */ jsx(FixedInputChip, { value })
17252
+ ] }) : isDropped && value !== "" ? (
17209
17253
  // Show as chip only when value came from drag-drop
17210
17254
  /* @__PURE__ */ jsxs(Fragment, { children: [
17211
17255
  /* @__PURE__ */ jsx(
@@ -17262,11 +17306,13 @@ const DragOptionChip = ({
17262
17306
  onOptionClick,
17263
17307
  className = ""
17264
17308
  }) => {
17265
- const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17309
+ const { activeItem, usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17266
17310
  const locked = useExerciseLocked();
17267
17311
  const isUsed = usedOptionIds.has(optionId);
17312
+ const isPickedUp = activeItem?.type === "drag-option" && activeItem.id === optionId;
17268
17313
  const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
17269
- return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
17314
+ const emptySlot = /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true", children: /* @__PURE__ */ jsx("button", { className: "drag-option", disabled: true, children: value }) });
17315
+ return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? emptySlot : /* @__PURE__ */ jsx(
17270
17316
  Draggable,
17271
17317
  {
17272
17318
  id: optionId,
@@ -17274,7 +17320,7 @@ const DragOptionChip = ({
17274
17320
  data: { value, label: value },
17275
17321
  disabled: locked,
17276
17322
  className: "drag-option-wrapper",
17277
- children: /* @__PURE__ */ jsx(
17323
+ children: isPickedUp ? emptySlot : /* @__PURE__ */ jsx(
17278
17324
  "button",
17279
17325
  {
17280
17326
  className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
@@ -19709,7 +19755,7 @@ function isGeometryWidget(widget) {
19709
19755
  return widget.type === JSON_TYPES.GEOMETRY;
19710
19756
  }
19711
19757
  const GeometryWidget = React.lazy(async () => ({
19712
- default: (await import("./GeometryWidget-h2yNZsas.mjs")).GeometryWidget
19758
+ default: (await import("./GeometryWidget-B-EQ53jz.mjs")).GeometryWidget
19713
19759
  }));
19714
19760
  const WidgetRenderer = ({
19715
19761
  widget,
@@ -22011,7 +22057,7 @@ const LayoutRenderer = ({
22011
22057
  return null;
22012
22058
  }
22013
22059
  };
22014
- function LionHand({ className }) {
22060
+ function ChameleonHand({ className }) {
22015
22061
  const id = useId();
22016
22062
  const shadowId = `${id}-shadow`;
22017
22063
  const maskId = `${id}-mask`;
@@ -22038,26 +22084,90 @@ function LionHand({ className }) {
22038
22084
  y: "19",
22039
22085
  width: "49",
22040
22086
  height: "42",
22041
- children: /* @__PURE__ */ jsx("path", { d: "M49.7484 33.9668C58.158 45.6281 37.9572 61.4399 28.1745 60.1051C17.1397 58.5995 5.03802 42.2474 10.4349 38.3224C15.8318 34.3974 18.9715 32.4031 23.1181 30.1278C24.1486 29.5624 25.1742 29.0263 26.1947 28.5358C33.7314 24.764 52.8138 16.0308 57.0969 21.97C59.9201 25.8848 54.8651 28.9522 48.1097 31.8806C48.658 32.518 49.2042 33.2123 49.7484 33.9668Z", fill: "#FCD592" })
22087
+ children: /* @__PURE__ */ jsx(
22088
+ "path",
22089
+ {
22090
+ d: "M49.7484 33.9668C58.158 45.6281 37.9572 61.4399 28.1745 60.1051C17.1397 58.5995 5.03802 42.2474 10.4349 38.3224C15.8318 34.3974 18.9715 32.4031 23.1181 30.1278C24.1486 29.5624 25.1742 29.0263 26.1947 28.5358C33.7314 24.764 52.8138 16.0308 57.0969 21.97C59.9201 25.8848 54.8651 28.9522 48.1097 31.8806C48.658 32.518 49.2042 33.2123 49.7484 33.9668Z",
22091
+ fill: "#B7DB4E"
22092
+ }
22093
+ )
22042
22094
  }
22043
22095
  ),
22044
22096
  /* @__PURE__ */ jsxs("g", { mask: `url(#${maskId})`, children: [
22045
- /* @__PURE__ */ jsx("path", { d: "M49.7484 33.9672C58.158 45.6285 38.3118 60.3151 32.1037 64.7332C22.6672 71.4486 2.68204 43.9969 12.2424 37.1926C15.1884 35.0959 18.9716 32.4035 23.1182 30.1282C24.7308 29.2433 26.3314 28.4303 27.9195 27.7514C36.611 23.6398 54.4269 16.1501 58.4471 21.7248C61.34 25.7362 55.5686 28.9294 48.2024 31.9893C48.7196 32.5968 49.2349 33.2552 49.7484 33.9672Z", fill: "#FCD592" }),
22046
- /* @__PURE__ */ jsx("path", { d: "M12.2426 37.1924C2.68223 43.9967 22.6674 71.4485 32.1039 64.733C31.3434 63.6785 26.4317 61.1668 25.3134 59.6161C24.1951 58.0654 22.794 50.5211 22.123 49.5907C21.4521 48.6603 16.3219 45.7409 15.5637 44.4032C14.8055 43.0655 13.8979 39.4877 14.7258 35.4253L12.2426 37.1924Z", fill: "#6B291E" }),
22047
- /* @__PURE__ */ jsx("path", { d: "M50.4102 36.1737C50.6823 37.524 52.0035 38.4016 53.3614 38.1339C54.7192 37.8662 55.5994 36.5546 55.3274 35.2043L52.8688 35.689L50.4102 36.1737ZM38.8014 26.9523C37.4241 27.0934 36.425 28.3178 36.5697 29.6872C36.7144 31.0565 37.9482 32.0522 39.3255 31.9111L39.0634 29.4317L38.8014 26.9523ZM52.8688 35.689C55.3274 35.2043 55.327 35.2023 55.3266 35.2004C55.3265 35.1997 55.326 35.1977 55.3258 35.1963C55.3252 35.1935 55.3246 35.1906 55.324 35.1875C55.3227 35.1815 55.3213 35.1749 55.3198 35.1679C55.3168 35.1539 55.3133 35.1379 55.3092 35.1201C55.3011 35.0845 55.2907 35.0415 55.2779 34.9916C55.2522 34.8918 55.2163 34.764 55.1676 34.6127C55.0703 34.3106 54.9205 33.911 54.6961 33.4507C54.247 32.5296 53.4925 31.3535 52.2552 30.241C49.7078 27.9508 45.5021 26.2658 38.8014 26.9523L39.0634 29.4317L39.3255 31.9111C44.9159 31.3384 47.6128 32.782 48.8954 33.9352C49.5731 34.5445 49.9653 35.1687 50.1842 35.6177C50.2938 35.8424 50.3584 36.0201 50.3919 36.1242C50.4086 36.1759 50.4172 36.2084 50.4197 36.2181C50.4209 36.223 50.4206 36.222 50.419 36.2148C50.4181 36.2111 50.417 36.2059 50.4155 36.1991C50.4148 36.1957 50.414 36.1919 50.4131 36.1876C50.4127 36.1855 50.4122 36.1833 50.4117 36.181C50.4115 36.1798 50.4111 36.178 50.411 36.1774C50.4106 36.1755 50.4102 36.1737 52.8688 35.689Z", fill: "#A2634A" }),
22048
- /* @__PURE__ */ jsx("path", { d: "M54.7526 47.3947C55.5602 48.5146 57.1269 48.7734 58.2518 47.9729C59.3767 47.1723 59.634 45.6155 58.8264 44.4956L56.7895 45.9452L54.7526 47.3947ZM38.1539 36.5617C36.7805 36.7357 35.8113 37.9837 35.9891 39.3493C36.1669 40.7148 37.4244 41.6808 38.7978 41.5068L38.4758 39.0342L38.1539 36.5617ZM56.7895 45.9452L58.8264 44.4956C57.4277 42.5562 55.3182 40.1317 51.9378 38.4204C48.5385 36.6995 44.0772 35.8112 38.1539 36.5617L38.4758 39.0342L38.7978 41.5068C43.8796 40.863 47.3003 41.6624 49.6691 42.8616C52.0569 44.0704 53.6015 45.7985 54.7526 47.3947L56.7895 45.9452Z", fill: "#A2634A" })
22097
+ /* @__PURE__ */ jsx(
22098
+ "path",
22099
+ {
22100
+ d: "M49.7484 33.9672C58.158 45.6285 38.3118 60.3151 32.1037 64.7332C22.6672 71.4486 2.68204 43.9969 12.2424 37.1926C15.1884 35.0959 18.9716 32.4035 23.1182 30.1282C24.7308 29.2433 26.3314 28.4303 27.9195 27.7514C36.611 23.6398 54.4269 16.1501 58.4471 21.7248C61.34 25.7362 55.5686 28.9294 48.2024 31.9893C48.7196 32.5968 49.2349 33.2552 49.7484 33.9672Z",
22101
+ fill: "#B7DB4E"
22102
+ }
22103
+ ),
22104
+ /* @__PURE__ */ jsx(
22105
+ "path",
22106
+ {
22107
+ d: "M50.4102 36.1737C50.6823 37.524 52.0035 38.4016 53.3614 38.1339C54.7192 37.8662 55.5994 36.5546 55.3274 35.2043L52.8688 35.689L50.4102 36.1737ZM38.8014 26.9523C37.4241 27.0934 36.425 28.3178 36.5697 29.6872C36.7144 31.0565 37.9482 32.0522 39.3255 31.9111L39.0634 29.4317L38.8014 26.9523ZM52.8688 35.689C55.3274 35.2043 55.327 35.2023 55.3266 35.2004C55.3265 35.1997 55.326 35.1977 55.3258 35.1963C55.3252 35.1935 55.3246 35.1906 55.324 35.1875C55.3227 35.1815 55.3213 35.1749 55.3198 35.1679C55.3168 35.1539 55.3133 35.1379 55.3092 35.1201C55.3011 35.0845 55.2907 35.0415 55.2779 34.9916C55.2522 34.8918 55.2163 34.764 55.1676 34.6127C55.0703 34.3106 54.9205 33.911 54.6961 33.4507C54.247 32.5296 53.4925 31.3535 52.2552 30.241C49.7078 27.9508 45.5021 26.2658 38.8014 26.9523L39.0634 29.4317L39.3255 31.9111C44.9159 31.3384 47.6128 32.782 48.8954 33.9352C49.5731 34.5445 49.9653 35.1687 50.1842 35.6177C50.2938 35.8424 50.3584 36.0201 50.3919 36.1242C50.4086 36.1759 50.4172 36.2084 50.4197 36.2181C50.4209 36.223 50.4206 36.222 50.419 36.2148C50.4181 36.2111 50.417 36.2059 50.4155 36.1991C50.4148 36.1957 50.414 36.1919 50.4131 36.1876C50.4127 36.1855 50.4122 36.1833 50.4117 36.181C50.4115 36.1798 50.4111 36.178 50.411 36.1774C50.4106 36.1755 50.4102 36.1737 52.8688 35.689Z",
22108
+ fill: "#9CAF63"
22109
+ }
22110
+ ),
22111
+ /* @__PURE__ */ jsx(
22112
+ "path",
22113
+ {
22114
+ d: "M54.7526 47.3947C55.5602 48.5146 57.1269 48.7734 58.2518 47.9729C59.3767 47.1723 59.634 45.6155 58.8264 44.4956L56.7895 45.9452L54.7526 47.3947ZM38.1539 36.5617C36.7805 36.7357 35.8113 37.9837 35.9891 39.3493C36.1669 40.7148 37.4244 41.6808 38.7978 41.5068L38.4758 39.0342L38.1539 36.5617ZM56.7895 45.9452L58.8264 44.4956C57.4277 42.5562 55.3182 40.1317 51.9378 38.4204C48.5385 36.6995 44.0772 35.8112 38.1539 36.5617L38.4758 39.0342L38.7978 41.5068C43.8796 40.863 47.3003 41.6624 49.6691 42.8616C52.0569 44.0704 53.6015 45.7985 54.7526 47.3947L56.7895 45.9452Z",
22115
+ fill: "#9CAF63"
22116
+ }
22117
+ )
22049
22118
  ] })
22050
22119
  ] }),
22051
- /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("filter", { id: shadowId, x: "-1.99995", y: "0", width: "78.3658", height: "81.8518", filterUnits: "userSpaceOnUse", colorInterpolationFilters: "sRGB", children: [
22052
- /* @__PURE__ */ jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22053
- /* @__PURE__ */ jsx("feColorMatrix", { in: "SourceAlpha", type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0", result: "hardAlpha" }),
22054
- /* @__PURE__ */ jsx("feOffset", { dy: "2" }),
22055
- /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: "1" }),
22056
- /* @__PURE__ */ jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22057
- /* @__PURE__ */ jsx("feColorMatrix", { type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" }),
22058
- /* @__PURE__ */ jsx("feBlend", { mode: "normal", in2: "BackgroundImageFix", result: "effect1_dropShadow" }),
22059
- /* @__PURE__ */ jsx("feBlend", { mode: "normal", in: "SourceGraphic", in2: "effect1_dropShadow", result: "shape" })
22060
- ] }) })
22120
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
22121
+ "filter",
22122
+ {
22123
+ id: shadowId,
22124
+ x: "-1.99995",
22125
+ y: "0",
22126
+ width: "78.3658",
22127
+ height: "81.8518",
22128
+ filterUnits: "userSpaceOnUse",
22129
+ colorInterpolationFilters: "sRGB",
22130
+ children: [
22131
+ /* @__PURE__ */ jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22132
+ /* @__PURE__ */ jsx(
22133
+ "feColorMatrix",
22134
+ {
22135
+ in: "SourceAlpha",
22136
+ type: "matrix",
22137
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
22138
+ result: "hardAlpha"
22139
+ }
22140
+ ),
22141
+ /* @__PURE__ */ jsx("feOffset", { dy: "2" }),
22142
+ /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: "1" }),
22143
+ /* @__PURE__ */ jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22144
+ /* @__PURE__ */ jsx(
22145
+ "feColorMatrix",
22146
+ {
22147
+ type: "matrix",
22148
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
22149
+ }
22150
+ ),
22151
+ /* @__PURE__ */ jsx(
22152
+ "feBlend",
22153
+ {
22154
+ mode: "normal",
22155
+ in2: "BackgroundImageFix",
22156
+ result: "effect1_dropShadow"
22157
+ }
22158
+ ),
22159
+ /* @__PURE__ */ jsx(
22160
+ "feBlend",
22161
+ {
22162
+ mode: "normal",
22163
+ in: "SourceGraphic",
22164
+ in2: "effect1_dropShadow",
22165
+ result: "shape"
22166
+ }
22167
+ )
22168
+ ]
22169
+ }
22170
+ ) })
22061
22171
  ]
22062
22172
  }
22063
22173
  );
@@ -22119,6 +22229,8 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22119
22229
  tick();
22120
22230
  return () => cancelAnimationFrame(raf);
22121
22231
  }, [step]);
22232
+ const showBubble = options.showBubble !== false;
22233
+ const autoAdvance = options.autoAdvance === true || !showBubble;
22122
22234
  const [tapIndex, setTapIndex] = useState(0);
22123
22235
  const [tapCount, setTapCount] = useState(0);
22124
22236
  const [shownStep, setShownStep] = useState(stepIndex);
@@ -22128,11 +22240,13 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22128
22240
  setTapCount(0);
22129
22241
  }
22130
22242
  useEffect(() => {
22243
+ const targets = step.taps(ctxRef.current);
22244
+ if (autoAdvance && tapIndex >= targets.length) return;
22131
22245
  let release;
22132
22246
  const press = setTimeout(() => {
22133
22247
  setTapCount((n) => n + 1);
22134
- const targets = step.taps(ctxRef.current);
22135
- const el = targets[tapIndex % Math.max(1, targets.length)];
22248
+ const targets2 = step.taps(ctxRef.current);
22249
+ const el = targets2[tapIndex % Math.max(1, targets2.length)];
22136
22250
  if (el instanceof Element) {
22137
22251
  el.classList.add("met-tutorial-pressed");
22138
22252
  release = setTimeout(() => el.classList.remove("met-tutorial-pressed"), PRESS_HOLD_MS);
@@ -22145,23 +22259,44 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22145
22259
  if (release) clearTimeout(release);
22146
22260
  root.querySelectorAll(".met-tutorial-pressed").forEach((el) => el.classList.remove("met-tutorial-pressed"));
22147
22261
  };
22148
- }, [step, tapIndex, root]);
22262
+ }, [step, tapIndex, root, autoAdvance]);
22149
22263
  const text = options.texts?.[step.id] ?? step.text;
22150
22264
  const textRef = useRef(text);
22151
22265
  textRef.current = text;
22266
+ const onCloseRef = useRef(onClose);
22267
+ onCloseRef.current = onClose;
22268
+ const isLast = stepIndex === steps.length - 1;
22269
+ const [completedStep, setCompletedStep] = useState(null);
22152
22270
  useEffect(() => {
22153
22271
  optionsRef.current.onStep?.(stepIndex, step.id);
22154
- optionsRef.current.onSpeak?.(step.id, textRef.current);
22155
- }, [stepIndex, step]);
22156
- const isLast = stepIndex === steps.length - 1;
22272
+ const speech = Promise.resolve(optionsRef.current.onSpeak?.(step.id, textRef.current)).catch(() => {
22273
+ });
22274
+ if (!autoAdvance) return;
22275
+ const duration = Math.max(2, step.taps(ctxRef.current).length) * CYCLE_MS;
22276
+ let cancelled = false;
22277
+ let timer;
22278
+ const animation = new Promise((resolve2) => {
22279
+ timer = setTimeout(resolve2, duration);
22280
+ });
22281
+ void Promise.all([speech, animation]).then(() => {
22282
+ if (cancelled) return;
22283
+ if (isLast) {
22284
+ if (showBubble) setCompletedStep(stepIndex);
22285
+ else onCloseRef.current(true);
22286
+ } else setStepIndex((i) => i + 1);
22287
+ });
22288
+ return () => {
22289
+ cancelled = true;
22290
+ clearTimeout(timer);
22291
+ };
22292
+ }, [stepIndex, step, autoAdvance, isLast, showBubble]);
22293
+ const showNext = !autoAdvance || isLast && completedStep === stepIndex;
22157
22294
  const next = () => {
22158
22295
  if (isLast) onClose(true);
22159
22296
  else setStepIndex((i) => i + 1);
22160
22297
  };
22161
22298
  const overlayRef = useRef(null);
22162
22299
  const nextRef = useRef(null);
22163
- const onCloseRef = useRef(onClose);
22164
- onCloseRef.current = onClose;
22165
22300
  useEffect(() => {
22166
22301
  const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22167
22302
  previous?.blur();
@@ -22194,15 +22329,15 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22194
22329
  }, []);
22195
22330
  const bubbleShown = layout !== null;
22196
22331
  useEffect(() => {
22197
- nextRef.current?.focus({ focusVisible: false });
22198
- }, [stepIndex, bubbleShown]);
22332
+ (nextRef.current ?? overlayRef.current)?.focus({ focusVisible: false });
22333
+ }, [stepIndex, bubbleShown, showNext]);
22199
22334
  const maskId = `met-tutorial-mask-${useId()}`;
22200
22335
  if (typeof document === "undefined") return null;
22201
22336
  const holes = (layout?.spotlight ?? []).filter((r) => r !== null);
22202
- const tapTarget = layout?.taps.length ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22337
+ const tapTarget = layout?.taps.length && (!autoAdvance || tapIndex < layout.taps.length) ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22203
22338
  const Scene = step.Scene;
22204
22339
  return createPortal(
22205
- /* @__PURE__ */ jsxs("div", { ref: overlayRef, className: "met-tutorial", role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22340
+ /* @__PURE__ */ jsxs("div", { ref: overlayRef, className: "met-tutorial", tabIndex: -1, role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22206
22341
  /* @__PURE__ */ jsxs("svg", { className: "met-tutorial-dim", "aria-hidden": "true", children: [
22207
22342
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", { id: maskId, children: [
22208
22343
  /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", fill: "white" }),
@@ -22237,7 +22372,7 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22237
22372
  )),
22238
22373
  Scene && layout && /* @__PURE__ */ jsx(Scene, { ctx, spotlight: layout.spotlight, taps: layout.taps, tapCount }),
22239
22374
  tapTarget && /* @__PURE__ */ jsx(Hand, { target: tapTarget, tapKey: `${stepIndex}-${tapIndex}` }),
22240
- layout && /* @__PURE__ */ jsx(
22375
+ layout && showBubble && /* @__PURE__ */ jsx(
22241
22376
  Bubble,
22242
22377
  {
22243
22378
  layout,
@@ -22245,7 +22380,8 @@ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22245
22380
  stepIndex,
22246
22381
  stepCount: steps.length,
22247
22382
  nextLabel: options.nextLabel ?? DEFAULT_NEXT_LABEL,
22248
- onSpeak: options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22383
+ showNext,
22384
+ onSpeak: !autoAdvance && options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22249
22385
  onNext: next,
22250
22386
  nextRef
22251
22387
  }
@@ -22273,12 +22409,12 @@ const Hand = ({ target, tapKey }) => {
22273
22409
  style: { left: TUTORIAL_HAND_TIP.x * HAND_W, top: TUTORIAL_HAND_TIP.y * HAND_H }
22274
22410
  }
22275
22411
  ),
22276
- /* @__PURE__ */ jsx(LionHand, {})
22412
+ /* @__PURE__ */ jsx(ChameleonHand, {})
22277
22413
  ] }, tapKey)
22278
22414
  }
22279
22415
  );
22280
22416
  };
22281
- const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext, nextRef }) => {
22417
+ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, showNext, onSpeak, onNext, nextRef }) => {
22282
22418
  const { anchor, root, vw, vh } = layout;
22283
22419
  const clampLeft = (x) => Math.min(Math.max(EDGE, x), Math.max(EDGE, vw - BUBBLE_W - EDGE));
22284
22420
  const style = anchor ? { left: clampLeft(anchor.x), bottom: Math.max(EDGE, vh - anchor.y + BUBBLE_GAP), width: BUBBLE_W } : { left: clampLeft((root?.x ?? 0) + EDGE * 2), top: (root?.y ?? 0) + EDGE * 2, width: BUBBLE_W };
@@ -22289,7 +22425,7 @@ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext
22289
22425
  /* @__PURE__ */ jsx("path", { d: "M4 9v6h4l5 4V5L8 9H4z", fill: "currentColor" }),
22290
22426
  /* @__PURE__ */ jsx("path", { d: "M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
22291
22427
  ] }) }),
22292
- /* @__PURE__ */ jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22428
+ showNext && /* @__PURE__ */ jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22293
22429
  ] }),
22294
22430
  stepCount > 1 && /* @__PURE__ */ jsx("div", { className: "met-tutorial-dots", "aria-hidden": "true", children: Array.from({ length: stepCount }, (_, i) => /* @__PURE__ */ jsx("span", { className: i === stepIndex ? "is-current" : i < stepIndex ? "is-done" : "" }, i)) })
22295
22431
  ] });
@@ -22309,7 +22445,16 @@ function answerBox(ctx) {
22309
22445
  const inputs = getTextInputs(ctx.root);
22310
22446
  return inputs.find(canTakeInput) ?? inputs[0] ?? null;
22311
22447
  }
22448
+ function answerDigits(ctx) {
22449
+ const id = answerBox(ctx)?.id.replace(/-input$/, "");
22450
+ const expected = id ? ctx.json.validation?.answers?.[id] : void 0;
22451
+ if (typeof expected !== "number" && typeof expected !== "string") return null;
22452
+ const value = String(expected);
22453
+ return /^\d+$/.test(value) ? [...value] : null;
22454
+ }
22312
22455
  function answerSlots(ctx) {
22456
+ const digits = answerDigits(ctx);
22457
+ if (digits) return digits.length;
22313
22458
  const id = answerBox(ctx)?.id.replace(/-input$/, "");
22314
22459
  const max = id ? getMaxInputLength(ctx.json, id) : void 0;
22315
22460
  return Math.min(MAX_SLOTS, Math.max(1, max ?? DEFAULT_SLOTS));
@@ -22325,13 +22470,14 @@ function checkTarget(ctx) {
22325
22470
  STAND_IN_CHECK_SIZE
22326
22471
  );
22327
22472
  }
22328
- const demoKeys = (ctx) => DEMO_KEYS.map((k) => key(ctx, k));
22329
- const StandInFill = ({ box, slots, filled }) => /* @__PURE__ */ jsx(
22473
+ const demoKeys = (ctx) => (answerDigits(ctx) ?? DEMO_KEYS).map((k) => key(ctx, k));
22474
+ const StandInFill = ({ box, slots, filled, digits }) => /* @__PURE__ */ jsx(
22330
22475
  "div",
22331
22476
  {
22332
22477
  className: "met-tutorial-fill",
22478
+ dir: "ltr",
22333
22479
  style: { left: box.x, top: box.y, width: box.width, height: box.height },
22334
- children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? "●" : "_" }, i))
22480
+ children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? digits?.[i] ?? "●" : "_" }, i))
22335
22481
  }
22336
22482
  );
22337
22483
  const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
@@ -22339,7 +22485,8 @@ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22339
22485
  const box = spotlight[1];
22340
22486
  if (!box) return null;
22341
22487
  const slots = answerSlots(ctx);
22342
- const filled = tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22488
+ const digits = answerDigits(ctx);
22489
+ const filled = digits ? Math.min(tapCount, slots) : tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22343
22490
  const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
22344
22491
  let arc = null;
22345
22492
  if (from) {
@@ -22350,15 +22497,23 @@ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22350
22497
  }
22351
22498
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22352
22499
  arc && /* @__PURE__ */ jsx("svg", { className: "met-tutorial-arc", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: arc }) }),
22353
- arc && /* @__PURE__ */ jsx("div", { className: "met-tutorial-hop", style: { offsetPath: `path('${arc}')` } }, tapCount),
22354
- /* @__PURE__ */ jsx(StandInFill, { box, slots, filled })
22500
+ arc && /* @__PURE__ */ jsx(
22501
+ "div",
22502
+ {
22503
+ className: `met-tutorial-hop${digits ? " met-tutorial-hop--digit" : ""}`,
22504
+ style: { offsetPath: `path('${arc}')` },
22505
+ children: digits?.[(tapCount - 1) % digits.length]
22506
+ },
22507
+ tapCount
22508
+ ),
22509
+ /* @__PURE__ */ jsx(StandInFill, { box, slots, filled, digits })
22355
22510
  ] });
22356
22511
  };
22357
22512
  const CheckScene = ({ ctx, spotlight }) => {
22358
22513
  const box = spotlight[0];
22359
22514
  const check = spotlight[1];
22360
22515
  return /* @__PURE__ */ jsxs(Fragment, { children: [
22361
- box && /* @__PURE__ */ jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx) }),
22516
+ box && /* @__PURE__ */ jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx), digits: answerDigits(ctx) }),
22362
22517
  check && !ctx.submitTarget && /* @__PURE__ */ jsx(
22363
22518
  "div",
22364
22519
  {
@@ -22399,13 +22554,39 @@ const WIDGET_TUTORIALS = [keypadTutorial];
22399
22554
  function findWidgetTutorial(json) {
22400
22555
  return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
22401
22556
  }
22557
+ function getAnswerLengthCompletion(json, answerable, values, touched) {
22558
+ const fields = [];
22559
+ for (const widgetId of answerable) {
22560
+ const widget = json.widgets[widgetId];
22561
+ const expected = json.validation?.answers?.[widgetId];
22562
+ if (widget?.type === JSON_TYPES.INLINE_INPUT && !widget.disabled) {
22563
+ fields.push({ id: widgetId, widgetId, expected });
22564
+ } else if (widget?.type === JSON_TYPES.FRACTION) {
22565
+ const parts = typeof expected === "string" ? expected.split("/") : [];
22566
+ const validParts = parts.length === 2;
22567
+ if (widget.numerator === null) {
22568
+ fields.push({ id: getFractionKey(widgetId, "num"), widgetId, expected: validParts ? parts[0] : void 0 });
22569
+ }
22570
+ if (widget.denominator === null) {
22571
+ fields.push({ id: getFractionKey(widgetId, "den"), widgetId, expected: validParts ? parts[1] : void 0 });
22572
+ }
22573
+ }
22574
+ }
22575
+ const answerLengthMatchedIds = fields.filter(({ id, widgetId, expected }) => {
22576
+ const value = values[id];
22577
+ return touched.has(widgetId) && (typeof expected === "string" || typeof expected === "number") && (typeof value === "string" || typeof value === "number") && String(value).length > 0 && String(value).length === String(expected).length;
22578
+ }).map(({ id }) => id);
22579
+ return {
22580
+ isAnswerLengthMatched: fields.length > 0 && answerLengthMatchedIds.length === fields.length,
22581
+ answerLengthMatchedIds
22582
+ };
22583
+ }
22402
22584
  const TOOL_ASPECT_RATIO = {
22403
22585
  ruler: RULER_ASPECT_RATIO,
22404
22586
  protractor: PROTRACTOR_ASPECT_RATIO,
22405
22587
  "set-square": SET_SQUARE_ASPECT_RATIO
22406
22588
  };
22407
22589
  const FLIP_BTN_EXTRA = 44;
22408
- const INPUT_FINISH_IDLE_MS = 1e3;
22409
22590
  function warnOnInvalidThemeColors(theme) {
22410
22591
  if (process.env.NODE_ENV === "production" || !theme) return;
22411
22592
  if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return;
@@ -22431,6 +22612,8 @@ function buildInitialValues(json) {
22431
22612
  initial[id] = String(widget.value);
22432
22613
  } else if (widget.type === JSON_TYPES.GEOMETRY) {
22433
22614
  initial[id] = serializeGeometryState(createEmptyGeometryAttempt());
22615
+ } else if (widget.type === JSON_TYPES.INLINE_INPUT && widget.prefill !== void 0) {
22616
+ initial[id] = widget.prefill;
22434
22617
  }
22435
22618
  }
22436
22619
  return initial;
@@ -22443,9 +22626,6 @@ function hasJustFilledBox(json, key2, previous, value) {
22443
22626
  if (maxLength === void 0) return false;
22444
22627
  return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22445
22628
  }
22446
- function isTypedInputKey(json, key2) {
22447
- return json.widgets[key2]?.type === JSON_TYPES.INLINE_INPUT || Boolean(parseFractionKey(key2));
22448
- }
22449
22629
  function buildInitialValuesSignature(json) {
22450
22630
  return JSON.stringify({
22451
22631
  widgets: json.widgets,
@@ -22517,7 +22697,7 @@ function buildRevealedDropSources(json, revealedValues) {
22517
22697
  return sources;
22518
22698
  }
22519
22699
  const ExerciseRenderer = forwardRef(({
22520
- json,
22700
+ json: suppliedJson,
22521
22701
  onSubmit,
22522
22702
  onChange,
22523
22703
  showSubmitButton = false,
@@ -22526,29 +22706,12 @@ const ExerciseRenderer = forwardRef(({
22526
22706
  sounds,
22527
22707
  theme
22528
22708
  }, ref) => {
22709
+ const json = useMemo(() => withDisabledPrefilledInputs(suppliedJson), [suppliedJson]);
22529
22710
  const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
22530
22711
  const [values, setValues] = useState(() => buildInitialValues(json));
22531
22712
  const valuesRef = useRef(values);
22532
22713
  valuesRef.current = values;
22533
22714
  const touchedRef = useRef(/* @__PURE__ */ new Set());
22534
- const pendingRef = useRef(/* @__PURE__ */ new Map());
22535
- const onChangeRef = useRef(onChange);
22536
- onChangeRef.current = onChange;
22537
- const clearPending = (key2) => {
22538
- const timer = pendingRef.current.get(key2);
22539
- if (timer === void 0) return false;
22540
- clearTimeout(timer);
22541
- pendingRef.current.delete(key2);
22542
- return true;
22543
- };
22544
- const clearAllPending = () => {
22545
- pendingRef.current.forEach((timer) => clearTimeout(timer));
22546
- pendingRef.current = /* @__PURE__ */ new Map();
22547
- };
22548
- const finishPending = (key2) => {
22549
- if (clearPending(key2)) onChangeRef.current?.(valuesRef.current);
22550
- };
22551
- useEffect(() => clearAllPending, []);
22552
22715
  const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
22553
22716
  const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
22554
22717
  const [focusedInputId, setFocusedInputId] = useState(null);
@@ -22579,13 +22742,11 @@ const ExerciseRenderer = forwardRef(({
22579
22742
  setLoadedValuesSignature(initialValuesSignature);
22580
22743
  touchedRef.current = /* @__PURE__ */ new Set();
22581
22744
  setTutorialRun(null);
22582
- clearAllPending();
22583
22745
  }, [json, initialValuesSignature, loadedValuesSignature]);
22584
22746
  const locked = validationDetails !== null || externallyLocked;
22585
22747
  const lastResultRef = useRef(null);
22586
22748
  const runSubmit = () => {
22587
22749
  if (locked) return lastResultRef.current ?? validate(json, values);
22588
- clearAllPending();
22589
22750
  const result = validate(json, values);
22590
22751
  playSound(result.isCorrect ? "answer-correct" : "answer-wrong");
22591
22752
  const details = { ...result.details };
@@ -22623,11 +22784,6 @@ const ExerciseRenderer = forwardRef(({
22623
22784
  for (const id of answerable) {
22624
22785
  if (!touchedRef.current.has(id)) missingSet.add(id);
22625
22786
  }
22626
- const isAnyFilled = answerable.some((id) => !missingSet.has(id));
22627
- for (const key2 of pendingRef.current.keys()) {
22628
- const id = toBaseId(key2);
22629
- if (answerable.includes(id)) missingSet.add(id);
22630
- }
22631
22787
  const missing = [...missingSet];
22632
22788
  const filledIds = answerable.filter((id) => !missingSet.has(id));
22633
22789
  return {
@@ -22636,7 +22792,8 @@ const ExerciseRenderer = forwardRef(({
22636
22792
  missing,
22637
22793
  filledIds,
22638
22794
  isComplete: answerable.length > 0 && missing.length === 0,
22639
- isAnyFilled
22795
+ isAnyFilled: filledIds.length > 0,
22796
+ ...getAnswerLengthCompletion(json, answerable, valuesRef.current, touchedRef.current)
22640
22797
  };
22641
22798
  },
22642
22799
  reset: () => {
@@ -22650,12 +22807,10 @@ const ExerciseRenderer = forwardRef(({
22650
22807
  setRevealed(false);
22651
22808
  setLoadedValuesSignature(initialValuesSignature);
22652
22809
  touchedRef.current = /* @__PURE__ */ new Set();
22653
- clearAllPending();
22654
22810
  },
22655
22811
  revealAnswers: () => {
22656
22812
  const revealedValues = buildRevealedValues(json);
22657
22813
  if (Object.keys(revealedValues).length === 0) return;
22658
- clearAllPending();
22659
22814
  playSound("answer-reveal");
22660
22815
  const details = {};
22661
22816
  for (const [widgetId, userValue] of Object.entries(revealedValues)) {
@@ -22680,7 +22835,7 @@ const ExerciseRenderer = forwardRef(({
22680
22835
  const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
22681
22836
  if (!container) return;
22682
22837
  const candidates = getTextInputs(container);
22683
- const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
22838
+ const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates.find(canAdvanceTo));
22684
22839
  target?.focus();
22685
22840
  },
22686
22841
  hasTutorial: () => tutorial !== null,
@@ -22715,28 +22870,11 @@ const ExerciseRenderer = forwardRef(({
22715
22870
  }
22716
22871
  return next2;
22717
22872
  });
22718
- if (!silent && isTypedInputKey(json, widgetId)) {
22719
- clearPending(widgetId);
22720
- const maxLength = getMaxInputLength(json, widgetId);
22721
- const isFull = maxLength !== void 0 && String(value).length >= maxLength;
22722
- if (!fromDrop && value !== "" && !isFull) {
22723
- pendingRef.current.set(
22724
- widgetId,
22725
- setTimeout(() => finishPending(widgetId), INPUT_FINISH_IDLE_MS)
22726
- );
22727
- }
22728
- }
22729
22873
  onChange?.(next);
22730
22874
  if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22731
22875
  focusNextInput(widgetId);
22732
22876
  }
22733
22877
  };
22734
- const handleInputFocus = (key2) => {
22735
- for (const pendingKey of [...pendingRef.current.keys()]) {
22736
- if (pendingKey !== key2) finishPending(pendingKey);
22737
- }
22738
- setFocusedInputId(key2);
22739
- };
22740
22878
  const handleSubmit = () => {
22741
22879
  runSubmit();
22742
22880
  };
@@ -22869,7 +23007,7 @@ const ExerciseRenderer = forwardRef(({
22869
23007
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
22870
23008
  return { tools: list, toolsMinHeight: maxH };
22871
23009
  }, [rulerWidget, protractorWidget, setSquareWidget]);
22872
- return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
23010
+ return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsx(KeypadContext.Provider, { value: Boolean(numericKeypadWidget), children: /* @__PURE__ */ jsx(DragBankContext.Provider, { value: json.widgets[DRAG_OPTIONS_WIDGET_ID] !== void 0, children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22873
23011
  /* @__PURE__ */ jsxs(
22874
23012
  "div",
22875
23013
  {
@@ -22894,7 +23032,7 @@ const ExerciseRenderer = forwardRef(({
22894
23032
  droppedInputs,
22895
23033
  validationDetails,
22896
23034
  onChange: handleChange,
22897
- onInputFocus: handleInputFocus,
23035
+ onInputFocus: setFocusedInputId,
22898
23036
  resetKey
22899
23037
  },
22900
23038
  item.id
@@ -22910,7 +23048,7 @@ const ExerciseRenderer = forwardRef(({
22910
23048
  droppedInputs,
22911
23049
  validationDetails,
22912
23050
  onChange: handleChange,
22913
- onInputFocus: handleInputFocus,
23051
+ onInputFocus: setFocusedInputId,
22914
23052
  resetKey
22915
23053
  },
22916
23054
  item.id
@@ -22927,7 +23065,7 @@ const ExerciseRenderer = forwardRef(({
22927
23065
  droppedInputs,
22928
23066
  validationDetails,
22929
23067
  onChange: handleChange,
22930
- onInputFocus: handleInputFocus,
23068
+ onInputFocus: setFocusedInputId,
22931
23069
  resetKey
22932
23070
  },
22933
23071
  item.id
@@ -22976,12 +23114,13 @@ const ExerciseRenderer = forwardRef(({
22976
23114
  options: tutorialRun.options,
22977
23115
  onClose: (finished) => {
22978
23116
  setTutorialRun(null);
23117
+ tutorialRun.options.onClose?.();
22979
23118
  if (finished) tutorialRun.options.onDone?.();
22980
23119
  }
22981
23120
  },
22982
23121
  tutorialRun.run
22983
23122
  )
22984
- ] }) }) }) }) });
23123
+ ] }) }) }) }) }) });
22985
23124
  });
22986
23125
  ExerciseRenderer.displayName = "ExerciseRenderer";
22987
23126
  function renderExercise(options) {
@@ -23327,4 +23466,4 @@ export {
23327
23466
  extractVariables as y,
23328
23467
  validateBooleanExpression as z
23329
23468
  };
23330
- //# sourceMappingURL=index-CAvdtiDQ.mjs.map
23469
+ //# sourceMappingURL=index-Daj5xH0X.mjs.map