@mathscan/math-exercise-engine 1.2.10 → 1.2.11

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.
@@ -240,6 +240,7 @@ const JSON_TYPES = {
240
240
  GEOMETRY: "geometry"
241
241
  };
242
242
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
243
+ const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
243
244
  const RULER_WIDGET_ID = "_ruler";
244
245
  const PROTRACTOR_WIDGET_ID = "_protractor";
245
246
  const SET_SQUARE_WIDGET_ID = "_set_square";
@@ -272,7 +273,8 @@ const INPUT_TYPES = {
272
273
  /** Numeric input (digits only) */
273
274
  NUMERIC: "numeric"
274
275
  };
275
- const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
276
+ const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line", "line-left"];
277
+ const DEFAULT_NUMBER_LINE_STYLE = "line-left";
276
278
  const INTERNAL_PRECISION = 9;
277
279
  const DISPLAY_PRECISION = 2;
278
280
  const SIDE_PAD = 7;
@@ -2055,6 +2057,8 @@ const VISUAL_FRACTION_SHAPE = {
2055
2057
  };
2056
2058
  const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
2057
2059
  const VISUAL_FRACTION_MAX_DIVISIONS = 100;
2060
+ const INLINE_INPUT_MAX_WIDTH = 20;
2061
+ const InlineInputWidthSchema = z.number().int().positive().max(INLINE_INPUT_MAX_WIDTH, { message: `width cannot exceed ${INLINE_INPUT_MAX_WIDTH}` }).optional();
2058
2062
  const FreeTextInlineSchema = z.object({
2059
2063
  type: z.literal(AST_TYPES.FREE_TEXT),
2060
2064
  value: z.string()
@@ -2084,6 +2088,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2084
2088
  name: z.string(),
2085
2089
  inputType: z.literal(INPUT_TYPES.STRING),
2086
2090
  length: z.number().int().positive(),
2091
+ width: InlineInputWidthSchema,
2087
2092
  placeholder: z.string().max(32).optional(),
2088
2093
  disabled: z.boolean().optional(),
2089
2094
  answer: z.union([z.string(), z.number()]).optional()
@@ -2093,6 +2098,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
2093
2098
  name: z.string(),
2094
2099
  inputType: z.literal(INPUT_TYPES.NUMERIC),
2095
2100
  digits: z.number().int().positive(),
2101
+ width: InlineInputWidthSchema,
2096
2102
  placeholder: z.string().max(32).optional(),
2097
2103
  disabled: z.boolean().optional(),
2098
2104
  answer: z.union([z.string(), z.number()]).optional()
@@ -2250,7 +2256,7 @@ function createBoldText(value, highlight) {
2250
2256
  highlight
2251
2257
  });
2252
2258
  }
2253
- function createStringInput(name, length, answer, placeholder, disabled) {
2259
+ function createStringInput(name, length, answer, placeholder, disabled, width) {
2254
2260
  return parseOrThrow(
2255
2261
  InlineInputWidgetSchema,
2256
2262
  {
@@ -2258,6 +2264,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2258
2264
  name,
2259
2265
  inputType: INPUT_TYPES.STRING,
2260
2266
  length,
2267
+ width,
2261
2268
  placeholder,
2262
2269
  disabled,
2263
2270
  // A disabled input is never graded, so its answer is dropped.
@@ -2266,7 +2273,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2266
2273
  "STRING_INPUT"
2267
2274
  );
2268
2275
  }
2269
- function createNumericInput(name, digits, answer, placeholder, disabled) {
2276
+ function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2270
2277
  return parseOrThrow(
2271
2278
  InlineInputWidgetSchema,
2272
2279
  {
@@ -2274,6 +2281,7 @@ function createNumericInput(name, digits, answer, placeholder, disabled) {
2274
2281
  name,
2275
2282
  inputType: INPUT_TYPES.NUMERIC,
2276
2283
  digits,
2284
+ width,
2277
2285
  placeholder,
2278
2286
  disabled,
2279
2287
  // A disabled input is never graded, so its answer is dropped.
@@ -2490,7 +2498,7 @@ const NumberLineBlockSchema = z.object({
2490
2498
  errorMap: () => ({
2491
2499
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
2492
2500
  })
2493
- }).optional(),
2501
+ }).default(DEFAULT_NUMBER_LINE_STYLE),
2494
2502
  labelVisibility: z.enum(NUMBER_LINE_LABELS, {
2495
2503
  errorMap: () => ({
2496
2504
  message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
@@ -4323,12 +4331,19 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4323
4331
  `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": disabled must be true or false, got ${JSON.stringify(params.disabled)}`
4324
4332
  );
4325
4333
  }
4334
+ const width = params.width;
4335
+ const maxChars = ctx.stringInput ? params.length : params.digits;
4336
+ if (width !== void 0 && width < maxChars) {
4337
+ throw new Error(
4338
+ `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": width (${width}) cannot be smaller than ${ctx.stringInput ? "length" : "digits"} (${maxChars})`
4339
+ );
4340
+ }
4326
4341
  if (ctx.stringInput) {
4327
4342
  const length = params.length;
4328
- return createStringInput(name, length, answer, placeholder, disabled);
4343
+ return createStringInput(name, length, answer, placeholder, disabled, width);
4329
4344
  }
4330
4345
  const digits = params.digits;
4331
- return createNumericInput(name, digits, answer, placeholder, disabled);
4346
+ return createNumericInput(name, digits, answer, placeholder, disabled, width);
4332
4347
  }
4333
4348
  inlineDragOption(ctx) {
4334
4349
  const valueToken = ctx.StringLiteral[0];
@@ -5068,6 +5083,8 @@ const StringInputJSONSchema = z.object({
5068
5083
  type: z.literal(JSON_TYPES.INLINE_INPUT),
5069
5084
  inputType: z.literal(INPUT_TYPES.STRING),
5070
5085
  length: z.number().int().positive(),
5086
+ /** Box width in characters; defaults to `length`. */
5087
+ width: InlineInputWidthSchema,
5071
5088
  placeholder: z.string().max(32).optional(),
5072
5089
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5073
5090
  disabled: z.boolean().optional()
@@ -5076,6 +5093,8 @@ const NumericInputJSONSchema = z.object({
5076
5093
  type: z.literal(JSON_TYPES.INLINE_INPUT),
5077
5094
  inputType: z.literal(INPUT_TYPES.NUMERIC),
5078
5095
  digits: z.number().int().positive(),
5096
+ /** Box width in characters; defaults to `digits`. */
5097
+ width: InlineInputWidthSchema,
5079
5098
  placeholder: z.string().max(32).optional(),
5080
5099
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5081
5100
  disabled: z.boolean().optional()
@@ -6066,7 +6085,7 @@ lodash.exports;
6066
6085
  var reIsNative = RegExp2(
6067
6086
  "^" + funcToString.call(hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"
6068
6087
  );
6069
- var Buffer = moduleExports ? context.Buffer : undefined$1, Symbol = context.Symbol, Uint8Array = context.Uint8Array, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined$1, getPrototype = overArg(Object2.getPrototypeOf, Object2), objectCreate = Object2.create, propertyIsEnumerable = objectProto.propertyIsEnumerable, splice = arrayProto.splice, spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined$1, symIterator = Symbol ? Symbol.iterator : undefined$1, symToStringTag = Symbol ? Symbol.toStringTag : undefined$1;
6088
+ var Buffer = moduleExports ? context.Buffer : undefined$1, Symbol2 = context.Symbol, Uint8Array = context.Uint8Array, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined$1, getPrototype = overArg(Object2.getPrototypeOf, Object2), objectCreate = Object2.create, propertyIsEnumerable = objectProto.propertyIsEnumerable, splice = arrayProto.splice, spreadableSymbol = Symbol2 ? Symbol2.isConcatSpreadable : undefined$1, symIterator = Symbol2 ? Symbol2.iterator : undefined$1, symToStringTag = Symbol2 ? Symbol2.toStringTag : undefined$1;
6070
6089
  var defineProperty = function() {
6071
6090
  try {
6072
6091
  var func = getNative(Object2, "defineProperty");
@@ -6081,7 +6100,7 @@ lodash.exports;
6081
6100
  var metaMap = WeakMap && new WeakMap();
6082
6101
  var realNames = {};
6083
6102
  var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap);
6084
- var symbolProto = Symbol ? Symbol.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
6103
+ var symbolProto = Symbol2 ? Symbol2.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
6085
6104
  function lodash2(value) {
6086
6105
  if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
6087
6106
  if (value instanceof LodashWrapper) {
@@ -10980,6 +10999,7 @@ function toJSON(ast) {
10980
10999
  type: JSON_TYPES.INLINE_INPUT,
10981
11000
  inputType: INPUT_TYPES.STRING,
10982
11001
  length: inline.length,
11002
+ ...inline.width !== void 0 && { width: inline.width },
10983
11003
  placeholder: inline.placeholder,
10984
11004
  ...inline.disabled && { disabled: true }
10985
11005
  };
@@ -10988,6 +11008,7 @@ function toJSON(ast) {
10988
11008
  type: JSON_TYPES.INLINE_INPUT,
10989
11009
  inputType: INPUT_TYPES.NUMERIC,
10990
11010
  digits: inline.digits,
11011
+ ...inline.width !== void 0 && { width: inline.width },
10991
11012
  placeholder: inline.placeholder,
10992
11013
  ...inline.disabled && { disabled: true }
10993
11014
  };
@@ -11083,7 +11104,7 @@ function toJSON(ast) {
11083
11104
  options: lodashExports.shuffle(tool.options)
11084
11105
  };
11085
11106
  } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
11086
- widgets["_numeric_keypad"] = {
11107
+ widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
11087
11108
  type: JSON_TYPES.NUMERIC_KEYPAD,
11088
11109
  includeOperators: tool.includeOperators,
11089
11110
  includeDecimal: tool.includeDecimal,
@@ -11346,7 +11367,7 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
11346
11367
  max: numberLine.max,
11347
11368
  step: numberLine.step,
11348
11369
  ...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
11349
- ...numberLine.style !== void 0 && { style: numberLine.style },
11370
+ style: numberLine.style,
11350
11371
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
11351
11372
  ...counterItem !== void 0 && { counter: counterItem },
11352
11373
  ...highlights !== void 0 && { highlights },
@@ -16003,22 +16024,22 @@ function createDefaultDropAnimation(options) {
16003
16024
  easing,
16004
16025
  fill: "forwards"
16005
16026
  });
16006
- return new Promise((resolve) => {
16027
+ return new Promise((resolve2) => {
16007
16028
  animation.onfinish = () => {
16008
16029
  cleanup == null ? void 0 : cleanup();
16009
- resolve();
16030
+ resolve2();
16010
16031
  };
16011
16032
  });
16012
16033
  };
16013
16034
  }
16014
- let key = 0;
16035
+ let key$1 = 0;
16015
16036
  function useKey(id) {
16016
16037
  return useMemo(() => {
16017
16038
  if (id == null) {
16018
16039
  return;
16019
16040
  }
16020
- key++;
16021
- return key;
16041
+ key$1++;
16042
+ return key$1;
16022
16043
  }, [id]);
16023
16044
  }
16024
16045
  const DragOverlay = /* @__PURE__ */ React.memo((_ref) => {
@@ -16100,6 +16121,7 @@ const DEFAULT_SOUNDS = {
16100
16121
  "drag-start": dragStart,
16101
16122
  "drag-drop": dragDrop,
16102
16123
  "drag-cancel": dragDrop,
16124
+ "drag-hint": buttonClick,
16103
16125
  "radio-select": buttonClick,
16104
16126
  "pair-select": buttonClick,
16105
16127
  "pair-link": buttonClick,
@@ -16203,14 +16225,24 @@ function useExerciseDnd() {
16203
16225
  }
16204
16226
  return ctx;
16205
16227
  }
16228
+ const NO_EXERCISE = Symbol("no-exercise");
16206
16229
  const ExerciseDndProvider = ({
16207
16230
  children,
16208
16231
  droppedSources,
16209
- onDragEnd
16232
+ onDragEnd,
16233
+ exerciseKey
16210
16234
  }) => {
16211
16235
  const [activeItem, setActiveItem] = useState(null);
16236
+ const [draggedExerciseKey, setDraggedExerciseKey] = useState(NO_EXERCISE);
16237
+ const hasDraggedOption = draggedExerciseKey === exerciseKey;
16212
16238
  const [dropHandlers] = useState(() => /* @__PURE__ */ new Map());
16213
16239
  const playSound = useSound();
16240
+ const hintSoundExerciseRef = useRef(NO_EXERCISE);
16241
+ const playHintSound = useCallback(() => {
16242
+ if (hintSoundExerciseRef.current === exerciseKey) return;
16243
+ hintSoundExerciseRef.current = exerciseKey;
16244
+ playSound("drag-hint");
16245
+ }, [playSound, exerciseKey]);
16214
16246
  const usedOptionIds = useMemo(() => {
16215
16247
  return new Set(droppedSources.values());
16216
16248
  }, [droppedSources]);
@@ -16234,6 +16266,7 @@ const ExerciseDndProvider = ({
16234
16266
  const raw = active.data.current;
16235
16267
  const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
16236
16268
  setActiveItem(item);
16269
+ if (item.type === "drag-option") setDraggedExerciseKey(exerciseKey);
16237
16270
  playSound("drag-start");
16238
16271
  };
16239
16272
  const handleDragEnd = (event) => {
@@ -16259,6 +16292,9 @@ const ExerciseDndProvider = ({
16259
16292
  value: {
16260
16293
  activeItem,
16261
16294
  isDragging: activeItem !== null,
16295
+ exerciseKey,
16296
+ hasDraggedOption,
16297
+ playHintSound,
16262
16298
  usedOptionIds,
16263
16299
  droppedSources,
16264
16300
  registerDropHandler,
@@ -17039,7 +17075,38 @@ function isUndoDrop(event, dragId) {
17039
17075
  const LockContext = createContext(false);
17040
17076
  const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsx(LockContext.Provider, { value: locked, children });
17041
17077
  const useExerciseLocked = () => useContext(LockContext);
17042
- const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
17078
+ const OVERLAY_STYLE = {
17079
+ position: "absolute",
17080
+ top: 0,
17081
+ left: 0,
17082
+ width: "100%",
17083
+ height: "100%",
17084
+ minWidth: 0,
17085
+ maxWidth: "none",
17086
+ margin: 0,
17087
+ boxSizing: "border-box"
17088
+ };
17089
+ const SlotSizer = ({
17090
+ style,
17091
+ ...props
17092
+ }) => /* @__PURE__ */ jsx(
17093
+ "input",
17094
+ {
17095
+ ...props,
17096
+ type: "text",
17097
+ value: "",
17098
+ readOnly: true,
17099
+ tabIndex: -1,
17100
+ "aria-hidden": "true",
17101
+ style: { ...style, visibility: "hidden" }
17102
+ }
17103
+ );
17104
+ const PlacedInputChip = ({
17105
+ widgetId,
17106
+ value,
17107
+ className = "",
17108
+ onRemove
17109
+ }) => {
17043
17110
  const { droppedSources } = useExerciseDnd();
17044
17111
  const locked = useExerciseLocked();
17045
17112
  const sourceOptionId = droppedSources.get(widgetId);
@@ -17063,13 +17130,41 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
17063
17130
  if (!locked && e.detail !== 0) onRemove();
17064
17131
  },
17065
17132
  title: locked ? void 0 : "Drag to move, or click to remove",
17066
- style: { touchAction: "none", cursor: locked ? "default" : "grab" },
17133
+ style: {
17134
+ ...OVERLAY_STYLE,
17135
+ touchAction: "none",
17136
+ cursor: locked ? "default" : "grab"
17137
+ },
17067
17138
  ...listeners,
17068
17139
  ...attributes,
17069
17140
  children: value
17070
17141
  }
17071
17142
  );
17072
17143
  };
17144
+ function getMaxInputLength(exercise, inputKey) {
17145
+ const widget = exercise.widgets[inputKey];
17146
+ if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
17147
+ return widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : widget.length;
17148
+ }
17149
+ function getInputElementId(key2) {
17150
+ return `${key2}-input`;
17151
+ }
17152
+ function getTextInputs(container) {
17153
+ return Array.from(
17154
+ container.querySelectorAll(
17155
+ 'input[type="text"], input[type="number"], textarea, select'
17156
+ )
17157
+ );
17158
+ }
17159
+ function canTakeInput(el) {
17160
+ return !el.readOnly && !el.disabled;
17161
+ }
17162
+ function findNextInput(container, currentKey) {
17163
+ const inputs = getTextInputs(container);
17164
+ const currentIndex = inputs.findIndex((el) => el.id === getInputElementId(currentKey));
17165
+ if (currentIndex === -1) return null;
17166
+ return inputs.slice(currentIndex + 1).find(canTakeInput) ?? null;
17167
+ }
17073
17168
  const InlineInput = ({
17074
17169
  widgetId,
17075
17170
  value,
@@ -17078,6 +17173,7 @@ const InlineInput = ({
17078
17173
  inputType,
17079
17174
  length,
17080
17175
  digits,
17176
+ width,
17081
17177
  placeholder,
17082
17178
  disabled = false,
17083
17179
  onChange,
@@ -17091,8 +17187,8 @@ const InlineInput = ({
17091
17187
  const handleDrop = useCallback((item) => {
17092
17188
  return applyDropToInput(widgetId, item, onChange);
17093
17189
  }, [onChange, widgetId]);
17094
- const expectedLength = inputType === "numeric" ? digits : length;
17095
- const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
17190
+ const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
17191
+ const dynamicWidth = `${(width ?? expectedLength ?? 5) * 0.8 + 1}em`;
17096
17192
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
17097
17193
  const disabledClass = disabled ? " inline-input--disabled" : "";
17098
17194
  return /* @__PURE__ */ jsx(
@@ -17105,21 +17201,31 @@ const InlineInput = ({
17105
17201
  className: "inline-widget inline-input-droppable",
17106
17202
  children: isDropped && value !== "" ? (
17107
17203
  // Show as chip only when value came from drag-drop
17108
- /* @__PURE__ */ jsx(
17109
- PlacedInputChip,
17110
- {
17111
- widgetId,
17112
- value,
17113
- className: statusClass.trim(),
17114
- onRemove: () => handleChange("", false)
17115
- }
17116
- )
17204
+ /* @__PURE__ */ jsxs(Fragment, { children: [
17205
+ /* @__PURE__ */ jsx(
17206
+ SlotSizer,
17207
+ {
17208
+ className: `inline-input${disabledClass}`,
17209
+ style: { width: dynamicWidth },
17210
+ placeholder
17211
+ }
17212
+ ),
17213
+ /* @__PURE__ */ jsx(
17214
+ PlacedInputChip,
17215
+ {
17216
+ widgetId,
17217
+ value,
17218
+ className: statusClass.trim(),
17219
+ onRemove: () => handleChange("", false)
17220
+ }
17221
+ )
17222
+ ] })
17117
17223
  ) : (
17118
17224
  // Regular input for typing
17119
17225
  /* @__PURE__ */ jsx(
17120
17226
  "input",
17121
17227
  {
17122
- id: widgetId + "-input",
17228
+ id: getInputElementId(widgetId),
17123
17229
  type: "text",
17124
17230
  className: `inline-input${statusClass}${disabledClass}`,
17125
17231
  value,
@@ -17139,15 +17245,20 @@ const InlineInput = ({
17139
17245
  }
17140
17246
  );
17141
17247
  };
17142
- const SingleDragOption = ({
17248
+ const SingleDragOption = (props) => {
17249
+ const { exerciseKey } = useExerciseDnd();
17250
+ return /* @__PURE__ */ jsx(DragOptionChip, { ...props }, exerciseKey ?? "");
17251
+ };
17252
+ const DragOptionChip = ({
17143
17253
  optionId,
17144
17254
  value,
17145
17255
  onOptionClick,
17146
17256
  className = ""
17147
17257
  }) => {
17148
- const { usedOptionIds } = useExerciseDnd();
17258
+ const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17149
17259
  const locked = useExerciseLocked();
17150
17260
  const isUsed = usedOptionIds.has(optionId);
17261
+ const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
17151
17262
  return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
17152
17263
  Draggable,
17153
17264
  {
@@ -17159,7 +17270,11 @@ const SingleDragOption = ({
17159
17270
  children: /* @__PURE__ */ jsx(
17160
17271
  "button",
17161
17272
  {
17162
- className: "drag-option",
17273
+ className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
17274
+ onAnimationStart: () => {
17275
+ if (hinting) playHintSound();
17276
+ },
17277
+ onAnimationEnd: onHintEnd,
17163
17278
  onClick: () => {
17164
17279
  if (!locked) onOptionClick?.(value);
17165
17280
  },
@@ -17288,23 +17403,23 @@ const NumericKeypad = ({
17288
17403
  playSound("keypad-press");
17289
17404
  onChange(value.slice(0, -1));
17290
17405
  };
17291
- return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
17292
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
17293
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
17294
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
17295
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
17296
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
17297
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
17298
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
17299
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
17300
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
17301
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
17302
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
17303
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
17304
- includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
17305
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
17306
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
17307
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
17406
+ return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
17407
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
17408
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
17409
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
17410
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
17411
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
17412
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
17413
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
17414
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
17415
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
17416
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
17417
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
17418
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
17419
+ includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
17420
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
17421
+ /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
17422
+ includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
17308
17423
  ] });
17309
17424
  };
17310
17425
  const FractionInput = ({
@@ -17344,18 +17459,28 @@ const FractionInput = ({
17344
17459
  onDrop: handleDrop(subKey),
17345
17460
  disabled: locked,
17346
17461
  className: "fraction-slot-droppable",
17347
- children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsx(
17348
- PlacedInputChip,
17349
- {
17350
- widgetId: subKey,
17351
- value: currentValue,
17352
- className: `fraction-chip${slotClass}`,
17353
- onRemove: () => onChange(subKey, "", false)
17354
- }
17355
- ) : /* @__PURE__ */ jsx(
17462
+ children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxs(Fragment, { children: [
17463
+ /* @__PURE__ */ jsx(
17464
+ SlotSizer,
17465
+ {
17466
+ className: "inline-input fraction-input",
17467
+ size: placeholder ? placeholder.length : void 0,
17468
+ placeholder
17469
+ }
17470
+ ),
17471
+ /* @__PURE__ */ jsx(
17472
+ PlacedInputChip,
17473
+ {
17474
+ widgetId: subKey,
17475
+ value: currentValue,
17476
+ className: `fraction-chip${slotClass}`,
17477
+ onRemove: () => onChange(subKey, "", false)
17478
+ }
17479
+ )
17480
+ ] }) : /* @__PURE__ */ jsx(
17356
17481
  "input",
17357
17482
  {
17358
- id: `${subKey}-input`,
17483
+ id: getInputElementId(subKey),
17359
17484
  type: "text",
17360
17485
  className: `inline-input fraction-input${slotClass}`,
17361
17486
  value: currentValue,
@@ -18933,11 +19058,12 @@ const RadioOptions = ({
18933
19058
  };
18934
19059
  const Btn = ({
18935
19060
  label,
19061
+ dataKey = label,
18936
19062
  extraClass = "keypad-btn-digit",
18937
19063
  onClick
18938
19064
  }) => {
18939
19065
  const locked = useExerciseLocked();
18940
- return /* @__PURE__ */ jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsx("span", { children: label }) });
19066
+ return /* @__PURE__ */ jsx("button", { className: `keypad-btn ${extraClass}`, "data-key": dataKey, disabled: locked, onClick, children: /* @__PURE__ */ jsx("span", { children: label }) });
18941
19067
  };
18942
19068
  const DiamondKeypad = ({ value, onChange }) => {
18943
19069
  const playSound = useSound();
@@ -18949,7 +19075,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18949
19075
  playSound("keypad-press");
18950
19076
  onChange(value.slice(0, -1));
18951
19077
  };
18952
- return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", children: [
19078
+ return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", "data-tutorial": "keypad", children: [
18953
19079
  /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
18954
19080
  /* @__PURE__ */ jsx(Btn, { label: "1", onClick: () => append("1") }),
18955
19081
  /* @__PURE__ */ jsx(Btn, { label: "2", onClick: () => append("2") }),
@@ -18968,7 +19094,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18968
19094
  ] }),
18969
19095
  /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
18970
19096
  /* @__PURE__ */ jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
18971
- /* @__PURE__ */ jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
19097
+ /* @__PURE__ */ jsx(Btn, { label: "⌫", dataKey: "backspace", extraClass: "keypad-btn-backspace", onClick: backspace })
18972
19098
  ] })
18973
19099
  ] });
18974
19100
  };
@@ -19574,7 +19700,7 @@ function isGeometryWidget(widget) {
19574
19700
  return widget.type === JSON_TYPES.GEOMETRY;
19575
19701
  }
19576
19702
  const GeometryWidget = React.lazy(async () => ({
19577
- default: (await import("./GeometryWidget-gtAP_-_S.mjs")).GeometryWidget
19703
+ default: (await import("./GeometryWidget-2TCLbTEv.mjs")).GeometryWidget
19578
19704
  }));
19579
19705
  const WidgetRenderer = ({
19580
19706
  widget,
@@ -19602,8 +19728,9 @@ const WidgetRenderer = ({
19602
19728
  isDropped,
19603
19729
  validationStatus,
19604
19730
  inputType: widget.inputType,
19605
- length: widget.inputType === "string" ? widget.length : void 0,
19606
- digits: widget.inputType === "numeric" ? widget.digits : void 0,
19731
+ length: widget.inputType === INPUT_TYPES.STRING ? widget.length : void 0,
19732
+ digits: widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : void 0,
19733
+ width: widget.width,
19607
19734
  placeholder: widget.placeholder,
19608
19735
  disabled: widget.disabled,
19609
19736
  onChange,
@@ -19974,6 +20101,13 @@ const EPSILON = 10 ** -INTERNAL_PRECISION;
19974
20101
  const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
19975
20102
  const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
19976
20103
  const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
20104
+ const NUMBER_LINE_AXIS_ENDS = {
20105
+ "arrow-right": { left: "tick", right: "arrow" },
20106
+ "arrow-left": { left: "arrow", right: "tick" },
20107
+ "arrow-both": { left: "arrow", right: "arrow" },
20108
+ "line": { left: "line", right: "line" },
20109
+ "line-left": { left: "line", right: "arrow" }
20110
+ };
19977
20111
  const toPercent = (value, min, max) => {
19978
20112
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
19979
20113
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -21021,7 +21155,7 @@ const NumberLineRenderer = ({
21021
21155
  max,
21022
21156
  step,
21023
21157
  minorTicks = 0,
21024
- style: lineStyle = "arrow-right",
21158
+ style: lineStyle = DEFAULT_NUMBER_LINE_STYLE,
21025
21159
  labelVisibility,
21026
21160
  highlights,
21027
21161
  arrows,
@@ -21085,8 +21219,9 @@ const NumberLineRenderer = ({
21085
21219
  () => getMinorTickValues(majorTicks, minorTicks),
21086
21220
  [majorTicks, minorTicks]
21087
21221
  );
21088
- const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
21089
- const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
21222
+ const axisEnds = NUMBER_LINE_AXIS_ENDS[lineStyle] ?? NUMBER_LINE_AXIS_ENDS[DEFAULT_NUMBER_LINE_STYLE];
21223
+ const showArrowLeft = axisEnds.left === "arrow";
21224
+ const showArrowRight = axisEnds.right === "arrow";
21090
21225
  const hasDragOptions = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
21091
21226
  const isAxisDragMode = hasDragOptions && item.dragMode === "axis";
21092
21227
  const inputTickSet = useMemo(() => {
@@ -21134,7 +21269,7 @@ const NumberLineRenderer = ({
21134
21269
  return /* @__PURE__ */ jsxs(
21135
21270
  "div",
21136
21271
  {
21137
- className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
21272
+ className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${axisEnds.left === "line" ? " number-line--extend-left" : ""}${axisEnds.right === "line" ? " number-line--extend-right" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
21138
21273
  style: {
21139
21274
  [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
21140
21275
  ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
@@ -21507,6 +21642,35 @@ function groupLineItems(items) {
21507
21642
  }
21508
21643
  return groups;
21509
21644
  }
21645
+ function placeTableCells(rows, cols) {
21646
+ const placements = /* @__PURE__ */ new Map();
21647
+ const occupied = rows.map(() => []);
21648
+ const totalRows = rows.length;
21649
+ rows.forEach((row, rowIndex) => {
21650
+ const taken = occupied[rowIndex] ?? [];
21651
+ let col = 0;
21652
+ for (const cell of row.cells) {
21653
+ while (taken[col]) col++;
21654
+ const colspan = cell.colspan || 1;
21655
+ const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
21656
+ for (let r = rowIndex; r < rowIndex + rowspan; r++) {
21657
+ const line = occupied[r];
21658
+ if (!line) continue;
21659
+ for (let c = col; c < col + colspan; c++) line[c] = true;
21660
+ }
21661
+ placements.set(cell.id, {
21662
+ column: col + 1,
21663
+ row: rowIndex + 1,
21664
+ colspan,
21665
+ rowspan,
21666
+ isLastColumn: col + colspan >= cols,
21667
+ isLastRow: rowIndex + rowspan >= totalRows
21668
+ });
21669
+ col += colspan;
21670
+ }
21671
+ });
21672
+ return placements;
21673
+ }
21510
21674
  const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
21511
21675
  const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
21512
21676
  const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
@@ -21630,21 +21794,28 @@ const LayoutRenderer = ({
21630
21794
  subItem.id
21631
21795
  )) });
21632
21796
  case JSON_TYPES.TABLE: {
21633
- const totalRows = item.rows.length;
21797
+ const placements = placeTableCells(item.rows, item.cols);
21634
21798
  const tableStyle = {
21635
- "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR
21799
+ "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR,
21800
+ "--exercise-table-cols": item.cols
21636
21801
  };
21637
- return /* @__PURE__ */ jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsx("tbody", { children: item.rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { className: "exercise-table-row", style: { backgroundColor: row.backgroundColor }, children: row.cells.map((cell) => {
21638
- const rowspan = cell.rowspan || 1;
21639
- const isLastRow = rowIndex + rowspan - 1 >= totalRows - 1;
21802
+ return /* @__PURE__ */ jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsx("div", { className: "exercise-table", role: "table", children: item.rows.map((row) => /* @__PURE__ */ jsx("div", { className: "exercise-table-row", role: "row", children: row.cells.map((cell) => {
21803
+ const placement = placements.get(cell.id);
21804
+ const modifiers = [
21805
+ placement?.isLastRow ? " exercise-table-cell--last-row" : "",
21806
+ placement?.isLastColumn ? " exercise-table-cell--last-col" : ""
21807
+ ].join("");
21640
21808
  return /* @__PURE__ */ jsx(
21641
- "td",
21809
+ "div",
21642
21810
  {
21643
- className: `exercise-table-cell${isLastRow ? " exercise-table-cell--last-row" : ""}`,
21644
- colSpan: cell.colspan || 1,
21645
- rowSpan: rowspan,
21646
- style: { backgroundColor: cell.backgroundColor },
21647
- children: cell.items.map((subItem) => /* @__PURE__ */ jsx(
21811
+ className: `exercise-table-cell${modifiers}`,
21812
+ role: "cell",
21813
+ style: {
21814
+ backgroundColor: cell.backgroundColor ?? row.backgroundColor,
21815
+ gridColumn: placement && `${placement.column} / span ${placement.colspan}`,
21816
+ gridRow: placement && `${placement.row} / span ${placement.rowspan}`
21817
+ },
21818
+ children: /* @__PURE__ */ jsx("div", { className: "exercise-table-cell-content", children: cell.items.map((subItem) => /* @__PURE__ */ jsx(
21648
21819
  LayoutRenderer,
21649
21820
  {
21650
21821
  item: subItem,
@@ -21657,11 +21828,11 @@ const LayoutRenderer = ({
21657
21828
  resetKey
21658
21829
  },
21659
21830
  subItem.id
21660
- ))
21831
+ )) })
21661
21832
  },
21662
21833
  cell.id
21663
21834
  );
21664
- }) }, row.id)) }) }) });
21835
+ }) }, row.id)) }) });
21665
21836
  }
21666
21837
  case JSON_TYPES.NUMBER_LINE:
21667
21838
  return /* @__PURE__ */ jsx(
@@ -21831,6 +22002,394 @@ const LayoutRenderer = ({
21831
22002
  return null;
21832
22003
  }
21833
22004
  };
22005
+ function LionHand({ className }) {
22006
+ const id = useId();
22007
+ const shadowId = `${id}-shadow`;
22008
+ const maskId = `${id}-mask`;
22009
+ return /* @__PURE__ */ jsxs(
22010
+ "svg",
22011
+ {
22012
+ "aria-hidden": "true",
22013
+ focusable: "false",
22014
+ width: "75",
22015
+ height: "78",
22016
+ viewBox: "0 0 75 78",
22017
+ fill: "none",
22018
+ xmlns: "http://www.w3.org/2000/svg",
22019
+ className,
22020
+ children: [
22021
+ /* @__PURE__ */ jsxs("g", { filter: `url(#${shadowId})`, children: [
22022
+ /* @__PURE__ */ jsx(
22023
+ "mask",
22024
+ {
22025
+ id: maskId,
22026
+ style: { maskType: "alpha" },
22027
+ maskUnits: "userSpaceOnUse",
22028
+ x: "9",
22029
+ y: "19",
22030
+ width: "49",
22031
+ height: "42",
22032
+ 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" })
22033
+ }
22034
+ ),
22035
+ /* @__PURE__ */ jsxs("g", { mask: `url(#${maskId})`, children: [
22036
+ /* @__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" }),
22037
+ /* @__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" }),
22038
+ /* @__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" }),
22039
+ /* @__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" })
22040
+ ] })
22041
+ ] }),
22042
+ /* @__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: [
22043
+ /* @__PURE__ */ jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22044
+ /* @__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" }),
22045
+ /* @__PURE__ */ jsx("feOffset", { dy: "2" }),
22046
+ /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: "1" }),
22047
+ /* @__PURE__ */ jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22048
+ /* @__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" }),
22049
+ /* @__PURE__ */ jsx("feBlend", { mode: "normal", in2: "BackgroundImageFix", result: "effect1_dropShadow" }),
22050
+ /* @__PURE__ */ jsx("feBlend", { mode: "normal", in: "SourceGraphic", in2: "effect1_dropShadow", result: "shape" })
22051
+ ] }) })
22052
+ ]
22053
+ }
22054
+ );
22055
+ }
22056
+ const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
22057
+ const CYCLE_MS = 1400;
22058
+ const PRESS_AT_MS = 650;
22059
+ const PRESS_HOLD_MS = 220;
22060
+ const HOLE_PAD = 10;
22061
+ const HOLE_RADIUS = 18;
22062
+ const HAND_W = 72;
22063
+ const HAND_H = 75;
22064
+ const BUBBLE_W = 240;
22065
+ const BUBBLE_GAP = 14;
22066
+ const EDGE = 8;
22067
+ const DEFAULT_NEXT_LABEL = "فهمت";
22068
+ function resolve(t) {
22069
+ return (typeof t === "function" ? t() : t) ?? null;
22070
+ }
22071
+ function toRect(t) {
22072
+ if (!t) return null;
22073
+ const r = t instanceof Element ? t.getBoundingClientRect() : t;
22074
+ if (r.width <= 0 || r.height <= 0) return null;
22075
+ return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
22076
+ }
22077
+ const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22078
+ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22079
+ const { steps } = tutorial;
22080
+ const [stepIndex, setStepIndex] = useState(
22081
+ () => Math.min(Math.max(0, Math.floor(options.startStep ?? 0)), steps.length - 1)
22082
+ );
22083
+ const step = steps[stepIndex];
22084
+ const optionsRef = useRef(options);
22085
+ optionsRef.current = options;
22086
+ const ctx = { root, json, submitTarget: resolve(options.submitTarget) };
22087
+ const ctxRef = useRef(ctx);
22088
+ ctxRef.current = ctx;
22089
+ const [layout, setLayout] = useState(null);
22090
+ useEffect(() => {
22091
+ let raf = 0;
22092
+ let last = "";
22093
+ const tick = () => {
22094
+ const c = ctxRef.current;
22095
+ const next2 = {
22096
+ spotlight: step.spotlight(c).map(toRect),
22097
+ taps: step.taps(c).map(toRect),
22098
+ anchor: toRect(resolve(optionsRef.current.bubbleAnchor)),
22099
+ root: toRect(c.root),
22100
+ vw: window.innerWidth,
22101
+ vh: window.innerHeight
22102
+ };
22103
+ const key2 = JSON.stringify(next2);
22104
+ if (key2 !== last) {
22105
+ last = key2;
22106
+ setLayout(next2);
22107
+ }
22108
+ raf = requestAnimationFrame(tick);
22109
+ };
22110
+ tick();
22111
+ return () => cancelAnimationFrame(raf);
22112
+ }, [step]);
22113
+ const [tapIndex, setTapIndex] = useState(0);
22114
+ const [tapCount, setTapCount] = useState(0);
22115
+ const [shownStep, setShownStep] = useState(stepIndex);
22116
+ if (shownStep !== stepIndex) {
22117
+ setShownStep(stepIndex);
22118
+ setTapIndex(0);
22119
+ setTapCount(0);
22120
+ }
22121
+ useEffect(() => {
22122
+ let release;
22123
+ const press = setTimeout(() => {
22124
+ setTapCount((n) => n + 1);
22125
+ const targets = step.taps(ctxRef.current);
22126
+ const el = targets[tapIndex % Math.max(1, targets.length)];
22127
+ if (el instanceof Element) {
22128
+ el.classList.add("met-tutorial-pressed");
22129
+ release = setTimeout(() => el.classList.remove("met-tutorial-pressed"), PRESS_HOLD_MS);
22130
+ }
22131
+ }, PRESS_AT_MS);
22132
+ const advance = setTimeout(() => setTapIndex((i) => i + 1), CYCLE_MS);
22133
+ return () => {
22134
+ clearTimeout(press);
22135
+ clearTimeout(advance);
22136
+ if (release) clearTimeout(release);
22137
+ root.querySelectorAll(".met-tutorial-pressed").forEach((el) => el.classList.remove("met-tutorial-pressed"));
22138
+ };
22139
+ }, [step, tapIndex, root]);
22140
+ const text = options.texts?.[step.id] ?? step.text;
22141
+ const textRef = useRef(text);
22142
+ textRef.current = text;
22143
+ useEffect(() => {
22144
+ optionsRef.current.onStep?.(stepIndex, step.id);
22145
+ optionsRef.current.onSpeak?.(step.id, textRef.current);
22146
+ }, [stepIndex, step]);
22147
+ const isLast = stepIndex === steps.length - 1;
22148
+ const next = () => {
22149
+ if (isLast) onClose(true);
22150
+ else setStepIndex((i) => i + 1);
22151
+ };
22152
+ const overlayRef = useRef(null);
22153
+ const nextRef = useRef(null);
22154
+ const onCloseRef = useRef(onClose);
22155
+ onCloseRef.current = onClose;
22156
+ useEffect(() => {
22157
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22158
+ previous?.blur();
22159
+ const onKeyDown = (e) => {
22160
+ const overlay = overlayRef.current;
22161
+ if (e.key === "Escape") {
22162
+ e.preventDefault();
22163
+ e.stopPropagation();
22164
+ onCloseRef.current(false);
22165
+ return;
22166
+ }
22167
+ if (e.key === "Tab" && overlay) {
22168
+ e.preventDefault();
22169
+ const buttons = Array.from(overlay.querySelectorAll("button"));
22170
+ if (buttons.length === 0) return;
22171
+ const at = buttons.indexOf(document.activeElement);
22172
+ const to = (at + (e.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
22173
+ buttons[to]?.focus();
22174
+ return;
22175
+ }
22176
+ if (overlay && e.target instanceof Node && overlay.contains(e.target)) return;
22177
+ e.preventDefault();
22178
+ e.stopPropagation();
22179
+ };
22180
+ document.addEventListener("keydown", onKeyDown, true);
22181
+ return () => {
22182
+ document.removeEventListener("keydown", onKeyDown, true);
22183
+ if (previous?.isConnected) previous.focus();
22184
+ };
22185
+ }, []);
22186
+ const bubbleShown = layout !== null;
22187
+ useEffect(() => {
22188
+ nextRef.current?.focus({ focusVisible: false });
22189
+ }, [stepIndex, bubbleShown]);
22190
+ const maskId = `met-tutorial-mask-${useId()}`;
22191
+ if (typeof document === "undefined") return null;
22192
+ const holes = (layout?.spotlight ?? []).filter((r) => r !== null);
22193
+ const tapTarget = layout?.taps.length ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22194
+ const Scene = step.Scene;
22195
+ return createPortal(
22196
+ /* @__PURE__ */ jsxs("div", { ref: overlayRef, className: "met-tutorial", role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22197
+ /* @__PURE__ */ jsxs("svg", { className: "met-tutorial-dim", "aria-hidden": "true", children: [
22198
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", { id: maskId, children: [
22199
+ /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", fill: "white" }),
22200
+ holes.map((r, i) => /* @__PURE__ */ jsx(
22201
+ "rect",
22202
+ {
22203
+ x: r.x - HOLE_PAD,
22204
+ y: r.y - HOLE_PAD,
22205
+ width: r.width + HOLE_PAD * 2,
22206
+ height: r.height + HOLE_PAD * 2,
22207
+ rx: HOLE_RADIUS,
22208
+ fill: "black"
22209
+ },
22210
+ i
22211
+ ))
22212
+ ] }) }),
22213
+ /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", mask: `url(#${maskId})` })
22214
+ ] }),
22215
+ holes.map((r, i) => /* @__PURE__ */ jsx(
22216
+ "div",
22217
+ {
22218
+ className: "met-tutorial-glow",
22219
+ style: {
22220
+ left: r.x - HOLE_PAD,
22221
+ top: r.y - HOLE_PAD,
22222
+ width: r.width + HOLE_PAD * 2,
22223
+ height: r.height + HOLE_PAD * 2,
22224
+ borderRadius: HOLE_RADIUS
22225
+ }
22226
+ },
22227
+ i
22228
+ )),
22229
+ Scene && layout && /* @__PURE__ */ jsx(Scene, { ctx, spotlight: layout.spotlight, taps: layout.taps, tapCount }),
22230
+ tapTarget && /* @__PURE__ */ jsx(Hand, { target: tapTarget, tapKey: `${stepIndex}-${tapIndex}` }),
22231
+ layout && /* @__PURE__ */ jsx(
22232
+ Bubble,
22233
+ {
22234
+ layout,
22235
+ text,
22236
+ stepIndex,
22237
+ stepCount: steps.length,
22238
+ nextLabel: options.nextLabel ?? DEFAULT_NEXT_LABEL,
22239
+ onSpeak: options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22240
+ onNext: next,
22241
+ nextRef
22242
+ }
22243
+ )
22244
+ ] }),
22245
+ document.body
22246
+ );
22247
+ };
22248
+ const Hand = ({ target, tapKey }) => {
22249
+ const c = centerOf(target);
22250
+ return /* @__PURE__ */ jsx(
22251
+ "div",
22252
+ {
22253
+ className: "met-tutorial-hand",
22254
+ style: {
22255
+ width: HAND_W,
22256
+ height: HAND_H,
22257
+ transform: `translate(${c.x - TUTORIAL_HAND_TIP.x * HAND_W}px, ${c.y - TUTORIAL_HAND_TIP.y * HAND_H}px)`
22258
+ },
22259
+ children: /* @__PURE__ */ jsxs("div", { className: "met-tutorial-hand-press", children: [
22260
+ /* @__PURE__ */ jsx(
22261
+ "span",
22262
+ {
22263
+ className: "met-tutorial-ripple",
22264
+ style: { left: TUTORIAL_HAND_TIP.x * HAND_W, top: TUTORIAL_HAND_TIP.y * HAND_H }
22265
+ }
22266
+ ),
22267
+ /* @__PURE__ */ jsx(LionHand, {})
22268
+ ] }, tapKey)
22269
+ }
22270
+ );
22271
+ };
22272
+ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext, nextRef }) => {
22273
+ const { anchor, root, vw, vh } = layout;
22274
+ const clampLeft = (x) => Math.min(Math.max(EDGE, x), Math.max(EDGE, vw - BUBBLE_W - EDGE));
22275
+ 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 };
22276
+ return /* @__PURE__ */ jsxs("div", { className: `met-tutorial-bubble${anchor ? " met-tutorial-bubble--anchored" : ""}`, style, children: [
22277
+ /* @__PURE__ */ jsx("p", { className: "met-tutorial-text", dir: "auto", children: text }),
22278
+ /* @__PURE__ */ jsxs("div", { className: "met-tutorial-actions", children: [
22279
+ onSpeak && /* @__PURE__ */ jsx("button", { type: "button", className: "met-tutorial-speak", "aria-label": "🔊", onClick: onSpeak, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-hidden": "true", children: [
22280
+ /* @__PURE__ */ jsx("path", { d: "M4 9v6h4l5 4V5L8 9H4z", fill: "currentColor" }),
22281
+ /* @__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" })
22282
+ ] }) }),
22283
+ /* @__PURE__ */ jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22284
+ ] }),
22285
+ 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)) })
22286
+ ] });
22287
+ };
22288
+ const DEMO_KEYS = ["2", "5", "8"];
22289
+ const DEFAULT_SLOTS = 3;
22290
+ const MAX_SLOTS = 6;
22291
+ const STAND_IN_CHECK_SIZE = 52;
22292
+ const STAND_IN_CHECK_GAP = 20;
22293
+ function keypad(ctx) {
22294
+ return ctx.root.querySelector('[data-tutorial="keypad"]');
22295
+ }
22296
+ function key(ctx, k) {
22297
+ return ctx.root.querySelector(`[data-tutorial="keypad"] [data-key="${k}"]`);
22298
+ }
22299
+ function answerBox(ctx) {
22300
+ const inputs = getTextInputs(ctx.root);
22301
+ return inputs.find(canTakeInput) ?? inputs[0] ?? null;
22302
+ }
22303
+ function answerSlots(ctx) {
22304
+ const id = answerBox(ctx)?.id.replace(/-input$/, "");
22305
+ const max = id ? getMaxInputLength(ctx.json, id) : void 0;
22306
+ return Math.min(MAX_SLOTS, Math.max(1, max ?? DEFAULT_SLOTS));
22307
+ }
22308
+ function checkTarget(ctx) {
22309
+ if (ctx.submitTarget) return ctx.submitTarget;
22310
+ const box = answerBox(ctx)?.getBoundingClientRect();
22311
+ if (!box) return null;
22312
+ return new DOMRect(
22313
+ box.right + STAND_IN_CHECK_GAP,
22314
+ box.top + box.height / 2 - STAND_IN_CHECK_SIZE / 2,
22315
+ STAND_IN_CHECK_SIZE,
22316
+ STAND_IN_CHECK_SIZE
22317
+ );
22318
+ }
22319
+ const demoKeys = (ctx) => DEMO_KEYS.map((k) => key(ctx, k));
22320
+ const StandInFill = ({ box, slots, filled }) => /* @__PURE__ */ jsx(
22321
+ "div",
22322
+ {
22323
+ className: "met-tutorial-fill",
22324
+ style: { left: box.x, top: box.y, width: box.width, height: box.height },
22325
+ children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? "●" : "_" }, i))
22326
+ }
22327
+ );
22328
+ const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22329
+ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22330
+ const box = spotlight[1];
22331
+ if (!box) return null;
22332
+ const slots = answerSlots(ctx);
22333
+ const filled = tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22334
+ const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
22335
+ let arc = null;
22336
+ if (from) {
22337
+ const a = center(from);
22338
+ const b = center(box);
22339
+ const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
22340
+ arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
22341
+ }
22342
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
22343
+ arc && /* @__PURE__ */ jsx("svg", { className: "met-tutorial-arc", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: arc }) }),
22344
+ arc && /* @__PURE__ */ jsx("div", { className: "met-tutorial-hop", style: { offsetPath: `path('${arc}')` } }, tapCount),
22345
+ /* @__PURE__ */ jsx(StandInFill, { box, slots, filled })
22346
+ ] });
22347
+ };
22348
+ const CheckScene = ({ ctx, spotlight }) => {
22349
+ const box = spotlight[0];
22350
+ const check = spotlight[1];
22351
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
22352
+ box && /* @__PURE__ */ jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx) }),
22353
+ check && !ctx.submitTarget && /* @__PURE__ */ jsx(
22354
+ "div",
22355
+ {
22356
+ className: "met-tutorial-stand-in-check",
22357
+ style: { left: check.x, top: check.y, width: check.width, height: check.height },
22358
+ children: /* @__PURE__ */ jsx("span", { children: "✓" })
22359
+ }
22360
+ )
22361
+ ] });
22362
+ };
22363
+ const keypadTutorial = {
22364
+ id: "keypad",
22365
+ appliesTo: (json) => json.widgets[NUMERIC_KEYPAD_WIDGET_ID]?.type === JSON_TYPES.NUMERIC_KEYPAD,
22366
+ steps: [
22367
+ {
22368
+ id: "keypad.tap-number",
22369
+ text: "اضغط على رقم",
22370
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22371
+ taps: demoKeys
22372
+ },
22373
+ {
22374
+ id: "keypad.hop",
22375
+ text: "يظهر الرقم في الخانة",
22376
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22377
+ taps: demoKeys,
22378
+ Scene: HopScene
22379
+ },
22380
+ {
22381
+ id: "keypad.tap-check",
22382
+ text: "اضغط على زر التحقق",
22383
+ spotlight: (ctx) => [answerBox(ctx), checkTarget(ctx)],
22384
+ taps: (ctx) => [checkTarget(ctx)],
22385
+ Scene: CheckScene
22386
+ }
22387
+ ]
22388
+ };
22389
+ const WIDGET_TUTORIALS = [keypadTutorial];
22390
+ function findWidgetTutorial(json) {
22391
+ return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
22392
+ }
21834
22393
  const TOOL_ASPECT_RATIO = {
21835
22394
  ruler: RULER_ASPECT_RATIO,
21836
22395
  protractor: PROTRACTOR_ASPECT_RATIO,
@@ -21869,6 +22428,11 @@ function buildInitialValues(json) {
21869
22428
  function toBaseId(widgetId) {
21870
22429
  return parseFractionKey(widgetId)?.widgetId ?? widgetId;
21871
22430
  }
22431
+ function hasJustFilledBox(json, key2, previous, value) {
22432
+ const maxLength = getMaxInputLength(json, key2);
22433
+ if (maxLength === void 0) return false;
22434
+ return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22435
+ }
21872
22436
  function buildInitialValuesSignature(json) {
21873
22437
  return JSON.stringify({
21874
22438
  widgets: json.widgets,
@@ -21918,6 +22482,27 @@ function buildRevealedValues(json) {
21918
22482
  }
21919
22483
  return revealed;
21920
22484
  }
22485
+ function buildRevealedDropSources(json, revealedValues) {
22486
+ const pool = [];
22487
+ const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
22488
+ if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
22489
+ bank.options.forEach((value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) }));
22490
+ }
22491
+ for (const [widgetId, widget] of Object.entries(json.widgets)) {
22492
+ if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION) pool.push({ optionId: widgetId, value: widget.value });
22493
+ }
22494
+ const sources = /* @__PURE__ */ new Map();
22495
+ if (pool.length === 0) return sources;
22496
+ const used = /* @__PURE__ */ new Set();
22497
+ for (const [inputId, value] of Object.entries(revealedValues)) {
22498
+ if (typeof value !== "string" || value === "") continue;
22499
+ const match = pool.find((o) => !used.has(o.optionId) && normalize(o.value) === normalize(value));
22500
+ if (!match) continue;
22501
+ used.add(match.optionId);
22502
+ sources.set(inputId, match.optionId);
22503
+ }
22504
+ return sources;
22505
+ }
21921
22506
  const ExerciseRenderer = forwardRef(({
21922
22507
  json,
21923
22508
  onSubmit,
@@ -21940,6 +22525,8 @@ const ExerciseRenderer = forwardRef(({
21940
22525
  const rootRef = useRef(null);
21941
22526
  const [resetKey, setResetKey] = useState(0);
21942
22527
  const [revealed, setRevealed] = useState(false);
22528
+ const tutorial = useMemo(() => findWidgetTutorial(json), [json]);
22529
+ const [tutorialRun, setTutorialRun] = useState(null);
21943
22530
  const themeStyle = useMemo(() => {
21944
22531
  const style = {};
21945
22532
  if (theme?.correct) style["--exercise-correct"] = theme.correct;
@@ -21960,6 +22547,7 @@ const ExerciseRenderer = forwardRef(({
21960
22547
  setResetKey((k) => k + 1);
21961
22548
  setLoadedValuesSignature(initialValuesSignature);
21962
22549
  touchedRef.current = /* @__PURE__ */ new Set();
22550
+ setTutorialRun(null);
21963
22551
  }, [json, initialValuesSignature, loadedValuesSignature]);
21964
22552
  const locked = validationDetails !== null || externallyLocked;
21965
22553
  const lastResultRef = useRef(null);
@@ -22043,7 +22631,7 @@ const ExerciseRenderer = forwardRef(({
22043
22631
  const next = { ...valuesRef.current, ...revealedValues };
22044
22632
  valuesRef.current = next;
22045
22633
  setValues(next);
22046
- setDroppedSources(/* @__PURE__ */ new Map());
22634
+ setDroppedSources(buildRevealedDropSources(json, revealedValues));
22047
22635
  setValidationDetails(details);
22048
22636
  setRevealed(true);
22049
22637
  setResetKey((k) => k + 1);
@@ -22051,20 +22639,29 @@ const ExerciseRenderer = forwardRef(({
22051
22639
  focusFirstInput: () => {
22052
22640
  const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
22053
22641
  if (!container) return;
22054
- const candidates = Array.from(
22055
- container.querySelectorAll('input[type="text"], input[type="number"], textarea, select')
22056
- );
22057
- const canTakeInput = (el) => !el.readOnly && !el.disabled;
22642
+ const candidates = getTextInputs(container);
22058
22643
  const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
22059
22644
  target?.focus();
22060
- }
22061
- }), [json, values, onSubmit, playSound, locked, initialValuesSignature]);
22645
+ },
22646
+ hasTutorial: () => tutorial !== null,
22647
+ startTutorial: (options = {}) => {
22648
+ if (!tutorial || !rootRef.current) return false;
22649
+ setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
22650
+ return true;
22651
+ },
22652
+ stopTutorial: () => setTutorialRun(null)
22653
+ }), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
22654
+ const focusNextInput = (currentKey) => {
22655
+ if (!rootRef.current) return;
22656
+ findNextInput(rootRef.current, currentKey)?.focus();
22657
+ };
22062
22658
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
22063
22659
  if (locked) return;
22064
22660
  if (!silent) {
22065
22661
  setRevealed(false);
22066
22662
  touchedRef.current.add(toBaseId(widgetId));
22067
22663
  }
22664
+ const previous = valuesRef.current[widgetId];
22068
22665
  const next = { ...valuesRef.current, [widgetId]: value };
22069
22666
  valuesRef.current = next;
22070
22667
  setValues(next);
@@ -22078,6 +22675,9 @@ const ExerciseRenderer = forwardRef(({
22078
22675
  return next2;
22079
22676
  });
22080
22677
  onChange?.(next);
22678
+ if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22679
+ focusNextInput(widgetId);
22680
+ }
22081
22681
  };
22082
22682
  const handleSubmit = () => {
22083
22683
  runSubmit();
@@ -22192,7 +22792,7 @@ const ExerciseRenderer = forwardRef(({
22192
22792
  const rulerWidget = json.widgets[RULER_WIDGET_ID];
22193
22793
  const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
22194
22794
  const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
22195
- const numericKeypadWidget = json.widgets["_numeric_keypad"];
22795
+ const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
22196
22796
  const { tools, toolsMinHeight } = useMemo(() => {
22197
22797
  const list = [];
22198
22798
  const PAD = 12;
@@ -22211,7 +22811,7 @@ const ExerciseRenderer = forwardRef(({
22211
22811
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
22212
22812
  return { tools: list, toolsMinHeight: maxH };
22213
22813
  }, [rulerWidget, protractorWidget, setSquareWidget]);
22214
- return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22814
+ return /* @__PURE__ */ jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22215
22815
  /* @__PURE__ */ jsxs(
22216
22816
  "div",
22217
22817
  {
@@ -22307,7 +22907,21 @@ const ExerciseRenderer = forwardRef(({
22307
22907
  }
22308
22908
  }
22309
22909
  ) }),
22310
- showSubmitButton && /* @__PURE__ */ jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
22910
+ showSubmitButton && /* @__PURE__ */ jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" }),
22911
+ tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsx(
22912
+ TutorialOverlay,
22913
+ {
22914
+ tutorial,
22915
+ root: rootRef.current,
22916
+ json,
22917
+ options: tutorialRun.options,
22918
+ onClose: (finished) => {
22919
+ setTutorialRun(null);
22920
+ if (finished) tutorialRun.options.onDone?.();
22921
+ }
22922
+ },
22923
+ tutorialRun.run
22924
+ )
22311
22925
  ] }) }) }) });
22312
22926
  });
22313
22927
  ExerciseRenderer.displayName = "ExerciseRenderer";
@@ -22441,7 +23055,7 @@ export {
22441
23055
  VopSeparatorItemSchema as ay,
22442
23056
  VopCellItemSchema as az,
22443
23057
  tokenizeSafe as b,
22444
- isTextBlock as b$,
23058
+ TextBlockSchema as b$,
22445
23059
  validateWidgetJSON as b0,
22446
23060
  validateWidgetJSONSafe as b1,
22447
23061
  EngineError as b2,
@@ -22452,181 +23066,185 @@ export {
22452
23066
  ExerciseRenderer as b7,
22453
23067
  LayoutRenderer as b8,
22454
23068
  WidgetRenderer as b9,
22455
- InlineNodeSchema as bA,
22456
- createFreeText as bB,
22457
- createSpace as bC,
22458
- createHighlightedText as bD,
22459
- createColorText as bE,
22460
- createBoldText as bF,
22461
- createStringInput as bG,
22462
- createNumericInput as bH,
22463
- createImage as bI,
22464
- createFraction as bJ,
22465
- createVisualFraction as bK,
22466
- createInlineDragOption as bL,
22467
- createDisplayClock as bM,
22468
- createInteractiveClock as bN,
22469
- isFreeText as bO,
22470
- isSpace as bP,
22471
- isInlineInput as bQ,
22472
- isFraction as bR,
22473
- isImage as bS,
22474
- isClock as bT,
22475
- isInlineNode as bU,
22476
- isVisualFractionInline as bV,
22477
- isInlineDragOption as bW,
22478
- TextBlockSchema as bX,
22479
- LineBlockSchema as bY,
22480
- createTextBlock as bZ,
22481
- createLineBlock as b_,
23069
+ CLOCK_MODES as bA,
23070
+ CLOCK_FORMATS as bB,
23071
+ CLOCK_TIME_REGEX as bC,
23072
+ ClockInlineSchema as bD,
23073
+ InlineNodeSchema as bE,
23074
+ createFreeText as bF,
23075
+ createSpace as bG,
23076
+ createHighlightedText as bH,
23077
+ createColorText as bI,
23078
+ createBoldText as bJ,
23079
+ createStringInput as bK,
23080
+ createNumericInput as bL,
23081
+ createImage as bM,
23082
+ createFraction as bN,
23083
+ createVisualFraction as bO,
23084
+ createInlineDragOption as bP,
23085
+ createDisplayClock as bQ,
23086
+ createInteractiveClock as bR,
23087
+ isFreeText as bS,
23088
+ isSpace as bT,
23089
+ isInlineInput as bU,
23090
+ isFraction as bV,
23091
+ isImage as bW,
23092
+ isClock as bX,
23093
+ isInlineNode as bY,
23094
+ isVisualFractionInline as bZ,
23095
+ isInlineDragOption as b_,
22482
23096
  renderExercise as ba,
22483
23097
  SoundProvider as bb,
22484
23098
  useSound as bc,
22485
23099
  ExerciseLockProvider as bd,
22486
- isPairMatcherWidget as be,
22487
- isMatcherWidget as bf,
22488
- VISUAL_FRACTION_SHAPES as bg,
22489
- VISUAL_FRACTION_SHAPE as bh,
22490
- VISUAL_FRACTION_DEFAULT_SIZE as bi,
22491
- VISUAL_FRACTION_MAX_DIVISIONS as bj,
22492
- FreeTextInlineSchema as bk,
22493
- SpaceInlineSchema as bl,
22494
- HighlightedTextInlineSchema as bm,
22495
- ColorTextInlineSchema as bn,
22496
- BoldTextInlineSchema as bo,
22497
- InlineInputWidgetSchema as bp,
22498
- FractionInlineSchema as bq,
22499
- ImageInlineSchema as br,
22500
- VisualFractionInlineSchema as bs,
22501
- validateVisualFractionCapacity as bt,
22502
- InlineDragOptionSchema as bu,
22503
- CLOCK_TYPES as bv,
22504
- CLOCK_MODES as bw,
22505
- CLOCK_FORMATS as bx,
22506
- CLOCK_TIME_REGEX as by,
22507
- ClockInlineSchema as bz,
23100
+ WIDGET_TUTORIALS as be,
23101
+ findWidgetTutorial as bf,
23102
+ isPairMatcherWidget as bg,
23103
+ isMatcherWidget as bh,
23104
+ VISUAL_FRACTION_SHAPES as bi,
23105
+ VISUAL_FRACTION_SHAPE as bj,
23106
+ VISUAL_FRACTION_DEFAULT_SIZE as bk,
23107
+ VISUAL_FRACTION_MAX_DIVISIONS as bl,
23108
+ INLINE_INPUT_MAX_WIDTH as bm,
23109
+ InlineInputWidthSchema as bn,
23110
+ FreeTextInlineSchema as bo,
23111
+ SpaceInlineSchema as bp,
23112
+ HighlightedTextInlineSchema as bq,
23113
+ ColorTextInlineSchema as br,
23114
+ BoldTextInlineSchema as bs,
23115
+ InlineInputWidgetSchema as bt,
23116
+ FractionInlineSchema as bu,
23117
+ ImageInlineSchema as bv,
23118
+ VisualFractionInlineSchema as bw,
23119
+ validateVisualFractionCapacity as bx,
23120
+ InlineDragOptionSchema as by,
23121
+ CLOCK_TYPES as bz,
22508
23122
  compile as c,
22509
- VopRowSchema as c$,
22510
- isLineBlock as c0,
22511
- DisplayInlineNodeSchema as c1,
22512
- MatcherItemSchema as c2,
22513
- createMatcherItem as c3,
22514
- isMatcherItem as c4,
22515
- MatcherRowSchema as c5,
22516
- createMatcherRow as c6,
22517
- isMatcherRow as c7,
22518
- MatcherBlockSchema as c8,
22519
- createMatcherBlock as c9,
22520
- createExpressionValidation as cA,
22521
- createMixedValidation as cB,
22522
- isSimpleValidation as cC,
22523
- isExpressionValidation as cD,
22524
- isMixedValidation as cE,
22525
- isValidationNode as cF,
22526
- ContentBlockSchema as cG,
22527
- ExerciseDocumentSchema as cH,
22528
- createExerciseDocument as cI,
22529
- isExerciseDocument as cJ,
22530
- VOP_OPERATORS as cK,
22531
- normalizeVopOperator as cL,
22532
- VopDigitCellSchema as cM,
22533
- VopLabelCellSchema as cN,
22534
- VopInputCellSchema as cO,
22535
- VopEmptyCellSchema as cP,
22536
- VopColorCellSchema as cQ,
22537
- VopBoldCellSchema as cR,
22538
- VopHighlightCellSchema as cS,
22539
- VopCellSchema as cT,
22540
- createVopDigitCell as cU,
22541
- createVopInputCell as cV,
22542
- createVopLabelCell as cW,
22543
- createVopEmptyCell as cX,
22544
- createVopColorCell as cY,
22545
- createVopBoldCell as cZ,
22546
- createVopHighlightCell as c_,
22547
- isMatcherBlock as ca,
22548
- PairMatcherBlockSchema as cb,
22549
- createPairMatcherBlock as cc,
22550
- isPairMatcherBlock as cd,
22551
- DragOptionsToolSchema as ce,
22552
- NumericKeypadToolSchema as cf,
22553
- RulerToolSchema as cg,
22554
- ProtractorToolSchema as ch,
22555
- SetSquareToolSchema as ci,
22556
- ToolNodeSchema as cj,
22557
- createDragOptions as ck,
22558
- createNumericKeypad as cl,
22559
- createRulerTool as cm,
22560
- createProtractorTool as cn,
22561
- createSetSquareTool as co,
22562
- isDragOptions as cp,
22563
- isNumericKeypad as cq,
22564
- isRulerTool as cr,
22565
- isProtractorTool as cs,
22566
- isSetSquareTool as ct,
22567
- isToolNode as cu,
22568
- SimpleValidationSchema as cv,
22569
- ExpressionValidationSchema as cw,
22570
- MixedValidationSchema as cx,
22571
- ValidationNodeSchema as cy,
22572
- createSimpleValidation as cz,
23123
+ createVopEmptyCell as c$,
23124
+ LineBlockSchema as c0,
23125
+ createTextBlock as c1,
23126
+ createLineBlock as c2,
23127
+ isTextBlock as c3,
23128
+ isLineBlock as c4,
23129
+ DisplayInlineNodeSchema as c5,
23130
+ MatcherItemSchema as c6,
23131
+ createMatcherItem as c7,
23132
+ isMatcherItem as c8,
23133
+ MatcherRowSchema as c9,
23134
+ ExpressionValidationSchema as cA,
23135
+ MixedValidationSchema as cB,
23136
+ ValidationNodeSchema as cC,
23137
+ createSimpleValidation as cD,
23138
+ createExpressionValidation as cE,
23139
+ createMixedValidation as cF,
23140
+ isSimpleValidation as cG,
23141
+ isExpressionValidation as cH,
23142
+ isMixedValidation as cI,
23143
+ isValidationNode as cJ,
23144
+ ContentBlockSchema as cK,
23145
+ ExerciseDocumentSchema as cL,
23146
+ createExerciseDocument as cM,
23147
+ isExerciseDocument as cN,
23148
+ VOP_OPERATORS as cO,
23149
+ normalizeVopOperator as cP,
23150
+ VopDigitCellSchema as cQ,
23151
+ VopLabelCellSchema as cR,
23152
+ VopInputCellSchema as cS,
23153
+ VopEmptyCellSchema as cT,
23154
+ VopColorCellSchema as cU,
23155
+ VopBoldCellSchema as cV,
23156
+ VopHighlightCellSchema as cW,
23157
+ VopCellSchema as cX,
23158
+ createVopDigitCell as cY,
23159
+ createVopInputCell as cZ,
23160
+ createVopLabelCell as c_,
23161
+ createMatcherRow as ca,
23162
+ isMatcherRow as cb,
23163
+ MatcherBlockSchema as cc,
23164
+ createMatcherBlock as cd,
23165
+ isMatcherBlock as ce,
23166
+ PairMatcherBlockSchema as cf,
23167
+ createPairMatcherBlock as cg,
23168
+ isPairMatcherBlock as ch,
23169
+ DragOptionsToolSchema as ci,
23170
+ NumericKeypadToolSchema as cj,
23171
+ RulerToolSchema as ck,
23172
+ ProtractorToolSchema as cl,
23173
+ SetSquareToolSchema as cm,
23174
+ ToolNodeSchema as cn,
23175
+ createDragOptions as co,
23176
+ createNumericKeypad as cp,
23177
+ createRulerTool as cq,
23178
+ createProtractorTool as cr,
23179
+ createSetSquareTool as cs,
23180
+ isDragOptions as ct,
23181
+ isNumericKeypad as cu,
23182
+ isRulerTool as cv,
23183
+ isProtractorTool as cw,
23184
+ isSetSquareTool as cx,
23185
+ isToolNode as cy,
23186
+ SimpleValidationSchema as cz,
22573
23187
  parse as d,
22574
- createVopRow as d0,
22575
- VopCarryRowSchema as d1,
22576
- createVopCarryRow as d2,
22577
- VopSeparatorSchema as d3,
22578
- createVopSeparator as d4,
22579
- VerticalOperationBlockSchema as d5,
22580
- createVerticalOperationBlock as d6,
22581
- isVopCell as d7,
22582
- isVopRow as d8,
22583
- isVopCarryRow as d9,
22584
- ChartBarSchema as dA,
22585
- createChartBar as dB,
22586
- isChartBar as dC,
22587
- ChartSeriesSchema as dD,
22588
- createChartSeries as dE,
22589
- isChartSeries as dF,
22590
- ChartSeriesBarSchema as dG,
22591
- createChartSeriesBar as dH,
22592
- isChartSeriesBar as dI,
22593
- ChartGroupSchema as dJ,
22594
- createChartGroup as dK,
22595
- isChartGroup as dL,
22596
- alignGroupBarsToSeries as dM,
22597
- ChartBlockSchema as dN,
22598
- resolveChartAxes as dO,
22599
- validateUniqueIds as dP,
22600
- createChartBlock as dQ,
22601
- isChartBlock as dR,
22602
- GeometryBlockSchema as dS,
22603
- createGeometryBlock as dT,
22604
- isGeometryBlock as dU,
22605
- isVopSeparator as da,
22606
- isVerticalOperationBlock as db,
22607
- GRID_SYMMETRY_AXES as dc,
22608
- GRID_SYMMETRY_INTERACTIVE_AREAS as dd,
22609
- GRID_SYMMETRY_MAX_ROWS as de,
22610
- GRID_SYMMETRY_MAX_COLS as df,
22611
- GRID_SYMMETRY_MIN_CELL_SIZE as dg,
22612
- GRID_SYMMETRY_MAX_CELL_SIZE as dh,
22613
- isCellInInteractiveArea as di,
22614
- reflectGridSymmetryCell as dj,
22615
- computeGridSymmetryExpectedCells as dk,
22616
- computeGridSymmetryCellsToFill as dl,
22617
- GridSymmetryBlockSchema as dm,
22618
- createGridSymmetryBlock as dn,
22619
- isGridSymmetryBlock as dp,
22620
- CHART_TYPES as dq,
22621
- CHART_AXIS_DEFAULTS as dr,
22622
- CHART_MAX_AXIS_TICKS as ds,
22623
- CHART_MAX_PICTO_SLOTS as dt,
22624
- ChartAxisSchema as du,
22625
- createChartAxis as dv,
22626
- isChartAxis as dw,
22627
- ChartPictoSchema as dx,
22628
- createChartPicto as dy,
22629
- isChartPicto as dz,
23188
+ createVopColorCell as d0,
23189
+ createVopBoldCell as d1,
23190
+ createVopHighlightCell as d2,
23191
+ VopRowSchema as d3,
23192
+ createVopRow as d4,
23193
+ VopCarryRowSchema as d5,
23194
+ createVopCarryRow as d6,
23195
+ VopSeparatorSchema as d7,
23196
+ createVopSeparator as d8,
23197
+ VerticalOperationBlockSchema as d9,
23198
+ isChartAxis as dA,
23199
+ ChartPictoSchema as dB,
23200
+ createChartPicto as dC,
23201
+ isChartPicto as dD,
23202
+ ChartBarSchema as dE,
23203
+ createChartBar as dF,
23204
+ isChartBar as dG,
23205
+ ChartSeriesSchema as dH,
23206
+ createChartSeries as dI,
23207
+ isChartSeries as dJ,
23208
+ ChartSeriesBarSchema as dK,
23209
+ createChartSeriesBar as dL,
23210
+ isChartSeriesBar as dM,
23211
+ ChartGroupSchema as dN,
23212
+ createChartGroup as dO,
23213
+ isChartGroup as dP,
23214
+ alignGroupBarsToSeries as dQ,
23215
+ ChartBlockSchema as dR,
23216
+ resolveChartAxes as dS,
23217
+ validateUniqueIds as dT,
23218
+ createChartBlock as dU,
23219
+ isChartBlock as dV,
23220
+ GeometryBlockSchema as dW,
23221
+ createGeometryBlock as dX,
23222
+ isGeometryBlock as dY,
23223
+ createVerticalOperationBlock as da,
23224
+ isVopCell as db,
23225
+ isVopRow as dc,
23226
+ isVopCarryRow as dd,
23227
+ isVopSeparator as de,
23228
+ isVerticalOperationBlock as df,
23229
+ GRID_SYMMETRY_AXES as dg,
23230
+ GRID_SYMMETRY_INTERACTIVE_AREAS as dh,
23231
+ GRID_SYMMETRY_MAX_ROWS as di,
23232
+ GRID_SYMMETRY_MAX_COLS as dj,
23233
+ GRID_SYMMETRY_MIN_CELL_SIZE as dk,
23234
+ GRID_SYMMETRY_MAX_CELL_SIZE as dl,
23235
+ isCellInInteractiveArea as dm,
23236
+ reflectGridSymmetryCell as dn,
23237
+ computeGridSymmetryExpectedCells as dp,
23238
+ computeGridSymmetryCellsToFill as dq,
23239
+ GridSymmetryBlockSchema as dr,
23240
+ createGridSymmetryBlock as ds,
23241
+ isGridSymmetryBlock as dt,
23242
+ CHART_TYPES as du,
23243
+ CHART_AXIS_DEFAULTS as dv,
23244
+ CHART_MAX_AXIS_TICKS as dw,
23245
+ CHART_MAX_PICTO_SLOTS as dx,
23246
+ ChartAxisSchema as dy,
23247
+ createChartAxis as dz,
22630
23248
  parseSafe as e,
22631
23249
  toAST as f,
22632
23250
  getFractionKey as g,
@@ -22650,4 +23268,4 @@ export {
22650
23268
  extractVariables as y,
22651
23269
  validateBooleanExpression as z
22652
23270
  };
22653
- //# sourceMappingURL=index-DBteHZkS.mjs.map
23271
+ //# sourceMappingURL=index-6z_8j1tM.mjs.map