@mathscan/math-exercise-engine 1.2.9 → 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.
@@ -100,6 +100,12 @@ const AST_TYPES = {
100
100
  CHART_BAR: "ChartBar",
101
101
  /** A single pictograph item inside a CHART[type="pictograph"] block */
102
102
  CHART_PICTO: "ChartPicto",
103
+ /** A named series (color + legend label) inside a CHART[type="grouped-bar"] block */
104
+ CHART_SERIES: "ChartSeries",
105
+ /** One category cluster inside a CHART[type="grouped-bar"] block */
106
+ CHART_GROUP: "ChartGroup",
107
+ /** A single bar inside a GROUP, referencing a declared SERIES by id */
108
+ CHART_SERIES_BAR: "ChartSeriesBar",
103
109
  // ═══════════════════════════════════════════════════════════════════════════
104
110
  // Tools
105
111
  // ═══════════════════════════════════════════════════════════════════════════
@@ -203,6 +209,10 @@ const JSON_TYPES = {
203
209
  CHART_BAR: "chart-bar",
204
210
  /** A single pictograph item rendered inside a chart */
205
211
  CHART_PICTO: "chart-picto",
212
+ /** A single bar rendered inside a grouped-bar chart's GROUP — distinct from
213
+ * CHART_BAR so the JSON self-identifies which shape it is (has `seriesId`),
214
+ * independent of the widget type used for validation. */
215
+ CHART_SERIES_BAR: "chart-series-bar",
206
216
  // ═══════════════════════════════════════════════════════════════════════════
207
217
  /** Inline input widget (string or numeric) */
208
218
  INLINE_INPUT: "inline-input",
@@ -230,6 +240,7 @@ const JSON_TYPES = {
230
240
  GEOMETRY: "geometry"
231
241
  };
232
242
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
243
+ const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
233
244
  const RULER_WIDGET_ID = "_ruler";
234
245
  const PROTRACTOR_WIDGET_ID = "_protractor";
235
246
  const SET_SQUARE_WIDGET_ID = "_set_square";
@@ -262,7 +273,8 @@ const INPUT_TYPES = {
262
273
  /** Numeric input (digits only) */
263
274
  NUMERIC: "numeric"
264
275
  };
265
- 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";
266
278
  const INTERNAL_PRECISION = 9;
267
279
  const DISPLAY_PRECISION = 2;
268
280
  const SIDE_PAD = 7;
@@ -436,7 +448,7 @@ const MATCHER = createToken({
436
448
  });
437
449
  const GROUP = createToken({
438
450
  name: "GROUP",
439
- pattern: /GROUP/
451
+ pattern: /GROUP(?=\s*[[{])/
440
452
  });
441
453
  const ITEM = createToken({
442
454
  name: "ITEM",
@@ -486,6 +498,18 @@ const PICTO = createToken({
486
498
  name: "PICTO",
487
499
  pattern: /PICTO/
488
500
  });
501
+ const LABEL = createToken({
502
+ name: "LABEL",
503
+ pattern: /LABEL(?=\s*\{)/
504
+ });
505
+ const SERIES = createToken({
506
+ name: "SERIES",
507
+ pattern: /SERIES(?=\s*\[)/
508
+ });
509
+ const SERIES_BAR = createToken({
510
+ name: "SERIES_BAR",
511
+ pattern: /SERIES_BAR(?=\s*\[)/
512
+ });
489
513
  const SEP = createToken({
490
514
  name: "SEP",
491
515
  pattern: /SEP(?![a-zA-Z0-9_])/
@@ -684,6 +708,9 @@ const allTokens = [
684
708
  Y_AXIS,
685
709
  BAR,
686
710
  PICTO,
711
+ LABEL,
712
+ SERIES_BAR,
713
+ SERIES,
687
714
  SEP,
688
715
  CARRY,
689
716
  INPUT,
@@ -1770,22 +1797,15 @@ class MathExerciseParser extends CstParser {
1770
1797
  });
1771
1798
  this.MANY6({
1772
1799
  GATE: () => {
1773
- let k = 1;
1774
- while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
1775
- const t = this.LA(k).tokenType;
1776
- return t === BAR || t === PICTO;
1800
+ const t = this.LA(1).tokenType;
1801
+ return t === BAR || t === PICTO || t === SERIES || t === GROUP;
1777
1802
  },
1778
1803
  DEF: () => {
1779
1804
  this.OR6([
1780
- {
1781
- GATE: () => {
1782
- let k = 1;
1783
- while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
1784
- return this.LA(k).tokenType === BAR;
1785
- },
1786
- ALT: () => this.SUBRULE(this.chartBarItem)
1787
- },
1788
- { ALT: () => this.SUBRULE(this.chartPictoItem) }
1805
+ { GATE: () => this.LA(1).tokenType === BAR, ALT: () => this.SUBRULE(this.chartBarItem) },
1806
+ { GATE: () => this.LA(1).tokenType === PICTO, ALT: () => this.SUBRULE(this.chartPictoItem) },
1807
+ { GATE: () => this.LA(1).tokenType === SERIES, ALT: () => this.SUBRULE(this.chartSeriesItem) },
1808
+ { ALT: () => this.SUBRULE(this.chartGroupItem) }
1789
1809
  ]);
1790
1810
  }
1791
1811
  });
@@ -1906,6 +1926,92 @@ class MathExerciseParser extends CstParser {
1906
1926
  this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1907
1927
  });
1908
1928
  });
1929
+ this.chartSeriesItem = this.RULE("chartSeriesItem", () => {
1930
+ this.MANY(() => {
1931
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1932
+ });
1933
+ this.CONSUME(SERIES);
1934
+ this.CONSUME(LBracket);
1935
+ this.SUBRULE(this.parameterList);
1936
+ this.CONSUME(RBracket);
1937
+ this.MANY2(() => {
1938
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1939
+ });
1940
+ this.CONSUME(LBrace);
1941
+ this.MANY3(() => {
1942
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
1943
+ });
1944
+ this.MANY4(() => {
1945
+ this.OR4([
1946
+ { ALT: () => this.SUBRULE(this.inlineElement) },
1947
+ { ALT: () => this.CONSUME4(Newline) }
1948
+ ]);
1949
+ });
1950
+ this.CONSUME(RBrace);
1951
+ this.MANY5(() => {
1952
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1953
+ });
1954
+ });
1955
+ this.chartGroupLabel = this.RULE("chartGroupLabel", () => {
1956
+ this.CONSUME(LABEL);
1957
+ this.MANY(() => {
1958
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1959
+ });
1960
+ this.CONSUME(LBrace);
1961
+ this.MANY2(() => {
1962
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1963
+ });
1964
+ this.MANY3(() => {
1965
+ this.OR3([
1966
+ { ALT: () => this.SUBRULE(this.inlineElement) },
1967
+ { ALT: () => this.CONSUME3(Newline) }
1968
+ ]);
1969
+ });
1970
+ this.CONSUME(RBrace);
1971
+ this.MANY4(() => {
1972
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
1973
+ });
1974
+ });
1975
+ this.chartSeriesBarItem = this.RULE("chartSeriesBarItem", () => {
1976
+ this.MANY(() => {
1977
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1978
+ });
1979
+ this.CONSUME(SERIES_BAR);
1980
+ this.CONSUME(LBracket);
1981
+ this.SUBRULE(this.parameterList);
1982
+ this.CONSUME(RBracket);
1983
+ this.MANY2(() => {
1984
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1985
+ });
1986
+ });
1987
+ this.chartGroupItem = this.RULE("chartGroupItem", () => {
1988
+ this.MANY(() => {
1989
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1990
+ });
1991
+ this.CONSUME(GROUP);
1992
+ this.CONSUME(LBracket);
1993
+ this.SUBRULE(this.parameterList);
1994
+ this.CONSUME(RBracket);
1995
+ this.MANY2(() => {
1996
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1997
+ });
1998
+ this.CONSUME(LBrace);
1999
+ this.MANY3(() => {
2000
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
2001
+ });
2002
+ this.MANY4({
2003
+ GATE: () => this.LA(1).tokenType === LABEL,
2004
+ DEF: () => this.SUBRULE(this.chartGroupLabel)
2005
+ });
2006
+ this.MANY5({
2007
+ GATE: () => this.LA(1).tokenType === SERIES_BAR,
2008
+ DEF: () => this.SUBRULE(this.chartSeriesBarItem)
2009
+ });
2010
+ this.CONSUME(RBrace);
2011
+ this.MANY6(() => {
2012
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
2013
+ });
2014
+ });
1909
2015
  this.performSelfAnalysis();
1910
2016
  }
1911
2017
  /** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
@@ -1951,6 +2057,8 @@ const VISUAL_FRACTION_SHAPE = {
1951
2057
  };
1952
2058
  const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
1953
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();
1954
2062
  const FreeTextInlineSchema = z.object({
1955
2063
  type: z.literal(AST_TYPES.FREE_TEXT),
1956
2064
  value: z.string()
@@ -1980,6 +2088,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1980
2088
  name: z.string(),
1981
2089
  inputType: z.literal(INPUT_TYPES.STRING),
1982
2090
  length: z.number().int().positive(),
2091
+ width: InlineInputWidthSchema,
1983
2092
  placeholder: z.string().max(32).optional(),
1984
2093
  disabled: z.boolean().optional(),
1985
2094
  answer: z.union([z.string(), z.number()]).optional()
@@ -1989,6 +2098,7 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
1989
2098
  name: z.string(),
1990
2099
  inputType: z.literal(INPUT_TYPES.NUMERIC),
1991
2100
  digits: z.number().int().positive(),
2101
+ width: InlineInputWidthSchema,
1992
2102
  placeholder: z.string().max(32).optional(),
1993
2103
  disabled: z.boolean().optional(),
1994
2104
  answer: z.union([z.string(), z.number()]).optional()
@@ -2146,7 +2256,7 @@ function createBoldText(value, highlight) {
2146
2256
  highlight
2147
2257
  });
2148
2258
  }
2149
- function createStringInput(name, length, answer, placeholder, disabled) {
2259
+ function createStringInput(name, length, answer, placeholder, disabled, width) {
2150
2260
  return parseOrThrow(
2151
2261
  InlineInputWidgetSchema,
2152
2262
  {
@@ -2154,6 +2264,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2154
2264
  name,
2155
2265
  inputType: INPUT_TYPES.STRING,
2156
2266
  length,
2267
+ width,
2157
2268
  placeholder,
2158
2269
  disabled,
2159
2270
  // A disabled input is never graded, so its answer is dropped.
@@ -2162,7 +2273,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2162
2273
  "STRING_INPUT"
2163
2274
  );
2164
2275
  }
2165
- function createNumericInput(name, digits, answer, placeholder, disabled) {
2276
+ function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2166
2277
  return parseOrThrow(
2167
2278
  InlineInputWidgetSchema,
2168
2279
  {
@@ -2170,6 +2281,7 @@ function createNumericInput(name, digits, answer, placeholder, disabled) {
2170
2281
  name,
2171
2282
  inputType: INPUT_TYPES.NUMERIC,
2172
2283
  digits,
2284
+ width,
2173
2285
  placeholder,
2174
2286
  disabled,
2175
2287
  // A disabled input is never graded, so its answer is dropped.
@@ -2386,7 +2498,7 @@ const NumberLineBlockSchema = z.object({
2386
2498
  errorMap: () => ({
2387
2499
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
2388
2500
  })
2389
- }).optional(),
2501
+ }).default(DEFAULT_NUMBER_LINE_STYLE),
2390
2502
  labelVisibility: z.enum(NUMBER_LINE_LABELS, {
2391
2503
  errorMap: () => ({
2392
2504
  message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
@@ -3065,7 +3177,7 @@ function isSetSquareTool(value) {
3065
3177
  function isToolNode(value) {
3066
3178
  return ToolNodeSchema.safeParse(value).success;
3067
3179
  }
3068
- const CHART_TYPES = ["bar", "pictograph"];
3180
+ const CHART_TYPES = ["bar", "pictograph", "grouped-bar"];
3069
3181
  const CHART_AXIS_DEFAULTS = { min: 0, max: 100, step: 10, visible: true };
3070
3182
  const CHART_MAX_AXIS_TICKS = 200;
3071
3183
  const CHART_MAX_PICTO_SLOTS = 60;
@@ -3145,6 +3257,63 @@ function createChartBar(id, opts) {
3145
3257
  function isChartBar(value) {
3146
3258
  return ChartBarSchema.safeParse(value).success;
3147
3259
  }
3260
+ const ChartSeriesSchema = z.object({
3261
+ type: z.literal(AST_TYPES.CHART_SERIES),
3262
+ id: z.string().min(1),
3263
+ color: z.string().min(1),
3264
+ inlines: z.array(DisplayInlineNodeSchema).default([])
3265
+ });
3266
+ function createChartSeries(id, opts) {
3267
+ return ChartSeriesSchema.parse({
3268
+ type: AST_TYPES.CHART_SERIES,
3269
+ id,
3270
+ color: opts.color,
3271
+ inlines: opts.inlines ?? []
3272
+ });
3273
+ }
3274
+ function isChartSeries(value) {
3275
+ return ChartSeriesSchema.safeParse(value).success;
3276
+ }
3277
+ const ChartSeriesBarSchema = z.object({
3278
+ type: z.literal(AST_TYPES.CHART_SERIES_BAR),
3279
+ id: z.string().min(1),
3280
+ seriesId: z.string().min(1),
3281
+ value: z.number().default(0),
3282
+ answer: z.number().optional()
3283
+ });
3284
+ function createChartSeriesBar(id, opts) {
3285
+ return ChartSeriesBarSchema.parse({
3286
+ type: AST_TYPES.CHART_SERIES_BAR,
3287
+ id,
3288
+ seriesId: opts.seriesId,
3289
+ value: opts.value ?? 0,
3290
+ answer: opts.answer
3291
+ });
3292
+ }
3293
+ function isChartSeriesBar(value) {
3294
+ return ChartSeriesBarSchema.safeParse(value).success;
3295
+ }
3296
+ const ChartGroupSchema = z.object({
3297
+ type: z.literal(AST_TYPES.CHART_GROUP),
3298
+ id: z.string().min(1),
3299
+ labelInlines: z.array(InlineNodeSchema).default([]),
3300
+ bars: z.array(ChartSeriesBarSchema).default([])
3301
+ });
3302
+ function createChartGroup(id, opts = {}) {
3303
+ return ChartGroupSchema.parse({
3304
+ type: AST_TYPES.CHART_GROUP,
3305
+ id,
3306
+ labelInlines: opts.labelInlines ?? [],
3307
+ bars: opts.bars ?? []
3308
+ });
3309
+ }
3310
+ function isChartGroup(value) {
3311
+ return ChartGroupSchema.safeParse(value).success;
3312
+ }
3313
+ function alignGroupBarsToSeries(group, series) {
3314
+ const bySeriesId = new Map(group.bars.map((bar) => [bar.seriesId, bar]));
3315
+ return series.map((s) => bySeriesId.get(s.id));
3316
+ }
3148
3317
  const ChartBlockSchema = z.object({
3149
3318
  type: z.literal(AST_TYPES.CHART_BLOCK),
3150
3319
  name: z.string().optional(),
@@ -3156,7 +3325,10 @@ const ChartBlockSchema = z.object({
3156
3325
  xAxis: ChartAxisSchema.optional(),
3157
3326
  yAxis: ChartAxisSchema.optional(),
3158
3327
  bars: z.array(ChartBarSchema).default([]),
3159
- pictos: z.array(ChartPictoSchema).default([])
3328
+ pictos: z.array(ChartPictoSchema).default([]),
3329
+ legend: z.boolean().default(false),
3330
+ series: z.array(ChartSeriesSchema).default([]),
3331
+ groups: z.array(ChartGroupSchema).default([])
3160
3332
  }).superRefine((block, ctx) => {
3161
3333
  validateChartBlock(block, ctx);
3162
3334
  });
@@ -3181,10 +3353,10 @@ function validateChartBlock(block, ctx) {
3181
3353
  const { valueAxis } = resolveChartAxes(block);
3182
3354
  validateValueAxisRange(valueAxis, ctx);
3183
3355
  if (block.chartType === "bar") {
3184
- if (block.pictos.length > 0) {
3356
+ if (block.pictos.length > 0 || block.series.length > 0 || block.groups.length > 0) {
3185
3357
  ctx.addIssue({
3186
3358
  code: z.ZodIssueCode.custom,
3187
- message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO',
3359
+ message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO, SERIES, or GROUP',
3188
3360
  path: ["pictos"]
3189
3361
  });
3190
3362
  return;
@@ -3197,16 +3369,16 @@ function validateChartBlock(block, ctx) {
3197
3369
  });
3198
3370
  return;
3199
3371
  }
3200
- validateUniqueIds(block.bars, ctx);
3372
+ validateUniqueIds(block.bars, ctx, (i) => ["bars", i]);
3201
3373
  validateChartSnap(block.snap, valueAxis.step, ctx);
3202
- validateBarAnswers(block.bars, valueAxis, block.snap, ctx);
3374
+ validateBarAnswers(block.bars, valueAxis, block.snap, ctx, (i) => ["bars", i]);
3203
3375
  return;
3204
3376
  }
3205
3377
  if (block.chartType === "pictograph") {
3206
- if (block.bars.length > 0) {
3378
+ if (block.bars.length > 0 || block.series.length > 0 || block.groups.length > 0) {
3207
3379
  ctx.addIssue({
3208
3380
  code: z.ZodIssueCode.custom,
3209
- message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR',
3381
+ message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR, SERIES, or GROUP',
3210
3382
  path: ["bars"]
3211
3383
  });
3212
3384
  return;
@@ -3227,7 +3399,7 @@ function validateChartBlock(block, ctx) {
3227
3399
  });
3228
3400
  return;
3229
3401
  }
3230
- validateUniqueIds(block.pictos, ctx);
3402
+ validateUniqueIds(block.pictos, ctx, (i) => ["pictos", i]);
3231
3403
  if (!axisHasNumericParams(valueAxis.axis)) {
3232
3404
  ctx.addIssue({
3233
3405
  code: z.ZodIssueCode.custom,
@@ -3248,6 +3420,63 @@ function validateChartBlock(block, ctx) {
3248
3420
  validatePictoAnswers(block.pictos, valueAxis, ctx);
3249
3421
  return;
3250
3422
  }
3423
+ if (block.chartType === "grouped-bar") {
3424
+ if (block.bars.length > 0 || block.pictos.length > 0) {
3425
+ ctx.addIssue({
3426
+ code: z.ZodIssueCode.custom,
3427
+ message: 'CHART[type="grouped-bar"]: body must contain SERIES and GROUP items only, not BAR or PICTO',
3428
+ path: ["bars"]
3429
+ });
3430
+ return;
3431
+ }
3432
+ if (block.series.length === 0) {
3433
+ ctx.addIssue({
3434
+ code: z.ZodIssueCode.custom,
3435
+ message: 'CHART[type="grouped-bar"]: at least one SERIES is required',
3436
+ path: ["series"]
3437
+ });
3438
+ return;
3439
+ }
3440
+ if (block.groups.length === 0) {
3441
+ ctx.addIssue({
3442
+ code: z.ZodIssueCode.custom,
3443
+ message: 'CHART[type="grouped-bar"]: at least one GROUP is required',
3444
+ path: ["groups"]
3445
+ });
3446
+ return;
3447
+ }
3448
+ validateUniqueIds(block.series, ctx, (i) => ["series", i]);
3449
+ validateUniqueIds(block.groups, ctx, (i) => ["groups", i]);
3450
+ const allBars = block.groups.flatMap((g, gi) => g.bars.map((bar, bi) => ({ ...bar, groupIndex: gi, barIndex: bi })));
3451
+ const pathForFlatBar = (i) => ["groups", allBars[i].groupIndex, "bars", allBars[i].barIndex];
3452
+ validateUniqueIds(allBars, ctx, pathForFlatBar);
3453
+ const seriesIds = new Set(block.series.map((s) => s.id));
3454
+ block.groups.forEach((group, gi) => {
3455
+ const seenInGroup = /* @__PURE__ */ new Set();
3456
+ group.bars.forEach((bar, bi) => {
3457
+ if (!seriesIds.has(bar.seriesId)) {
3458
+ ctx.addIssue({
3459
+ code: z.ZodIssueCode.custom,
3460
+ message: `CHART: GROUP "${group.id}" SERIES_BAR "${bar.id}" references unknown series "${bar.seriesId}" — no SERIES with that id is declared`,
3461
+ path: ["groups", gi, "bars", bi, "seriesId"]
3462
+ });
3463
+ return;
3464
+ }
3465
+ if (seenInGroup.has(bar.seriesId)) {
3466
+ ctx.addIssue({
3467
+ code: z.ZodIssueCode.custom,
3468
+ message: `CHART: GROUP "${group.id}" references series "${bar.seriesId}" twice — each SERIES can appear at most once per GROUP`,
3469
+ path: ["groups", gi, "bars", bi, "seriesId"]
3470
+ });
3471
+ return;
3472
+ }
3473
+ seenInGroup.add(bar.seriesId);
3474
+ });
3475
+ });
3476
+ validateChartSnap(block.snap, valueAxis.step, ctx);
3477
+ validateBarAnswers(allBars, valueAxis, block.snap, ctx, pathForFlatBar);
3478
+ return;
3479
+ }
3251
3480
  ctx.addIssue({
3252
3481
  code: z.ZodIssueCode.custom,
3253
3482
  message: `CHART: unsupported chartType "${block.chartType}" — add a validation branch to validateChartBlock`,
@@ -3268,14 +3497,14 @@ function resolveNumericAxis(axis) {
3268
3497
  function hasAmbiguousValueAxes(block) {
3269
3498
  return axisHasNumericParams(block.xAxis) && axisHasNumericParams(block.yAxis);
3270
3499
  }
3271
- function validateUniqueIds(items, ctx) {
3500
+ function validateUniqueIds(items, ctx, pathFor) {
3272
3501
  const seen = /* @__PURE__ */ new Set();
3273
3502
  items.forEach((item, index) => {
3274
3503
  if (seen.has(item.id)) {
3275
3504
  ctx.addIssue({
3276
3505
  code: z.ZodIssueCode.custom,
3277
3506
  message: `CHART: duplicate id "${item.id}" — ids must be unique within the chart`,
3278
- path: [index, "id"]
3507
+ path: [...pathFor(index), "id"]
3279
3508
  });
3280
3509
  return;
3281
3510
  }
@@ -3317,14 +3546,14 @@ function validateChartSnap(snap, step, ctx) {
3317
3546
  });
3318
3547
  }
3319
3548
  }
3320
- function validateBarAnswers(bars, valueAxis, snap, ctx) {
3549
+ function validateBarAnswers(bars, valueAxis, snap, ctx, pathFor) {
3321
3550
  const gridStep = snap ?? valueAxis.step;
3322
3551
  bars.forEach((bar, index) => {
3323
3552
  if (bar.value < valueAxis.min || bar.value > valueAxis.max) {
3324
3553
  ctx.addIssue({
3325
3554
  code: z.ZodIssueCode.custom,
3326
3555
  message: `CHART: BAR "${bar.id}" value (${bar.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the bar will not render correctly`,
3327
- path: ["bars", index, "value"]
3556
+ path: [...pathFor(index), "value"]
3328
3557
  });
3329
3558
  }
3330
3559
  if (bar.answer === void 0) return;
@@ -3332,7 +3561,7 @@ function validateBarAnswers(bars, valueAxis, snap, ctx) {
3332
3561
  ctx.addIssue({
3333
3562
  code: z.ZodIssueCode.custom,
3334
3563
  message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
3335
- path: ["bars", index, "answer"]
3564
+ path: [...pathFor(index), "answer"]
3336
3565
  });
3337
3566
  return;
3338
3567
  }
@@ -3341,7 +3570,7 @@ function validateBarAnswers(bars, valueAxis, snap, ctx) {
3341
3570
  ctx.addIssue({
3342
3571
  code: z.ZodIssueCode.custom,
3343
3572
  message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is not on the drag grid (min ${valueAxis.min} + multiples of ${gridStep}) — the student can never land on it. Align the answer to the step/snap grid.`,
3344
- path: ["bars", index, "answer"]
3573
+ path: [...pathFor(index), "answer"]
3345
3574
  });
3346
3575
  }
3347
3576
  });
@@ -3403,7 +3632,10 @@ function createChartBlock(chartType, opts = {}) {
3403
3632
  xAxis: opts.xAxis,
3404
3633
  yAxis: opts.yAxis,
3405
3634
  bars: opts.bars ?? [],
3406
- pictos: opts.pictos ?? []
3635
+ pictos: opts.pictos ?? [],
3636
+ series: opts.series ?? [],
3637
+ groups: opts.groups ?? [],
3638
+ legend: opts.legend ?? false
3407
3639
  });
3408
3640
  }
3409
3641
  function isChartBlock(value) {
@@ -3490,7 +3722,9 @@ const ExerciseDocumentSchema = z.object({
3490
3722
  };
3491
3723
  const visitInlines = (inlines, basePath) => {
3492
3724
  inlines.forEach((inline, i) => {
3493
- if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE) {
3725
+ if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE || inline.type === AST_TYPES.VISUAL_FRACTION) {
3726
+ record(inline.name, [...basePath, i, "name"]);
3727
+ } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
3494
3728
  record(inline.name, [...basePath, i, "name"]);
3495
3729
  }
3496
3730
  });
@@ -3522,6 +3756,28 @@ const ExerciseDocumentSchema = z.object({
3522
3756
  pm.groups.flat().forEach((item, ii) => {
3523
3757
  visitInlines(item.inlines, ["lines", bi, "items", ii, "inlines"]);
3524
3758
  });
3759
+ } else if (block.type === AST_TYPES.CHART_BLOCK) {
3760
+ const chart = block;
3761
+ const base = ["lines", bi];
3762
+ chart.bars.forEach((bar, i) => {
3763
+ if (bar.answer !== void 0) record(bar.id, [...base, "bars", i, "id"]);
3764
+ visitInlines(bar.inlines, [...base, "bars", i, "inlines"]);
3765
+ });
3766
+ chart.pictos.forEach((picto, i) => {
3767
+ if (picto.answer !== void 0) record(picto.id, [...base, "pictos", i, "id"]);
3768
+ visitInlines(picto.inlines, [...base, "pictos", i, "inlines"]);
3769
+ });
3770
+ chart.groups.forEach((group, gi) => {
3771
+ group.bars.forEach((bar, i) => {
3772
+ if (bar.answer !== void 0) record(bar.id, [...base, "groups", gi, "bars", i, "id"]);
3773
+ });
3774
+ visitInlines(group.labelInlines, [...base, "groups", gi, "labelInlines"]);
3775
+ });
3776
+ chart.series.forEach((series, i) => {
3777
+ visitInlines(series.inlines, [...base, "series", i, "inlines"]);
3778
+ });
3779
+ if (chart.xAxis) visitInlines(chart.xAxis.inlines, [...base, "xAxis", "inlines"]);
3780
+ if (chart.yAxis) visitInlines(chart.yAxis.inlines, [...base, "yAxis", "inlines"]);
3525
3781
  } else if (block.type === AST_TYPES.RADIO_OPTIONS) {
3526
3782
  record(block.name, ["lines", bi, "name"]);
3527
3783
  } else if (block.type === AST_TYPES.GEOMETRY_BLOCK && block.name !== void 0) {
@@ -4075,12 +4331,19 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4075
4331
  `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": disabled must be true or false, got ${JSON.stringify(params.disabled)}`
4076
4332
  );
4077
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
+ }
4078
4341
  if (ctx.stringInput) {
4079
4342
  const length = params.length;
4080
- return createStringInput(name, length, answer, placeholder, disabled);
4343
+ return createStringInput(name, length, answer, placeholder, disabled, width);
4081
4344
  }
4082
4345
  const digits = params.digits;
4083
- return createNumericInput(name, digits, answer, placeholder, disabled);
4346
+ return createNumericInput(name, digits, answer, placeholder, disabled, width);
4084
4347
  }
4085
4348
  inlineDragOption(ctx) {
4086
4349
  const valueToken = ctx.StringLiteral[0];
@@ -4093,7 +4356,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4093
4356
  chartBlock(ctx) {
4094
4357
  const params = this.visit(ctx.parameterList);
4095
4358
  const chartType = params.type;
4096
- if (!chartType) throw new Error('CHART: type is required ("bar" | "pictograph")');
4359
+ if (!chartType) throw new Error(`CHART: type is required (${CHART_TYPES.map((t) => `"${t}"`).join(" | ")})`);
4097
4360
  if (!CHART_TYPES.includes(chartType)) {
4098
4361
  throw new Error(`CHART: type must be one of ${CHART_TYPES.join(", ")} (got "${chartType}")`);
4099
4362
  }
@@ -4112,7 +4375,26 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4112
4375
  const yAxis = ctx.chartYAxis ? this.visit(ctx.chartYAxis) : void 0;
4113
4376
  const bars = ctx.chartBarItem ? ctx.chartBarItem.map((b) => this.visit(b)) : [];
4114
4377
  const pictos = ctx.chartPictoItem ? ctx.chartPictoItem.map((p) => this.visit(p)) : [];
4115
- return createChartBlock(chartType, { bars, pictos, name, title, grid, scale, snap, xAxis, yAxis });
4378
+ const series = ctx.chartSeriesItem ? ctx.chartSeriesItem.map((s) => this.visit(s)) : [];
4379
+ const groups = ctx.chartGroupItem ? ctx.chartGroupItem.map((g) => this.visit(g)) : [];
4380
+ if (params.legend !== void 0 && chartType !== "grouped-bar") {
4381
+ throw new Error(`CHART: "legend" is only supported for type="grouped-bar" (got "${chartType}")`);
4382
+ }
4383
+ const legend = params.legend === true || params.legend === "true";
4384
+ return createChartBlock(chartType, {
4385
+ bars,
4386
+ pictos,
4387
+ series,
4388
+ groups,
4389
+ legend,
4390
+ name,
4391
+ title,
4392
+ grid,
4393
+ scale,
4394
+ snap,
4395
+ xAxis,
4396
+ yAxis
4397
+ });
4116
4398
  }
4117
4399
  /** X_AXIS and Y_AXIS carry identical params — only the produced AST type differs.
4118
4400
  * Chevrotain needs two separate grammar rules, but one visitor body serves both. */
@@ -4151,6 +4433,52 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4151
4433
  inlines
4152
4434
  });
4153
4435
  }
4436
+ chartSeriesItem(ctx) {
4437
+ const params = this.visit(ctx.parameterList);
4438
+ const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
4439
+ if (inlines.some((n) => !DisplayInlineNodeSchema.safeParse(n).success)) {
4440
+ throw new Error(
4441
+ `CHART: SERIES "${params.id}" label must be display-only — inputs, drag options, clocks and interactive fractions are not allowed in a legend label`
4442
+ );
4443
+ }
4444
+ if (!params.color) {
4445
+ throw new Error(`CHART: SERIES "${params.id}" requires "color"`);
4446
+ }
4447
+ return createChartSeries(params.id, {
4448
+ color: params.color,
4449
+ inlines
4450
+ });
4451
+ }
4452
+ chartGroupLabel(ctx) {
4453
+ return ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
4454
+ }
4455
+ chartGroupItem(ctx) {
4456
+ const params = this.visit(ctx.parameterList);
4457
+ if (ctx.chartGroupLabel && ctx.chartGroupLabel.length > 1) {
4458
+ throw new Error(`CHART: GROUP "${params.id}" has more than one LABEL — only one is allowed`);
4459
+ }
4460
+ const labelInlines = ctx.chartGroupLabel ? this.visit(ctx.chartGroupLabel) : [];
4461
+ const bars = ctx.chartSeriesBarItem ? ctx.chartSeriesBarItem.map((b) => this.visit(b)) : [];
4462
+ return createChartGroup(params.id, { labelInlines, bars });
4463
+ }
4464
+ // `params.series` is the raw DSL attribute name (SERIES_BAR[series="..."])
4465
+ // — unrelated to the AST field's own `seriesId` name.
4466
+ chartSeriesBarItem(ctx) {
4467
+ const params = this.visit(ctx.parameterList);
4468
+ if (!params.series) {
4469
+ throw new Error(`CHART: SERIES_BAR "${params.id}" requires "series"`);
4470
+ }
4471
+ if (params.color !== void 0) {
4472
+ throw new Error(
4473
+ `CHART: SERIES_BAR "${params.id}" cannot set "color" — it comes from its SERIES`
4474
+ );
4475
+ }
4476
+ return createChartSeriesBar(params.id, {
4477
+ seriesId: params.series,
4478
+ value: params.value !== void 0 ? Number(params.value) : void 0,
4479
+ answer: params.answer !== void 0 ? Number(params.answer) : void 0
4480
+ });
4481
+ }
4154
4482
  // ═══════════════════════════════════════════════════════════════════════════
4155
4483
  // Tools
4156
4484
  // ═══════════════════════════════════════════════════════════════════════════
@@ -4605,23 +4933,23 @@ const ChartAxisJSONSchema = z.object({
4605
4933
  });
4606
4934
  }
4607
4935
  });
4608
- const ChartBarJSONSchema = z.discriminatedUnion("interactive", [
4609
- z.object({
4610
- id: z.string(),
4611
- type: z.literal(JSON_TYPES.CHART_BAR),
4612
- interactive: z.literal(false),
4613
- value: z.number(),
4614
- color: z.string().optional(),
4615
- label: z.array(LayoutItemSchema).optional()
4616
- }),
4617
- z.object({
4618
- id: z.string(),
4619
- type: z.literal(JSON_TYPES.CHART_BAR),
4936
+ const ChartBarJSONBaseSchema = z.object({
4937
+ id: z.string(),
4938
+ type: z.literal(JSON_TYPES.CHART_BAR),
4939
+ value: z.number(),
4940
+ answer: z.number().optional(),
4941
+ color: z.string().optional(),
4942
+ label: z.array(LayoutItemSchema).optional()
4943
+ });
4944
+ const ChartBarJSONSchema = z.union([
4945
+ ChartBarJSONBaseSchema.extend({
4620
4946
  interactive: z.literal(true),
4621
- value: z.number(),
4622
- widgetId: z.string(),
4623
- color: z.string().optional(),
4624
- label: z.array(LayoutItemSchema).optional()
4947
+ /** The widgets/answers key, equal to `id` when compiled. */
4948
+ widgetId: z.string()
4949
+ }),
4950
+ ChartBarJSONBaseSchema.extend({
4951
+ interactive: z.literal(false).optional(),
4952
+ widgetId: z.undefined().optional()
4625
4953
  })
4626
4954
  ]);
4627
4955
  const ChartPictoJSONSchema = z.object({
@@ -4635,21 +4963,55 @@ const ChartPictoJSONSchema = z.object({
4635
4963
  ),
4636
4964
  label: z.array(LayoutItemSchema).optional()
4637
4965
  });
4638
- const ChartItemSchema = z.object({
4966
+ const ChartSeriesJSONSchema = z.object({
4967
+ id: z.string(),
4968
+ color: z.string(),
4969
+ label: z.array(LayoutItemSchema).optional()
4970
+ });
4971
+ const ChartSeriesBarJSONSchema = z.object({
4972
+ id: z.string(),
4973
+ type: z.literal(JSON_TYPES.CHART_SERIES_BAR),
4974
+ value: z.number(),
4975
+ answer: z.number().optional(),
4976
+ color: z.string(),
4977
+ seriesId: z.string()
4978
+ });
4979
+ const ChartGroupJSONSchema = z.object({
4980
+ id: z.string(),
4981
+ label: z.array(LayoutItemSchema).optional(),
4982
+ bars: z.array(ChartSeriesBarJSONSchema)
4983
+ });
4984
+ const ChartItemBaseSchema = z.object({
4639
4985
  id: z.string(),
4640
4986
  type: z.literal(JSON_TYPES.CHART),
4641
4987
  name: z.string().optional(),
4642
4988
  title: z.string().optional(),
4643
- chartType: z.enum(CHART_TYPES),
4644
4989
  orientation: z.enum(["vertical", "horizontal"]).default("vertical"),
4645
4990
  grid: z.boolean().default(false),
4646
4991
  scale: z.number().positive().default(1),
4647
4992
  snap: z.number().positive().optional(),
4648
4993
  xAxis: ChartAxisJSONSchema.optional(),
4649
- yAxis: ChartAxisJSONSchema.optional(),
4650
- bars: z.array(ChartBarJSONSchema).default([]),
4651
- pictos: z.array(ChartPictoJSONSchema).default([])
4652
- }).superRefine((item, ctx) => {
4994
+ yAxis: ChartAxisJSONSchema.optional()
4995
+ });
4996
+ const ChartItemBarSchema = ChartItemBaseSchema.extend({
4997
+ chartType: z.literal("bar"),
4998
+ bars: z.array(ChartBarJSONSchema)
4999
+ });
5000
+ const ChartItemPictographSchema = ChartItemBaseSchema.extend({
5001
+ chartType: z.literal("pictograph"),
5002
+ pictos: z.array(ChartPictoJSONSchema)
5003
+ });
5004
+ const ChartItemGroupedBarSchema = ChartItemBaseSchema.extend({
5005
+ chartType: z.literal("grouped-bar"),
5006
+ series: z.array(ChartSeriesJSONSchema),
5007
+ groups: z.array(ChartGroupJSONSchema),
5008
+ legend: z.boolean().default(false)
5009
+ });
5010
+ const ChartItemSchema = z.discriminatedUnion("chartType", [
5011
+ ChartItemBarSchema,
5012
+ ChartItemPictographSchema,
5013
+ ChartItemGroupedBarSchema
5014
+ ]).superRefine((item, ctx) => {
4653
5015
  if (item.chartType === "bar" && item.bars.length === 0) {
4654
5016
  ctx.addIssue({
4655
5017
  code: z.ZodIssueCode.custom,
@@ -4657,18 +5019,63 @@ const ChartItemSchema = z.object({
4657
5019
  path: ["bars"]
4658
5020
  });
4659
5021
  }
4660
- if (item.chartType === "pictograph" && item.pictos.length === 0) {
4661
- ctx.addIssue({
4662
- code: z.ZodIssueCode.custom,
4663
- message: 'CHART[type="pictograph"]: at least one PICTO is required',
4664
- path: ["pictos"]
4665
- });
5022
+ if (item.chartType === "pictograph") {
5023
+ if (item.pictos.length === 0) {
5024
+ ctx.addIssue({
5025
+ code: z.ZodIssueCode.custom,
5026
+ message: 'CHART[type="pictograph"]: at least one PICTO is required',
5027
+ path: ["pictos"]
5028
+ });
5029
+ }
5030
+ if (item.snap !== void 0) {
5031
+ ctx.addIssue({
5032
+ code: z.ZodIssueCode.custom,
5033
+ message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
5034
+ path: ["snap"]
5035
+ });
5036
+ }
4666
5037
  }
4667
- if (item.chartType === "pictograph" && item.snap !== void 0) {
4668
- ctx.addIssue({
4669
- code: z.ZodIssueCode.custom,
4670
- message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
4671
- path: ["snap"]
5038
+ if (item.chartType === "grouped-bar") {
5039
+ if (item.series.length === 0) {
5040
+ ctx.addIssue({
5041
+ code: z.ZodIssueCode.custom,
5042
+ message: 'CHART[type="grouped-bar"]: at least one SERIES is required',
5043
+ path: ["series"]
5044
+ });
5045
+ }
5046
+ if (item.groups.length === 0) {
5047
+ ctx.addIssue({
5048
+ code: z.ZodIssueCode.custom,
5049
+ message: 'CHART[type="grouped-bar"]: at least one GROUP is required',
5050
+ path: ["groups"]
5051
+ });
5052
+ }
5053
+ validateUniqueIds(item.series, ctx, (i) => ["series", i]);
5054
+ validateUniqueIds(item.groups, ctx, (i) => ["groups", i]);
5055
+ const allBars = item.groups.flatMap((g, gi) => g.bars.map((bar, bi) => ({ id: bar.id, gi, bi })));
5056
+ validateUniqueIds(allBars, ctx, (i) => ["groups", allBars[i].gi, "bars", allBars[i].bi]);
5057
+ const seriesIds = new Set(item.series.map((s) => s.id));
5058
+ item.groups.forEach((group, gi) => {
5059
+ const seenInGroup = /* @__PURE__ */ new Set();
5060
+ group.bars.forEach((bar, bi) => {
5061
+ if (!seriesIds.has(bar.seriesId)) {
5062
+ ctx.addIssue({
5063
+ code: z.ZodIssueCode.custom,
5064
+ message: `CHART[type="grouped-bar"]: GROUP "${group.id}" bar "${bar.id}" references unknown seriesId "${bar.seriesId}" — no SERIES with that id is declared`,
5065
+ path: ["groups", gi, "bars", bi, "seriesId"]
5066
+ });
5067
+ return;
5068
+ }
5069
+ if (seenInGroup.has(bar.seriesId)) {
5070
+ ctx.addIssue({
5071
+ code: z.ZodIssueCode.custom,
5072
+ message: `CHART[type="grouped-bar"]: GROUP "${group.id}" has two bars for seriesId "${bar.seriesId}" — each series can appear at most once per group`,
5073
+ path: ["groups", gi, "bars", bi, "seriesId"]
5074
+ });
5075
+ return;
5076
+ }
5077
+ seenInGroup.add(bar.seriesId);
5078
+ });
4672
5079
  });
4673
5080
  }
4674
5081
  });
@@ -4676,6 +5083,8 @@ const StringInputJSONSchema = z.object({
4676
5083
  type: z.literal(JSON_TYPES.INLINE_INPUT),
4677
5084
  inputType: z.literal(INPUT_TYPES.STRING),
4678
5085
  length: z.number().int().positive(),
5086
+ /** Box width in characters; defaults to `length`. */
5087
+ width: InlineInputWidthSchema,
4679
5088
  placeholder: z.string().max(32).optional(),
4680
5089
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
4681
5090
  disabled: z.boolean().optional()
@@ -4684,6 +5093,8 @@ const NumericInputJSONSchema = z.object({
4684
5093
  type: z.literal(JSON_TYPES.INLINE_INPUT),
4685
5094
  inputType: z.literal(INPUT_TYPES.NUMERIC),
4686
5095
  digits: z.number().int().positive(),
5096
+ /** Box width in characters; defaults to `digits`. */
5097
+ width: InlineInputWidthSchema,
4687
5098
  placeholder: z.string().max(32).optional(),
4688
5099
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
4689
5100
  disabled: z.boolean().optional()
@@ -5674,7 +6085,7 @@ lodash.exports;
5674
6085
  var reIsNative = RegExp2(
5675
6086
  "^" + funcToString.call(hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"
5676
6087
  );
5677
- 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;
5678
6089
  var defineProperty = function() {
5679
6090
  try {
5680
6091
  var func = getNative(Object2, "defineProperty");
@@ -5689,7 +6100,7 @@ lodash.exports;
5689
6100
  var metaMap = WeakMap && new WeakMap();
5690
6101
  var realNames = {};
5691
6102
  var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap);
5692
- 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;
5693
6104
  function lodash2(value) {
5694
6105
  if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
5695
6106
  if (value instanceof LodashWrapper) {
@@ -10588,6 +10999,7 @@ function toJSON(ast) {
10588
10999
  type: JSON_TYPES.INLINE_INPUT,
10589
11000
  inputType: INPUT_TYPES.STRING,
10590
11001
  length: inline.length,
11002
+ ...inline.width !== void 0 && { width: inline.width },
10591
11003
  placeholder: inline.placeholder,
10592
11004
  ...inline.disabled && { disabled: true }
10593
11005
  };
@@ -10596,6 +11008,7 @@ function toJSON(ast) {
10596
11008
  type: JSON_TYPES.INLINE_INPUT,
10597
11009
  inputType: INPUT_TYPES.NUMERIC,
10598
11010
  digits: inline.digits,
11011
+ ...inline.width !== void 0 && { width: inline.width },
10599
11012
  placeholder: inline.placeholder,
10600
11013
  ...inline.disabled && { disabled: true }
10601
11014
  };
@@ -10691,7 +11104,7 @@ function toJSON(ast) {
10691
11104
  options: lodashExports.shuffle(tool.options)
10692
11105
  };
10693
11106
  } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
10694
- widgets["_numeric_keypad"] = {
11107
+ widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
10695
11108
  type: JSON_TYPES.NUMERIC_KEYPAD,
10696
11109
  includeOperators: tool.includeOperators,
10697
11110
  includeDecimal: tool.includeDecimal,
@@ -10767,6 +11180,8 @@ function collectAllInlines(blocks) {
10767
11180
  for (const picto of chart.pictos) result.push(...picto.inlines);
10768
11181
  if (chart.xAxis) result.push(...chart.xAxis.inlines);
10769
11182
  if (chart.yAxis) result.push(...chart.yAxis.inlines);
11183
+ for (const series of chart.series) result.push(...series.inlines);
11184
+ for (const group of chart.groups) result.push(...group.labelInlines);
10770
11185
  }
10771
11186
  }
10772
11187
  return result;
@@ -10801,27 +11216,19 @@ function transformChart(block, idGenerator, inlineDragIds) {
10801
11216
  const chartAnswers = {};
10802
11217
  const bars = block.bars.map((bar) => {
10803
11218
  const label = toLabel(bar.inlines);
10804
- if (bar.answer !== void 0) {
10805
- chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
10806
- chartAnswers[bar.id] = bar.answer;
10807
- return {
10808
- id: bar.id,
10809
- type: JSON_TYPES.CHART_BAR,
10810
- interactive: true,
10811
- value: bar.value,
10812
- widgetId: bar.id,
10813
- ...bar.color !== void 0 && { color: bar.color },
10814
- ...label !== void 0 && { label }
10815
- };
10816
- }
10817
- return {
11219
+ const common = {
10818
11220
  id: bar.id,
10819
11221
  type: JSON_TYPES.CHART_BAR,
10820
- interactive: false,
10821
11222
  value: bar.value,
10822
11223
  ...bar.color !== void 0 && { color: bar.color },
10823
11224
  ...label !== void 0 && { label }
10824
11225
  };
11226
+ if (bar.answer !== void 0) {
11227
+ chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
11228
+ chartAnswers[bar.id] = bar.answer;
11229
+ return { ...common, interactive: true, widgetId: bar.id, answer: bar.answer };
11230
+ }
11231
+ return { ...common, interactive: false };
10825
11232
  });
10826
11233
  const pictos = block.pictos.map((picto) => {
10827
11234
  const label = toLabel(picto.inlines);
@@ -10838,21 +11245,62 @@ function transformChart(block, idGenerator, inlineDragIds) {
10838
11245
  ...label !== void 0 && { label }
10839
11246
  };
10840
11247
  });
10841
- const item = {
11248
+ const series = block.series.map((s) => {
11249
+ const label = toLabel(s.inlines);
11250
+ return {
11251
+ id: s.id,
11252
+ color: s.color,
11253
+ ...label !== void 0 && { label }
11254
+ };
11255
+ });
11256
+ const groups = block.groups.map((group) => {
11257
+ const slots = alignGroupBarsToSeries(group, block.series);
11258
+ const groupBars = slots.map((bar, i) => {
11259
+ if (bar === void 0) return void 0;
11260
+ const series2 = block.series[i];
11261
+ if (bar.answer !== void 0) {
11262
+ chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
11263
+ chartAnswers[bar.id] = bar.answer;
11264
+ }
11265
+ return {
11266
+ id: bar.id,
11267
+ type: JSON_TYPES.CHART_SERIES_BAR,
11268
+ value: bar.value,
11269
+ ...bar.answer !== void 0 && { answer: bar.answer },
11270
+ color: series2.color,
11271
+ seriesId: series2.id
11272
+ };
11273
+ }).filter((bar) => bar !== void 0);
11274
+ const label = toLabel(group.labelInlines);
11275
+ return {
11276
+ id: group.id,
11277
+ ...label !== void 0 && { label },
11278
+ bars: groupBars
11279
+ };
11280
+ });
11281
+ const base = {
10842
11282
  id: idGenerator.generate(JSON_TYPES.CHART),
10843
11283
  type: JSON_TYPES.CHART,
10844
11284
  ...block.name !== void 0 && { name: block.name },
10845
11285
  ...block.title !== void 0 && { title: block.title },
10846
- chartType: block.chartType,
10847
11286
  orientation,
10848
11287
  grid: block.grid,
10849
11288
  scale: block.scale,
10850
11289
  ...block.snap !== void 0 && { snap: block.snap },
10851
11290
  ...xAxis !== void 0 && { xAxis },
10852
- ...yAxis !== void 0 && { yAxis },
10853
- bars,
10854
- pictos
11291
+ ...yAxis !== void 0 && { yAxis }
10855
11292
  };
11293
+ let item;
11294
+ if (block.chartType === "bar") {
11295
+ item = { ...base, chartType: "bar", bars };
11296
+ } else if (block.chartType === "pictograph") {
11297
+ item = { ...base, chartType: "pictograph", pictos };
11298
+ } else if (block.chartType === "grouped-bar") {
11299
+ item = { ...base, chartType: "grouped-bar", series, groups, legend: block.legend };
11300
+ } else {
11301
+ const unhandled = block.chartType;
11302
+ throw new Error(`CHART: unsupported chart type "${String(unhandled)}"`);
11303
+ }
10856
11304
  return { item, chartWidgets, chartAnswers };
10857
11305
  }
10858
11306
  function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
@@ -10919,7 +11367,7 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
10919
11367
  max: numberLine.max,
10920
11368
  step: numberLine.step,
10921
11369
  ...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
10922
- ...numberLine.style !== void 0 && { style: numberLine.style },
11370
+ style: numberLine.style,
10923
11371
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
10924
11372
  ...counterItem !== void 0 && { counter: counterItem },
10925
11373
  ...highlights !== void 0 && { highlights },
@@ -15576,22 +16024,22 @@ function createDefaultDropAnimation(options) {
15576
16024
  easing,
15577
16025
  fill: "forwards"
15578
16026
  });
15579
- return new Promise((resolve) => {
16027
+ return new Promise((resolve2) => {
15580
16028
  animation.onfinish = () => {
15581
16029
  cleanup == null ? void 0 : cleanup();
15582
- resolve();
16030
+ resolve2();
15583
16031
  };
15584
16032
  });
15585
16033
  };
15586
16034
  }
15587
- let key = 0;
16035
+ let key$1 = 0;
15588
16036
  function useKey(id) {
15589
16037
  return useMemo(() => {
15590
16038
  if (id == null) {
15591
16039
  return;
15592
16040
  }
15593
- key++;
15594
- return key;
16041
+ key$1++;
16042
+ return key$1;
15595
16043
  }, [id]);
15596
16044
  }
15597
16045
  const DragOverlay = /* @__PURE__ */ React.memo((_ref) => {
@@ -15673,6 +16121,7 @@ const DEFAULT_SOUNDS = {
15673
16121
  "drag-start": dragStart,
15674
16122
  "drag-drop": dragDrop,
15675
16123
  "drag-cancel": dragDrop,
16124
+ "drag-hint": buttonClick,
15676
16125
  "radio-select": buttonClick,
15677
16126
  "pair-select": buttonClick,
15678
16127
  "pair-link": buttonClick,
@@ -15776,14 +16225,24 @@ function useExerciseDnd() {
15776
16225
  }
15777
16226
  return ctx;
15778
16227
  }
16228
+ const NO_EXERCISE = Symbol("no-exercise");
15779
16229
  const ExerciseDndProvider = ({
15780
16230
  children,
15781
16231
  droppedSources,
15782
- onDragEnd
16232
+ onDragEnd,
16233
+ exerciseKey
15783
16234
  }) => {
15784
16235
  const [activeItem, setActiveItem] = useState(null);
16236
+ const [draggedExerciseKey, setDraggedExerciseKey] = useState(NO_EXERCISE);
16237
+ const hasDraggedOption = draggedExerciseKey === exerciseKey;
15785
16238
  const [dropHandlers] = useState(() => /* @__PURE__ */ new Map());
15786
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]);
15787
16246
  const usedOptionIds = useMemo(() => {
15788
16247
  return new Set(droppedSources.values());
15789
16248
  }, [droppedSources]);
@@ -15807,6 +16266,7 @@ const ExerciseDndProvider = ({
15807
16266
  const raw = active.data.current;
15808
16267
  const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
15809
16268
  setActiveItem(item);
16269
+ if (item.type === "drag-option") setDraggedExerciseKey(exerciseKey);
15810
16270
  playSound("drag-start");
15811
16271
  };
15812
16272
  const handleDragEnd = (event) => {
@@ -15832,6 +16292,9 @@ const ExerciseDndProvider = ({
15832
16292
  value: {
15833
16293
  activeItem,
15834
16294
  isDragging: activeItem !== null,
16295
+ exerciseKey,
16296
+ hasDraggedOption,
16297
+ playHintSound,
15835
16298
  usedOptionIds,
15836
16299
  droppedSources,
15837
16300
  registerDropHandler,
@@ -16612,9 +17075,40 @@ function isUndoDrop(event, dragId) {
16612
17075
  const LockContext = createContext(false);
16613
17076
  const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsx(LockContext.Provider, { value: locked, children });
16614
17077
  const useExerciseLocked = () => useContext(LockContext);
16615
- const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
16616
- const { droppedSources } = useExerciseDnd();
16617
- const locked = useExerciseLocked();
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
+ }) => {
17110
+ const { droppedSources } = useExerciseDnd();
17111
+ const locked = useExerciseLocked();
16618
17112
  const sourceOptionId = droppedSources.get(widgetId);
16619
17113
  const dragId = `${widgetId}__chip`;
16620
17114
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
@@ -16636,13 +17130,41 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
16636
17130
  if (!locked && e.detail !== 0) onRemove();
16637
17131
  },
16638
17132
  title: locked ? void 0 : "Drag to move, or click to remove",
16639
- style: { touchAction: "none", cursor: locked ? "default" : "grab" },
17133
+ style: {
17134
+ ...OVERLAY_STYLE,
17135
+ touchAction: "none",
17136
+ cursor: locked ? "default" : "grab"
17137
+ },
16640
17138
  ...listeners,
16641
17139
  ...attributes,
16642
17140
  children: value
16643
17141
  }
16644
17142
  );
16645
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
+ }
16646
17168
  const InlineInput = ({
16647
17169
  widgetId,
16648
17170
  value,
@@ -16651,6 +17173,7 @@ const InlineInput = ({
16651
17173
  inputType,
16652
17174
  length,
16653
17175
  digits,
17176
+ width,
16654
17177
  placeholder,
16655
17178
  disabled = false,
16656
17179
  onChange,
@@ -16664,8 +17187,8 @@ const InlineInput = ({
16664
17187
  const handleDrop = useCallback((item) => {
16665
17188
  return applyDropToInput(widgetId, item, onChange);
16666
17189
  }, [onChange, widgetId]);
16667
- const expectedLength = inputType === "numeric" ? digits : length;
16668
- 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`;
16669
17192
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
16670
17193
  const disabledClass = disabled ? " inline-input--disabled" : "";
16671
17194
  return /* @__PURE__ */ jsx(
@@ -16678,21 +17201,31 @@ const InlineInput = ({
16678
17201
  className: "inline-widget inline-input-droppable",
16679
17202
  children: isDropped && value !== "" ? (
16680
17203
  // Show as chip only when value came from drag-drop
16681
- /* @__PURE__ */ jsx(
16682
- PlacedInputChip,
16683
- {
16684
- widgetId,
16685
- value,
16686
- className: statusClass.trim(),
16687
- onRemove: () => handleChange("", false)
16688
- }
16689
- )
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
+ ] })
16690
17223
  ) : (
16691
17224
  // Regular input for typing
16692
17225
  /* @__PURE__ */ jsx(
16693
17226
  "input",
16694
17227
  {
16695
- id: widgetId + "-input",
17228
+ id: getInputElementId(widgetId),
16696
17229
  type: "text",
16697
17230
  className: `inline-input${statusClass}${disabledClass}`,
16698
17231
  value,
@@ -16712,15 +17245,20 @@ const InlineInput = ({
16712
17245
  }
16713
17246
  );
16714
17247
  };
16715
- const SingleDragOption = ({
17248
+ const SingleDragOption = (props) => {
17249
+ const { exerciseKey } = useExerciseDnd();
17250
+ return /* @__PURE__ */ jsx(DragOptionChip, { ...props }, exerciseKey ?? "");
17251
+ };
17252
+ const DragOptionChip = ({
16716
17253
  optionId,
16717
17254
  value,
16718
17255
  onOptionClick,
16719
17256
  className = ""
16720
17257
  }) => {
16721
- const { usedOptionIds } = useExerciseDnd();
17258
+ const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
16722
17259
  const locked = useExerciseLocked();
16723
17260
  const isUsed = usedOptionIds.has(optionId);
17261
+ const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
16724
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(
16725
17263
  Draggable,
16726
17264
  {
@@ -16732,7 +17270,11 @@ const SingleDragOption = ({
16732
17270
  children: /* @__PURE__ */ jsx(
16733
17271
  "button",
16734
17272
  {
16735
- className: "drag-option",
17273
+ className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
17274
+ onAnimationStart: () => {
17275
+ if (hinting) playHintSound();
17276
+ },
17277
+ onAnimationEnd: onHintEnd,
16736
17278
  onClick: () => {
16737
17279
  if (!locked) onOptionClick?.(value);
16738
17280
  },
@@ -16861,23 +17403,23 @@ const NumericKeypad = ({
16861
17403
  playSound("keypad-press");
16862
17404
  onChange(value.slice(0, -1));
16863
17405
  };
16864
- return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
16865
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
16866
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
16867
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
16868
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
16869
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
16870
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
16871
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
16872
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
16873
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
16874
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
16875
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
16876
- includeOperators && /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
16877
- includeDecimal ? /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
16878
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
16879
- /* @__PURE__ */ jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
16880
- 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: "÷" })
16881
17423
  ] });
16882
17424
  };
16883
17425
  const FractionInput = ({
@@ -16917,18 +17459,28 @@ const FractionInput = ({
16917
17459
  onDrop: handleDrop(subKey),
16918
17460
  disabled: locked,
16919
17461
  className: "fraction-slot-droppable",
16920
- children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsx(
16921
- PlacedInputChip,
16922
- {
16923
- widgetId: subKey,
16924
- value: currentValue,
16925
- className: `fraction-chip${slotClass}`,
16926
- onRemove: () => onChange(subKey, "", false)
16927
- }
16928
- ) : /* @__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(
16929
17481
  "input",
16930
17482
  {
16931
- id: `${subKey}-input`,
17483
+ id: getInputElementId(subKey),
16932
17484
  type: "text",
16933
17485
  className: `inline-input fraction-input${slotClass}`,
16934
17486
  value: currentValue,
@@ -17517,12 +18069,17 @@ const GridSymmetry = ({
17517
18069
  }
17518
18070
  ) });
17519
18071
  };
18072
+ function interactiveBarKey(bar) {
18073
+ if (bar.answer === void 0 && bar.interactive !== true) return void 0;
18074
+ return bar.widgetId ?? bar.id;
18075
+ }
17520
18076
  function getChartClassName(item) {
17521
18077
  const isHorizontal = item.orientation === "horizontal";
17522
18078
  return [
17523
18079
  "chart",
17524
18080
  isHorizontal ? "chart--horizontal" : "",
17525
18081
  item.chartType === "pictograph" ? "chart--pictograph" : "",
18082
+ item.chartType === "grouped-bar" ? "chart--grouped-bar" : "",
17526
18083
  item.xAxis?.visible === false ? "chart--no-xaxis" : "",
17527
18084
  item.yAxis?.visible === false ? "chart--no-yaxis" : ""
17528
18085
  ].filter(Boolean).join(" ");
@@ -17533,7 +18090,8 @@ function resolveItemValue(id, defaultValue2, values) {
17533
18090
  return isNaN(parsed) ? defaultValue2 : parsed;
17534
18091
  }
17535
18092
  function resolveValue(bar, values) {
17536
- return bar.interactive ? resolveItemValue(bar.widgetId, bar.value, values) : bar.value;
18093
+ const widgetKey = interactiveBarKey(bar);
18094
+ return widgetKey !== void 0 ? resolveItemValue(widgetKey, bar.value, values) : bar.value;
17537
18095
  }
17538
18096
  function clamp(v, min, max) {
17539
18097
  return Math.min(max, Math.max(min, v));
@@ -17592,6 +18150,8 @@ const CHART_METRICS = {
17592
18150
  gapWidth: 1,
17593
18151
  axisWidth: 2.35,
17594
18152
  barWidth: 3.35,
18153
+ // Fixed narrow width so several series fit in one cluster; not derived from count.
18154
+ groupedBarWidth: 1.8,
17595
18155
  fontSize: 0.9,
17596
18156
  labelFontSize: 0.98,
17597
18157
  handleSize: 1.15,
@@ -17644,6 +18204,41 @@ function computePictographChartStyle(scale, columnCount, totalSlots, orientation
17644
18204
  "--chart-height": slotSpan
17645
18205
  };
17646
18206
  }
18207
+ function computeGroupedChartStyle(scale, groupCount, seriesCount, orientation = "vertical", isInteractive = false) {
18208
+ const visualScale = clamp(scale, CHART_METRICS.scaleMin, CHART_METRICS.scaleMax);
18209
+ const barWidth = (isInteractive ? CHART_METRICS.barWidth : CHART_METRICS.groupedBarWidth) * visualScale;
18210
+ const barGap = CHART_METRICS.gapWidth * visualScale * 0.5;
18211
+ const groupGap = CHART_METRICS.gapWidth * visualScale * 2;
18212
+ const seriesPerGroup = Math.max(1, seriesCount);
18213
+ const groupWidth = seriesPerGroup * barWidth + Math.max(0, seriesPerGroup - 1) * barGap;
18214
+ const trackLength = groupCount * groupWidth + Math.max(0, groupCount - 1) * groupGap + groupGap * 2;
18215
+ const cssVars = {
18216
+ "--chart-axis-width": `${CHART_METRICS.axisWidth * visualScale}rem`,
18217
+ "--chart-bar-width": `${barWidth}rem`,
18218
+ // A bar slot inside a group is just the bar; reuse the plain bar-chart var.
18219
+ "--chart-bar-slot-width": `${barWidth}rem`,
18220
+ "--chart-bar-gap": `${barGap}rem`,
18221
+ "--chart-group-gap": `${groupGap}rem`,
18222
+ "--chart-group-width": `${groupWidth}rem`,
18223
+ "--chart-font-size": `${CHART_METRICS.fontSize * visualScale}rem`,
18224
+ "--chart-label-font-size": `${CHART_METRICS.labelFontSize * visualScale}rem`,
18225
+ "--chart-handle-size": `${CHART_METRICS.handleSize * visualScale}rem`
18226
+ };
18227
+ if (orientation === "horizontal") {
18228
+ const valueWidth = CHART_METRICS.chartHeight * visualScale;
18229
+ return {
18230
+ ...cssVars,
18231
+ "--chart-height": `${trackLength}rem`,
18232
+ "--chart-h-value-width": `${valueWidth}rem`,
18233
+ "--chart-content-width": `${valueWidth}rem`
18234
+ };
18235
+ }
18236
+ return {
18237
+ ...cssVars,
18238
+ "--chart-height": `${CHART_METRICS.chartHeight * visualScale}rem`,
18239
+ "--chart-content-width": `${trackLength}rem`
18240
+ };
18241
+ }
17647
18242
  function computeHorizontalChartStyle(scale, barCount) {
17648
18243
  const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
17649
18244
  const valueWidth = CHART_METRICS.chartHeight * visualScale;
@@ -17695,6 +18290,7 @@ const DraggableBar = ({
17695
18290
  const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
17696
18291
  const axisStep = axis.step ?? CHART_AXIS_DEFAULTS.step;
17697
18292
  const snapStep = snap ?? axisStep;
18293
+ const widgetKey = bar.widgetId ?? bar.id;
17698
18294
  const fillFromPointer = useCallback(
17699
18295
  (clientY, clientX) => {
17700
18296
  const track = trackRef.current;
@@ -17712,10 +18308,10 @@ const DraggableBar = ({
17712
18308
  if (snapped !== lastSnap.current) {
17713
18309
  lastSnap.current = snapped;
17714
18310
  playSound("bar-step");
17715
- onChange(bar.widgetId, String(snapped));
18311
+ onChange(widgetKey, String(snapped));
17716
18312
  }
17717
18313
  },
17718
- [axisMin, axisMax, snapStep, bar.widgetId, onChange, orientation, playSound]
18314
+ [axisMin, axisMax, snapStep, widgetKey, onChange, orientation, playSound]
17719
18315
  );
17720
18316
  const onPointerDown = useCallback(
17721
18317
  (e) => {
@@ -17748,8 +18344,8 @@ const DraggableBar = ({
17748
18344
  if (!dragging.current) return;
17749
18345
  dragging.current = false;
17750
18346
  setIsDragging(false);
17751
- onChange(bar.widgetId, String(preDragValue.current));
17752
- }, [bar.widgetId, onChange]);
18347
+ onChange(widgetKey, String(preDragValue.current));
18348
+ }, [widgetKey, onChange]);
17753
18349
  const onKeyDown = useCallback(
17754
18350
  (e) => {
17755
18351
  if (exerciseLocked) return;
@@ -17758,22 +18354,22 @@ const DraggableBar = ({
17758
18354
  if (isInc) {
17759
18355
  e.preventDefault();
17760
18356
  playSound("bar-step");
17761
- onChange(bar.widgetId, String(nextStepValue(currentValue, axis, 1, snap)));
18357
+ onChange(widgetKey, String(nextStepValue(currentValue, axis, 1, snap)));
17762
18358
  } else if (isDec) {
17763
18359
  e.preventDefault();
17764
18360
  playSound("bar-step");
17765
- onChange(bar.widgetId, String(nextStepValue(currentValue, axis, -1, snap)));
18361
+ onChange(widgetKey, String(nextStepValue(currentValue, axis, -1, snap)));
17766
18362
  } else if (e.key === "Home") {
17767
18363
  e.preventDefault();
17768
18364
  playSound("bar-step");
17769
- onChange(bar.widgetId, String(axisMin));
18365
+ onChange(widgetKey, String(axisMin));
17770
18366
  } else if (e.key === "End") {
17771
18367
  e.preventDefault();
17772
18368
  playSound("bar-step");
17773
- onChange(bar.widgetId, String(axisMax));
18369
+ onChange(widgetKey, String(axisMax));
17774
18370
  }
17775
18371
  },
17776
- [axisMin, axisMax, bar.widgetId, currentValue, axis, snap, onChange, playSound, exerciseLocked]
18372
+ [axisMin, axisMax, widgetKey, currentValue, axis, snap, onChange, playSound, exerciseLocked]
17777
18373
  );
17778
18374
  const fill = toFill(currentValue, axis);
17779
18375
  const isAtMin = currentValue <= axisMin;
@@ -17849,7 +18445,7 @@ const StaticBar = ({
17849
18445
  }
17850
18446
  ) });
17851
18447
  };
17852
- const EMPTY_SET$2 = /* @__PURE__ */ new Set();
18448
+ const EMPTY_SET$4 = /* @__PURE__ */ new Set();
17853
18449
  const XAxisLabel = ({
17854
18450
  bar,
17855
18451
  widgets,
@@ -17864,14 +18460,14 @@ const XAxisLabel = ({
17864
18460
  item: subItem,
17865
18461
  widgets,
17866
18462
  values,
17867
- droppedInputs: EMPTY_SET$2,
18463
+ droppedInputs: EMPTY_SET$4,
17868
18464
  validationDetails,
17869
18465
  onChange
17870
18466
  },
17871
18467
  subItem.id
17872
18468
  )) }) });
17873
18469
  };
17874
- const EMPTY_SET$1 = /* @__PURE__ */ new Set();
18470
+ const EMPTY_SET$3 = /* @__PURE__ */ new Set();
17875
18471
  const BarChart = ({
17876
18472
  item,
17877
18473
  widgets,
@@ -17879,6 +18475,7 @@ const BarChart = ({
17879
18475
  validationDetails,
17880
18476
  onChange
17881
18477
  }) => {
18478
+ if (item.chartType !== "bar") return null;
17882
18479
  const { bars, grid } = item;
17883
18480
  const isHorizontal = item.orientation === "horizontal";
17884
18481
  const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
@@ -17892,7 +18489,7 @@ const BarChart = ({
17892
18489
  item: subItem,
17893
18490
  widgets,
17894
18491
  values,
17895
- droppedInputs: EMPTY_SET$1,
18492
+ droppedInputs: EMPTY_SET$3,
17896
18493
  validationDetails,
17897
18494
  onChange
17898
18495
  },
@@ -17902,9 +18499,10 @@ const BarChart = ({
17902
18499
  const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
17903
18500
  const renderBar = (bar, orient = "vertical") => {
17904
18501
  const currentValue = resolveValue(bar, values);
17905
- const vStatus = bar.interactive ? validationDetails?.[bar.widgetId] : void 0;
18502
+ const widgetKey = interactiveBarKey(bar);
18503
+ const vStatus = widgetKey !== void 0 ? validationDetails?.[widgetKey] : void 0;
17906
18504
  const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
17907
- return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: bar.interactive ? /* @__PURE__ */ jsx(
18505
+ return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: widgetKey !== void 0 ? /* @__PURE__ */ jsx(
17908
18506
  DraggableBar,
17909
18507
  {
17910
18508
  bar,
@@ -17937,7 +18535,7 @@ const BarChart = ({
17937
18535
  item: subItem,
17938
18536
  widgets,
17939
18537
  values,
17940
- droppedInputs: EMPTY_SET$1,
18538
+ droppedInputs: EMPTY_SET$3,
17941
18539
  validationDetails,
17942
18540
  onChange
17943
18541
  },
@@ -18142,7 +18740,7 @@ const PictographGrid = ({
18142
18740
  ))
18143
18741
  ] });
18144
18742
  };
18145
- const EMPTY_SET = /* @__PURE__ */ new Set();
18743
+ const EMPTY_SET$2 = /* @__PURE__ */ new Set();
18146
18744
  const PictographChart = ({
18147
18745
  item,
18148
18746
  widgets,
@@ -18150,7 +18748,8 @@ const PictographChart = ({
18150
18748
  validationDetails,
18151
18749
  onChange
18152
18750
  }) => {
18153
- const { pictos = [], grid } = item;
18751
+ if (item.chartType !== "pictograph") return null;
18752
+ const { pictos, grid } = item;
18154
18753
  const isHorizontal = item.orientation === "horizontal";
18155
18754
  const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
18156
18755
  const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
@@ -18163,7 +18762,7 @@ const PictographChart = ({
18163
18762
  item: subItem,
18164
18763
  widgets,
18165
18764
  values,
18166
- droppedInputs: EMPTY_SET,
18765
+ droppedInputs: EMPTY_SET$2,
18167
18766
  validationDetails,
18168
18767
  onChange
18169
18768
  },
@@ -18201,7 +18800,7 @@ const PictographChart = ({
18201
18800
  item: subItem,
18202
18801
  widgets,
18203
18802
  values,
18204
- droppedInputs: EMPTY_SET,
18803
+ droppedInputs: EMPTY_SET$2,
18205
18804
  validationDetails,
18206
18805
  onChange
18207
18806
  },
@@ -18222,7 +18821,7 @@ const PictographChart = ({
18222
18821
  item: subItem,
18223
18822
  widgets,
18224
18823
  values,
18225
- droppedInputs: EMPTY_SET,
18824
+ droppedInputs: EMPTY_SET$2,
18226
18825
  validationDetails,
18227
18826
  onChange
18228
18827
  },
@@ -18241,9 +18840,183 @@ const PictographChart = ({
18241
18840
  xAxisTitle
18242
18841
  ] }) }) });
18243
18842
  };
18843
+ const GroupSlot = ({
18844
+ group,
18845
+ series,
18846
+ axis,
18847
+ snap,
18848
+ orientation = "vertical",
18849
+ values,
18850
+ validationDetails,
18851
+ onChange
18852
+ }) => {
18853
+ return /* @__PURE__ */ jsx("div", { className: "chart-group", children: series.map((s) => {
18854
+ const bar = group.bars.find((b) => b.seriesId === s.id);
18855
+ if (!bar) {
18856
+ return /* @__PURE__ */ jsx("div", { className: "chart-group-bar-gap", "aria-hidden": "true" }, s.id);
18857
+ }
18858
+ const currentValue = resolveValue(bar, values);
18859
+ const vStatus = bar.answer !== void 0 ? validationDetails?.[bar.id] : void 0;
18860
+ const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
18861
+ return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: bar.answer !== void 0 ? /* @__PURE__ */ jsx(
18862
+ DraggableBar,
18863
+ {
18864
+ bar: { ...bar, answer: bar.answer },
18865
+ currentValue,
18866
+ axis,
18867
+ snap,
18868
+ validationStatus,
18869
+ orientation,
18870
+ onChange
18871
+ }
18872
+ ) : /* @__PURE__ */ jsx(
18873
+ StaticBar,
18874
+ {
18875
+ bar,
18876
+ currentValue,
18877
+ axis,
18878
+ validationStatus,
18879
+ orientation
18880
+ }
18881
+ ) }, bar.id);
18882
+ }) });
18883
+ };
18884
+ const EMPTY_SET$1 = /* @__PURE__ */ new Set();
18885
+ const ChartLegend = ({
18886
+ series,
18887
+ widgets,
18888
+ values,
18889
+ validationDetails,
18890
+ onChange
18891
+ }) => {
18892
+ return /* @__PURE__ */ jsx("div", { className: "chart-legend", children: series.map((s) => /* @__PURE__ */ jsxs("div", { className: "chart-legend-item", children: [
18893
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-swatch", "aria-hidden": "true", style: { backgroundColor: s.color } }),
18894
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-label", children: s.label?.map((subItem) => /* @__PURE__ */ jsx(
18895
+ LayoutRenderer,
18896
+ {
18897
+ item: subItem,
18898
+ widgets,
18899
+ values,
18900
+ droppedInputs: EMPTY_SET$1,
18901
+ validationDetails,
18902
+ onChange
18903
+ },
18904
+ subItem.id
18905
+ )) })
18906
+ ] }, s.id)) });
18907
+ };
18908
+ const EMPTY_SET = /* @__PURE__ */ new Set();
18909
+ const GroupedBarChart = ({
18910
+ item,
18911
+ widgets,
18912
+ values,
18913
+ validationDetails,
18914
+ onChange
18915
+ }) => {
18916
+ if (item.chartType !== "grouped-bar") return null;
18917
+ const { groups, series, grid } = item;
18918
+ const isHorizontal = item.orientation === "horizontal";
18919
+ const isInteractive = groups.some((g) => g.bars.some((bar) => bar.answer !== void 0));
18920
+ const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
18921
+ const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
18922
+ const chartClass = getChartClassName(item);
18923
+ const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
18924
+ const renderAxisTitle = (title) => {
18925
+ if (!title?.length) return null;
18926
+ return /* @__PURE__ */ jsx(Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsx(
18927
+ LayoutRenderer,
18928
+ {
18929
+ item: subItem,
18930
+ widgets,
18931
+ values,
18932
+ droppedInputs: EMPTY_SET,
18933
+ validationDetails,
18934
+ onChange
18935
+ },
18936
+ subItem.id
18937
+ )) });
18938
+ };
18939
+ const renderGroupSlots = (orient) => /* @__PURE__ */ jsx("div", { className: "chart-groups", children: groups.map((group) => /* @__PURE__ */ jsx(
18940
+ GroupSlot,
18941
+ {
18942
+ group,
18943
+ series,
18944
+ axis: fillAxis,
18945
+ snap: item.snap,
18946
+ orientation: orient,
18947
+ values,
18948
+ validationDetails,
18949
+ onChange
18950
+ },
18951
+ group.id
18952
+ )) });
18953
+ const legend = item.legend ? /* @__PURE__ */ jsx(
18954
+ ChartLegend,
18955
+ {
18956
+ series,
18957
+ widgets,
18958
+ values,
18959
+ validationDetails,
18960
+ onChange
18961
+ }
18962
+ ) : null;
18963
+ if (isHorizontal) {
18964
+ const chartStyle2 = computeGroupedChartStyle(item.scale, groups.length, series.length, "horizontal", isInteractive);
18965
+ return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
18966
+ item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
18967
+ categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
18968
+ /* @__PURE__ */ jsx("div", { className: "chart-axis-y--categories", children: groups.map((group) => /* @__PURE__ */ jsx("div", { className: "chart-bar-y-label", children: group.label?.map((subItem) => /* @__PURE__ */ jsx(
18969
+ LayoutRenderer,
18970
+ {
18971
+ item: subItem,
18972
+ widgets,
18973
+ values,
18974
+ droppedInputs: EMPTY_SET,
18975
+ validationDetails,
18976
+ onChange
18977
+ },
18978
+ subItem.id
18979
+ )) }, group.id)) }),
18980
+ /* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
18981
+ grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis, orientation: "horizontal" }),
18982
+ renderGroupSlots("horizontal")
18983
+ ] }),
18984
+ valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) }),
18985
+ legend
18986
+ ] }) }) });
18987
+ }
18988
+ const chartStyle = computeGroupedChartStyle(item.scale, groups.length, series.length, "vertical", isInteractive);
18989
+ const hasAnyGroupLabel = groups.some((g) => g.label && g.label.length > 0);
18990
+ const groupLabelRow = hasAnyGroupLabel ? /* @__PURE__ */ jsx("div", { className: "chart-axis-x", children: groups.map((group) => /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x-inner", children: group.label?.map((subItem) => /* @__PURE__ */ jsx(
18991
+ LayoutRenderer,
18992
+ {
18993
+ item: subItem,
18994
+ widgets,
18995
+ values,
18996
+ droppedInputs: EMPTY_SET,
18997
+ validationDetails,
18998
+ onChange
18999
+ },
19000
+ subItem.id
19001
+ )) }) }, group.id)) }) : null;
19002
+ const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
19003
+ return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
19004
+ item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
19005
+ valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
19006
+ valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(YAxis, { axis: valueAxis }),
19007
+ /* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
19008
+ grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis }),
19009
+ renderGroupSlots("vertical")
19010
+ ] }),
19011
+ groupLabelRow,
19012
+ xAxisTitle,
19013
+ legend
19014
+ ] }) }) });
19015
+ };
18244
19016
  const CHART_TYPE_COMPONENTS = {
18245
19017
  bar: BarChart,
18246
- pictograph: PictographChart
19018
+ pictograph: PictographChart,
19019
+ "grouped-bar": GroupedBarChart
18247
19020
  // line: LineChart,
18248
19021
  // pie: PieChart,
18249
19022
  };
@@ -18285,11 +19058,12 @@ const RadioOptions = ({
18285
19058
  };
18286
19059
  const Btn = ({
18287
19060
  label,
19061
+ dataKey = label,
18288
19062
  extraClass = "keypad-btn-digit",
18289
19063
  onClick
18290
19064
  }) => {
18291
19065
  const locked = useExerciseLocked();
18292
- 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 }) });
18293
19067
  };
18294
19068
  const DiamondKeypad = ({ value, onChange }) => {
18295
19069
  const playSound = useSound();
@@ -18301,7 +19075,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18301
19075
  playSound("keypad-press");
18302
19076
  onChange(value.slice(0, -1));
18303
19077
  };
18304
- return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", children: [
19078
+ return /* @__PURE__ */ jsxs("div", { className: "diamond-keypad", "data-tutorial": "keypad", children: [
18305
19079
  /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
18306
19080
  /* @__PURE__ */ jsx(Btn, { label: "1", onClick: () => append("1") }),
18307
19081
  /* @__PURE__ */ jsx(Btn, { label: "2", onClick: () => append("2") }),
@@ -18320,7 +19094,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18320
19094
  ] }),
18321
19095
  /* @__PURE__ */ jsxs("div", { className: "diamond-keypad-row", children: [
18322
19096
  /* @__PURE__ */ jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
18323
- /* @__PURE__ */ jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
19097
+ /* @__PURE__ */ jsx(Btn, { label: "⌫", dataKey: "backspace", extraClass: "keypad-btn-backspace", onClick: backspace })
18324
19098
  ] })
18325
19099
  ] });
18326
19100
  };
@@ -18926,7 +19700,7 @@ function isGeometryWidget(widget) {
18926
19700
  return widget.type === JSON_TYPES.GEOMETRY;
18927
19701
  }
18928
19702
  const GeometryWidget = React.lazy(async () => ({
18929
- default: (await import("./GeometryWidget-BRV6LLcW.mjs")).GeometryWidget
19703
+ default: (await import("./GeometryWidget-2TCLbTEv.mjs")).GeometryWidget
18930
19704
  }));
18931
19705
  const WidgetRenderer = ({
18932
19706
  widget,
@@ -18954,8 +19728,9 @@ const WidgetRenderer = ({
18954
19728
  isDropped,
18955
19729
  validationStatus,
18956
19730
  inputType: widget.inputType,
18957
- length: widget.inputType === "string" ? widget.length : void 0,
18958
- 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,
18959
19734
  placeholder: widget.placeholder,
18960
19735
  disabled: widget.disabled,
18961
19736
  onChange,
@@ -19326,6 +20101,13 @@ const EPSILON = 10 ** -INTERNAL_PRECISION;
19326
20101
  const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
19327
20102
  const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
19328
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
+ };
19329
20111
  const toPercent = (value, min, max) => {
19330
20112
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
19331
20113
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -20373,7 +21155,7 @@ const NumberLineRenderer = ({
20373
21155
  max,
20374
21156
  step,
20375
21157
  minorTicks = 0,
20376
- style: lineStyle = "arrow-right",
21158
+ style: lineStyle = DEFAULT_NUMBER_LINE_STYLE,
20377
21159
  labelVisibility,
20378
21160
  highlights,
20379
21161
  arrows,
@@ -20437,8 +21219,9 @@ const NumberLineRenderer = ({
20437
21219
  () => getMinorTickValues(majorTicks, minorTicks),
20438
21220
  [majorTicks, minorTicks]
20439
21221
  );
20440
- const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
20441
- 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";
20442
21225
  const hasDragOptions = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
20443
21226
  const isAxisDragMode = hasDragOptions && item.dragMode === "axis";
20444
21227
  const inputTickSet = useMemo(() => {
@@ -20486,7 +21269,7 @@ const NumberLineRenderer = ({
20486
21269
  return /* @__PURE__ */ jsxs(
20487
21270
  "div",
20488
21271
  {
20489
- 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}` : ""}`,
20490
21273
  style: {
20491
21274
  [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
20492
21275
  ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
@@ -20859,6 +21642,35 @@ function groupLineItems(items) {
20859
21642
  }
20860
21643
  return groups;
20861
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
+ }
20862
21674
  const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
20863
21675
  const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
20864
21676
  const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
@@ -20982,21 +21794,28 @@ const LayoutRenderer = ({
20982
21794
  subItem.id
20983
21795
  )) });
20984
21796
  case JSON_TYPES.TABLE: {
20985
- const totalRows = item.rows.length;
21797
+ const placements = placeTableCells(item.rows, item.cols);
20986
21798
  const tableStyle = {
20987
- "--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
20988
21801
  };
20989
- 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) => {
20990
- const rowspan = cell.rowspan || 1;
20991
- 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("");
20992
21808
  return /* @__PURE__ */ jsx(
20993
- "td",
21809
+ "div",
20994
21810
  {
20995
- className: `exercise-table-cell${isLastRow ? " exercise-table-cell--last-row" : ""}`,
20996
- colSpan: cell.colspan || 1,
20997
- rowSpan: rowspan,
20998
- style: { backgroundColor: cell.backgroundColor },
20999
- 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(
21000
21819
  LayoutRenderer,
21001
21820
  {
21002
21821
  item: subItem,
@@ -21009,11 +21828,11 @@ const LayoutRenderer = ({
21009
21828
  resetKey
21010
21829
  },
21011
21830
  subItem.id
21012
- ))
21831
+ )) })
21013
21832
  },
21014
21833
  cell.id
21015
21834
  );
21016
- }) }, row.id)) }) }) });
21835
+ }) }, row.id)) }) });
21017
21836
  }
21018
21837
  case JSON_TYPES.NUMBER_LINE:
21019
21838
  return /* @__PURE__ */ jsx(
@@ -21183,6 +22002,394 @@ const LayoutRenderer = ({
21183
22002
  return null;
21184
22003
  }
21185
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
+ }
21186
22393
  const TOOL_ASPECT_RATIO = {
21187
22394
  ruler: RULER_ASPECT_RATIO,
21188
22395
  protractor: PROTRACTOR_ASPECT_RATIO,
@@ -21221,6 +22428,11 @@ function buildInitialValues(json) {
21221
22428
  function toBaseId(widgetId) {
21222
22429
  return parseFractionKey(widgetId)?.widgetId ?? widgetId;
21223
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
+ }
21224
22436
  function buildInitialValuesSignature(json) {
21225
22437
  return JSON.stringify({
21226
22438
  widgets: json.widgets,
@@ -21270,6 +22482,27 @@ function buildRevealedValues(json) {
21270
22482
  }
21271
22483
  return revealed;
21272
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
+ }
21273
22506
  const ExerciseRenderer = forwardRef(({
21274
22507
  json,
21275
22508
  onSubmit,
@@ -21292,6 +22525,8 @@ const ExerciseRenderer = forwardRef(({
21292
22525
  const rootRef = useRef(null);
21293
22526
  const [resetKey, setResetKey] = useState(0);
21294
22527
  const [revealed, setRevealed] = useState(false);
22528
+ const tutorial = useMemo(() => findWidgetTutorial(json), [json]);
22529
+ const [tutorialRun, setTutorialRun] = useState(null);
21295
22530
  const themeStyle = useMemo(() => {
21296
22531
  const style = {};
21297
22532
  if (theme?.correct) style["--exercise-correct"] = theme.correct;
@@ -21312,6 +22547,7 @@ const ExerciseRenderer = forwardRef(({
21312
22547
  setResetKey((k) => k + 1);
21313
22548
  setLoadedValuesSignature(initialValuesSignature);
21314
22549
  touchedRef.current = /* @__PURE__ */ new Set();
22550
+ setTutorialRun(null);
21315
22551
  }, [json, initialValuesSignature, loadedValuesSignature]);
21316
22552
  const locked = validationDetails !== null || externallyLocked;
21317
22553
  const lastResultRef = useRef(null);
@@ -21395,7 +22631,7 @@ const ExerciseRenderer = forwardRef(({
21395
22631
  const next = { ...valuesRef.current, ...revealedValues };
21396
22632
  valuesRef.current = next;
21397
22633
  setValues(next);
21398
- setDroppedSources(/* @__PURE__ */ new Map());
22634
+ setDroppedSources(buildRevealedDropSources(json, revealedValues));
21399
22635
  setValidationDetails(details);
21400
22636
  setRevealed(true);
21401
22637
  setResetKey((k) => k + 1);
@@ -21403,20 +22639,29 @@ const ExerciseRenderer = forwardRef(({
21403
22639
  focusFirstInput: () => {
21404
22640
  const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
21405
22641
  if (!container) return;
21406
- const candidates = Array.from(
21407
- container.querySelectorAll('input[type="text"], input[type="number"], textarea, select')
21408
- );
21409
- const canTakeInput = (el) => !el.readOnly && !el.disabled;
22642
+ const candidates = getTextInputs(container);
21410
22643
  const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
21411
22644
  target?.focus();
21412
- }
21413
- }), [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
+ };
21414
22658
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
21415
22659
  if (locked) return;
21416
22660
  if (!silent) {
21417
22661
  setRevealed(false);
21418
22662
  touchedRef.current.add(toBaseId(widgetId));
21419
22663
  }
22664
+ const previous = valuesRef.current[widgetId];
21420
22665
  const next = { ...valuesRef.current, [widgetId]: value };
21421
22666
  valuesRef.current = next;
21422
22667
  setValues(next);
@@ -21430,6 +22675,9 @@ const ExerciseRenderer = forwardRef(({
21430
22675
  return next2;
21431
22676
  });
21432
22677
  onChange?.(next);
22678
+ if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22679
+ focusNextInput(widgetId);
22680
+ }
21433
22681
  };
21434
22682
  const handleSubmit = () => {
21435
22683
  runSubmit();
@@ -21544,7 +22792,7 @@ const ExerciseRenderer = forwardRef(({
21544
22792
  const rulerWidget = json.widgets[RULER_WIDGET_ID];
21545
22793
  const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
21546
22794
  const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
21547
- const numericKeypadWidget = json.widgets["_numeric_keypad"];
22795
+ const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
21548
22796
  const { tools, toolsMinHeight } = useMemo(() => {
21549
22797
  const list = [];
21550
22798
  const PAD = 12;
@@ -21563,7 +22811,7 @@ const ExerciseRenderer = forwardRef(({
21563
22811
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
21564
22812
  return { tools: list, toolsMinHeight: maxH };
21565
22813
  }, [rulerWidget, protractorWidget, setSquareWidget]);
21566
- 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: [
21567
22815
  /* @__PURE__ */ jsxs(
21568
22816
  "div",
21569
22817
  {
@@ -21659,7 +22907,21 @@ const ExerciseRenderer = forwardRef(({
21659
22907
  }
21660
22908
  }
21661
22909
  ) }),
21662
- 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
+ )
21663
22925
  ] }) }) }) });
21664
22926
  });
21665
22927
  ExerciseRenderer.displayName = "ExerciseRenderer";
@@ -21728,7 +22990,7 @@ export {
21728
22990
  isTableRowItem as Z,
21729
22991
  isTableCellItem as _,
21730
22992
  compileWithAST as a,
21731
- EngineError as a$,
22993
+ validateExerciseJSONSafe as a$,
21732
22994
  isVerticalOperationItem as a0,
21733
22995
  isClockItem as a1,
21734
22996
  isRulerWidget as a2,
@@ -21747,25 +23009,25 @@ export {
21747
23009
  ChartAxisJSONSchema as aF,
21748
23010
  ChartBarJSONSchema as aG,
21749
23011
  ChartPictoJSONSchema as aH,
21750
- WidgetJSONSchema as aI,
21751
- InlineInputJSONSchema as aJ,
21752
- DragOptionsJSONSchema as aK,
21753
- RadioOptionsJSONSchema as aL,
21754
- NumericKeypadJSONSchema as aM,
21755
- FractionJSONSchema as aN,
21756
- MatcherWidgetJSONSchema as aO,
21757
- PairMatcherWidgetJSONSchema as aP,
21758
- GridSymmetryJSONSchema as aQ,
21759
- ChartBarWidgetJSONSchema as aR,
21760
- ChartPictoWidgetJSONSchema as aS,
21761
- RulerWidgetSchema as aT,
21762
- ProtractorWidgetSchema as aU,
21763
- SetSquareWidgetSchema as aV,
21764
- GeometryJSONSchema as aW,
21765
- validateExerciseJSON as aX,
21766
- validateExerciseJSONSafe as aY,
21767
- validateWidgetJSON as aZ,
21768
- validateWidgetJSONSafe as a_,
23012
+ ChartSeriesJSONSchema as aI,
23013
+ ChartSeriesBarJSONSchema as aJ,
23014
+ ChartGroupJSONSchema as aK,
23015
+ WidgetJSONSchema as aL,
23016
+ InlineInputJSONSchema as aM,
23017
+ DragOptionsJSONSchema as aN,
23018
+ RadioOptionsJSONSchema as aO,
23019
+ NumericKeypadJSONSchema as aP,
23020
+ FractionJSONSchema as aQ,
23021
+ MatcherWidgetJSONSchema as aR,
23022
+ PairMatcherWidgetJSONSchema as aS,
23023
+ GridSymmetryJSONSchema as aT,
23024
+ ChartBarWidgetJSONSchema as aU,
23025
+ ChartPictoWidgetJSONSchema as aV,
23026
+ RulerWidgetSchema as aW,
23027
+ ProtractorWidgetSchema as aX,
23028
+ SetSquareWidgetSchema as aY,
23029
+ GeometryJSONSchema as aZ,
23030
+ validateExerciseJSON as a_,
21769
23031
  QuestionTextSchema as aa,
21770
23032
  TextItemSchema as ab,
21771
23033
  WidgetRefSchema as ac,
@@ -21793,178 +23055,196 @@ export {
21793
23055
  VopSeparatorItemSchema as ay,
21794
23056
  VopCellItemSchema as az,
21795
23057
  tokenizeSafe as b,
21796
- MatcherItemSchema as b$,
21797
- LexerError as b0,
21798
- ParseError as b1,
21799
- TransformError as b2,
21800
- ValidationError as b3,
21801
- ExerciseRenderer as b4,
21802
- LayoutRenderer as b5,
21803
- WidgetRenderer as b6,
21804
- renderExercise as b7,
21805
- SoundProvider as b8,
21806
- useSound as b9,
21807
- createHighlightedText as bA,
21808
- createColorText as bB,
21809
- createBoldText as bC,
21810
- createStringInput as bD,
21811
- createNumericInput as bE,
21812
- createImage as bF,
21813
- createFraction as bG,
21814
- createVisualFraction as bH,
21815
- createInlineDragOption as bI,
21816
- createDisplayClock as bJ,
21817
- createInteractiveClock as bK,
21818
- isFreeText as bL,
21819
- isSpace as bM,
21820
- isInlineInput as bN,
21821
- isFraction as bO,
21822
- isImage as bP,
21823
- isClock as bQ,
21824
- isInlineNode as bR,
21825
- isVisualFractionInline as bS,
21826
- isInlineDragOption as bT,
21827
- TextBlockSchema as bU,
21828
- LineBlockSchema as bV,
21829
- createTextBlock as bW,
21830
- createLineBlock as bX,
21831
- isTextBlock as bY,
21832
- isLineBlock as bZ,
21833
- DisplayInlineNodeSchema as b_,
21834
- ExerciseLockProvider as ba,
21835
- isPairMatcherWidget as bb,
21836
- isMatcherWidget as bc,
21837
- VISUAL_FRACTION_SHAPES as bd,
21838
- VISUAL_FRACTION_SHAPE as be,
21839
- VISUAL_FRACTION_DEFAULT_SIZE as bf,
21840
- VISUAL_FRACTION_MAX_DIVISIONS as bg,
21841
- FreeTextInlineSchema as bh,
21842
- SpaceInlineSchema as bi,
21843
- HighlightedTextInlineSchema as bj,
21844
- ColorTextInlineSchema as bk,
21845
- BoldTextInlineSchema as bl,
21846
- InlineInputWidgetSchema as bm,
21847
- FractionInlineSchema as bn,
21848
- ImageInlineSchema as bo,
21849
- VisualFractionInlineSchema as bp,
21850
- validateVisualFractionCapacity as bq,
21851
- InlineDragOptionSchema as br,
21852
- CLOCK_TYPES as bs,
21853
- CLOCK_MODES as bt,
21854
- CLOCK_FORMATS as bu,
21855
- CLOCK_TIME_REGEX as bv,
21856
- ClockInlineSchema as bw,
21857
- InlineNodeSchema as bx,
21858
- createFreeText as by,
21859
- createSpace as bz,
23058
+ TextBlockSchema as b$,
23059
+ validateWidgetJSON as b0,
23060
+ validateWidgetJSONSafe as b1,
23061
+ EngineError as b2,
23062
+ LexerError as b3,
23063
+ ParseError as b4,
23064
+ TransformError as b5,
23065
+ ValidationError as b6,
23066
+ ExerciseRenderer as b7,
23067
+ LayoutRenderer as b8,
23068
+ WidgetRenderer as b9,
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_,
23096
+ renderExercise as ba,
23097
+ SoundProvider as bb,
23098
+ useSound as bc,
23099
+ ExerciseLockProvider as bd,
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,
21860
23122
  compile as c,
21861
- createVopCarryRow as c$,
21862
- createMatcherItem as c0,
21863
- isMatcherItem as c1,
21864
- MatcherRowSchema as c2,
21865
- createMatcherRow as c3,
21866
- isMatcherRow as c4,
21867
- MatcherBlockSchema as c5,
21868
- createMatcherBlock as c6,
21869
- isMatcherBlock as c7,
21870
- PairMatcherBlockSchema as c8,
21871
- createPairMatcherBlock as c9,
21872
- isExpressionValidation as cA,
21873
- isMixedValidation as cB,
21874
- isValidationNode as cC,
21875
- ContentBlockSchema as cD,
21876
- ExerciseDocumentSchema as cE,
21877
- createExerciseDocument as cF,
21878
- isExerciseDocument as cG,
21879
- VOP_OPERATORS as cH,
21880
- normalizeVopOperator as cI,
21881
- VopDigitCellSchema as cJ,
21882
- VopLabelCellSchema as cK,
21883
- VopInputCellSchema as cL,
21884
- VopEmptyCellSchema as cM,
21885
- VopColorCellSchema as cN,
21886
- VopBoldCellSchema as cO,
21887
- VopHighlightCellSchema as cP,
21888
- VopCellSchema as cQ,
21889
- createVopDigitCell as cR,
21890
- createVopInputCell as cS,
21891
- createVopLabelCell as cT,
21892
- createVopEmptyCell as cU,
21893
- createVopColorCell as cV,
21894
- createVopBoldCell as cW,
21895
- createVopHighlightCell as cX,
21896
- VopRowSchema as cY,
21897
- createVopRow as cZ,
21898
- VopCarryRowSchema as c_,
21899
- isPairMatcherBlock as ca,
21900
- DragOptionsToolSchema as cb,
21901
- NumericKeypadToolSchema as cc,
21902
- RulerToolSchema as cd,
21903
- ProtractorToolSchema as ce,
21904
- SetSquareToolSchema as cf,
21905
- ToolNodeSchema as cg,
21906
- createDragOptions as ch,
21907
- createNumericKeypad as ci,
21908
- createRulerTool as cj,
21909
- createProtractorTool as ck,
21910
- createSetSquareTool as cl,
21911
- isDragOptions as cm,
21912
- isNumericKeypad as cn,
21913
- isRulerTool as co,
21914
- isProtractorTool as cp,
21915
- isSetSquareTool as cq,
21916
- isToolNode as cr,
21917
- SimpleValidationSchema as cs,
21918
- ExpressionValidationSchema as ct,
21919
- MixedValidationSchema as cu,
21920
- ValidationNodeSchema as cv,
21921
- createSimpleValidation as cw,
21922
- createExpressionValidation as cx,
21923
- createMixedValidation as cy,
21924
- isSimpleValidation 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,
21925
23187
  parse as d,
21926
- VopSeparatorSchema as d0,
21927
- createVopSeparator as d1,
21928
- VerticalOperationBlockSchema as d2,
21929
- createVerticalOperationBlock as d3,
21930
- isVopCell as d4,
21931
- isVopRow as d5,
21932
- isVopCarryRow as d6,
21933
- isVopSeparator as d7,
21934
- isVerticalOperationBlock as d8,
21935
- GRID_SYMMETRY_AXES as d9,
21936
- ChartBlockSchema as dA,
21937
- resolveChartAxes as dB,
21938
- createChartBlock as dC,
21939
- isChartBlock as dD,
21940
- GeometryBlockSchema as dE,
21941
- createGeometryBlock as dF,
21942
- isGeometryBlock as dG,
21943
- GRID_SYMMETRY_INTERACTIVE_AREAS as da,
21944
- GRID_SYMMETRY_MAX_ROWS as db,
21945
- GRID_SYMMETRY_MAX_COLS as dc,
21946
- GRID_SYMMETRY_MIN_CELL_SIZE as dd,
21947
- GRID_SYMMETRY_MAX_CELL_SIZE as de,
21948
- isCellInInteractiveArea as df,
21949
- reflectGridSymmetryCell as dg,
21950
- computeGridSymmetryExpectedCells as dh,
21951
- computeGridSymmetryCellsToFill as di,
21952
- GridSymmetryBlockSchema as dj,
21953
- createGridSymmetryBlock as dk,
21954
- isGridSymmetryBlock as dl,
21955
- CHART_TYPES as dm,
21956
- CHART_AXIS_DEFAULTS as dn,
21957
- CHART_MAX_AXIS_TICKS as dp,
21958
- CHART_MAX_PICTO_SLOTS as dq,
21959
- ChartAxisSchema as dr,
21960
- createChartAxis as ds,
21961
- isChartAxis as dt,
21962
- ChartPictoSchema as du,
21963
- createChartPicto as dv,
21964
- isChartPicto as dw,
21965
- ChartBarSchema as dx,
21966
- createChartBar as dy,
21967
- isChartBar 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,
21968
23248
  parseSafe as e,
21969
23249
  toAST as f,
21970
23250
  getFractionKey as g,
@@ -21988,4 +23268,4 @@ export {
21988
23268
  extractVariables as y,
21989
23269
  validateBooleanExpression as z
21990
23270
  };
21991
- //# sourceMappingURL=index-XAInSHXa.mjs.map
23271
+ //# sourceMappingURL=index-6z_8j1tM.mjs.map