@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.
@@ -241,6 +241,7 @@ const JSON_TYPES = {
241
241
  GEOMETRY: "geometry"
242
242
  };
243
243
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
244
+ const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
244
245
  const RULER_WIDGET_ID = "_ruler";
245
246
  const PROTRACTOR_WIDGET_ID = "_protractor";
246
247
  const SET_SQUARE_WIDGET_ID = "_set_square";
@@ -273,7 +274,8 @@ const INPUT_TYPES = {
273
274
  /** Numeric input (digits only) */
274
275
  NUMERIC: "numeric"
275
276
  };
276
- const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
277
+ const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line", "line-left"];
278
+ const DEFAULT_NUMBER_LINE_STYLE = "line-left";
277
279
  const INTERNAL_PRECISION = 9;
278
280
  const DISPLAY_PRECISION = 2;
279
281
  const SIDE_PAD = 7;
@@ -2056,6 +2058,8 @@ const VISUAL_FRACTION_SHAPE = {
2056
2058
  };
2057
2059
  const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
2058
2060
  const VISUAL_FRACTION_MAX_DIVISIONS = 100;
2061
+ const INLINE_INPUT_MAX_WIDTH = 20;
2062
+ const InlineInputWidthSchema = zod.z.number().int().positive().max(INLINE_INPUT_MAX_WIDTH, { message: `width cannot exceed ${INLINE_INPUT_MAX_WIDTH}` }).optional();
2059
2063
  const FreeTextInlineSchema = zod.z.object({
2060
2064
  type: zod.z.literal(AST_TYPES.FREE_TEXT),
2061
2065
  value: zod.z.string()
@@ -2085,6 +2089,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
2085
2089
  name: zod.z.string(),
2086
2090
  inputType: zod.z.literal(INPUT_TYPES.STRING),
2087
2091
  length: zod.z.number().int().positive(),
2092
+ width: InlineInputWidthSchema,
2088
2093
  placeholder: zod.z.string().max(32).optional(),
2089
2094
  disabled: zod.z.boolean().optional(),
2090
2095
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
@@ -2094,6 +2099,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
2094
2099
  name: zod.z.string(),
2095
2100
  inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
2096
2101
  digits: zod.z.number().int().positive(),
2102
+ width: InlineInputWidthSchema,
2097
2103
  placeholder: zod.z.string().max(32).optional(),
2098
2104
  disabled: zod.z.boolean().optional(),
2099
2105
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
@@ -2251,7 +2257,7 @@ function createBoldText(value, highlight) {
2251
2257
  highlight
2252
2258
  });
2253
2259
  }
2254
- function createStringInput(name, length, answer, placeholder, disabled) {
2260
+ function createStringInput(name, length, answer, placeholder, disabled, width) {
2255
2261
  return parseOrThrow(
2256
2262
  InlineInputWidgetSchema,
2257
2263
  {
@@ -2259,6 +2265,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2259
2265
  name,
2260
2266
  inputType: INPUT_TYPES.STRING,
2261
2267
  length,
2268
+ width,
2262
2269
  placeholder,
2263
2270
  disabled,
2264
2271
  // A disabled input is never graded, so its answer is dropped.
@@ -2267,7 +2274,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2267
2274
  "STRING_INPUT"
2268
2275
  );
2269
2276
  }
2270
- function createNumericInput(name, digits, answer, placeholder, disabled) {
2277
+ function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2271
2278
  return parseOrThrow(
2272
2279
  InlineInputWidgetSchema,
2273
2280
  {
@@ -2275,6 +2282,7 @@ function createNumericInput(name, digits, answer, placeholder, disabled) {
2275
2282
  name,
2276
2283
  inputType: INPUT_TYPES.NUMERIC,
2277
2284
  digits,
2285
+ width,
2278
2286
  placeholder,
2279
2287
  disabled,
2280
2288
  // A disabled input is never graded, so its answer is dropped.
@@ -2491,7 +2499,7 @@ const NumberLineBlockSchema = zod.z.object({
2491
2499
  errorMap: () => ({
2492
2500
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
2493
2501
  })
2494
- }).optional(),
2502
+ }).default(DEFAULT_NUMBER_LINE_STYLE),
2495
2503
  labelVisibility: zod.z.enum(NUMBER_LINE_LABELS, {
2496
2504
  errorMap: () => ({
2497
2505
  message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
@@ -4324,12 +4332,19 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4324
4332
  `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": disabled must be true or false, got ${JSON.stringify(params.disabled)}`
4325
4333
  );
4326
4334
  }
4335
+ const width = params.width;
4336
+ const maxChars = ctx.stringInput ? params.length : params.digits;
4337
+ if (width !== void 0 && width < maxChars) {
4338
+ throw new Error(
4339
+ `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": width (${width}) cannot be smaller than ${ctx.stringInput ? "length" : "digits"} (${maxChars})`
4340
+ );
4341
+ }
4327
4342
  if (ctx.stringInput) {
4328
4343
  const length = params.length;
4329
- return createStringInput(name, length, answer, placeholder, disabled);
4344
+ return createStringInput(name, length, answer, placeholder, disabled, width);
4330
4345
  }
4331
4346
  const digits = params.digits;
4332
- return createNumericInput(name, digits, answer, placeholder, disabled);
4347
+ return createNumericInput(name, digits, answer, placeholder, disabled, width);
4333
4348
  }
4334
4349
  inlineDragOption(ctx) {
4335
4350
  const valueToken = ctx.StringLiteral[0];
@@ -5069,6 +5084,8 @@ const StringInputJSONSchema = zod.z.object({
5069
5084
  type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
5070
5085
  inputType: zod.z.literal(INPUT_TYPES.STRING),
5071
5086
  length: zod.z.number().int().positive(),
5087
+ /** Box width in characters; defaults to `length`. */
5088
+ width: InlineInputWidthSchema,
5072
5089
  placeholder: zod.z.string().max(32).optional(),
5073
5090
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5074
5091
  disabled: zod.z.boolean().optional()
@@ -5077,6 +5094,8 @@ const NumericInputJSONSchema = zod.z.object({
5077
5094
  type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
5078
5095
  inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
5079
5096
  digits: zod.z.number().int().positive(),
5097
+ /** Box width in characters; defaults to `digits`. */
5098
+ width: InlineInputWidthSchema,
5080
5099
  placeholder: zod.z.string().max(32).optional(),
5081
5100
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
5082
5101
  disabled: zod.z.boolean().optional()
@@ -6067,7 +6086,7 @@ lodash.exports;
6067
6086
  var reIsNative = RegExp2(
6068
6087
  "^" + funcToString.call(hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"
6069
6088
  );
6070
- 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;
6089
+ 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;
6071
6090
  var defineProperty = function() {
6072
6091
  try {
6073
6092
  var func = getNative(Object2, "defineProperty");
@@ -6082,7 +6101,7 @@ lodash.exports;
6082
6101
  var metaMap = WeakMap && new WeakMap();
6083
6102
  var realNames = {};
6084
6103
  var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap);
6085
- var symbolProto = Symbol ? Symbol.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
6104
+ var symbolProto = Symbol2 ? Symbol2.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
6086
6105
  function lodash2(value) {
6087
6106
  if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
6088
6107
  if (value instanceof LodashWrapper) {
@@ -10981,6 +11000,7 @@ function toJSON(ast) {
10981
11000
  type: JSON_TYPES.INLINE_INPUT,
10982
11001
  inputType: INPUT_TYPES.STRING,
10983
11002
  length: inline.length,
11003
+ ...inline.width !== void 0 && { width: inline.width },
10984
11004
  placeholder: inline.placeholder,
10985
11005
  ...inline.disabled && { disabled: true }
10986
11006
  };
@@ -10989,6 +11009,7 @@ function toJSON(ast) {
10989
11009
  type: JSON_TYPES.INLINE_INPUT,
10990
11010
  inputType: INPUT_TYPES.NUMERIC,
10991
11011
  digits: inline.digits,
11012
+ ...inline.width !== void 0 && { width: inline.width },
10992
11013
  placeholder: inline.placeholder,
10993
11014
  ...inline.disabled && { disabled: true }
10994
11015
  };
@@ -11084,7 +11105,7 @@ function toJSON(ast) {
11084
11105
  options: lodashExports.shuffle(tool.options)
11085
11106
  };
11086
11107
  } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
11087
- widgets["_numeric_keypad"] = {
11108
+ widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
11088
11109
  type: JSON_TYPES.NUMERIC_KEYPAD,
11089
11110
  includeOperators: tool.includeOperators,
11090
11111
  includeDecimal: tool.includeDecimal,
@@ -11347,7 +11368,7 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
11347
11368
  max: numberLine.max,
11348
11369
  step: numberLine.step,
11349
11370
  ...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
11350
- ...numberLine.style !== void 0 && { style: numberLine.style },
11371
+ style: numberLine.style,
11351
11372
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
11352
11373
  ...counterItem !== void 0 && { counter: counterItem },
11353
11374
  ...highlights !== void 0 && { highlights },
@@ -16004,22 +16025,22 @@ function createDefaultDropAnimation(options) {
16004
16025
  easing,
16005
16026
  fill: "forwards"
16006
16027
  });
16007
- return new Promise((resolve) => {
16028
+ return new Promise((resolve2) => {
16008
16029
  animation.onfinish = () => {
16009
16030
  cleanup == null ? void 0 : cleanup();
16010
- resolve();
16031
+ resolve2();
16011
16032
  };
16012
16033
  });
16013
16034
  };
16014
16035
  }
16015
- let key = 0;
16036
+ let key$1 = 0;
16016
16037
  function useKey(id) {
16017
16038
  return React.useMemo(() => {
16018
16039
  if (id == null) {
16019
16040
  return;
16020
16041
  }
16021
- key++;
16022
- return key;
16042
+ key$1++;
16043
+ return key$1;
16023
16044
  }, [id]);
16024
16045
  }
16025
16046
  const DragOverlay = /* @__PURE__ */ React.memo((_ref) => {
@@ -16101,6 +16122,7 @@ const DEFAULT_SOUNDS = {
16101
16122
  "drag-start": dragStart,
16102
16123
  "drag-drop": dragDrop,
16103
16124
  "drag-cancel": dragDrop,
16125
+ "drag-hint": buttonClick,
16104
16126
  "radio-select": buttonClick,
16105
16127
  "pair-select": buttonClick,
16106
16128
  "pair-link": buttonClick,
@@ -16204,14 +16226,24 @@ function useExerciseDnd() {
16204
16226
  }
16205
16227
  return ctx;
16206
16228
  }
16229
+ const NO_EXERCISE = Symbol("no-exercise");
16207
16230
  const ExerciseDndProvider = ({
16208
16231
  children,
16209
16232
  droppedSources,
16210
- onDragEnd
16233
+ onDragEnd,
16234
+ exerciseKey
16211
16235
  }) => {
16212
16236
  const [activeItem, setActiveItem] = React.useState(null);
16237
+ const [draggedExerciseKey, setDraggedExerciseKey] = React.useState(NO_EXERCISE);
16238
+ const hasDraggedOption = draggedExerciseKey === exerciseKey;
16213
16239
  const [dropHandlers] = React.useState(() => /* @__PURE__ */ new Map());
16214
16240
  const playSound = useSound();
16241
+ const hintSoundExerciseRef = React.useRef(NO_EXERCISE);
16242
+ const playHintSound = React.useCallback(() => {
16243
+ if (hintSoundExerciseRef.current === exerciseKey) return;
16244
+ hintSoundExerciseRef.current = exerciseKey;
16245
+ playSound("drag-hint");
16246
+ }, [playSound, exerciseKey]);
16215
16247
  const usedOptionIds = React.useMemo(() => {
16216
16248
  return new Set(droppedSources.values());
16217
16249
  }, [droppedSources]);
@@ -16235,6 +16267,7 @@ const ExerciseDndProvider = ({
16235
16267
  const raw = active.data.current;
16236
16268
  const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
16237
16269
  setActiveItem(item);
16270
+ if (item.type === "drag-option") setDraggedExerciseKey(exerciseKey);
16238
16271
  playSound("drag-start");
16239
16272
  };
16240
16273
  const handleDragEnd = (event) => {
@@ -16260,6 +16293,9 @@ const ExerciseDndProvider = ({
16260
16293
  value: {
16261
16294
  activeItem,
16262
16295
  isDragging: activeItem !== null,
16296
+ exerciseKey,
16297
+ hasDraggedOption,
16298
+ playHintSound,
16263
16299
  usedOptionIds,
16264
16300
  droppedSources,
16265
16301
  registerDropHandler,
@@ -17040,7 +17076,38 @@ function isUndoDrop(event, dragId) {
17040
17076
  const LockContext = React.createContext(false);
17041
17077
  const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsxRuntime.jsx(LockContext.Provider, { value: locked, children });
17042
17078
  const useExerciseLocked = () => React.useContext(LockContext);
17043
- const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
17079
+ const OVERLAY_STYLE = {
17080
+ position: "absolute",
17081
+ top: 0,
17082
+ left: 0,
17083
+ width: "100%",
17084
+ height: "100%",
17085
+ minWidth: 0,
17086
+ maxWidth: "none",
17087
+ margin: 0,
17088
+ boxSizing: "border-box"
17089
+ };
17090
+ const SlotSizer = ({
17091
+ style,
17092
+ ...props
17093
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
17094
+ "input",
17095
+ {
17096
+ ...props,
17097
+ type: "text",
17098
+ value: "",
17099
+ readOnly: true,
17100
+ tabIndex: -1,
17101
+ "aria-hidden": "true",
17102
+ style: { ...style, visibility: "hidden" }
17103
+ }
17104
+ );
17105
+ const PlacedInputChip = ({
17106
+ widgetId,
17107
+ value,
17108
+ className = "",
17109
+ onRemove
17110
+ }) => {
17044
17111
  const { droppedSources } = useExerciseDnd();
17045
17112
  const locked = useExerciseLocked();
17046
17113
  const sourceOptionId = droppedSources.get(widgetId);
@@ -17064,13 +17131,41 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
17064
17131
  if (!locked && e.detail !== 0) onRemove();
17065
17132
  },
17066
17133
  title: locked ? void 0 : "Drag to move, or click to remove",
17067
- style: { touchAction: "none", cursor: locked ? "default" : "grab" },
17134
+ style: {
17135
+ ...OVERLAY_STYLE,
17136
+ touchAction: "none",
17137
+ cursor: locked ? "default" : "grab"
17138
+ },
17068
17139
  ...listeners,
17069
17140
  ...attributes,
17070
17141
  children: value
17071
17142
  }
17072
17143
  );
17073
17144
  };
17145
+ function getMaxInputLength(exercise, inputKey) {
17146
+ const widget = exercise.widgets[inputKey];
17147
+ if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
17148
+ return widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : widget.length;
17149
+ }
17150
+ function getInputElementId(key2) {
17151
+ return `${key2}-input`;
17152
+ }
17153
+ function getTextInputs(container) {
17154
+ return Array.from(
17155
+ container.querySelectorAll(
17156
+ 'input[type="text"], input[type="number"], textarea, select'
17157
+ )
17158
+ );
17159
+ }
17160
+ function canTakeInput(el) {
17161
+ return !el.readOnly && !el.disabled;
17162
+ }
17163
+ function findNextInput(container, currentKey) {
17164
+ const inputs = getTextInputs(container);
17165
+ const currentIndex = inputs.findIndex((el) => el.id === getInputElementId(currentKey));
17166
+ if (currentIndex === -1) return null;
17167
+ return inputs.slice(currentIndex + 1).find(canTakeInput) ?? null;
17168
+ }
17074
17169
  const InlineInput = ({
17075
17170
  widgetId,
17076
17171
  value,
@@ -17079,6 +17174,7 @@ const InlineInput = ({
17079
17174
  inputType,
17080
17175
  length,
17081
17176
  digits,
17177
+ width,
17082
17178
  placeholder,
17083
17179
  disabled = false,
17084
17180
  onChange,
@@ -17092,8 +17188,8 @@ const InlineInput = ({
17092
17188
  const handleDrop = React.useCallback((item) => {
17093
17189
  return applyDropToInput(widgetId, item, onChange);
17094
17190
  }, [onChange, widgetId]);
17095
- const expectedLength = inputType === "numeric" ? digits : length;
17096
- const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
17191
+ const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
17192
+ const dynamicWidth = `${(width ?? expectedLength ?? 5) * 0.8 + 1}em`;
17097
17193
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
17098
17194
  const disabledClass = disabled ? " inline-input--disabled" : "";
17099
17195
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -17106,21 +17202,31 @@ const InlineInput = ({
17106
17202
  className: "inline-widget inline-input-droppable",
17107
17203
  children: isDropped && value !== "" ? (
17108
17204
  // Show as chip only when value came from drag-drop
17109
- /* @__PURE__ */ jsxRuntime.jsx(
17110
- PlacedInputChip,
17111
- {
17112
- widgetId,
17113
- value,
17114
- className: statusClass.trim(),
17115
- onRemove: () => handleChange("", false)
17116
- }
17117
- )
17205
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17206
+ /* @__PURE__ */ jsxRuntime.jsx(
17207
+ SlotSizer,
17208
+ {
17209
+ className: `inline-input${disabledClass}`,
17210
+ style: { width: dynamicWidth },
17211
+ placeholder
17212
+ }
17213
+ ),
17214
+ /* @__PURE__ */ jsxRuntime.jsx(
17215
+ PlacedInputChip,
17216
+ {
17217
+ widgetId,
17218
+ value,
17219
+ className: statusClass.trim(),
17220
+ onRemove: () => handleChange("", false)
17221
+ }
17222
+ )
17223
+ ] })
17118
17224
  ) : (
17119
17225
  // Regular input for typing
17120
17226
  /* @__PURE__ */ jsxRuntime.jsx(
17121
17227
  "input",
17122
17228
  {
17123
- id: widgetId + "-input",
17229
+ id: getInputElementId(widgetId),
17124
17230
  type: "text",
17125
17231
  className: `inline-input${statusClass}${disabledClass}`,
17126
17232
  value,
@@ -17140,15 +17246,20 @@ const InlineInput = ({
17140
17246
  }
17141
17247
  );
17142
17248
  };
17143
- const SingleDragOption = ({
17249
+ const SingleDragOption = (props) => {
17250
+ const { exerciseKey } = useExerciseDnd();
17251
+ return /* @__PURE__ */ jsxRuntime.jsx(DragOptionChip, { ...props }, exerciseKey ?? "");
17252
+ };
17253
+ const DragOptionChip = ({
17144
17254
  optionId,
17145
17255
  value,
17146
17256
  onOptionClick,
17147
17257
  className = ""
17148
17258
  }) => {
17149
- const { usedOptionIds } = useExerciseDnd();
17259
+ const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
17150
17260
  const locked = useExerciseLocked();
17151
17261
  const isUsed = usedOptionIds.has(optionId);
17262
+ const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
17152
17263
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(
17153
17264
  Draggable,
17154
17265
  {
@@ -17160,7 +17271,11 @@ const SingleDragOption = ({
17160
17271
  children: /* @__PURE__ */ jsxRuntime.jsx(
17161
17272
  "button",
17162
17273
  {
17163
- className: "drag-option",
17274
+ className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
17275
+ onAnimationStart: () => {
17276
+ if (hinting) playHintSound();
17277
+ },
17278
+ onAnimationEnd: onHintEnd,
17164
17279
  onClick: () => {
17165
17280
  if (!locked) onOptionClick?.(value);
17166
17281
  },
@@ -17289,23 +17404,23 @@ const NumericKeypad = ({
17289
17404
  playSound("keypad-press");
17290
17405
  onChange(value.slice(0, -1));
17291
17406
  };
17292
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
17293
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
17294
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
17295
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
17296
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
17297
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
17298
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
17299
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
17300
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
17301
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
17302
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
17303
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
17304
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
17305
- includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
17306
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
17307
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
17308
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
17407
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
17408
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
17409
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
17410
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
17411
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
17412
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
17413
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
17414
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
17415
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
17416
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
17417
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
17418
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
17419
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
17420
+ includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
17421
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
17422
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
17423
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
17309
17424
  ] });
17310
17425
  };
17311
17426
  const FractionInput = ({
@@ -17345,18 +17460,28 @@ const FractionInput = ({
17345
17460
  onDrop: handleDrop(subKey),
17346
17461
  disabled: locked,
17347
17462
  className: "fraction-slot-droppable",
17348
- children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
17349
- PlacedInputChip,
17350
- {
17351
- widgetId: subKey,
17352
- value: currentValue,
17353
- className: `fraction-chip${slotClass}`,
17354
- onRemove: () => onChange(subKey, "", false)
17355
- }
17356
- ) : /* @__PURE__ */ jsxRuntime.jsx(
17463
+ children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17464
+ /* @__PURE__ */ jsxRuntime.jsx(
17465
+ SlotSizer,
17466
+ {
17467
+ className: "inline-input fraction-input",
17468
+ size: placeholder ? placeholder.length : void 0,
17469
+ placeholder
17470
+ }
17471
+ ),
17472
+ /* @__PURE__ */ jsxRuntime.jsx(
17473
+ PlacedInputChip,
17474
+ {
17475
+ widgetId: subKey,
17476
+ value: currentValue,
17477
+ className: `fraction-chip${slotClass}`,
17478
+ onRemove: () => onChange(subKey, "", false)
17479
+ }
17480
+ )
17481
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
17357
17482
  "input",
17358
17483
  {
17359
- id: `${subKey}-input`,
17484
+ id: getInputElementId(subKey),
17360
17485
  type: "text",
17361
17486
  className: `inline-input fraction-input${slotClass}`,
17362
17487
  value: currentValue,
@@ -18934,11 +19059,12 @@ const RadioOptions = ({
18934
19059
  };
18935
19060
  const Btn = ({
18936
19061
  label,
19062
+ dataKey = label,
18937
19063
  extraClass = "keypad-btn-digit",
18938
19064
  onClick
18939
19065
  }) => {
18940
19066
  const locked = useExerciseLocked();
18941
- return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
19067
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, "data-key": dataKey, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
18942
19068
  };
18943
19069
  const DiamondKeypad = ({ value, onChange }) => {
18944
19070
  const playSound = useSound();
@@ -18950,7 +19076,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18950
19076
  playSound("keypad-press");
18951
19077
  onChange(value.slice(0, -1));
18952
19078
  };
18953
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad", children: [
19079
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad", "data-tutorial": "keypad", children: [
18954
19080
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad-row", children: [
18955
19081
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "1", onClick: () => append("1") }),
18956
19082
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "2", onClick: () => append("2") }),
@@ -18969,7 +19095,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18969
19095
  ] }),
18970
19096
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad-row", children: [
18971
19097
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
18972
- /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
19098
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "⌫", dataKey: "backspace", extraClass: "keypad-btn-backspace", onClick: backspace })
18973
19099
  ] })
18974
19100
  ] });
18975
19101
  };
@@ -19575,7 +19701,7 @@ function isGeometryWidget(widget) {
19575
19701
  return widget.type === JSON_TYPES.GEOMETRY;
19576
19702
  }
19577
19703
  const GeometryWidget = React.lazy(async () => ({
19578
- default: (await Promise.resolve().then(() => require("./GeometryWidget-DgPfqyQv.js"))).GeometryWidget
19704
+ default: (await Promise.resolve().then(() => require("./GeometryWidget-BTPjXJz-.js"))).GeometryWidget
19579
19705
  }));
19580
19706
  const WidgetRenderer = ({
19581
19707
  widget,
@@ -19603,8 +19729,9 @@ const WidgetRenderer = ({
19603
19729
  isDropped,
19604
19730
  validationStatus,
19605
19731
  inputType: widget.inputType,
19606
- length: widget.inputType === "string" ? widget.length : void 0,
19607
- digits: widget.inputType === "numeric" ? widget.digits : void 0,
19732
+ length: widget.inputType === INPUT_TYPES.STRING ? widget.length : void 0,
19733
+ digits: widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : void 0,
19734
+ width: widget.width,
19608
19735
  placeholder: widget.placeholder,
19609
19736
  disabled: widget.disabled,
19610
19737
  onChange,
@@ -19975,6 +20102,13 @@ const EPSILON = 10 ** -INTERNAL_PRECISION;
19975
20102
  const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
19976
20103
  const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
19977
20104
  const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
20105
+ const NUMBER_LINE_AXIS_ENDS = {
20106
+ "arrow-right": { left: "tick", right: "arrow" },
20107
+ "arrow-left": { left: "arrow", right: "tick" },
20108
+ "arrow-both": { left: "arrow", right: "arrow" },
20109
+ "line": { left: "line", right: "line" },
20110
+ "line-left": { left: "line", right: "arrow" }
20111
+ };
19978
20112
  const toPercent = (value, min, max) => {
19979
20113
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
19980
20114
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -21022,7 +21156,7 @@ const NumberLineRenderer = ({
21022
21156
  max,
21023
21157
  step,
21024
21158
  minorTicks = 0,
21025
- style: lineStyle = "arrow-right",
21159
+ style: lineStyle = DEFAULT_NUMBER_LINE_STYLE,
21026
21160
  labelVisibility,
21027
21161
  highlights,
21028
21162
  arrows,
@@ -21086,8 +21220,9 @@ const NumberLineRenderer = ({
21086
21220
  () => getMinorTickValues(majorTicks, minorTicks),
21087
21221
  [majorTicks, minorTicks]
21088
21222
  );
21089
- const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
21090
- const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
21223
+ const axisEnds = NUMBER_LINE_AXIS_ENDS[lineStyle] ?? NUMBER_LINE_AXIS_ENDS[DEFAULT_NUMBER_LINE_STYLE];
21224
+ const showArrowLeft = axisEnds.left === "arrow";
21225
+ const showArrowRight = axisEnds.right === "arrow";
21091
21226
  const hasDragOptions = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
21092
21227
  const isAxisDragMode = hasDragOptions && item.dragMode === "axis";
21093
21228
  const inputTickSet = React.useMemo(() => {
@@ -21135,7 +21270,7 @@ const NumberLineRenderer = ({
21135
21270
  return /* @__PURE__ */ jsxRuntime.jsxs(
21136
21271
  "div",
21137
21272
  {
21138
- className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
21273
+ 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}` : ""}`,
21139
21274
  style: {
21140
21275
  [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
21141
21276
  ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
@@ -21508,6 +21643,35 @@ function groupLineItems(items) {
21508
21643
  }
21509
21644
  return groups;
21510
21645
  }
21646
+ function placeTableCells(rows, cols) {
21647
+ const placements = /* @__PURE__ */ new Map();
21648
+ const occupied = rows.map(() => []);
21649
+ const totalRows = rows.length;
21650
+ rows.forEach((row, rowIndex) => {
21651
+ const taken = occupied[rowIndex] ?? [];
21652
+ let col = 0;
21653
+ for (const cell of row.cells) {
21654
+ while (taken[col]) col++;
21655
+ const colspan = cell.colspan || 1;
21656
+ const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
21657
+ for (let r = rowIndex; r < rowIndex + rowspan; r++) {
21658
+ const line = occupied[r];
21659
+ if (!line) continue;
21660
+ for (let c = col; c < col + colspan; c++) line[c] = true;
21661
+ }
21662
+ placements.set(cell.id, {
21663
+ column: col + 1,
21664
+ row: rowIndex + 1,
21665
+ colspan,
21666
+ rowspan,
21667
+ isLastColumn: col + colspan >= cols,
21668
+ isLastRow: rowIndex + rowspan >= totalRows
21669
+ });
21670
+ col += colspan;
21671
+ }
21672
+ });
21673
+ return placements;
21674
+ }
21511
21675
  const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
21512
21676
  const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
21513
21677
  const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
@@ -21631,21 +21795,28 @@ const LayoutRenderer = ({
21631
21795
  subItem.id
21632
21796
  )) });
21633
21797
  case JSON_TYPES.TABLE: {
21634
- const totalRows = item.rows.length;
21798
+ const placements = placeTableCells(item.rows, item.cols);
21635
21799
  const tableStyle = {
21636
- "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR
21800
+ "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR,
21801
+ "--exercise-table-cols": item.cols
21637
21802
  };
21638
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: item.rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "exercise-table-row", style: { backgroundColor: row.backgroundColor }, children: row.cells.map((cell) => {
21639
- const rowspan = cell.rowspan || 1;
21640
- const isLastRow = rowIndex + rowspan - 1 >= totalRows - 1;
21803
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table", role: "table", children: item.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-row", role: "row", children: row.cells.map((cell) => {
21804
+ const placement = placements.get(cell.id);
21805
+ const modifiers = [
21806
+ placement?.isLastRow ? " exercise-table-cell--last-row" : "",
21807
+ placement?.isLastColumn ? " exercise-table-cell--last-col" : ""
21808
+ ].join("");
21641
21809
  return /* @__PURE__ */ jsxRuntime.jsx(
21642
- "td",
21810
+ "div",
21643
21811
  {
21644
- className: `exercise-table-cell${isLastRow ? " exercise-table-cell--last-row" : ""}`,
21645
- colSpan: cell.colspan || 1,
21646
- rowSpan: rowspan,
21647
- style: { backgroundColor: cell.backgroundColor },
21648
- children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
21812
+ className: `exercise-table-cell${modifiers}`,
21813
+ role: "cell",
21814
+ style: {
21815
+ backgroundColor: cell.backgroundColor ?? row.backgroundColor,
21816
+ gridColumn: placement && `${placement.column} / span ${placement.colspan}`,
21817
+ gridRow: placement && `${placement.row} / span ${placement.rowspan}`
21818
+ },
21819
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-cell-content", children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
21649
21820
  LayoutRenderer,
21650
21821
  {
21651
21822
  item: subItem,
@@ -21658,11 +21829,11 @@ const LayoutRenderer = ({
21658
21829
  resetKey
21659
21830
  },
21660
21831
  subItem.id
21661
- ))
21832
+ )) })
21662
21833
  },
21663
21834
  cell.id
21664
21835
  );
21665
- }) }, row.id)) }) }) });
21836
+ }) }, row.id)) }) });
21666
21837
  }
21667
21838
  case JSON_TYPES.NUMBER_LINE:
21668
21839
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -21832,6 +22003,394 @@ const LayoutRenderer = ({
21832
22003
  return null;
21833
22004
  }
21834
22005
  };
22006
+ function LionHand({ className }) {
22007
+ const id = React.useId();
22008
+ const shadowId = `${id}-shadow`;
22009
+ const maskId = `${id}-mask`;
22010
+ return /* @__PURE__ */ jsxRuntime.jsxs(
22011
+ "svg",
22012
+ {
22013
+ "aria-hidden": "true",
22014
+ focusable: "false",
22015
+ width: "75",
22016
+ height: "78",
22017
+ viewBox: "0 0 75 78",
22018
+ fill: "none",
22019
+ xmlns: "http://www.w3.org/2000/svg",
22020
+ className,
22021
+ children: [
22022
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { filter: `url(#${shadowId})`, children: [
22023
+ /* @__PURE__ */ jsxRuntime.jsx(
22024
+ "mask",
22025
+ {
22026
+ id: maskId,
22027
+ style: { maskType: "alpha" },
22028
+ maskUnits: "userSpaceOnUse",
22029
+ x: "9",
22030
+ y: "19",
22031
+ width: "49",
22032
+ height: "42",
22033
+ children: /* @__PURE__ */ jsxRuntime.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" })
22034
+ }
22035
+ ),
22036
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { mask: `url(#${maskId})`, children: [
22037
+ /* @__PURE__ */ jsxRuntime.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" }),
22038
+ /* @__PURE__ */ jsxRuntime.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" }),
22039
+ /* @__PURE__ */ jsxRuntime.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" }),
22040
+ /* @__PURE__ */ jsxRuntime.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" })
22041
+ ] })
22042
+ ] }),
22043
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("filter", { id: shadowId, x: "-1.99995", y: "0", width: "78.3658", height: "81.8518", filterUnits: "userSpaceOnUse", colorInterpolationFilters: "sRGB", children: [
22044
+ /* @__PURE__ */ jsxRuntime.jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22045
+ /* @__PURE__ */ jsxRuntime.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" }),
22046
+ /* @__PURE__ */ jsxRuntime.jsx("feOffset", { dy: "2" }),
22047
+ /* @__PURE__ */ jsxRuntime.jsx("feGaussianBlur", { stdDeviation: "1" }),
22048
+ /* @__PURE__ */ jsxRuntime.jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22049
+ /* @__PURE__ */ jsxRuntime.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" }),
22050
+ /* @__PURE__ */ jsxRuntime.jsx("feBlend", { mode: "normal", in2: "BackgroundImageFix", result: "effect1_dropShadow" }),
22051
+ /* @__PURE__ */ jsxRuntime.jsx("feBlend", { mode: "normal", in: "SourceGraphic", in2: "effect1_dropShadow", result: "shape" })
22052
+ ] }) })
22053
+ ]
22054
+ }
22055
+ );
22056
+ }
22057
+ const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
22058
+ const CYCLE_MS = 1400;
22059
+ const PRESS_AT_MS = 650;
22060
+ const PRESS_HOLD_MS = 220;
22061
+ const HOLE_PAD = 10;
22062
+ const HOLE_RADIUS = 18;
22063
+ const HAND_W = 72;
22064
+ const HAND_H = 75;
22065
+ const BUBBLE_W = 240;
22066
+ const BUBBLE_GAP = 14;
22067
+ const EDGE = 8;
22068
+ const DEFAULT_NEXT_LABEL = "فهمت";
22069
+ function resolve(t) {
22070
+ return (typeof t === "function" ? t() : t) ?? null;
22071
+ }
22072
+ function toRect(t) {
22073
+ if (!t) return null;
22074
+ const r = t instanceof Element ? t.getBoundingClientRect() : t;
22075
+ if (r.width <= 0 || r.height <= 0) return null;
22076
+ return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
22077
+ }
22078
+ const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22079
+ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22080
+ const { steps } = tutorial;
22081
+ const [stepIndex, setStepIndex] = React.useState(
22082
+ () => Math.min(Math.max(0, Math.floor(options.startStep ?? 0)), steps.length - 1)
22083
+ );
22084
+ const step = steps[stepIndex];
22085
+ const optionsRef = React.useRef(options);
22086
+ optionsRef.current = options;
22087
+ const ctx = { root, json, submitTarget: resolve(options.submitTarget) };
22088
+ const ctxRef = React.useRef(ctx);
22089
+ ctxRef.current = ctx;
22090
+ const [layout, setLayout] = React.useState(null);
22091
+ React.useEffect(() => {
22092
+ let raf = 0;
22093
+ let last = "";
22094
+ const tick = () => {
22095
+ const c = ctxRef.current;
22096
+ const next2 = {
22097
+ spotlight: step.spotlight(c).map(toRect),
22098
+ taps: step.taps(c).map(toRect),
22099
+ anchor: toRect(resolve(optionsRef.current.bubbleAnchor)),
22100
+ root: toRect(c.root),
22101
+ vw: window.innerWidth,
22102
+ vh: window.innerHeight
22103
+ };
22104
+ const key2 = JSON.stringify(next2);
22105
+ if (key2 !== last) {
22106
+ last = key2;
22107
+ setLayout(next2);
22108
+ }
22109
+ raf = requestAnimationFrame(tick);
22110
+ };
22111
+ tick();
22112
+ return () => cancelAnimationFrame(raf);
22113
+ }, [step]);
22114
+ const [tapIndex, setTapIndex] = React.useState(0);
22115
+ const [tapCount, setTapCount] = React.useState(0);
22116
+ const [shownStep, setShownStep] = React.useState(stepIndex);
22117
+ if (shownStep !== stepIndex) {
22118
+ setShownStep(stepIndex);
22119
+ setTapIndex(0);
22120
+ setTapCount(0);
22121
+ }
22122
+ React.useEffect(() => {
22123
+ let release;
22124
+ const press = setTimeout(() => {
22125
+ setTapCount((n) => n + 1);
22126
+ const targets = step.taps(ctxRef.current);
22127
+ const el = targets[tapIndex % Math.max(1, targets.length)];
22128
+ if (el instanceof Element) {
22129
+ el.classList.add("met-tutorial-pressed");
22130
+ release = setTimeout(() => el.classList.remove("met-tutorial-pressed"), PRESS_HOLD_MS);
22131
+ }
22132
+ }, PRESS_AT_MS);
22133
+ const advance = setTimeout(() => setTapIndex((i) => i + 1), CYCLE_MS);
22134
+ return () => {
22135
+ clearTimeout(press);
22136
+ clearTimeout(advance);
22137
+ if (release) clearTimeout(release);
22138
+ root.querySelectorAll(".met-tutorial-pressed").forEach((el) => el.classList.remove("met-tutorial-pressed"));
22139
+ };
22140
+ }, [step, tapIndex, root]);
22141
+ const text = options.texts?.[step.id] ?? step.text;
22142
+ const textRef = React.useRef(text);
22143
+ textRef.current = text;
22144
+ React.useEffect(() => {
22145
+ optionsRef.current.onStep?.(stepIndex, step.id);
22146
+ optionsRef.current.onSpeak?.(step.id, textRef.current);
22147
+ }, [stepIndex, step]);
22148
+ const isLast = stepIndex === steps.length - 1;
22149
+ const next = () => {
22150
+ if (isLast) onClose(true);
22151
+ else setStepIndex((i) => i + 1);
22152
+ };
22153
+ const overlayRef = React.useRef(null);
22154
+ const nextRef = React.useRef(null);
22155
+ const onCloseRef = React.useRef(onClose);
22156
+ onCloseRef.current = onClose;
22157
+ React.useEffect(() => {
22158
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22159
+ previous?.blur();
22160
+ const onKeyDown = (e) => {
22161
+ const overlay = overlayRef.current;
22162
+ if (e.key === "Escape") {
22163
+ e.preventDefault();
22164
+ e.stopPropagation();
22165
+ onCloseRef.current(false);
22166
+ return;
22167
+ }
22168
+ if (e.key === "Tab" && overlay) {
22169
+ e.preventDefault();
22170
+ const buttons = Array.from(overlay.querySelectorAll("button"));
22171
+ if (buttons.length === 0) return;
22172
+ const at = buttons.indexOf(document.activeElement);
22173
+ const to = (at + (e.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
22174
+ buttons[to]?.focus();
22175
+ return;
22176
+ }
22177
+ if (overlay && e.target instanceof Node && overlay.contains(e.target)) return;
22178
+ e.preventDefault();
22179
+ e.stopPropagation();
22180
+ };
22181
+ document.addEventListener("keydown", onKeyDown, true);
22182
+ return () => {
22183
+ document.removeEventListener("keydown", onKeyDown, true);
22184
+ if (previous?.isConnected) previous.focus();
22185
+ };
22186
+ }, []);
22187
+ const bubbleShown = layout !== null;
22188
+ React.useEffect(() => {
22189
+ nextRef.current?.focus({ focusVisible: false });
22190
+ }, [stepIndex, bubbleShown]);
22191
+ const maskId = `met-tutorial-mask-${React.useId()}`;
22192
+ if (typeof document === "undefined") return null;
22193
+ const holes = (layout?.spotlight ?? []).filter((r) => r !== null);
22194
+ const tapTarget = layout?.taps.length ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22195
+ const Scene = step.Scene;
22196
+ return reactDom.createPortal(
22197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: overlayRef, className: "met-tutorial", role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22198
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "met-tutorial-dim", "aria-hidden": "true", children: [
22199
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("mask", { id: maskId, children: [
22200
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", fill: "white" }),
22201
+ holes.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx(
22202
+ "rect",
22203
+ {
22204
+ x: r.x - HOLE_PAD,
22205
+ y: r.y - HOLE_PAD,
22206
+ width: r.width + HOLE_PAD * 2,
22207
+ height: r.height + HOLE_PAD * 2,
22208
+ rx: HOLE_RADIUS,
22209
+ fill: "black"
22210
+ },
22211
+ i
22212
+ ))
22213
+ ] }) }),
22214
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", mask: `url(#${maskId})` })
22215
+ ] }),
22216
+ holes.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx(
22217
+ "div",
22218
+ {
22219
+ className: "met-tutorial-glow",
22220
+ style: {
22221
+ left: r.x - HOLE_PAD,
22222
+ top: r.y - HOLE_PAD,
22223
+ width: r.width + HOLE_PAD * 2,
22224
+ height: r.height + HOLE_PAD * 2,
22225
+ borderRadius: HOLE_RADIUS
22226
+ }
22227
+ },
22228
+ i
22229
+ )),
22230
+ Scene && layout && /* @__PURE__ */ jsxRuntime.jsx(Scene, { ctx, spotlight: layout.spotlight, taps: layout.taps, tapCount }),
22231
+ tapTarget && /* @__PURE__ */ jsxRuntime.jsx(Hand, { target: tapTarget, tapKey: `${stepIndex}-${tapIndex}` }),
22232
+ layout && /* @__PURE__ */ jsxRuntime.jsx(
22233
+ Bubble,
22234
+ {
22235
+ layout,
22236
+ text,
22237
+ stepIndex,
22238
+ stepCount: steps.length,
22239
+ nextLabel: options.nextLabel ?? DEFAULT_NEXT_LABEL,
22240
+ onSpeak: options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22241
+ onNext: next,
22242
+ nextRef
22243
+ }
22244
+ )
22245
+ ] }),
22246
+ document.body
22247
+ );
22248
+ };
22249
+ const Hand = ({ target, tapKey }) => {
22250
+ const c = centerOf(target);
22251
+ return /* @__PURE__ */ jsxRuntime.jsx(
22252
+ "div",
22253
+ {
22254
+ className: "met-tutorial-hand",
22255
+ style: {
22256
+ width: HAND_W,
22257
+ height: HAND_H,
22258
+ transform: `translate(${c.x - TUTORIAL_HAND_TIP.x * HAND_W}px, ${c.y - TUTORIAL_HAND_TIP.y * HAND_H}px)`
22259
+ },
22260
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "met-tutorial-hand-press", children: [
22261
+ /* @__PURE__ */ jsxRuntime.jsx(
22262
+ "span",
22263
+ {
22264
+ className: "met-tutorial-ripple",
22265
+ style: { left: TUTORIAL_HAND_TIP.x * HAND_W, top: TUTORIAL_HAND_TIP.y * HAND_H }
22266
+ }
22267
+ ),
22268
+ /* @__PURE__ */ jsxRuntime.jsx(LionHand, {})
22269
+ ] }, tapKey)
22270
+ }
22271
+ );
22272
+ };
22273
+ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext, nextRef }) => {
22274
+ const { anchor, root, vw, vh } = layout;
22275
+ const clampLeft = (x) => Math.min(Math.max(EDGE, x), Math.max(EDGE, vw - BUBBLE_W - EDGE));
22276
+ 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 };
22277
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `met-tutorial-bubble${anchor ? " met-tutorial-bubble--anchored" : ""}`, style, children: [
22278
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "met-tutorial-text", dir: "auto", children: text }),
22279
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "met-tutorial-actions", children: [
22280
+ onSpeak && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "met-tutorial-speak", "aria-label": "🔊", onClick: onSpeak, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-hidden": "true", children: [
22281
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9v6h4l5 4V5L8 9H4z", fill: "currentColor" }),
22282
+ /* @__PURE__ */ jsxRuntime.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" })
22283
+ ] }) }),
22284
+ /* @__PURE__ */ jsxRuntime.jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22285
+ ] }),
22286
+ stepCount > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "met-tutorial-dots", "aria-hidden": "true", children: Array.from({ length: stepCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: i === stepIndex ? "is-current" : i < stepIndex ? "is-done" : "" }, i)) })
22287
+ ] });
22288
+ };
22289
+ const DEMO_KEYS = ["2", "5", "8"];
22290
+ const DEFAULT_SLOTS = 3;
22291
+ const MAX_SLOTS = 6;
22292
+ const STAND_IN_CHECK_SIZE = 52;
22293
+ const STAND_IN_CHECK_GAP = 20;
22294
+ function keypad(ctx) {
22295
+ return ctx.root.querySelector('[data-tutorial="keypad"]');
22296
+ }
22297
+ function key(ctx, k) {
22298
+ return ctx.root.querySelector(`[data-tutorial="keypad"] [data-key="${k}"]`);
22299
+ }
22300
+ function answerBox(ctx) {
22301
+ const inputs = getTextInputs(ctx.root);
22302
+ return inputs.find(canTakeInput) ?? inputs[0] ?? null;
22303
+ }
22304
+ function answerSlots(ctx) {
22305
+ const id = answerBox(ctx)?.id.replace(/-input$/, "");
22306
+ const max = id ? getMaxInputLength(ctx.json, id) : void 0;
22307
+ return Math.min(MAX_SLOTS, Math.max(1, max ?? DEFAULT_SLOTS));
22308
+ }
22309
+ function checkTarget(ctx) {
22310
+ if (ctx.submitTarget) return ctx.submitTarget;
22311
+ const box = answerBox(ctx)?.getBoundingClientRect();
22312
+ if (!box) return null;
22313
+ return new DOMRect(
22314
+ box.right + STAND_IN_CHECK_GAP,
22315
+ box.top + box.height / 2 - STAND_IN_CHECK_SIZE / 2,
22316
+ STAND_IN_CHECK_SIZE,
22317
+ STAND_IN_CHECK_SIZE
22318
+ );
22319
+ }
22320
+ const demoKeys = (ctx) => DEMO_KEYS.map((k) => key(ctx, k));
22321
+ const StandInFill = ({ box, slots, filled }) => /* @__PURE__ */ jsxRuntime.jsx(
22322
+ "div",
22323
+ {
22324
+ className: "met-tutorial-fill",
22325
+ style: { left: box.x, top: box.y, width: box.width, height: box.height },
22326
+ children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? "●" : "_" }, i))
22327
+ }
22328
+ );
22329
+ const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22330
+ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22331
+ const box = spotlight[1];
22332
+ if (!box) return null;
22333
+ const slots = answerSlots(ctx);
22334
+ const filled = tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22335
+ const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
22336
+ let arc = null;
22337
+ if (from) {
22338
+ const a = center(from);
22339
+ const b = center(box);
22340
+ const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
22341
+ arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
22342
+ }
22343
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22344
+ arc && /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "met-tutorial-arc", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: arc }) }),
22345
+ arc && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "met-tutorial-hop", style: { offsetPath: `path('${arc}')` } }, tapCount),
22346
+ /* @__PURE__ */ jsxRuntime.jsx(StandInFill, { box, slots, filled })
22347
+ ] });
22348
+ };
22349
+ const CheckScene = ({ ctx, spotlight }) => {
22350
+ const box = spotlight[0];
22351
+ const check = spotlight[1];
22352
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22353
+ box && /* @__PURE__ */ jsxRuntime.jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx) }),
22354
+ check && !ctx.submitTarget && /* @__PURE__ */ jsxRuntime.jsx(
22355
+ "div",
22356
+ {
22357
+ className: "met-tutorial-stand-in-check",
22358
+ style: { left: check.x, top: check.y, width: check.width, height: check.height },
22359
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "✓" })
22360
+ }
22361
+ )
22362
+ ] });
22363
+ };
22364
+ const keypadTutorial = {
22365
+ id: "keypad",
22366
+ appliesTo: (json) => json.widgets[NUMERIC_KEYPAD_WIDGET_ID]?.type === JSON_TYPES.NUMERIC_KEYPAD,
22367
+ steps: [
22368
+ {
22369
+ id: "keypad.tap-number",
22370
+ text: "اضغط على رقم",
22371
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22372
+ taps: demoKeys
22373
+ },
22374
+ {
22375
+ id: "keypad.hop",
22376
+ text: "يظهر الرقم في الخانة",
22377
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22378
+ taps: demoKeys,
22379
+ Scene: HopScene
22380
+ },
22381
+ {
22382
+ id: "keypad.tap-check",
22383
+ text: "اضغط على زر التحقق",
22384
+ spotlight: (ctx) => [answerBox(ctx), checkTarget(ctx)],
22385
+ taps: (ctx) => [checkTarget(ctx)],
22386
+ Scene: CheckScene
22387
+ }
22388
+ ]
22389
+ };
22390
+ const WIDGET_TUTORIALS = [keypadTutorial];
22391
+ function findWidgetTutorial(json) {
22392
+ return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
22393
+ }
21835
22394
  const TOOL_ASPECT_RATIO = {
21836
22395
  ruler: RULER_ASPECT_RATIO,
21837
22396
  protractor: PROTRACTOR_ASPECT_RATIO,
@@ -21870,6 +22429,11 @@ function buildInitialValues(json) {
21870
22429
  function toBaseId(widgetId) {
21871
22430
  return parseFractionKey(widgetId)?.widgetId ?? widgetId;
21872
22431
  }
22432
+ function hasJustFilledBox(json, key2, previous, value) {
22433
+ const maxLength = getMaxInputLength(json, key2);
22434
+ if (maxLength === void 0) return false;
22435
+ return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22436
+ }
21873
22437
  function buildInitialValuesSignature(json) {
21874
22438
  return JSON.stringify({
21875
22439
  widgets: json.widgets,
@@ -21919,6 +22483,27 @@ function buildRevealedValues(json) {
21919
22483
  }
21920
22484
  return revealed;
21921
22485
  }
22486
+ function buildRevealedDropSources(json, revealedValues) {
22487
+ const pool = [];
22488
+ const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
22489
+ if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
22490
+ bank.options.forEach((value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) }));
22491
+ }
22492
+ for (const [widgetId, widget] of Object.entries(json.widgets)) {
22493
+ if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION) pool.push({ optionId: widgetId, value: widget.value });
22494
+ }
22495
+ const sources = /* @__PURE__ */ new Map();
22496
+ if (pool.length === 0) return sources;
22497
+ const used = /* @__PURE__ */ new Set();
22498
+ for (const [inputId, value] of Object.entries(revealedValues)) {
22499
+ if (typeof value !== "string" || value === "") continue;
22500
+ const match = pool.find((o) => !used.has(o.optionId) && normalize(o.value) === normalize(value));
22501
+ if (!match) continue;
22502
+ used.add(match.optionId);
22503
+ sources.set(inputId, match.optionId);
22504
+ }
22505
+ return sources;
22506
+ }
21922
22507
  const ExerciseRenderer = React.forwardRef(({
21923
22508
  json,
21924
22509
  onSubmit,
@@ -21941,6 +22526,8 @@ const ExerciseRenderer = React.forwardRef(({
21941
22526
  const rootRef = React.useRef(null);
21942
22527
  const [resetKey, setResetKey] = React.useState(0);
21943
22528
  const [revealed, setRevealed] = React.useState(false);
22529
+ const tutorial = React.useMemo(() => findWidgetTutorial(json), [json]);
22530
+ const [tutorialRun, setTutorialRun] = React.useState(null);
21944
22531
  const themeStyle = React.useMemo(() => {
21945
22532
  const style = {};
21946
22533
  if (theme?.correct) style["--exercise-correct"] = theme.correct;
@@ -21961,6 +22548,7 @@ const ExerciseRenderer = React.forwardRef(({
21961
22548
  setResetKey((k) => k + 1);
21962
22549
  setLoadedValuesSignature(initialValuesSignature);
21963
22550
  touchedRef.current = /* @__PURE__ */ new Set();
22551
+ setTutorialRun(null);
21964
22552
  }, [json, initialValuesSignature, loadedValuesSignature]);
21965
22553
  const locked = validationDetails !== null || externallyLocked;
21966
22554
  const lastResultRef = React.useRef(null);
@@ -22044,7 +22632,7 @@ const ExerciseRenderer = React.forwardRef(({
22044
22632
  const next = { ...valuesRef.current, ...revealedValues };
22045
22633
  valuesRef.current = next;
22046
22634
  setValues(next);
22047
- setDroppedSources(/* @__PURE__ */ new Map());
22635
+ setDroppedSources(buildRevealedDropSources(json, revealedValues));
22048
22636
  setValidationDetails(details);
22049
22637
  setRevealed(true);
22050
22638
  setResetKey((k) => k + 1);
@@ -22052,20 +22640,29 @@ const ExerciseRenderer = React.forwardRef(({
22052
22640
  focusFirstInput: () => {
22053
22641
  const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
22054
22642
  if (!container) return;
22055
- const candidates = Array.from(
22056
- container.querySelectorAll('input[type="text"], input[type="number"], textarea, select')
22057
- );
22058
- const canTakeInput = (el) => !el.readOnly && !el.disabled;
22643
+ const candidates = getTextInputs(container);
22059
22644
  const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
22060
22645
  target?.focus();
22061
- }
22062
- }), [json, values, onSubmit, playSound, locked, initialValuesSignature]);
22646
+ },
22647
+ hasTutorial: () => tutorial !== null,
22648
+ startTutorial: (options = {}) => {
22649
+ if (!tutorial || !rootRef.current) return false;
22650
+ setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
22651
+ return true;
22652
+ },
22653
+ stopTutorial: () => setTutorialRun(null)
22654
+ }), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
22655
+ const focusNextInput = (currentKey) => {
22656
+ if (!rootRef.current) return;
22657
+ findNextInput(rootRef.current, currentKey)?.focus();
22658
+ };
22063
22659
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
22064
22660
  if (locked) return;
22065
22661
  if (!silent) {
22066
22662
  setRevealed(false);
22067
22663
  touchedRef.current.add(toBaseId(widgetId));
22068
22664
  }
22665
+ const previous = valuesRef.current[widgetId];
22069
22666
  const next = { ...valuesRef.current, [widgetId]: value };
22070
22667
  valuesRef.current = next;
22071
22668
  setValues(next);
@@ -22079,6 +22676,9 @@ const ExerciseRenderer = React.forwardRef(({
22079
22676
  return next2;
22080
22677
  });
22081
22678
  onChange?.(next);
22679
+ if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22680
+ focusNextInput(widgetId);
22681
+ }
22082
22682
  };
22083
22683
  const handleSubmit = () => {
22084
22684
  runSubmit();
@@ -22193,7 +22793,7 @@ const ExerciseRenderer = React.forwardRef(({
22193
22793
  const rulerWidget = json.widgets[RULER_WIDGET_ID];
22194
22794
  const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
22195
22795
  const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
22196
- const numericKeypadWidget = json.widgets["_numeric_keypad"];
22796
+ const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
22197
22797
  const { tools, toolsMinHeight } = React.useMemo(() => {
22198
22798
  const list = [];
22199
22799
  const PAD = 12;
@@ -22212,7 +22812,7 @@ const ExerciseRenderer = React.forwardRef(({
22212
22812
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
22213
22813
  return { tools: list, toolsMinHeight: maxH };
22214
22814
  }, [rulerWidget, protractorWidget, setSquareWidget]);
22215
- return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22815
+ return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22216
22816
  /* @__PURE__ */ jsxRuntime.jsxs(
22217
22817
  "div",
22218
22818
  {
@@ -22308,7 +22908,21 @@ const ExerciseRenderer = React.forwardRef(({
22308
22908
  }
22309
22909
  }
22310
22910
  ) }),
22311
- showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
22911
+ showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" }),
22912
+ tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsxRuntime.jsx(
22913
+ TutorialOverlay,
22914
+ {
22915
+ tutorial,
22916
+ root: rootRef.current,
22917
+ json,
22918
+ options: tutorialRun.options,
22919
+ onClose: (finished) => {
22920
+ setTutorialRun(null);
22921
+ if (finished) tutorialRun.options.onDone?.();
22922
+ }
22923
+ },
22924
+ tutorialRun.run
22925
+ )
22312
22926
  ] }) }) }) });
22313
22927
  });
22314
22928
  ExerciseRenderer.displayName = "ExerciseRenderer";
@@ -22407,11 +23021,13 @@ exports.GridSymmetryJSONSchema = GridSymmetryJSONSchema;
22407
23021
  exports.GroupItemSchema = GroupItemSchema;
22408
23022
  exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
22409
23023
  exports.HighlightedTextSchema = HighlightedTextSchema;
23024
+ exports.INLINE_INPUT_MAX_WIDTH = INLINE_INPUT_MAX_WIDTH;
22410
23025
  exports.ImageInlineSchema = ImageInlineSchema;
22411
23026
  exports.ImageItemSchema = ImageItemSchema;
22412
23027
  exports.InlineDragOptionSchema = InlineDragOptionSchema;
22413
23028
  exports.InlineInputJSONSchema = InlineInputJSONSchema;
22414
23029
  exports.InlineInputWidgetSchema = InlineInputWidgetSchema;
23030
+ exports.InlineInputWidthSchema = InlineInputWidthSchema;
22415
23031
  exports.InlineNodeSchema = InlineNodeSchema;
22416
23032
  exports.JSON_TYPES = JSON_TYPES;
22417
23033
  exports.LayoutItemSchema = LayoutItemSchema;
@@ -22485,6 +23101,7 @@ exports.VopRowItemSchema = VopRowItemSchema;
22485
23101
  exports.VopRowSchema = VopRowSchema;
22486
23102
  exports.VopSeparatorItemSchema = VopSeparatorItemSchema;
22487
23103
  exports.VopSeparatorSchema = VopSeparatorSchema;
23104
+ exports.WIDGET_TUTORIALS = WIDGET_TUTORIALS;
22488
23105
  exports.WidgetJSONSchema = WidgetJSONSchema;
22489
23106
  exports.WidgetRefSchema = WidgetRefSchema;
22490
23107
  exports.WidgetRenderer = WidgetRenderer;
@@ -22544,6 +23161,7 @@ exports.createVopSeparator = createVopSeparator;
22544
23161
  exports.evaluate = evaluate;
22545
23162
  exports.extractVariables = extractVariables;
22546
23163
  exports.findValidCombination = findValidCombination;
23164
+ exports.findWidgetTutorial = findWidgetTutorial;
22547
23165
  exports.getAnswerableInputs = getAnswerableInputs;
22548
23166
  exports.getFractionKey = getFractionKey;
22549
23167
  exports.getMissingInputs = getMissingInputs;
@@ -22649,4 +23267,4 @@ exports.validateUniqueIds = validateUniqueIds;
22649
23267
  exports.validateVisualFractionCapacity = validateVisualFractionCapacity;
22650
23268
  exports.validateWidgetJSON = validateWidgetJSON;
22651
23269
  exports.validateWidgetJSONSafe = validateWidgetJSONSafe;
22652
- //# sourceMappingURL=index-tEgJE6m2.js.map
23270
+ //# sourceMappingURL=index-C3qDr71I.js.map