@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.
@@ -101,6 +101,12 @@ const AST_TYPES = {
101
101
  CHART_BAR: "ChartBar",
102
102
  /** A single pictograph item inside a CHART[type="pictograph"] block */
103
103
  CHART_PICTO: "ChartPicto",
104
+ /** A named series (color + legend label) inside a CHART[type="grouped-bar"] block */
105
+ CHART_SERIES: "ChartSeries",
106
+ /** One category cluster inside a CHART[type="grouped-bar"] block */
107
+ CHART_GROUP: "ChartGroup",
108
+ /** A single bar inside a GROUP, referencing a declared SERIES by id */
109
+ CHART_SERIES_BAR: "ChartSeriesBar",
104
110
  // ═══════════════════════════════════════════════════════════════════════════
105
111
  // Tools
106
112
  // ═══════════════════════════════════════════════════════════════════════════
@@ -204,6 +210,10 @@ const JSON_TYPES = {
204
210
  CHART_BAR: "chart-bar",
205
211
  /** A single pictograph item rendered inside a chart */
206
212
  CHART_PICTO: "chart-picto",
213
+ /** A single bar rendered inside a grouped-bar chart's GROUP — distinct from
214
+ * CHART_BAR so the JSON self-identifies which shape it is (has `seriesId`),
215
+ * independent of the widget type used for validation. */
216
+ CHART_SERIES_BAR: "chart-series-bar",
207
217
  // ═══════════════════════════════════════════════════════════════════════════
208
218
  /** Inline input widget (string or numeric) */
209
219
  INLINE_INPUT: "inline-input",
@@ -231,6 +241,7 @@ const JSON_TYPES = {
231
241
  GEOMETRY: "geometry"
232
242
  };
233
243
  const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
244
+ const NUMERIC_KEYPAD_WIDGET_ID = "_numeric_keypad";
234
245
  const RULER_WIDGET_ID = "_ruler";
235
246
  const PROTRACTOR_WIDGET_ID = "_protractor";
236
247
  const SET_SQUARE_WIDGET_ID = "_set_square";
@@ -263,7 +274,8 @@ const INPUT_TYPES = {
263
274
  /** Numeric input (digits only) */
264
275
  NUMERIC: "numeric"
265
276
  };
266
- const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
277
+ const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line", "line-left"];
278
+ const DEFAULT_NUMBER_LINE_STYLE = "line-left";
267
279
  const INTERNAL_PRECISION = 9;
268
280
  const DISPLAY_PRECISION = 2;
269
281
  const SIDE_PAD = 7;
@@ -437,7 +449,7 @@ const MATCHER = chevrotain.createToken({
437
449
  });
438
450
  const GROUP = chevrotain.createToken({
439
451
  name: "GROUP",
440
- pattern: /GROUP/
452
+ pattern: /GROUP(?=\s*[[{])/
441
453
  });
442
454
  const ITEM = chevrotain.createToken({
443
455
  name: "ITEM",
@@ -487,6 +499,18 @@ const PICTO = chevrotain.createToken({
487
499
  name: "PICTO",
488
500
  pattern: /PICTO/
489
501
  });
502
+ const LABEL = chevrotain.createToken({
503
+ name: "LABEL",
504
+ pattern: /LABEL(?=\s*\{)/
505
+ });
506
+ const SERIES = chevrotain.createToken({
507
+ name: "SERIES",
508
+ pattern: /SERIES(?=\s*\[)/
509
+ });
510
+ const SERIES_BAR = chevrotain.createToken({
511
+ name: "SERIES_BAR",
512
+ pattern: /SERIES_BAR(?=\s*\[)/
513
+ });
490
514
  const SEP = chevrotain.createToken({
491
515
  name: "SEP",
492
516
  pattern: /SEP(?![a-zA-Z0-9_])/
@@ -685,6 +709,9 @@ const allTokens = [
685
709
  Y_AXIS,
686
710
  BAR,
687
711
  PICTO,
712
+ LABEL,
713
+ SERIES_BAR,
714
+ SERIES,
688
715
  SEP,
689
716
  CARRY,
690
717
  INPUT,
@@ -1771,22 +1798,15 @@ class MathExerciseParser extends chevrotain.CstParser {
1771
1798
  });
1772
1799
  this.MANY6({
1773
1800
  GATE: () => {
1774
- let k = 1;
1775
- while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
1776
- const t = this.LA(k).tokenType;
1777
- return t === BAR || t === PICTO;
1801
+ const t = this.LA(1).tokenType;
1802
+ return t === BAR || t === PICTO || t === SERIES || t === GROUP;
1778
1803
  },
1779
1804
  DEF: () => {
1780
1805
  this.OR6([
1781
- {
1782
- GATE: () => {
1783
- let k = 1;
1784
- while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
1785
- return this.LA(k).tokenType === BAR;
1786
- },
1787
- ALT: () => this.SUBRULE(this.chartBarItem)
1788
- },
1789
- { ALT: () => this.SUBRULE(this.chartPictoItem) }
1806
+ { GATE: () => this.LA(1).tokenType === BAR, ALT: () => this.SUBRULE(this.chartBarItem) },
1807
+ { GATE: () => this.LA(1).tokenType === PICTO, ALT: () => this.SUBRULE(this.chartPictoItem) },
1808
+ { GATE: () => this.LA(1).tokenType === SERIES, ALT: () => this.SUBRULE(this.chartSeriesItem) },
1809
+ { ALT: () => this.SUBRULE(this.chartGroupItem) }
1790
1810
  ]);
1791
1811
  }
1792
1812
  });
@@ -1907,6 +1927,92 @@ class MathExerciseParser extends chevrotain.CstParser {
1907
1927
  this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1908
1928
  });
1909
1929
  });
1930
+ this.chartSeriesItem = this.RULE("chartSeriesItem", () => {
1931
+ this.MANY(() => {
1932
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1933
+ });
1934
+ this.CONSUME(SERIES);
1935
+ this.CONSUME(LBracket);
1936
+ this.SUBRULE(this.parameterList);
1937
+ this.CONSUME(RBracket);
1938
+ this.MANY2(() => {
1939
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1940
+ });
1941
+ this.CONSUME(LBrace);
1942
+ this.MANY3(() => {
1943
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
1944
+ });
1945
+ this.MANY4(() => {
1946
+ this.OR4([
1947
+ { ALT: () => this.SUBRULE(this.inlineElement) },
1948
+ { ALT: () => this.CONSUME4(Newline) }
1949
+ ]);
1950
+ });
1951
+ this.CONSUME(RBrace);
1952
+ this.MANY5(() => {
1953
+ this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
1954
+ });
1955
+ });
1956
+ this.chartGroupLabel = this.RULE("chartGroupLabel", () => {
1957
+ this.CONSUME(LABEL);
1958
+ this.MANY(() => {
1959
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1960
+ });
1961
+ this.CONSUME(LBrace);
1962
+ this.MANY2(() => {
1963
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1964
+ });
1965
+ this.MANY3(() => {
1966
+ this.OR3([
1967
+ { ALT: () => this.SUBRULE(this.inlineElement) },
1968
+ { ALT: () => this.CONSUME3(Newline) }
1969
+ ]);
1970
+ });
1971
+ this.CONSUME(RBrace);
1972
+ this.MANY4(() => {
1973
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
1974
+ });
1975
+ });
1976
+ this.chartSeriesBarItem = this.RULE("chartSeriesBarItem", () => {
1977
+ this.MANY(() => {
1978
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1979
+ });
1980
+ this.CONSUME(SERIES_BAR);
1981
+ this.CONSUME(LBracket);
1982
+ this.SUBRULE(this.parameterList);
1983
+ this.CONSUME(RBracket);
1984
+ this.MANY2(() => {
1985
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1986
+ });
1987
+ });
1988
+ this.chartGroupItem = this.RULE("chartGroupItem", () => {
1989
+ this.MANY(() => {
1990
+ this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
1991
+ });
1992
+ this.CONSUME(GROUP);
1993
+ this.CONSUME(LBracket);
1994
+ this.SUBRULE(this.parameterList);
1995
+ this.CONSUME(RBracket);
1996
+ this.MANY2(() => {
1997
+ this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
1998
+ });
1999
+ this.CONSUME(LBrace);
2000
+ this.MANY3(() => {
2001
+ this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
2002
+ });
2003
+ this.MANY4({
2004
+ GATE: () => this.LA(1).tokenType === LABEL,
2005
+ DEF: () => this.SUBRULE(this.chartGroupLabel)
2006
+ });
2007
+ this.MANY5({
2008
+ GATE: () => this.LA(1).tokenType === SERIES_BAR,
2009
+ DEF: () => this.SUBRULE(this.chartSeriesBarItem)
2010
+ });
2011
+ this.CONSUME(RBrace);
2012
+ this.MANY6(() => {
2013
+ this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
2014
+ });
2015
+ });
1910
2016
  this.performSelfAnalysis();
1911
2017
  }
1912
2018
  /** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
@@ -1952,6 +2058,8 @@ const VISUAL_FRACTION_SHAPE = {
1952
2058
  };
1953
2059
  const VISUAL_FRACTION_DEFAULT_SIZE = 1.35;
1954
2060
  const VISUAL_FRACTION_MAX_DIVISIONS = 100;
2061
+ const INLINE_INPUT_MAX_WIDTH = 20;
2062
+ const InlineInputWidthSchema = zod.z.number().int().positive().max(INLINE_INPUT_MAX_WIDTH, { message: `width cannot exceed ${INLINE_INPUT_MAX_WIDTH}` }).optional();
1955
2063
  const FreeTextInlineSchema = zod.z.object({
1956
2064
  type: zod.z.literal(AST_TYPES.FREE_TEXT),
1957
2065
  value: zod.z.string()
@@ -1981,6 +2089,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
1981
2089
  name: zod.z.string(),
1982
2090
  inputType: zod.z.literal(INPUT_TYPES.STRING),
1983
2091
  length: zod.z.number().int().positive(),
2092
+ width: InlineInputWidthSchema,
1984
2093
  placeholder: zod.z.string().max(32).optional(),
1985
2094
  disabled: zod.z.boolean().optional(),
1986
2095
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
@@ -1990,6 +2099,7 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
1990
2099
  name: zod.z.string(),
1991
2100
  inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
1992
2101
  digits: zod.z.number().int().positive(),
2102
+ width: InlineInputWidthSchema,
1993
2103
  placeholder: zod.z.string().max(32).optional(),
1994
2104
  disabled: zod.z.boolean().optional(),
1995
2105
  answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
@@ -2147,7 +2257,7 @@ function createBoldText(value, highlight) {
2147
2257
  highlight
2148
2258
  });
2149
2259
  }
2150
- function createStringInput(name, length, answer, placeholder, disabled) {
2260
+ function createStringInput(name, length, answer, placeholder, disabled, width) {
2151
2261
  return parseOrThrow(
2152
2262
  InlineInputWidgetSchema,
2153
2263
  {
@@ -2155,6 +2265,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2155
2265
  name,
2156
2266
  inputType: INPUT_TYPES.STRING,
2157
2267
  length,
2268
+ width,
2158
2269
  placeholder,
2159
2270
  disabled,
2160
2271
  // A disabled input is never graded, so its answer is dropped.
@@ -2163,7 +2274,7 @@ function createStringInput(name, length, answer, placeholder, disabled) {
2163
2274
  "STRING_INPUT"
2164
2275
  );
2165
2276
  }
2166
- function createNumericInput(name, digits, answer, placeholder, disabled) {
2277
+ function createNumericInput(name, digits, answer, placeholder, disabled, width) {
2167
2278
  return parseOrThrow(
2168
2279
  InlineInputWidgetSchema,
2169
2280
  {
@@ -2171,6 +2282,7 @@ function createNumericInput(name, digits, answer, placeholder, disabled) {
2171
2282
  name,
2172
2283
  inputType: INPUT_TYPES.NUMERIC,
2173
2284
  digits,
2285
+ width,
2174
2286
  placeholder,
2175
2287
  disabled,
2176
2288
  // A disabled input is never graded, so its answer is dropped.
@@ -2387,7 +2499,7 @@ const NumberLineBlockSchema = zod.z.object({
2387
2499
  errorMap: () => ({
2388
2500
  message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
2389
2501
  })
2390
- }).optional(),
2502
+ }).default(DEFAULT_NUMBER_LINE_STYLE),
2391
2503
  labelVisibility: zod.z.enum(NUMBER_LINE_LABELS, {
2392
2504
  errorMap: () => ({
2393
2505
  message: `NUMBER_LINE: labelVisibility must be one of ${NUMBER_LINE_LABELS.join(", ")}`
@@ -3066,7 +3178,7 @@ function isSetSquareTool(value) {
3066
3178
  function isToolNode(value) {
3067
3179
  return ToolNodeSchema.safeParse(value).success;
3068
3180
  }
3069
- const CHART_TYPES = ["bar", "pictograph"];
3181
+ const CHART_TYPES = ["bar", "pictograph", "grouped-bar"];
3070
3182
  const CHART_AXIS_DEFAULTS = { min: 0, max: 100, step: 10, visible: true };
3071
3183
  const CHART_MAX_AXIS_TICKS = 200;
3072
3184
  const CHART_MAX_PICTO_SLOTS = 60;
@@ -3146,6 +3258,63 @@ function createChartBar(id, opts) {
3146
3258
  function isChartBar(value) {
3147
3259
  return ChartBarSchema.safeParse(value).success;
3148
3260
  }
3261
+ const ChartSeriesSchema = zod.z.object({
3262
+ type: zod.z.literal(AST_TYPES.CHART_SERIES),
3263
+ id: zod.z.string().min(1),
3264
+ color: zod.z.string().min(1),
3265
+ inlines: zod.z.array(DisplayInlineNodeSchema).default([])
3266
+ });
3267
+ function createChartSeries(id, opts) {
3268
+ return ChartSeriesSchema.parse({
3269
+ type: AST_TYPES.CHART_SERIES,
3270
+ id,
3271
+ color: opts.color,
3272
+ inlines: opts.inlines ?? []
3273
+ });
3274
+ }
3275
+ function isChartSeries(value) {
3276
+ return ChartSeriesSchema.safeParse(value).success;
3277
+ }
3278
+ const ChartSeriesBarSchema = zod.z.object({
3279
+ type: zod.z.literal(AST_TYPES.CHART_SERIES_BAR),
3280
+ id: zod.z.string().min(1),
3281
+ seriesId: zod.z.string().min(1),
3282
+ value: zod.z.number().default(0),
3283
+ answer: zod.z.number().optional()
3284
+ });
3285
+ function createChartSeriesBar(id, opts) {
3286
+ return ChartSeriesBarSchema.parse({
3287
+ type: AST_TYPES.CHART_SERIES_BAR,
3288
+ id,
3289
+ seriesId: opts.seriesId,
3290
+ value: opts.value ?? 0,
3291
+ answer: opts.answer
3292
+ });
3293
+ }
3294
+ function isChartSeriesBar(value) {
3295
+ return ChartSeriesBarSchema.safeParse(value).success;
3296
+ }
3297
+ const ChartGroupSchema = zod.z.object({
3298
+ type: zod.z.literal(AST_TYPES.CHART_GROUP),
3299
+ id: zod.z.string().min(1),
3300
+ labelInlines: zod.z.array(InlineNodeSchema).default([]),
3301
+ bars: zod.z.array(ChartSeriesBarSchema).default([])
3302
+ });
3303
+ function createChartGroup(id, opts = {}) {
3304
+ return ChartGroupSchema.parse({
3305
+ type: AST_TYPES.CHART_GROUP,
3306
+ id,
3307
+ labelInlines: opts.labelInlines ?? [],
3308
+ bars: opts.bars ?? []
3309
+ });
3310
+ }
3311
+ function isChartGroup(value) {
3312
+ return ChartGroupSchema.safeParse(value).success;
3313
+ }
3314
+ function alignGroupBarsToSeries(group, series) {
3315
+ const bySeriesId = new Map(group.bars.map((bar) => [bar.seriesId, bar]));
3316
+ return series.map((s) => bySeriesId.get(s.id));
3317
+ }
3149
3318
  const ChartBlockSchema = zod.z.object({
3150
3319
  type: zod.z.literal(AST_TYPES.CHART_BLOCK),
3151
3320
  name: zod.z.string().optional(),
@@ -3157,7 +3326,10 @@ const ChartBlockSchema = zod.z.object({
3157
3326
  xAxis: ChartAxisSchema.optional(),
3158
3327
  yAxis: ChartAxisSchema.optional(),
3159
3328
  bars: zod.z.array(ChartBarSchema).default([]),
3160
- pictos: zod.z.array(ChartPictoSchema).default([])
3329
+ pictos: zod.z.array(ChartPictoSchema).default([]),
3330
+ legend: zod.z.boolean().default(false),
3331
+ series: zod.z.array(ChartSeriesSchema).default([]),
3332
+ groups: zod.z.array(ChartGroupSchema).default([])
3161
3333
  }).superRefine((block, ctx) => {
3162
3334
  validateChartBlock(block, ctx);
3163
3335
  });
@@ -3182,10 +3354,10 @@ function validateChartBlock(block, ctx) {
3182
3354
  const { valueAxis } = resolveChartAxes(block);
3183
3355
  validateValueAxisRange(valueAxis, ctx);
3184
3356
  if (block.chartType === "bar") {
3185
- if (block.pictos.length > 0) {
3357
+ if (block.pictos.length > 0 || block.series.length > 0 || block.groups.length > 0) {
3186
3358
  ctx.addIssue({
3187
3359
  code: zod.z.ZodIssueCode.custom,
3188
- message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO',
3360
+ message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO, SERIES, or GROUP',
3189
3361
  path: ["pictos"]
3190
3362
  });
3191
3363
  return;
@@ -3198,16 +3370,16 @@ function validateChartBlock(block, ctx) {
3198
3370
  });
3199
3371
  return;
3200
3372
  }
3201
- validateUniqueIds(block.bars, ctx);
3373
+ validateUniqueIds(block.bars, ctx, (i) => ["bars", i]);
3202
3374
  validateChartSnap(block.snap, valueAxis.step, ctx);
3203
- validateBarAnswers(block.bars, valueAxis, block.snap, ctx);
3375
+ validateBarAnswers(block.bars, valueAxis, block.snap, ctx, (i) => ["bars", i]);
3204
3376
  return;
3205
3377
  }
3206
3378
  if (block.chartType === "pictograph") {
3207
- if (block.bars.length > 0) {
3379
+ if (block.bars.length > 0 || block.series.length > 0 || block.groups.length > 0) {
3208
3380
  ctx.addIssue({
3209
3381
  code: zod.z.ZodIssueCode.custom,
3210
- message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR',
3382
+ message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR, SERIES, or GROUP',
3211
3383
  path: ["bars"]
3212
3384
  });
3213
3385
  return;
@@ -3228,7 +3400,7 @@ function validateChartBlock(block, ctx) {
3228
3400
  });
3229
3401
  return;
3230
3402
  }
3231
- validateUniqueIds(block.pictos, ctx);
3403
+ validateUniqueIds(block.pictos, ctx, (i) => ["pictos", i]);
3232
3404
  if (!axisHasNumericParams(valueAxis.axis)) {
3233
3405
  ctx.addIssue({
3234
3406
  code: zod.z.ZodIssueCode.custom,
@@ -3249,6 +3421,63 @@ function validateChartBlock(block, ctx) {
3249
3421
  validatePictoAnswers(block.pictos, valueAxis, ctx);
3250
3422
  return;
3251
3423
  }
3424
+ if (block.chartType === "grouped-bar") {
3425
+ if (block.bars.length > 0 || block.pictos.length > 0) {
3426
+ ctx.addIssue({
3427
+ code: zod.z.ZodIssueCode.custom,
3428
+ message: 'CHART[type="grouped-bar"]: body must contain SERIES and GROUP items only, not BAR or PICTO',
3429
+ path: ["bars"]
3430
+ });
3431
+ return;
3432
+ }
3433
+ if (block.series.length === 0) {
3434
+ ctx.addIssue({
3435
+ code: zod.z.ZodIssueCode.custom,
3436
+ message: 'CHART[type="grouped-bar"]: at least one SERIES is required',
3437
+ path: ["series"]
3438
+ });
3439
+ return;
3440
+ }
3441
+ if (block.groups.length === 0) {
3442
+ ctx.addIssue({
3443
+ code: zod.z.ZodIssueCode.custom,
3444
+ message: 'CHART[type="grouped-bar"]: at least one GROUP is required',
3445
+ path: ["groups"]
3446
+ });
3447
+ return;
3448
+ }
3449
+ validateUniqueIds(block.series, ctx, (i) => ["series", i]);
3450
+ validateUniqueIds(block.groups, ctx, (i) => ["groups", i]);
3451
+ const allBars = block.groups.flatMap((g, gi) => g.bars.map((bar, bi) => ({ ...bar, groupIndex: gi, barIndex: bi })));
3452
+ const pathForFlatBar = (i) => ["groups", allBars[i].groupIndex, "bars", allBars[i].barIndex];
3453
+ validateUniqueIds(allBars, ctx, pathForFlatBar);
3454
+ const seriesIds = new Set(block.series.map((s) => s.id));
3455
+ block.groups.forEach((group, gi) => {
3456
+ const seenInGroup = /* @__PURE__ */ new Set();
3457
+ group.bars.forEach((bar, bi) => {
3458
+ if (!seriesIds.has(bar.seriesId)) {
3459
+ ctx.addIssue({
3460
+ code: zod.z.ZodIssueCode.custom,
3461
+ message: `CHART: GROUP "${group.id}" SERIES_BAR "${bar.id}" references unknown series "${bar.seriesId}" — no SERIES with that id is declared`,
3462
+ path: ["groups", gi, "bars", bi, "seriesId"]
3463
+ });
3464
+ return;
3465
+ }
3466
+ if (seenInGroup.has(bar.seriesId)) {
3467
+ ctx.addIssue({
3468
+ code: zod.z.ZodIssueCode.custom,
3469
+ message: `CHART: GROUP "${group.id}" references series "${bar.seriesId}" twice — each SERIES can appear at most once per GROUP`,
3470
+ path: ["groups", gi, "bars", bi, "seriesId"]
3471
+ });
3472
+ return;
3473
+ }
3474
+ seenInGroup.add(bar.seriesId);
3475
+ });
3476
+ });
3477
+ validateChartSnap(block.snap, valueAxis.step, ctx);
3478
+ validateBarAnswers(allBars, valueAxis, block.snap, ctx, pathForFlatBar);
3479
+ return;
3480
+ }
3252
3481
  ctx.addIssue({
3253
3482
  code: zod.z.ZodIssueCode.custom,
3254
3483
  message: `CHART: unsupported chartType "${block.chartType}" — add a validation branch to validateChartBlock`,
@@ -3269,14 +3498,14 @@ function resolveNumericAxis(axis) {
3269
3498
  function hasAmbiguousValueAxes(block) {
3270
3499
  return axisHasNumericParams(block.xAxis) && axisHasNumericParams(block.yAxis);
3271
3500
  }
3272
- function validateUniqueIds(items, ctx) {
3501
+ function validateUniqueIds(items, ctx, pathFor) {
3273
3502
  const seen = /* @__PURE__ */ new Set();
3274
3503
  items.forEach((item, index) => {
3275
3504
  if (seen.has(item.id)) {
3276
3505
  ctx.addIssue({
3277
3506
  code: zod.z.ZodIssueCode.custom,
3278
3507
  message: `CHART: duplicate id "${item.id}" — ids must be unique within the chart`,
3279
- path: [index, "id"]
3508
+ path: [...pathFor(index), "id"]
3280
3509
  });
3281
3510
  return;
3282
3511
  }
@@ -3318,14 +3547,14 @@ function validateChartSnap(snap, step, ctx) {
3318
3547
  });
3319
3548
  }
3320
3549
  }
3321
- function validateBarAnswers(bars, valueAxis, snap, ctx) {
3550
+ function validateBarAnswers(bars, valueAxis, snap, ctx, pathFor) {
3322
3551
  const gridStep = snap ?? valueAxis.step;
3323
3552
  bars.forEach((bar, index) => {
3324
3553
  if (bar.value < valueAxis.min || bar.value > valueAxis.max) {
3325
3554
  ctx.addIssue({
3326
3555
  code: zod.z.ZodIssueCode.custom,
3327
3556
  message: `CHART: BAR "${bar.id}" value (${bar.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the bar will not render correctly`,
3328
- path: ["bars", index, "value"]
3557
+ path: [...pathFor(index), "value"]
3329
3558
  });
3330
3559
  }
3331
3560
  if (bar.answer === void 0) return;
@@ -3333,7 +3562,7 @@ function validateBarAnswers(bars, valueAxis, snap, ctx) {
3333
3562
  ctx.addIssue({
3334
3563
  code: zod.z.ZodIssueCode.custom,
3335
3564
  message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
3336
- path: ["bars", index, "answer"]
3565
+ path: [...pathFor(index), "answer"]
3337
3566
  });
3338
3567
  return;
3339
3568
  }
@@ -3342,7 +3571,7 @@ function validateBarAnswers(bars, valueAxis, snap, ctx) {
3342
3571
  ctx.addIssue({
3343
3572
  code: zod.z.ZodIssueCode.custom,
3344
3573
  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.`,
3345
- path: ["bars", index, "answer"]
3574
+ path: [...pathFor(index), "answer"]
3346
3575
  });
3347
3576
  }
3348
3577
  });
@@ -3404,7 +3633,10 @@ function createChartBlock(chartType, opts = {}) {
3404
3633
  xAxis: opts.xAxis,
3405
3634
  yAxis: opts.yAxis,
3406
3635
  bars: opts.bars ?? [],
3407
- pictos: opts.pictos ?? []
3636
+ pictos: opts.pictos ?? [],
3637
+ series: opts.series ?? [],
3638
+ groups: opts.groups ?? [],
3639
+ legend: opts.legend ?? false
3408
3640
  });
3409
3641
  }
3410
3642
  function isChartBlock(value) {
@@ -3491,7 +3723,9 @@ const ExerciseDocumentSchema = zod.z.object({
3491
3723
  };
3492
3724
  const visitInlines = (inlines, basePath) => {
3493
3725
  inlines.forEach((inline, i) => {
3494
- if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE) {
3726
+ if (inline.type === AST_TYPES.INLINE_INPUT || inline.type === AST_TYPES.FRACTION || inline.type === AST_TYPES.IMAGE || inline.type === AST_TYPES.VISUAL_FRACTION) {
3727
+ record(inline.name, [...basePath, i, "name"]);
3728
+ } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
3495
3729
  record(inline.name, [...basePath, i, "name"]);
3496
3730
  }
3497
3731
  });
@@ -3523,6 +3757,28 @@ const ExerciseDocumentSchema = zod.z.object({
3523
3757
  pm.groups.flat().forEach((item, ii) => {
3524
3758
  visitInlines(item.inlines, ["lines", bi, "items", ii, "inlines"]);
3525
3759
  });
3760
+ } else if (block.type === AST_TYPES.CHART_BLOCK) {
3761
+ const chart = block;
3762
+ const base = ["lines", bi];
3763
+ chart.bars.forEach((bar, i) => {
3764
+ if (bar.answer !== void 0) record(bar.id, [...base, "bars", i, "id"]);
3765
+ visitInlines(bar.inlines, [...base, "bars", i, "inlines"]);
3766
+ });
3767
+ chart.pictos.forEach((picto, i) => {
3768
+ if (picto.answer !== void 0) record(picto.id, [...base, "pictos", i, "id"]);
3769
+ visitInlines(picto.inlines, [...base, "pictos", i, "inlines"]);
3770
+ });
3771
+ chart.groups.forEach((group, gi) => {
3772
+ group.bars.forEach((bar, i) => {
3773
+ if (bar.answer !== void 0) record(bar.id, [...base, "groups", gi, "bars", i, "id"]);
3774
+ });
3775
+ visitInlines(group.labelInlines, [...base, "groups", gi, "labelInlines"]);
3776
+ });
3777
+ chart.series.forEach((series, i) => {
3778
+ visitInlines(series.inlines, [...base, "series", i, "inlines"]);
3779
+ });
3780
+ if (chart.xAxis) visitInlines(chart.xAxis.inlines, [...base, "xAxis", "inlines"]);
3781
+ if (chart.yAxis) visitInlines(chart.yAxis.inlines, [...base, "yAxis", "inlines"]);
3526
3782
  } else if (block.type === AST_TYPES.RADIO_OPTIONS) {
3527
3783
  record(block.name, ["lines", bi, "name"]);
3528
3784
  } else if (block.type === AST_TYPES.GEOMETRY_BLOCK && block.name !== void 0) {
@@ -4076,12 +4332,19 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4076
4332
  `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": disabled must be true or false, got ${JSON.stringify(params.disabled)}`
4077
4333
  );
4078
4334
  }
4335
+ const width = params.width;
4336
+ const maxChars = ctx.stringInput ? params.length : params.digits;
4337
+ if (width !== void 0 && width < maxChars) {
4338
+ throw new Error(
4339
+ `${ctx.stringInput ? "STRING_INPUT" : "NUMERIC_INPUT"} "${name}": width (${width}) cannot be smaller than ${ctx.stringInput ? "length" : "digits"} (${maxChars})`
4340
+ );
4341
+ }
4079
4342
  if (ctx.stringInput) {
4080
4343
  const length = params.length;
4081
- return createStringInput(name, length, answer, placeholder, disabled);
4344
+ return createStringInput(name, length, answer, placeholder, disabled, width);
4082
4345
  }
4083
4346
  const digits = params.digits;
4084
- return createNumericInput(name, digits, answer, placeholder, disabled);
4347
+ return createNumericInput(name, digits, answer, placeholder, disabled, width);
4085
4348
  }
4086
4349
  inlineDragOption(ctx) {
4087
4350
  const valueToken = ctx.StringLiteral[0];
@@ -4094,7 +4357,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4094
4357
  chartBlock(ctx) {
4095
4358
  const params = this.visit(ctx.parameterList);
4096
4359
  const chartType = params.type;
4097
- if (!chartType) throw new Error('CHART: type is required ("bar" | "pictograph")');
4360
+ if (!chartType) throw new Error(`CHART: type is required (${CHART_TYPES.map((t) => `"${t}"`).join(" | ")})`);
4098
4361
  if (!CHART_TYPES.includes(chartType)) {
4099
4362
  throw new Error(`CHART: type must be one of ${CHART_TYPES.join(", ")} (got "${chartType}")`);
4100
4363
  }
@@ -4113,7 +4376,26 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4113
4376
  const yAxis = ctx.chartYAxis ? this.visit(ctx.chartYAxis) : void 0;
4114
4377
  const bars = ctx.chartBarItem ? ctx.chartBarItem.map((b) => this.visit(b)) : [];
4115
4378
  const pictos = ctx.chartPictoItem ? ctx.chartPictoItem.map((p) => this.visit(p)) : [];
4116
- return createChartBlock(chartType, { bars, pictos, name, title, grid, scale, snap, xAxis, yAxis });
4379
+ const series = ctx.chartSeriesItem ? ctx.chartSeriesItem.map((s) => this.visit(s)) : [];
4380
+ const groups = ctx.chartGroupItem ? ctx.chartGroupItem.map((g) => this.visit(g)) : [];
4381
+ if (params.legend !== void 0 && chartType !== "grouped-bar") {
4382
+ throw new Error(`CHART: "legend" is only supported for type="grouped-bar" (got "${chartType}")`);
4383
+ }
4384
+ const legend = params.legend === true || params.legend === "true";
4385
+ return createChartBlock(chartType, {
4386
+ bars,
4387
+ pictos,
4388
+ series,
4389
+ groups,
4390
+ legend,
4391
+ name,
4392
+ title,
4393
+ grid,
4394
+ scale,
4395
+ snap,
4396
+ xAxis,
4397
+ yAxis
4398
+ });
4117
4399
  }
4118
4400
  /** X_AXIS and Y_AXIS carry identical params — only the produced AST type differs.
4119
4401
  * Chevrotain needs two separate grammar rules, but one visitor body serves both. */
@@ -4152,6 +4434,52 @@ class NaturalDSLVisitor extends BaseCstVisitor {
4152
4434
  inlines
4153
4435
  });
4154
4436
  }
4437
+ chartSeriesItem(ctx) {
4438
+ const params = this.visit(ctx.parameterList);
4439
+ const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
4440
+ if (inlines.some((n) => !DisplayInlineNodeSchema.safeParse(n).success)) {
4441
+ throw new Error(
4442
+ `CHART: SERIES "${params.id}" label must be display-only — inputs, drag options, clocks and interactive fractions are not allowed in a legend label`
4443
+ );
4444
+ }
4445
+ if (!params.color) {
4446
+ throw new Error(`CHART: SERIES "${params.id}" requires "color"`);
4447
+ }
4448
+ return createChartSeries(params.id, {
4449
+ color: params.color,
4450
+ inlines
4451
+ });
4452
+ }
4453
+ chartGroupLabel(ctx) {
4454
+ return ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
4455
+ }
4456
+ chartGroupItem(ctx) {
4457
+ const params = this.visit(ctx.parameterList);
4458
+ if (ctx.chartGroupLabel && ctx.chartGroupLabel.length > 1) {
4459
+ throw new Error(`CHART: GROUP "${params.id}" has more than one LABEL — only one is allowed`);
4460
+ }
4461
+ const labelInlines = ctx.chartGroupLabel ? this.visit(ctx.chartGroupLabel) : [];
4462
+ const bars = ctx.chartSeriesBarItem ? ctx.chartSeriesBarItem.map((b) => this.visit(b)) : [];
4463
+ return createChartGroup(params.id, { labelInlines, bars });
4464
+ }
4465
+ // `params.series` is the raw DSL attribute name (SERIES_BAR[series="..."])
4466
+ // — unrelated to the AST field's own `seriesId` name.
4467
+ chartSeriesBarItem(ctx) {
4468
+ const params = this.visit(ctx.parameterList);
4469
+ if (!params.series) {
4470
+ throw new Error(`CHART: SERIES_BAR "${params.id}" requires "series"`);
4471
+ }
4472
+ if (params.color !== void 0) {
4473
+ throw new Error(
4474
+ `CHART: SERIES_BAR "${params.id}" cannot set "color" — it comes from its SERIES`
4475
+ );
4476
+ }
4477
+ return createChartSeriesBar(params.id, {
4478
+ seriesId: params.series,
4479
+ value: params.value !== void 0 ? Number(params.value) : void 0,
4480
+ answer: params.answer !== void 0 ? Number(params.answer) : void 0
4481
+ });
4482
+ }
4155
4483
  // ═══════════════════════════════════════════════════════════════════════════
4156
4484
  // Tools
4157
4485
  // ═══════════════════════════════════════════════════════════════════════════
@@ -4606,23 +4934,23 @@ const ChartAxisJSONSchema = zod.z.object({
4606
4934
  });
4607
4935
  }
4608
4936
  });
4609
- const ChartBarJSONSchema = zod.z.discriminatedUnion("interactive", [
4610
- zod.z.object({
4611
- id: zod.z.string(),
4612
- type: zod.z.literal(JSON_TYPES.CHART_BAR),
4613
- interactive: zod.z.literal(false),
4614
- value: zod.z.number(),
4615
- color: zod.z.string().optional(),
4616
- label: zod.z.array(LayoutItemSchema).optional()
4617
- }),
4618
- zod.z.object({
4619
- id: zod.z.string(),
4620
- type: zod.z.literal(JSON_TYPES.CHART_BAR),
4937
+ const ChartBarJSONBaseSchema = zod.z.object({
4938
+ id: zod.z.string(),
4939
+ type: zod.z.literal(JSON_TYPES.CHART_BAR),
4940
+ value: zod.z.number(),
4941
+ answer: zod.z.number().optional(),
4942
+ color: zod.z.string().optional(),
4943
+ label: zod.z.array(LayoutItemSchema).optional()
4944
+ });
4945
+ const ChartBarJSONSchema = zod.z.union([
4946
+ ChartBarJSONBaseSchema.extend({
4621
4947
  interactive: zod.z.literal(true),
4622
- value: zod.z.number(),
4623
- widgetId: zod.z.string(),
4624
- color: zod.z.string().optional(),
4625
- label: zod.z.array(LayoutItemSchema).optional()
4948
+ /** The widgets/answers key, equal to `id` when compiled. */
4949
+ widgetId: zod.z.string()
4950
+ }),
4951
+ ChartBarJSONBaseSchema.extend({
4952
+ interactive: zod.z.literal(false).optional(),
4953
+ widgetId: zod.z.undefined().optional()
4626
4954
  })
4627
4955
  ]);
4628
4956
  const ChartPictoJSONSchema = zod.z.object({
@@ -4636,21 +4964,55 @@ const ChartPictoJSONSchema = zod.z.object({
4636
4964
  ),
4637
4965
  label: zod.z.array(LayoutItemSchema).optional()
4638
4966
  });
4639
- const ChartItemSchema = zod.z.object({
4967
+ const ChartSeriesJSONSchema = zod.z.object({
4968
+ id: zod.z.string(),
4969
+ color: zod.z.string(),
4970
+ label: zod.z.array(LayoutItemSchema).optional()
4971
+ });
4972
+ const ChartSeriesBarJSONSchema = zod.z.object({
4973
+ id: zod.z.string(),
4974
+ type: zod.z.literal(JSON_TYPES.CHART_SERIES_BAR),
4975
+ value: zod.z.number(),
4976
+ answer: zod.z.number().optional(),
4977
+ color: zod.z.string(),
4978
+ seriesId: zod.z.string()
4979
+ });
4980
+ const ChartGroupJSONSchema = zod.z.object({
4981
+ id: zod.z.string(),
4982
+ label: zod.z.array(LayoutItemSchema).optional(),
4983
+ bars: zod.z.array(ChartSeriesBarJSONSchema)
4984
+ });
4985
+ const ChartItemBaseSchema = zod.z.object({
4640
4986
  id: zod.z.string(),
4641
4987
  type: zod.z.literal(JSON_TYPES.CHART),
4642
4988
  name: zod.z.string().optional(),
4643
4989
  title: zod.z.string().optional(),
4644
- chartType: zod.z.enum(CHART_TYPES),
4645
4990
  orientation: zod.z.enum(["vertical", "horizontal"]).default("vertical"),
4646
4991
  grid: zod.z.boolean().default(false),
4647
4992
  scale: zod.z.number().positive().default(1),
4648
4993
  snap: zod.z.number().positive().optional(),
4649
4994
  xAxis: ChartAxisJSONSchema.optional(),
4650
- yAxis: ChartAxisJSONSchema.optional(),
4651
- bars: zod.z.array(ChartBarJSONSchema).default([]),
4652
- pictos: zod.z.array(ChartPictoJSONSchema).default([])
4653
- }).superRefine((item, ctx) => {
4995
+ yAxis: ChartAxisJSONSchema.optional()
4996
+ });
4997
+ const ChartItemBarSchema = ChartItemBaseSchema.extend({
4998
+ chartType: zod.z.literal("bar"),
4999
+ bars: zod.z.array(ChartBarJSONSchema)
5000
+ });
5001
+ const ChartItemPictographSchema = ChartItemBaseSchema.extend({
5002
+ chartType: zod.z.literal("pictograph"),
5003
+ pictos: zod.z.array(ChartPictoJSONSchema)
5004
+ });
5005
+ const ChartItemGroupedBarSchema = ChartItemBaseSchema.extend({
5006
+ chartType: zod.z.literal("grouped-bar"),
5007
+ series: zod.z.array(ChartSeriesJSONSchema),
5008
+ groups: zod.z.array(ChartGroupJSONSchema),
5009
+ legend: zod.z.boolean().default(false)
5010
+ });
5011
+ const ChartItemSchema = zod.z.discriminatedUnion("chartType", [
5012
+ ChartItemBarSchema,
5013
+ ChartItemPictographSchema,
5014
+ ChartItemGroupedBarSchema
5015
+ ]).superRefine((item, ctx) => {
4654
5016
  if (item.chartType === "bar" && item.bars.length === 0) {
4655
5017
  ctx.addIssue({
4656
5018
  code: zod.z.ZodIssueCode.custom,
@@ -4658,18 +5020,63 @@ const ChartItemSchema = zod.z.object({
4658
5020
  path: ["bars"]
4659
5021
  });
4660
5022
  }
4661
- if (item.chartType === "pictograph" && item.pictos.length === 0) {
4662
- ctx.addIssue({
4663
- code: zod.z.ZodIssueCode.custom,
4664
- message: 'CHART[type="pictograph"]: at least one PICTO is required',
4665
- path: ["pictos"]
4666
- });
5023
+ if (item.chartType === "pictograph") {
5024
+ if (item.pictos.length === 0) {
5025
+ ctx.addIssue({
5026
+ code: zod.z.ZodIssueCode.custom,
5027
+ message: 'CHART[type="pictograph"]: at least one PICTO is required',
5028
+ path: ["pictos"]
5029
+ });
5030
+ }
5031
+ if (item.snap !== void 0) {
5032
+ ctx.addIssue({
5033
+ code: zod.z.ZodIssueCode.custom,
5034
+ message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
5035
+ path: ["snap"]
5036
+ });
5037
+ }
4667
5038
  }
4668
- if (item.chartType === "pictograph" && item.snap !== void 0) {
4669
- ctx.addIssue({
4670
- code: zod.z.ZodIssueCode.custom,
4671
- message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
4672
- path: ["snap"]
5039
+ if (item.chartType === "grouped-bar") {
5040
+ if (item.series.length === 0) {
5041
+ ctx.addIssue({
5042
+ code: zod.z.ZodIssueCode.custom,
5043
+ message: 'CHART[type="grouped-bar"]: at least one SERIES is required',
5044
+ path: ["series"]
5045
+ });
5046
+ }
5047
+ if (item.groups.length === 0) {
5048
+ ctx.addIssue({
5049
+ code: zod.z.ZodIssueCode.custom,
5050
+ message: 'CHART[type="grouped-bar"]: at least one GROUP is required',
5051
+ path: ["groups"]
5052
+ });
5053
+ }
5054
+ validateUniqueIds(item.series, ctx, (i) => ["series", i]);
5055
+ validateUniqueIds(item.groups, ctx, (i) => ["groups", i]);
5056
+ const allBars = item.groups.flatMap((g, gi) => g.bars.map((bar, bi) => ({ id: bar.id, gi, bi })));
5057
+ validateUniqueIds(allBars, ctx, (i) => ["groups", allBars[i].gi, "bars", allBars[i].bi]);
5058
+ const seriesIds = new Set(item.series.map((s) => s.id));
5059
+ item.groups.forEach((group, gi) => {
5060
+ const seenInGroup = /* @__PURE__ */ new Set();
5061
+ group.bars.forEach((bar, bi) => {
5062
+ if (!seriesIds.has(bar.seriesId)) {
5063
+ ctx.addIssue({
5064
+ code: zod.z.ZodIssueCode.custom,
5065
+ message: `CHART[type="grouped-bar"]: GROUP "${group.id}" bar "${bar.id}" references unknown seriesId "${bar.seriesId}" — no SERIES with that id is declared`,
5066
+ path: ["groups", gi, "bars", bi, "seriesId"]
5067
+ });
5068
+ return;
5069
+ }
5070
+ if (seenInGroup.has(bar.seriesId)) {
5071
+ ctx.addIssue({
5072
+ code: zod.z.ZodIssueCode.custom,
5073
+ message: `CHART[type="grouped-bar"]: GROUP "${group.id}" has two bars for seriesId "${bar.seriesId}" — each series can appear at most once per group`,
5074
+ path: ["groups", gi, "bars", bi, "seriesId"]
5075
+ });
5076
+ return;
5077
+ }
5078
+ seenInGroup.add(bar.seriesId);
5079
+ });
4673
5080
  });
4674
5081
  }
4675
5082
  });
@@ -4677,6 +5084,8 @@ const StringInputJSONSchema = zod.z.object({
4677
5084
  type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
4678
5085
  inputType: zod.z.literal(INPUT_TYPES.STRING),
4679
5086
  length: zod.z.number().int().positive(),
5087
+ /** Box width in characters; defaults to `length`. */
5088
+ width: InlineInputWidthSchema,
4680
5089
  placeholder: zod.z.string().max(32).optional(),
4681
5090
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
4682
5091
  disabled: zod.z.boolean().optional()
@@ -4685,6 +5094,8 @@ const NumericInputJSONSchema = zod.z.object({
4685
5094
  type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
4686
5095
  inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
4687
5096
  digits: zod.z.number().int().positive(),
5097
+ /** Box width in characters; defaults to `digits`. */
5098
+ width: InlineInputWidthSchema,
4688
5099
  placeholder: zod.z.string().max(32).optional(),
4689
5100
  /** Gray, non-interactive, never graded (any answer for it is ignored). */
4690
5101
  disabled: zod.z.boolean().optional()
@@ -5675,7 +6086,7 @@ lodash.exports;
5675
6086
  var reIsNative = RegExp2(
5676
6087
  "^" + funcToString.call(hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"
5677
6088
  );
5678
- var Buffer = moduleExports ? context.Buffer : undefined$1, Symbol = context.Symbol, Uint8Array = context.Uint8Array, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined$1, getPrototype = overArg(Object2.getPrototypeOf, Object2), objectCreate = Object2.create, propertyIsEnumerable = objectProto.propertyIsEnumerable, splice = arrayProto.splice, spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined$1, symIterator = Symbol ? Symbol.iterator : undefined$1, symToStringTag = Symbol ? Symbol.toStringTag : undefined$1;
6089
+ var Buffer = moduleExports ? context.Buffer : undefined$1, Symbol2 = context.Symbol, Uint8Array = context.Uint8Array, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined$1, getPrototype = overArg(Object2.getPrototypeOf, Object2), objectCreate = Object2.create, propertyIsEnumerable = objectProto.propertyIsEnumerable, splice = arrayProto.splice, spreadableSymbol = Symbol2 ? Symbol2.isConcatSpreadable : undefined$1, symIterator = Symbol2 ? Symbol2.iterator : undefined$1, symToStringTag = Symbol2 ? Symbol2.toStringTag : undefined$1;
5679
6090
  var defineProperty = function() {
5680
6091
  try {
5681
6092
  var func = getNative(Object2, "defineProperty");
@@ -5690,7 +6101,7 @@ lodash.exports;
5690
6101
  var metaMap = WeakMap && new WeakMap();
5691
6102
  var realNames = {};
5692
6103
  var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap);
5693
- var symbolProto = Symbol ? Symbol.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
6104
+ var symbolProto = Symbol2 ? Symbol2.prototype : undefined$1, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined$1, symbolToString = symbolProto ? symbolProto.toString : undefined$1;
5694
6105
  function lodash2(value) {
5695
6106
  if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) {
5696
6107
  if (value instanceof LodashWrapper) {
@@ -10589,6 +11000,7 @@ function toJSON(ast) {
10589
11000
  type: JSON_TYPES.INLINE_INPUT,
10590
11001
  inputType: INPUT_TYPES.STRING,
10591
11002
  length: inline.length,
11003
+ ...inline.width !== void 0 && { width: inline.width },
10592
11004
  placeholder: inline.placeholder,
10593
11005
  ...inline.disabled && { disabled: true }
10594
11006
  };
@@ -10597,6 +11009,7 @@ function toJSON(ast) {
10597
11009
  type: JSON_TYPES.INLINE_INPUT,
10598
11010
  inputType: INPUT_TYPES.NUMERIC,
10599
11011
  digits: inline.digits,
11012
+ ...inline.width !== void 0 && { width: inline.width },
10600
11013
  placeholder: inline.placeholder,
10601
11014
  ...inline.disabled && { disabled: true }
10602
11015
  };
@@ -10692,7 +11105,7 @@ function toJSON(ast) {
10692
11105
  options: lodashExports.shuffle(tool.options)
10693
11106
  };
10694
11107
  } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
10695
- widgets["_numeric_keypad"] = {
11108
+ widgets[NUMERIC_KEYPAD_WIDGET_ID] = {
10696
11109
  type: JSON_TYPES.NUMERIC_KEYPAD,
10697
11110
  includeOperators: tool.includeOperators,
10698
11111
  includeDecimal: tool.includeDecimal,
@@ -10768,6 +11181,8 @@ function collectAllInlines(blocks) {
10768
11181
  for (const picto of chart.pictos) result.push(...picto.inlines);
10769
11182
  if (chart.xAxis) result.push(...chart.xAxis.inlines);
10770
11183
  if (chart.yAxis) result.push(...chart.yAxis.inlines);
11184
+ for (const series of chart.series) result.push(...series.inlines);
11185
+ for (const group of chart.groups) result.push(...group.labelInlines);
10771
11186
  }
10772
11187
  }
10773
11188
  return result;
@@ -10802,27 +11217,19 @@ function transformChart(block, idGenerator, inlineDragIds) {
10802
11217
  const chartAnswers = {};
10803
11218
  const bars = block.bars.map((bar) => {
10804
11219
  const label = toLabel(bar.inlines);
10805
- if (bar.answer !== void 0) {
10806
- chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
10807
- chartAnswers[bar.id] = bar.answer;
10808
- return {
10809
- id: bar.id,
10810
- type: JSON_TYPES.CHART_BAR,
10811
- interactive: true,
10812
- value: bar.value,
10813
- widgetId: bar.id,
10814
- ...bar.color !== void 0 && { color: bar.color },
10815
- ...label !== void 0 && { label }
10816
- };
10817
- }
10818
- return {
11220
+ const common = {
10819
11221
  id: bar.id,
10820
11222
  type: JSON_TYPES.CHART_BAR,
10821
- interactive: false,
10822
11223
  value: bar.value,
10823
11224
  ...bar.color !== void 0 && { color: bar.color },
10824
11225
  ...label !== void 0 && { label }
10825
11226
  };
11227
+ if (bar.answer !== void 0) {
11228
+ chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
11229
+ chartAnswers[bar.id] = bar.answer;
11230
+ return { ...common, interactive: true, widgetId: bar.id, answer: bar.answer };
11231
+ }
11232
+ return { ...common, interactive: false };
10826
11233
  });
10827
11234
  const pictos = block.pictos.map((picto) => {
10828
11235
  const label = toLabel(picto.inlines);
@@ -10839,21 +11246,62 @@ function transformChart(block, idGenerator, inlineDragIds) {
10839
11246
  ...label !== void 0 && { label }
10840
11247
  };
10841
11248
  });
10842
- const item = {
11249
+ const series = block.series.map((s) => {
11250
+ const label = toLabel(s.inlines);
11251
+ return {
11252
+ id: s.id,
11253
+ color: s.color,
11254
+ ...label !== void 0 && { label }
11255
+ };
11256
+ });
11257
+ const groups = block.groups.map((group) => {
11258
+ const slots = alignGroupBarsToSeries(group, block.series);
11259
+ const groupBars = slots.map((bar, i) => {
11260
+ if (bar === void 0) return void 0;
11261
+ const series2 = block.series[i];
11262
+ if (bar.answer !== void 0) {
11263
+ chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
11264
+ chartAnswers[bar.id] = bar.answer;
11265
+ }
11266
+ return {
11267
+ id: bar.id,
11268
+ type: JSON_TYPES.CHART_SERIES_BAR,
11269
+ value: bar.value,
11270
+ ...bar.answer !== void 0 && { answer: bar.answer },
11271
+ color: series2.color,
11272
+ seriesId: series2.id
11273
+ };
11274
+ }).filter((bar) => bar !== void 0);
11275
+ const label = toLabel(group.labelInlines);
11276
+ return {
11277
+ id: group.id,
11278
+ ...label !== void 0 && { label },
11279
+ bars: groupBars
11280
+ };
11281
+ });
11282
+ const base = {
10843
11283
  id: idGenerator.generate(JSON_TYPES.CHART),
10844
11284
  type: JSON_TYPES.CHART,
10845
11285
  ...block.name !== void 0 && { name: block.name },
10846
11286
  ...block.title !== void 0 && { title: block.title },
10847
- chartType: block.chartType,
10848
11287
  orientation,
10849
11288
  grid: block.grid,
10850
11289
  scale: block.scale,
10851
11290
  ...block.snap !== void 0 && { snap: block.snap },
10852
11291
  ...xAxis !== void 0 && { xAxis },
10853
- ...yAxis !== void 0 && { yAxis },
10854
- bars,
10855
- pictos
11292
+ ...yAxis !== void 0 && { yAxis }
10856
11293
  };
11294
+ let item;
11295
+ if (block.chartType === "bar") {
11296
+ item = { ...base, chartType: "bar", bars };
11297
+ } else if (block.chartType === "pictograph") {
11298
+ item = { ...base, chartType: "pictograph", pictos };
11299
+ } else if (block.chartType === "grouped-bar") {
11300
+ item = { ...base, chartType: "grouped-bar", series, groups, legend: block.legend };
11301
+ } else {
11302
+ const unhandled = block.chartType;
11303
+ throw new Error(`CHART: unsupported chart type "${String(unhandled)}"`);
11304
+ }
10857
11305
  return { item, chartWidgets, chartAnswers };
10858
11306
  }
10859
11307
  function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
@@ -10920,7 +11368,7 @@ function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
10920
11368
  max: numberLine.max,
10921
11369
  step: numberLine.step,
10922
11370
  ...numberLine.minorTicks !== void 0 && { minorTicks: numberLine.minorTicks },
10923
- ...numberLine.style !== void 0 && { style: numberLine.style },
11371
+ style: numberLine.style,
10924
11372
  ...numberLine.labelVisibility !== void 0 && { labelVisibility: numberLine.labelVisibility },
10925
11373
  ...counterItem !== void 0 && { counter: counterItem },
10926
11374
  ...highlights !== void 0 && { highlights },
@@ -15577,22 +16025,22 @@ function createDefaultDropAnimation(options) {
15577
16025
  easing,
15578
16026
  fill: "forwards"
15579
16027
  });
15580
- return new Promise((resolve) => {
16028
+ return new Promise((resolve2) => {
15581
16029
  animation.onfinish = () => {
15582
16030
  cleanup == null ? void 0 : cleanup();
15583
- resolve();
16031
+ resolve2();
15584
16032
  };
15585
16033
  });
15586
16034
  };
15587
16035
  }
15588
- let key = 0;
16036
+ let key$1 = 0;
15589
16037
  function useKey(id) {
15590
16038
  return React.useMemo(() => {
15591
16039
  if (id == null) {
15592
16040
  return;
15593
16041
  }
15594
- key++;
15595
- return key;
16042
+ key$1++;
16043
+ return key$1;
15596
16044
  }, [id]);
15597
16045
  }
15598
16046
  const DragOverlay = /* @__PURE__ */ React.memo((_ref) => {
@@ -15674,6 +16122,7 @@ const DEFAULT_SOUNDS = {
15674
16122
  "drag-start": dragStart,
15675
16123
  "drag-drop": dragDrop,
15676
16124
  "drag-cancel": dragDrop,
16125
+ "drag-hint": buttonClick,
15677
16126
  "radio-select": buttonClick,
15678
16127
  "pair-select": buttonClick,
15679
16128
  "pair-link": buttonClick,
@@ -15777,14 +16226,24 @@ function useExerciseDnd() {
15777
16226
  }
15778
16227
  return ctx;
15779
16228
  }
16229
+ const NO_EXERCISE = Symbol("no-exercise");
15780
16230
  const ExerciseDndProvider = ({
15781
16231
  children,
15782
16232
  droppedSources,
15783
- onDragEnd
16233
+ onDragEnd,
16234
+ exerciseKey
15784
16235
  }) => {
15785
16236
  const [activeItem, setActiveItem] = React.useState(null);
16237
+ const [draggedExerciseKey, setDraggedExerciseKey] = React.useState(NO_EXERCISE);
16238
+ const hasDraggedOption = draggedExerciseKey === exerciseKey;
15786
16239
  const [dropHandlers] = React.useState(() => /* @__PURE__ */ new Map());
15787
16240
  const playSound = useSound();
16241
+ const hintSoundExerciseRef = React.useRef(NO_EXERCISE);
16242
+ const playHintSound = React.useCallback(() => {
16243
+ if (hintSoundExerciseRef.current === exerciseKey) return;
16244
+ hintSoundExerciseRef.current = exerciseKey;
16245
+ playSound("drag-hint");
16246
+ }, [playSound, exerciseKey]);
15788
16247
  const usedOptionIds = React.useMemo(() => {
15789
16248
  return new Set(droppedSources.values());
15790
16249
  }, [droppedSources]);
@@ -15808,6 +16267,7 @@ const ExerciseDndProvider = ({
15808
16267
  const raw = active.data.current;
15809
16268
  const item = raw?.type === "placed-chip" ? { type: "placed-chip", widgetId: raw.widgetId, label: raw.label, sourceOptionId: raw.sourceOptionId } : raw;
15810
16269
  setActiveItem(item);
16270
+ if (item.type === "drag-option") setDraggedExerciseKey(exerciseKey);
15811
16271
  playSound("drag-start");
15812
16272
  };
15813
16273
  const handleDragEnd = (event) => {
@@ -15833,6 +16293,9 @@ const ExerciseDndProvider = ({
15833
16293
  value: {
15834
16294
  activeItem,
15835
16295
  isDragging: activeItem !== null,
16296
+ exerciseKey,
16297
+ hasDraggedOption,
16298
+ playHintSound,
15836
16299
  usedOptionIds,
15837
16300
  droppedSources,
15838
16301
  registerDropHandler,
@@ -16613,9 +17076,40 @@ function isUndoDrop(event, dragId) {
16613
17076
  const LockContext = React.createContext(false);
16614
17077
  const ExerciseLockProvider = ({ locked, children }) => /* @__PURE__ */ jsxRuntime.jsx(LockContext.Provider, { value: locked, children });
16615
17078
  const useExerciseLocked = () => React.useContext(LockContext);
16616
- const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
16617
- const { droppedSources } = useExerciseDnd();
16618
- const locked = useExerciseLocked();
17079
+ const OVERLAY_STYLE = {
17080
+ position: "absolute",
17081
+ top: 0,
17082
+ left: 0,
17083
+ width: "100%",
17084
+ height: "100%",
17085
+ minWidth: 0,
17086
+ maxWidth: "none",
17087
+ margin: 0,
17088
+ boxSizing: "border-box"
17089
+ };
17090
+ const SlotSizer = ({
17091
+ style,
17092
+ ...props
17093
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
17094
+ "input",
17095
+ {
17096
+ ...props,
17097
+ type: "text",
17098
+ value: "",
17099
+ readOnly: true,
17100
+ tabIndex: -1,
17101
+ "aria-hidden": "true",
17102
+ style: { ...style, visibility: "hidden" }
17103
+ }
17104
+ );
17105
+ const PlacedInputChip = ({
17106
+ widgetId,
17107
+ value,
17108
+ className = "",
17109
+ onRemove
17110
+ }) => {
17111
+ const { droppedSources } = useExerciseDnd();
17112
+ const locked = useExerciseLocked();
16619
17113
  const sourceOptionId = droppedSources.get(widgetId);
16620
17114
  const dragId = `${widgetId}__chip`;
16621
17115
  const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
@@ -16637,13 +17131,41 @@ const PlacedInputChip = ({ widgetId, value, className = "", onRemove }) => {
16637
17131
  if (!locked && e.detail !== 0) onRemove();
16638
17132
  },
16639
17133
  title: locked ? void 0 : "Drag to move, or click to remove",
16640
- style: { touchAction: "none", cursor: locked ? "default" : "grab" },
17134
+ style: {
17135
+ ...OVERLAY_STYLE,
17136
+ touchAction: "none",
17137
+ cursor: locked ? "default" : "grab"
17138
+ },
16641
17139
  ...listeners,
16642
17140
  ...attributes,
16643
17141
  children: value
16644
17142
  }
16645
17143
  );
16646
17144
  };
17145
+ function getMaxInputLength(exercise, inputKey) {
17146
+ const widget = exercise.widgets[inputKey];
17147
+ if (widget?.type !== JSON_TYPES.INLINE_INPUT) return void 0;
17148
+ return widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : widget.length;
17149
+ }
17150
+ function getInputElementId(key2) {
17151
+ return `${key2}-input`;
17152
+ }
17153
+ function getTextInputs(container) {
17154
+ return Array.from(
17155
+ container.querySelectorAll(
17156
+ 'input[type="text"], input[type="number"], textarea, select'
17157
+ )
17158
+ );
17159
+ }
17160
+ function canTakeInput(el) {
17161
+ return !el.readOnly && !el.disabled;
17162
+ }
17163
+ function findNextInput(container, currentKey) {
17164
+ const inputs = getTextInputs(container);
17165
+ const currentIndex = inputs.findIndex((el) => el.id === getInputElementId(currentKey));
17166
+ if (currentIndex === -1) return null;
17167
+ return inputs.slice(currentIndex + 1).find(canTakeInput) ?? null;
17168
+ }
16647
17169
  const InlineInput = ({
16648
17170
  widgetId,
16649
17171
  value,
@@ -16652,6 +17174,7 @@ const InlineInput = ({
16652
17174
  inputType,
16653
17175
  length,
16654
17176
  digits,
17177
+ width,
16655
17178
  placeholder,
16656
17179
  disabled = false,
16657
17180
  onChange,
@@ -16665,8 +17188,8 @@ const InlineInput = ({
16665
17188
  const handleDrop = React.useCallback((item) => {
16666
17189
  return applyDropToInput(widgetId, item, onChange);
16667
17190
  }, [onChange, widgetId]);
16668
- const expectedLength = inputType === "numeric" ? digits : length;
16669
- const dynamicWidth = `${(expectedLength || 5) * 0.8 + 1}em`;
17191
+ const expectedLength = inputType === INPUT_TYPES.NUMERIC ? digits : length;
17192
+ const dynamicWidth = `${(width ?? expectedLength ?? 5) * 0.8 + 1}em`;
16670
17193
  const statusClass = validationStatus ? ` widget--${validationStatus}` : "";
16671
17194
  const disabledClass = disabled ? " inline-input--disabled" : "";
16672
17195
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -16679,21 +17202,31 @@ const InlineInput = ({
16679
17202
  className: "inline-widget inline-input-droppable",
16680
17203
  children: isDropped && value !== "" ? (
16681
17204
  // Show as chip only when value came from drag-drop
16682
- /* @__PURE__ */ jsxRuntime.jsx(
16683
- PlacedInputChip,
16684
- {
16685
- widgetId,
16686
- value,
16687
- className: statusClass.trim(),
16688
- onRemove: () => handleChange("", false)
16689
- }
16690
- )
17205
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17206
+ /* @__PURE__ */ jsxRuntime.jsx(
17207
+ SlotSizer,
17208
+ {
17209
+ className: `inline-input${disabledClass}`,
17210
+ style: { width: dynamicWidth },
17211
+ placeholder
17212
+ }
17213
+ ),
17214
+ /* @__PURE__ */ jsxRuntime.jsx(
17215
+ PlacedInputChip,
17216
+ {
17217
+ widgetId,
17218
+ value,
17219
+ className: statusClass.trim(),
17220
+ onRemove: () => handleChange("", false)
17221
+ }
17222
+ )
17223
+ ] })
16691
17224
  ) : (
16692
17225
  // Regular input for typing
16693
17226
  /* @__PURE__ */ jsxRuntime.jsx(
16694
17227
  "input",
16695
17228
  {
16696
- id: widgetId + "-input",
17229
+ id: getInputElementId(widgetId),
16697
17230
  type: "text",
16698
17231
  className: `inline-input${statusClass}${disabledClass}`,
16699
17232
  value,
@@ -16713,15 +17246,20 @@ const InlineInput = ({
16713
17246
  }
16714
17247
  );
16715
17248
  };
16716
- const SingleDragOption = ({
17249
+ const SingleDragOption = (props) => {
17250
+ const { exerciseKey } = useExerciseDnd();
17251
+ return /* @__PURE__ */ jsxRuntime.jsx(DragOptionChip, { ...props }, exerciseKey ?? "");
17252
+ };
17253
+ const DragOptionChip = ({
16717
17254
  optionId,
16718
17255
  value,
16719
17256
  onOptionClick,
16720
17257
  className = ""
16721
17258
  }) => {
16722
- const { usedOptionIds } = useExerciseDnd();
17259
+ const { usedOptionIds, hasDraggedOption, playHintSound } = useExerciseDnd();
16723
17260
  const locked = useExerciseLocked();
16724
17261
  const isUsed = usedOptionIds.has(optionId);
17262
+ const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasDraggedOption);
16725
17263
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(
16726
17264
  Draggable,
16727
17265
  {
@@ -16733,7 +17271,11 @@ const SingleDragOption = ({
16733
17271
  children: /* @__PURE__ */ jsxRuntime.jsx(
16734
17272
  "button",
16735
17273
  {
16736
- className: "drag-option",
17274
+ className: hinting && !locked ? "drag-option drag-option--hint" : "drag-option",
17275
+ onAnimationStart: () => {
17276
+ if (hinting) playHintSound();
17277
+ },
17278
+ onAnimationEnd: onHintEnd,
16737
17279
  onClick: () => {
16738
17280
  if (!locked) onOptionClick?.(value);
16739
17281
  },
@@ -16862,23 +17404,23 @@ const NumericKeypad = ({
16862
17404
  playSound("keypad-press");
16863
17405
  onChange(value.slice(0, -1));
16864
17406
  };
16865
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
16866
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
16867
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
16868
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
16869
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
16870
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
16871
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
16872
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
16873
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
16874
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
16875
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
16876
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
16877
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
16878
- includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
16879
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
16880
- /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
16881
- includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
17407
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, "data-tutorial": "keypad", children: [
17408
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "1", onClick: () => handleDigit("1"), children: "1" }),
17409
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "2", onClick: () => handleDigit("2"), children: "2" }),
17410
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "3", onClick: () => handleDigit("3"), children: "3" }),
17411
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "+", onClick: () => handleDigit("+"), children: "+" }),
17412
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "4", onClick: () => handleDigit("4"), children: "4" }),
17413
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "5", onClick: () => handleDigit("5"), children: "5" }),
17414
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "6", onClick: () => handleDigit("6"), children: "6" }),
17415
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "-", onClick: () => handleDigit("-"), children: "-" }),
17416
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "7", onClick: () => handleDigit("7"), children: "7" }),
17417
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "8", onClick: () => handleDigit("8"), children: "8" }),
17418
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "9", onClick: () => handleDigit("9"), children: "9" }),
17419
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "×", onClick: () => handleDigit("×"), children: "×" }),
17420
+ includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-decimal", "data-key": ",", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
17421
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-digit", "data-key": "0", onClick: () => handleDigit("0"), children: "0" }),
17422
+ /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-backspace", "data-key": "backspace", onClick: handleBackspace, children: "⌫" }),
17423
+ includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: locked, className: "keypad-btn keypad-btn-operator", "data-key": "÷", onClick: () => handleDigit("÷"), children: "÷" })
16882
17424
  ] });
16883
17425
  };
16884
17426
  const FractionInput = ({
@@ -16918,18 +17460,28 @@ const FractionInput = ({
16918
17460
  onDrop: handleDrop(subKey),
16919
17461
  disabled: locked,
16920
17462
  className: "fraction-slot-droppable",
16921
- children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsx(
16922
- PlacedInputChip,
16923
- {
16924
- widgetId: subKey,
16925
- value: currentValue,
16926
- className: `fraction-chip${slotClass}`,
16927
- onRemove: () => onChange(subKey, "", false)
16928
- }
16929
- ) : /* @__PURE__ */ jsxRuntime.jsx(
17463
+ children: isDroppedHere && currentValue !== "" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
17464
+ /* @__PURE__ */ jsxRuntime.jsx(
17465
+ SlotSizer,
17466
+ {
17467
+ className: "inline-input fraction-input",
17468
+ size: placeholder ? placeholder.length : void 0,
17469
+ placeholder
17470
+ }
17471
+ ),
17472
+ /* @__PURE__ */ jsxRuntime.jsx(
17473
+ PlacedInputChip,
17474
+ {
17475
+ widgetId: subKey,
17476
+ value: currentValue,
17477
+ className: `fraction-chip${slotClass}`,
17478
+ onRemove: () => onChange(subKey, "", false)
17479
+ }
17480
+ )
17481
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
16930
17482
  "input",
16931
17483
  {
16932
- id: `${subKey}-input`,
17484
+ id: getInputElementId(subKey),
16933
17485
  type: "text",
16934
17486
  className: `inline-input fraction-input${slotClass}`,
16935
17487
  value: currentValue,
@@ -17518,12 +18070,17 @@ const GridSymmetry = ({
17518
18070
  }
17519
18071
  ) });
17520
18072
  };
18073
+ function interactiveBarKey(bar) {
18074
+ if (bar.answer === void 0 && bar.interactive !== true) return void 0;
18075
+ return bar.widgetId ?? bar.id;
18076
+ }
17521
18077
  function getChartClassName(item) {
17522
18078
  const isHorizontal = item.orientation === "horizontal";
17523
18079
  return [
17524
18080
  "chart",
17525
18081
  isHorizontal ? "chart--horizontal" : "",
17526
18082
  item.chartType === "pictograph" ? "chart--pictograph" : "",
18083
+ item.chartType === "grouped-bar" ? "chart--grouped-bar" : "",
17527
18084
  item.xAxis?.visible === false ? "chart--no-xaxis" : "",
17528
18085
  item.yAxis?.visible === false ? "chart--no-yaxis" : ""
17529
18086
  ].filter(Boolean).join(" ");
@@ -17534,7 +18091,8 @@ function resolveItemValue(id, defaultValue2, values) {
17534
18091
  return isNaN(parsed) ? defaultValue2 : parsed;
17535
18092
  }
17536
18093
  function resolveValue(bar, values) {
17537
- return bar.interactive ? resolveItemValue(bar.widgetId, bar.value, values) : bar.value;
18094
+ const widgetKey = interactiveBarKey(bar);
18095
+ return widgetKey !== void 0 ? resolveItemValue(widgetKey, bar.value, values) : bar.value;
17538
18096
  }
17539
18097
  function clamp(v, min, max) {
17540
18098
  return Math.min(max, Math.max(min, v));
@@ -17593,6 +18151,8 @@ const CHART_METRICS = {
17593
18151
  gapWidth: 1,
17594
18152
  axisWidth: 2.35,
17595
18153
  barWidth: 3.35,
18154
+ // Fixed narrow width so several series fit in one cluster; not derived from count.
18155
+ groupedBarWidth: 1.8,
17596
18156
  fontSize: 0.9,
17597
18157
  labelFontSize: 0.98,
17598
18158
  handleSize: 1.15,
@@ -17645,6 +18205,41 @@ function computePictographChartStyle(scale, columnCount, totalSlots, orientation
17645
18205
  "--chart-height": slotSpan
17646
18206
  };
17647
18207
  }
18208
+ function computeGroupedChartStyle(scale, groupCount, seriesCount, orientation = "vertical", isInteractive = false) {
18209
+ const visualScale = clamp(scale, CHART_METRICS.scaleMin, CHART_METRICS.scaleMax);
18210
+ const barWidth = (isInteractive ? CHART_METRICS.barWidth : CHART_METRICS.groupedBarWidth) * visualScale;
18211
+ const barGap = CHART_METRICS.gapWidth * visualScale * 0.5;
18212
+ const groupGap = CHART_METRICS.gapWidth * visualScale * 2;
18213
+ const seriesPerGroup = Math.max(1, seriesCount);
18214
+ const groupWidth = seriesPerGroup * barWidth + Math.max(0, seriesPerGroup - 1) * barGap;
18215
+ const trackLength = groupCount * groupWidth + Math.max(0, groupCount - 1) * groupGap + groupGap * 2;
18216
+ const cssVars = {
18217
+ "--chart-axis-width": `${CHART_METRICS.axisWidth * visualScale}rem`,
18218
+ "--chart-bar-width": `${barWidth}rem`,
18219
+ // A bar slot inside a group is just the bar; reuse the plain bar-chart var.
18220
+ "--chart-bar-slot-width": `${barWidth}rem`,
18221
+ "--chart-bar-gap": `${barGap}rem`,
18222
+ "--chart-group-gap": `${groupGap}rem`,
18223
+ "--chart-group-width": `${groupWidth}rem`,
18224
+ "--chart-font-size": `${CHART_METRICS.fontSize * visualScale}rem`,
18225
+ "--chart-label-font-size": `${CHART_METRICS.labelFontSize * visualScale}rem`,
18226
+ "--chart-handle-size": `${CHART_METRICS.handleSize * visualScale}rem`
18227
+ };
18228
+ if (orientation === "horizontal") {
18229
+ const valueWidth = CHART_METRICS.chartHeight * visualScale;
18230
+ return {
18231
+ ...cssVars,
18232
+ "--chart-height": `${trackLength}rem`,
18233
+ "--chart-h-value-width": `${valueWidth}rem`,
18234
+ "--chart-content-width": `${valueWidth}rem`
18235
+ };
18236
+ }
18237
+ return {
18238
+ ...cssVars,
18239
+ "--chart-height": `${CHART_METRICS.chartHeight * visualScale}rem`,
18240
+ "--chart-content-width": `${trackLength}rem`
18241
+ };
18242
+ }
17648
18243
  function computeHorizontalChartStyle(scale, barCount) {
17649
18244
  const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
17650
18245
  const valueWidth = CHART_METRICS.chartHeight * visualScale;
@@ -17696,6 +18291,7 @@ const DraggableBar = ({
17696
18291
  const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
17697
18292
  const axisStep = axis.step ?? CHART_AXIS_DEFAULTS.step;
17698
18293
  const snapStep = snap ?? axisStep;
18294
+ const widgetKey = bar.widgetId ?? bar.id;
17699
18295
  const fillFromPointer = React.useCallback(
17700
18296
  (clientY, clientX) => {
17701
18297
  const track = trackRef.current;
@@ -17713,10 +18309,10 @@ const DraggableBar = ({
17713
18309
  if (snapped !== lastSnap.current) {
17714
18310
  lastSnap.current = snapped;
17715
18311
  playSound("bar-step");
17716
- onChange(bar.widgetId, String(snapped));
18312
+ onChange(widgetKey, String(snapped));
17717
18313
  }
17718
18314
  },
17719
- [axisMin, axisMax, snapStep, bar.widgetId, onChange, orientation, playSound]
18315
+ [axisMin, axisMax, snapStep, widgetKey, onChange, orientation, playSound]
17720
18316
  );
17721
18317
  const onPointerDown = React.useCallback(
17722
18318
  (e) => {
@@ -17749,8 +18345,8 @@ const DraggableBar = ({
17749
18345
  if (!dragging.current) return;
17750
18346
  dragging.current = false;
17751
18347
  setIsDragging(false);
17752
- onChange(bar.widgetId, String(preDragValue.current));
17753
- }, [bar.widgetId, onChange]);
18348
+ onChange(widgetKey, String(preDragValue.current));
18349
+ }, [widgetKey, onChange]);
17754
18350
  const onKeyDown = React.useCallback(
17755
18351
  (e) => {
17756
18352
  if (exerciseLocked) return;
@@ -17759,22 +18355,22 @@ const DraggableBar = ({
17759
18355
  if (isInc) {
17760
18356
  e.preventDefault();
17761
18357
  playSound("bar-step");
17762
- onChange(bar.widgetId, String(nextStepValue(currentValue, axis, 1, snap)));
18358
+ onChange(widgetKey, String(nextStepValue(currentValue, axis, 1, snap)));
17763
18359
  } else if (isDec) {
17764
18360
  e.preventDefault();
17765
18361
  playSound("bar-step");
17766
- onChange(bar.widgetId, String(nextStepValue(currentValue, axis, -1, snap)));
18362
+ onChange(widgetKey, String(nextStepValue(currentValue, axis, -1, snap)));
17767
18363
  } else if (e.key === "Home") {
17768
18364
  e.preventDefault();
17769
18365
  playSound("bar-step");
17770
- onChange(bar.widgetId, String(axisMin));
18366
+ onChange(widgetKey, String(axisMin));
17771
18367
  } else if (e.key === "End") {
17772
18368
  e.preventDefault();
17773
18369
  playSound("bar-step");
17774
- onChange(bar.widgetId, String(axisMax));
18370
+ onChange(widgetKey, String(axisMax));
17775
18371
  }
17776
18372
  },
17777
- [axisMin, axisMax, bar.widgetId, currentValue, axis, snap, onChange, playSound, exerciseLocked]
18373
+ [axisMin, axisMax, widgetKey, currentValue, axis, snap, onChange, playSound, exerciseLocked]
17778
18374
  );
17779
18375
  const fill = toFill(currentValue, axis);
17780
18376
  const isAtMin = currentValue <= axisMin;
@@ -17850,7 +18446,7 @@ const StaticBar = ({
17850
18446
  }
17851
18447
  ) });
17852
18448
  };
17853
- const EMPTY_SET$2 = /* @__PURE__ */ new Set();
18449
+ const EMPTY_SET$4 = /* @__PURE__ */ new Set();
17854
18450
  const XAxisLabel = ({
17855
18451
  bar,
17856
18452
  widgets,
@@ -17865,14 +18461,14 @@ const XAxisLabel = ({
17865
18461
  item: subItem,
17866
18462
  widgets,
17867
18463
  values,
17868
- droppedInputs: EMPTY_SET$2,
18464
+ droppedInputs: EMPTY_SET$4,
17869
18465
  validationDetails,
17870
18466
  onChange
17871
18467
  },
17872
18468
  subItem.id
17873
18469
  )) }) });
17874
18470
  };
17875
- const EMPTY_SET$1 = /* @__PURE__ */ new Set();
18471
+ const EMPTY_SET$3 = /* @__PURE__ */ new Set();
17876
18472
  const BarChart = ({
17877
18473
  item,
17878
18474
  widgets,
@@ -17880,6 +18476,7 @@ const BarChart = ({
17880
18476
  validationDetails,
17881
18477
  onChange
17882
18478
  }) => {
18479
+ if (item.chartType !== "bar") return null;
17883
18480
  const { bars, grid } = item;
17884
18481
  const isHorizontal = item.orientation === "horizontal";
17885
18482
  const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
@@ -17893,7 +18490,7 @@ const BarChart = ({
17893
18490
  item: subItem,
17894
18491
  widgets,
17895
18492
  values,
17896
- droppedInputs: EMPTY_SET$1,
18493
+ droppedInputs: EMPTY_SET$3,
17897
18494
  validationDetails,
17898
18495
  onChange
17899
18496
  },
@@ -17903,9 +18500,10 @@ const BarChart = ({
17903
18500
  const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
17904
18501
  const renderBar = (bar, orient = "vertical") => {
17905
18502
  const currentValue = resolveValue(bar, values);
17906
- const vStatus = bar.interactive ? validationDetails?.[bar.widgetId] : void 0;
18503
+ const widgetKey = interactiveBarKey(bar);
18504
+ const vStatus = widgetKey !== void 0 ? validationDetails?.[widgetKey] : void 0;
17907
18505
  const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
17908
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-bar-wrapper", children: bar.interactive ? /* @__PURE__ */ jsxRuntime.jsx(
18506
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-bar-wrapper", children: widgetKey !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
17909
18507
  DraggableBar,
17910
18508
  {
17911
18509
  bar,
@@ -17938,7 +18536,7 @@ const BarChart = ({
17938
18536
  item: subItem,
17939
18537
  widgets,
17940
18538
  values,
17941
- droppedInputs: EMPTY_SET$1,
18539
+ droppedInputs: EMPTY_SET$3,
17942
18540
  validationDetails,
17943
18541
  onChange
17944
18542
  },
@@ -18143,7 +18741,7 @@ const PictographGrid = ({
18143
18741
  ))
18144
18742
  ] });
18145
18743
  };
18146
- const EMPTY_SET = /* @__PURE__ */ new Set();
18744
+ const EMPTY_SET$2 = /* @__PURE__ */ new Set();
18147
18745
  const PictographChart = ({
18148
18746
  item,
18149
18747
  widgets,
@@ -18151,7 +18749,8 @@ const PictographChart = ({
18151
18749
  validationDetails,
18152
18750
  onChange
18153
18751
  }) => {
18154
- const { pictos = [], grid } = item;
18752
+ if (item.chartType !== "pictograph") return null;
18753
+ const { pictos, grid } = item;
18155
18754
  const isHorizontal = item.orientation === "horizontal";
18156
18755
  const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
18157
18756
  const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
@@ -18164,7 +18763,7 @@ const PictographChart = ({
18164
18763
  item: subItem,
18165
18764
  widgets,
18166
18765
  values,
18167
- droppedInputs: EMPTY_SET,
18766
+ droppedInputs: EMPTY_SET$2,
18168
18767
  validationDetails,
18169
18768
  onChange
18170
18769
  },
@@ -18202,7 +18801,7 @@ const PictographChart = ({
18202
18801
  item: subItem,
18203
18802
  widgets,
18204
18803
  values,
18205
- droppedInputs: EMPTY_SET,
18804
+ droppedInputs: EMPTY_SET$2,
18206
18805
  validationDetails,
18207
18806
  onChange
18208
18807
  },
@@ -18223,7 +18822,7 @@ const PictographChart = ({
18223
18822
  item: subItem,
18224
18823
  widgets,
18225
18824
  values,
18226
- droppedInputs: EMPTY_SET,
18825
+ droppedInputs: EMPTY_SET$2,
18227
18826
  validationDetails,
18228
18827
  onChange
18229
18828
  },
@@ -18242,9 +18841,183 @@ const PictographChart = ({
18242
18841
  xAxisTitle
18243
18842
  ] }) }) });
18244
18843
  };
18844
+ const GroupSlot = ({
18845
+ group,
18846
+ series,
18847
+ axis,
18848
+ snap,
18849
+ orientation = "vertical",
18850
+ values,
18851
+ validationDetails,
18852
+ onChange
18853
+ }) => {
18854
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-group", children: series.map((s) => {
18855
+ const bar = group.bars.find((b) => b.seriesId === s.id);
18856
+ if (!bar) {
18857
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-group-bar-gap", "aria-hidden": "true" }, s.id);
18858
+ }
18859
+ const currentValue = resolveValue(bar, values);
18860
+ const vStatus = bar.answer !== void 0 ? validationDetails?.[bar.id] : void 0;
18861
+ const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
18862
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-bar-wrapper", children: bar.answer !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
18863
+ DraggableBar,
18864
+ {
18865
+ bar: { ...bar, answer: bar.answer },
18866
+ currentValue,
18867
+ axis,
18868
+ snap,
18869
+ validationStatus,
18870
+ orientation,
18871
+ onChange
18872
+ }
18873
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
18874
+ StaticBar,
18875
+ {
18876
+ bar,
18877
+ currentValue,
18878
+ axis,
18879
+ validationStatus,
18880
+ orientation
18881
+ }
18882
+ ) }, bar.id);
18883
+ }) });
18884
+ };
18885
+ const EMPTY_SET$1 = /* @__PURE__ */ new Set();
18886
+ const ChartLegend = ({
18887
+ series,
18888
+ widgets,
18889
+ values,
18890
+ validationDetails,
18891
+ onChange
18892
+ }) => {
18893
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-legend", children: series.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "chart-legend-item", children: [
18894
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chart-legend-swatch", "aria-hidden": "true", style: { backgroundColor: s.color } }),
18895
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chart-legend-label", children: s.label?.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18896
+ LayoutRenderer,
18897
+ {
18898
+ item: subItem,
18899
+ widgets,
18900
+ values,
18901
+ droppedInputs: EMPTY_SET$1,
18902
+ validationDetails,
18903
+ onChange
18904
+ },
18905
+ subItem.id
18906
+ )) })
18907
+ ] }, s.id)) });
18908
+ };
18909
+ const EMPTY_SET = /* @__PURE__ */ new Set();
18910
+ const GroupedBarChart = ({
18911
+ item,
18912
+ widgets,
18913
+ values,
18914
+ validationDetails,
18915
+ onChange
18916
+ }) => {
18917
+ if (item.chartType !== "grouped-bar") return null;
18918
+ const { groups, series, grid } = item;
18919
+ const isHorizontal = item.orientation === "horizontal";
18920
+ const isInteractive = groups.some((g) => g.bars.some((bar) => bar.answer !== void 0));
18921
+ const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
18922
+ const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
18923
+ const chartClass = getChartClassName(item);
18924
+ const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
18925
+ const renderAxisTitle = (title) => {
18926
+ if (!title?.length) return null;
18927
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18928
+ LayoutRenderer,
18929
+ {
18930
+ item: subItem,
18931
+ widgets,
18932
+ values,
18933
+ droppedInputs: EMPTY_SET,
18934
+ validationDetails,
18935
+ onChange
18936
+ },
18937
+ subItem.id
18938
+ )) });
18939
+ };
18940
+ const renderGroupSlots = (orient) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-groups", children: groups.map((group) => /* @__PURE__ */ jsxRuntime.jsx(
18941
+ GroupSlot,
18942
+ {
18943
+ group,
18944
+ series,
18945
+ axis: fillAxis,
18946
+ snap: item.snap,
18947
+ orientation: orient,
18948
+ values,
18949
+ validationDetails,
18950
+ onChange
18951
+ },
18952
+ group.id
18953
+ )) });
18954
+ const legend = item.legend ? /* @__PURE__ */ jsxRuntime.jsx(
18955
+ ChartLegend,
18956
+ {
18957
+ series,
18958
+ widgets,
18959
+ values,
18960
+ validationDetails,
18961
+ onChange
18962
+ }
18963
+ ) : null;
18964
+ if (isHorizontal) {
18965
+ const chartStyle2 = computeGroupedChartStyle(item.scale, groups.length, series.length, "horizontal", isInteractive);
18966
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
18967
+ item.title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-title", children: item.title }),
18968
+ categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
18969
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-y--categories", children: groups.map((group) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-bar-y-label", children: group.label?.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18970
+ LayoutRenderer,
18971
+ {
18972
+ item: subItem,
18973
+ widgets,
18974
+ values,
18975
+ droppedInputs: EMPTY_SET,
18976
+ validationDetails,
18977
+ onChange
18978
+ },
18979
+ subItem.id
18980
+ )) }, group.id)) }),
18981
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "chart-plot", children: [
18982
+ grid && /* @__PURE__ */ jsxRuntime.jsx(Grid, { axis: fillAxis, orientation: "horizontal" }),
18983
+ renderGroupSlots("horizontal")
18984
+ ] }),
18985
+ valueAxis && valueAxis.visible && /* @__PURE__ */ jsxRuntime.jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) }),
18986
+ legend
18987
+ ] }) }) });
18988
+ }
18989
+ const chartStyle = computeGroupedChartStyle(item.scale, groups.length, series.length, "vertical", isInteractive);
18990
+ const hasAnyGroupLabel = groups.some((g) => g.label && g.label.length > 0);
18991
+ const groupLabelRow = hasAnyGroupLabel ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-x", children: groups.map((group) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-tick-x-inner", children: group.label?.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
18992
+ LayoutRenderer,
18993
+ {
18994
+ item: subItem,
18995
+ widgets,
18996
+ values,
18997
+ droppedInputs: EMPTY_SET,
18998
+ validationDetails,
18999
+ onChange
19000
+ },
19001
+ subItem.id
19002
+ )) }) }, group.id)) }) : null;
19003
+ const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
19004
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
19005
+ item.title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-title", children: item.title }),
19006
+ valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
19007
+ valueAxis && valueAxis.visible && /* @__PURE__ */ jsxRuntime.jsx(YAxis, { axis: valueAxis }),
19008
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "chart-plot", children: [
19009
+ grid && /* @__PURE__ */ jsxRuntime.jsx(Grid, { axis: fillAxis }),
19010
+ renderGroupSlots("vertical")
19011
+ ] }),
19012
+ groupLabelRow,
19013
+ xAxisTitle,
19014
+ legend
19015
+ ] }) }) });
19016
+ };
18245
19017
  const CHART_TYPE_COMPONENTS = {
18246
19018
  bar: BarChart,
18247
- pictograph: PictographChart
19019
+ pictograph: PictographChart,
19020
+ "grouped-bar": GroupedBarChart
18248
19021
  // line: LineChart,
18249
19022
  // pie: PieChart,
18250
19023
  };
@@ -18286,11 +19059,12 @@ const RadioOptions = ({
18286
19059
  };
18287
19060
  const Btn = ({
18288
19061
  label,
19062
+ dataKey = label,
18289
19063
  extraClass = "keypad-btn-digit",
18290
19064
  onClick
18291
19065
  }) => {
18292
19066
  const locked = useExerciseLocked();
18293
- return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
19067
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { className: `keypad-btn ${extraClass}`, "data-key": dataKey, disabled: locked, onClick, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }) });
18294
19068
  };
18295
19069
  const DiamondKeypad = ({ value, onChange }) => {
18296
19070
  const playSound = useSound();
@@ -18302,7 +19076,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18302
19076
  playSound("keypad-press");
18303
19077
  onChange(value.slice(0, -1));
18304
19078
  };
18305
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad", children: [
19079
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad", "data-tutorial": "keypad", children: [
18306
19080
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad-row", children: [
18307
19081
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "1", onClick: () => append("1") }),
18308
19082
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "2", onClick: () => append("2") }),
@@ -18321,7 +19095,7 @@ const DiamondKeypad = ({ value, onChange }) => {
18321
19095
  ] }),
18322
19096
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "diamond-keypad-row", children: [
18323
19097
  /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: ".", extraClass: "keypad-btn-decimal", onClick: () => append(".") }),
18324
- /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "⌫", extraClass: "keypad-btn-backspace", onClick: backspace })
19098
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { label: "⌫", dataKey: "backspace", extraClass: "keypad-btn-backspace", onClick: backspace })
18325
19099
  ] })
18326
19100
  ] });
18327
19101
  };
@@ -18927,7 +19701,7 @@ function isGeometryWidget(widget) {
18927
19701
  return widget.type === JSON_TYPES.GEOMETRY;
18928
19702
  }
18929
19703
  const GeometryWidget = React.lazy(async () => ({
18930
- default: (await Promise.resolve().then(() => require("./GeometryWidget-D2QyGzW-.js"))).GeometryWidget
19704
+ default: (await Promise.resolve().then(() => require("./GeometryWidget-BTPjXJz-.js"))).GeometryWidget
18931
19705
  }));
18932
19706
  const WidgetRenderer = ({
18933
19707
  widget,
@@ -18955,8 +19729,9 @@ const WidgetRenderer = ({
18955
19729
  isDropped,
18956
19730
  validationStatus,
18957
19731
  inputType: widget.inputType,
18958
- length: widget.inputType === "string" ? widget.length : void 0,
18959
- digits: widget.inputType === "numeric" ? widget.digits : void 0,
19732
+ length: widget.inputType === INPUT_TYPES.STRING ? widget.length : void 0,
19733
+ digits: widget.inputType === INPUT_TYPES.NUMERIC ? widget.digits : void 0,
19734
+ width: widget.width,
18960
19735
  placeholder: widget.placeholder,
18961
19736
  disabled: widget.disabled,
18962
19737
  onChange,
@@ -19327,6 +20102,13 @@ const EPSILON = 10 ** -INTERNAL_PRECISION;
19327
20102
  const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
19328
20103
  const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
19329
20104
  const almostEqual = (a, b) => Math.abs(a - b) <= EPSILON;
20105
+ const NUMBER_LINE_AXIS_ENDS = {
20106
+ "arrow-right": { left: "tick", right: "arrow" },
20107
+ "arrow-left": { left: "arrow", right: "tick" },
20108
+ "arrow-both": { left: "arrow", right: "arrow" },
20109
+ "line": { left: "line", right: "line" },
20110
+ "line-left": { left: "line", right: "arrow" }
20111
+ };
19330
20112
  const toPercent = (value, min, max) => {
19331
20113
  const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
19332
20114
  return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
@@ -20374,7 +21156,7 @@ const NumberLineRenderer = ({
20374
21156
  max,
20375
21157
  step,
20376
21158
  minorTicks = 0,
20377
- style: lineStyle = "arrow-right",
21159
+ style: lineStyle = DEFAULT_NUMBER_LINE_STYLE,
20378
21160
  labelVisibility,
20379
21161
  highlights,
20380
21162
  arrows,
@@ -20438,8 +21220,9 @@ const NumberLineRenderer = ({
20438
21220
  () => getMinorTickValues(majorTicks, minorTicks),
20439
21221
  [majorTicks, minorTicks]
20440
21222
  );
20441
- const showArrowRight = lineStyle === "arrow-right" || lineStyle === "arrow-both";
20442
- const showArrowLeft = lineStyle === "arrow-left" || lineStyle === "arrow-both";
21223
+ const axisEnds = NUMBER_LINE_AXIS_ENDS[lineStyle] ?? NUMBER_LINE_AXIS_ENDS[DEFAULT_NUMBER_LINE_STYLE];
21224
+ const showArrowLeft = axisEnds.left === "arrow";
21225
+ const showArrowRight = axisEnds.right === "arrow";
20443
21226
  const hasDragOptions = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
20444
21227
  const isAxisDragMode = hasDragOptions && item.dragMode === "axis";
20445
21228
  const inputTickSet = React.useMemo(() => {
@@ -20487,7 +21270,7 @@ const NumberLineRenderer = ({
20487
21270
  return /* @__PURE__ */ jsxRuntime.jsxs(
20488
21271
  "div",
20489
21272
  {
20490
- className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
21273
+ className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${axisEnds.left === "line" ? " number-line--extend-left" : ""}${axisEnds.right === "line" ? " number-line--extend-right" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
20491
21274
  style: {
20492
21275
  [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
20493
21276
  ...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
@@ -20860,6 +21643,35 @@ function groupLineItems(items) {
20860
21643
  }
20861
21644
  return groups;
20862
21645
  }
21646
+ function placeTableCells(rows, cols) {
21647
+ const placements = /* @__PURE__ */ new Map();
21648
+ const occupied = rows.map(() => []);
21649
+ const totalRows = rows.length;
21650
+ rows.forEach((row, rowIndex) => {
21651
+ const taken = occupied[rowIndex] ?? [];
21652
+ let col = 0;
21653
+ for (const cell of row.cells) {
21654
+ while (taken[col]) col++;
21655
+ const colspan = cell.colspan || 1;
21656
+ const rowspan = Math.min(cell.rowspan || 1, totalRows - rowIndex);
21657
+ for (let r = rowIndex; r < rowIndex + rowspan; r++) {
21658
+ const line = occupied[r];
21659
+ if (!line) continue;
21660
+ for (let c = col; c < col + colspan; c++) line[c] = true;
21661
+ }
21662
+ placements.set(cell.id, {
21663
+ column: col + 1,
21664
+ row: rowIndex + 1,
21665
+ colspan,
21666
+ rowspan,
21667
+ isLastColumn: col + colspan >= cols,
21668
+ isLastRow: rowIndex + rowspan >= totalRows
21669
+ });
21670
+ col += colspan;
21671
+ }
21672
+ });
21673
+ return placements;
21674
+ }
20863
21675
  const toValidationStatus = (v) => v === true ? "correct" : v === false ? "incorrect" : null;
20864
21676
  const isNumericText = (value) => /^[\d.,\s]+$/.test(value);
20865
21677
  const numberClass = (value) => isNumericText(value) ? "text-item-number" : "";
@@ -20983,21 +21795,28 @@ const LayoutRenderer = ({
20983
21795
  subItem.id
20984
21796
  )) });
20985
21797
  case JSON_TYPES.TABLE: {
20986
- const totalRows = item.rows.length;
21798
+ const placements = placeTableCells(item.rows, item.cols);
20987
21799
  const tableStyle = {
20988
- "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR
21800
+ "--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR,
21801
+ "--exercise-table-cols": item.cols
20989
21802
  };
20990
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: item.rows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "exercise-table-row", style: { backgroundColor: row.backgroundColor }, children: row.cells.map((cell) => {
20991
- const rowspan = cell.rowspan || 1;
20992
- const isLastRow = rowIndex + rowspan - 1 >= totalRows - 1;
21803
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table", role: "table", children: item.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-row", role: "row", children: row.cells.map((cell) => {
21804
+ const placement = placements.get(cell.id);
21805
+ const modifiers = [
21806
+ placement?.isLastRow ? " exercise-table-cell--last-row" : "",
21807
+ placement?.isLastColumn ? " exercise-table-cell--last-col" : ""
21808
+ ].join("");
20993
21809
  return /* @__PURE__ */ jsxRuntime.jsx(
20994
- "td",
21810
+ "div",
20995
21811
  {
20996
- className: `exercise-table-cell${isLastRow ? " exercise-table-cell--last-row" : ""}`,
20997
- colSpan: cell.colspan || 1,
20998
- rowSpan: rowspan,
20999
- style: { backgroundColor: cell.backgroundColor },
21000
- children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
21812
+ className: `exercise-table-cell${modifiers}`,
21813
+ role: "cell",
21814
+ style: {
21815
+ backgroundColor: cell.backgroundColor ?? row.backgroundColor,
21816
+ gridColumn: placement && `${placement.column} / span ${placement.colspan}`,
21817
+ gridRow: placement && `${placement.row} / span ${placement.rowspan}`
21818
+ },
21819
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "exercise-table-cell-content", children: cell.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
21001
21820
  LayoutRenderer,
21002
21821
  {
21003
21822
  item: subItem,
@@ -21010,11 +21829,11 @@ const LayoutRenderer = ({
21010
21829
  resetKey
21011
21830
  },
21012
21831
  subItem.id
21013
- ))
21832
+ )) })
21014
21833
  },
21015
21834
  cell.id
21016
21835
  );
21017
- }) }, row.id)) }) }) });
21836
+ }) }, row.id)) }) });
21018
21837
  }
21019
21838
  case JSON_TYPES.NUMBER_LINE:
21020
21839
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -21184,6 +22003,394 @@ const LayoutRenderer = ({
21184
22003
  return null;
21185
22004
  }
21186
22005
  };
22006
+ function LionHand({ className }) {
22007
+ const id = React.useId();
22008
+ const shadowId = `${id}-shadow`;
22009
+ const maskId = `${id}-mask`;
22010
+ return /* @__PURE__ */ jsxRuntime.jsxs(
22011
+ "svg",
22012
+ {
22013
+ "aria-hidden": "true",
22014
+ focusable: "false",
22015
+ width: "75",
22016
+ height: "78",
22017
+ viewBox: "0 0 75 78",
22018
+ fill: "none",
22019
+ xmlns: "http://www.w3.org/2000/svg",
22020
+ className,
22021
+ children: [
22022
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { filter: `url(#${shadowId})`, children: [
22023
+ /* @__PURE__ */ jsxRuntime.jsx(
22024
+ "mask",
22025
+ {
22026
+ id: maskId,
22027
+ style: { maskType: "alpha" },
22028
+ maskUnits: "userSpaceOnUse",
22029
+ x: "9",
22030
+ y: "19",
22031
+ width: "49",
22032
+ height: "42",
22033
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M49.7484 33.9668C58.158 45.6281 37.9572 61.4399 28.1745 60.1051C17.1397 58.5995 5.03802 42.2474 10.4349 38.3224C15.8318 34.3974 18.9715 32.4031 23.1181 30.1278C24.1486 29.5624 25.1742 29.0263 26.1947 28.5358C33.7314 24.764 52.8138 16.0308 57.0969 21.97C59.9201 25.8848 54.8651 28.9522 48.1097 31.8806C48.658 32.518 49.2042 33.2123 49.7484 33.9668Z", fill: "#FCD592" })
22034
+ }
22035
+ ),
22036
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { mask: `url(#${maskId})`, children: [
22037
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M49.7484 33.9672C58.158 45.6285 38.3118 60.3151 32.1037 64.7332C22.6672 71.4486 2.68204 43.9969 12.2424 37.1926C15.1884 35.0959 18.9716 32.4035 23.1182 30.1282C24.7308 29.2433 26.3314 28.4303 27.9195 27.7514C36.611 23.6398 54.4269 16.1501 58.4471 21.7248C61.34 25.7362 55.5686 28.9294 48.2024 31.9893C48.7196 32.5968 49.2349 33.2552 49.7484 33.9672Z", fill: "#FCD592" }),
22038
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.2426 37.1924C2.68223 43.9967 22.6674 71.4485 32.1039 64.733C31.3434 63.6785 26.4317 61.1668 25.3134 59.6161C24.1951 58.0654 22.794 50.5211 22.123 49.5907C21.4521 48.6603 16.3219 45.7409 15.5637 44.4032C14.8055 43.0655 13.8979 39.4877 14.7258 35.4253L12.2426 37.1924Z", fill: "#6B291E" }),
22039
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M50.4102 36.1737C50.6823 37.524 52.0035 38.4016 53.3614 38.1339C54.7192 37.8662 55.5994 36.5546 55.3274 35.2043L52.8688 35.689L50.4102 36.1737ZM38.8014 26.9523C37.4241 27.0934 36.425 28.3178 36.5697 29.6872C36.7144 31.0565 37.9482 32.0522 39.3255 31.9111L39.0634 29.4317L38.8014 26.9523ZM52.8688 35.689C55.3274 35.2043 55.327 35.2023 55.3266 35.2004C55.3265 35.1997 55.326 35.1977 55.3258 35.1963C55.3252 35.1935 55.3246 35.1906 55.324 35.1875C55.3227 35.1815 55.3213 35.1749 55.3198 35.1679C55.3168 35.1539 55.3133 35.1379 55.3092 35.1201C55.3011 35.0845 55.2907 35.0415 55.2779 34.9916C55.2522 34.8918 55.2163 34.764 55.1676 34.6127C55.0703 34.3106 54.9205 33.911 54.6961 33.4507C54.247 32.5296 53.4925 31.3535 52.2552 30.241C49.7078 27.9508 45.5021 26.2658 38.8014 26.9523L39.0634 29.4317L39.3255 31.9111C44.9159 31.3384 47.6128 32.782 48.8954 33.9352C49.5731 34.5445 49.9653 35.1687 50.1842 35.6177C50.2938 35.8424 50.3584 36.0201 50.3919 36.1242C50.4086 36.1759 50.4172 36.2084 50.4197 36.2181C50.4209 36.223 50.4206 36.222 50.419 36.2148C50.4181 36.2111 50.417 36.2059 50.4155 36.1991C50.4148 36.1957 50.414 36.1919 50.4131 36.1876C50.4127 36.1855 50.4122 36.1833 50.4117 36.181C50.4115 36.1798 50.4111 36.178 50.411 36.1774C50.4106 36.1755 50.4102 36.1737 52.8688 35.689Z", fill: "#A2634A" }),
22040
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M54.7526 47.3947C55.5602 48.5146 57.1269 48.7734 58.2518 47.9729C59.3767 47.1723 59.634 45.6155 58.8264 44.4956L56.7895 45.9452L54.7526 47.3947ZM38.1539 36.5617C36.7805 36.7357 35.8113 37.9837 35.9891 39.3493C36.1669 40.7148 37.4244 41.6808 38.7978 41.5068L38.4758 39.0342L38.1539 36.5617ZM56.7895 45.9452L58.8264 44.4956C57.4277 42.5562 55.3182 40.1317 51.9378 38.4204C48.5385 36.6995 44.0772 35.8112 38.1539 36.5617L38.4758 39.0342L38.7978 41.5068C43.8796 40.863 47.3003 41.6624 49.6691 42.8616C52.0569 44.0704 53.6015 45.7985 54.7526 47.3947L56.7895 45.9452Z", fill: "#A2634A" })
22041
+ ] })
22042
+ ] }),
22043
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("filter", { id: shadowId, x: "-1.99995", y: "0", width: "78.3658", height: "81.8518", filterUnits: "userSpaceOnUse", colorInterpolationFilters: "sRGB", children: [
22044
+ /* @__PURE__ */ jsxRuntime.jsx("feFlood", { floodOpacity: "0", result: "BackgroundImageFix" }),
22045
+ /* @__PURE__ */ jsxRuntime.jsx("feColorMatrix", { in: "SourceAlpha", type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0", result: "hardAlpha" }),
22046
+ /* @__PURE__ */ jsxRuntime.jsx("feOffset", { dy: "2" }),
22047
+ /* @__PURE__ */ jsxRuntime.jsx("feGaussianBlur", { stdDeviation: "1" }),
22048
+ /* @__PURE__ */ jsxRuntime.jsx("feComposite", { in2: "hardAlpha", operator: "out" }),
22049
+ /* @__PURE__ */ jsxRuntime.jsx("feColorMatrix", { type: "matrix", values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" }),
22050
+ /* @__PURE__ */ jsxRuntime.jsx("feBlend", { mode: "normal", in2: "BackgroundImageFix", result: "effect1_dropShadow" }),
22051
+ /* @__PURE__ */ jsxRuntime.jsx("feBlend", { mode: "normal", in: "SourceGraphic", in2: "effect1_dropShadow", result: "shape" })
22052
+ ] }) })
22053
+ ]
22054
+ }
22055
+ );
22056
+ }
22057
+ const TUTORIAL_HAND_TIP = { x: 56 / 75, y: 22 / 78 };
22058
+ const CYCLE_MS = 1400;
22059
+ const PRESS_AT_MS = 650;
22060
+ const PRESS_HOLD_MS = 220;
22061
+ const HOLE_PAD = 10;
22062
+ const HOLE_RADIUS = 18;
22063
+ const HAND_W = 72;
22064
+ const HAND_H = 75;
22065
+ const BUBBLE_W = 240;
22066
+ const BUBBLE_GAP = 14;
22067
+ const EDGE = 8;
22068
+ const DEFAULT_NEXT_LABEL = "فهمت";
22069
+ function resolve(t) {
22070
+ return (typeof t === "function" ? t() : t) ?? null;
22071
+ }
22072
+ function toRect(t) {
22073
+ if (!t) return null;
22074
+ const r = t instanceof Element ? t.getBoundingClientRect() : t;
22075
+ if (r.width <= 0 || r.height <= 0) return null;
22076
+ return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
22077
+ }
22078
+ const centerOf = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22079
+ const TutorialOverlay = ({ tutorial, root, json, options, onClose }) => {
22080
+ const { steps } = tutorial;
22081
+ const [stepIndex, setStepIndex] = React.useState(
22082
+ () => Math.min(Math.max(0, Math.floor(options.startStep ?? 0)), steps.length - 1)
22083
+ );
22084
+ const step = steps[stepIndex];
22085
+ const optionsRef = React.useRef(options);
22086
+ optionsRef.current = options;
22087
+ const ctx = { root, json, submitTarget: resolve(options.submitTarget) };
22088
+ const ctxRef = React.useRef(ctx);
22089
+ ctxRef.current = ctx;
22090
+ const [layout, setLayout] = React.useState(null);
22091
+ React.useEffect(() => {
22092
+ let raf = 0;
22093
+ let last = "";
22094
+ const tick = () => {
22095
+ const c = ctxRef.current;
22096
+ const next2 = {
22097
+ spotlight: step.spotlight(c).map(toRect),
22098
+ taps: step.taps(c).map(toRect),
22099
+ anchor: toRect(resolve(optionsRef.current.bubbleAnchor)),
22100
+ root: toRect(c.root),
22101
+ vw: window.innerWidth,
22102
+ vh: window.innerHeight
22103
+ };
22104
+ const key2 = JSON.stringify(next2);
22105
+ if (key2 !== last) {
22106
+ last = key2;
22107
+ setLayout(next2);
22108
+ }
22109
+ raf = requestAnimationFrame(tick);
22110
+ };
22111
+ tick();
22112
+ return () => cancelAnimationFrame(raf);
22113
+ }, [step]);
22114
+ const [tapIndex, setTapIndex] = React.useState(0);
22115
+ const [tapCount, setTapCount] = React.useState(0);
22116
+ const [shownStep, setShownStep] = React.useState(stepIndex);
22117
+ if (shownStep !== stepIndex) {
22118
+ setShownStep(stepIndex);
22119
+ setTapIndex(0);
22120
+ setTapCount(0);
22121
+ }
22122
+ React.useEffect(() => {
22123
+ let release;
22124
+ const press = setTimeout(() => {
22125
+ setTapCount((n) => n + 1);
22126
+ const targets = step.taps(ctxRef.current);
22127
+ const el = targets[tapIndex % Math.max(1, targets.length)];
22128
+ if (el instanceof Element) {
22129
+ el.classList.add("met-tutorial-pressed");
22130
+ release = setTimeout(() => el.classList.remove("met-tutorial-pressed"), PRESS_HOLD_MS);
22131
+ }
22132
+ }, PRESS_AT_MS);
22133
+ const advance = setTimeout(() => setTapIndex((i) => i + 1), CYCLE_MS);
22134
+ return () => {
22135
+ clearTimeout(press);
22136
+ clearTimeout(advance);
22137
+ if (release) clearTimeout(release);
22138
+ root.querySelectorAll(".met-tutorial-pressed").forEach((el) => el.classList.remove("met-tutorial-pressed"));
22139
+ };
22140
+ }, [step, tapIndex, root]);
22141
+ const text = options.texts?.[step.id] ?? step.text;
22142
+ const textRef = React.useRef(text);
22143
+ textRef.current = text;
22144
+ React.useEffect(() => {
22145
+ optionsRef.current.onStep?.(stepIndex, step.id);
22146
+ optionsRef.current.onSpeak?.(step.id, textRef.current);
22147
+ }, [stepIndex, step]);
22148
+ const isLast = stepIndex === steps.length - 1;
22149
+ const next = () => {
22150
+ if (isLast) onClose(true);
22151
+ else setStepIndex((i) => i + 1);
22152
+ };
22153
+ const overlayRef = React.useRef(null);
22154
+ const nextRef = React.useRef(null);
22155
+ const onCloseRef = React.useRef(onClose);
22156
+ onCloseRef.current = onClose;
22157
+ React.useEffect(() => {
22158
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
22159
+ previous?.blur();
22160
+ const onKeyDown = (e) => {
22161
+ const overlay = overlayRef.current;
22162
+ if (e.key === "Escape") {
22163
+ e.preventDefault();
22164
+ e.stopPropagation();
22165
+ onCloseRef.current(false);
22166
+ return;
22167
+ }
22168
+ if (e.key === "Tab" && overlay) {
22169
+ e.preventDefault();
22170
+ const buttons = Array.from(overlay.querySelectorAll("button"));
22171
+ if (buttons.length === 0) return;
22172
+ const at = buttons.indexOf(document.activeElement);
22173
+ const to = (at + (e.shiftKey ? -1 : 1) + buttons.length) % buttons.length;
22174
+ buttons[to]?.focus();
22175
+ return;
22176
+ }
22177
+ if (overlay && e.target instanceof Node && overlay.contains(e.target)) return;
22178
+ e.preventDefault();
22179
+ e.stopPropagation();
22180
+ };
22181
+ document.addEventListener("keydown", onKeyDown, true);
22182
+ return () => {
22183
+ document.removeEventListener("keydown", onKeyDown, true);
22184
+ if (previous?.isConnected) previous.focus();
22185
+ };
22186
+ }, []);
22187
+ const bubbleShown = layout !== null;
22188
+ React.useEffect(() => {
22189
+ nextRef.current?.focus({ focusVisible: false });
22190
+ }, [stepIndex, bubbleShown]);
22191
+ const maskId = `met-tutorial-mask-${React.useId()}`;
22192
+ if (typeof document === "undefined") return null;
22193
+ const holes = (layout?.spotlight ?? []).filter((r) => r !== null);
22194
+ const tapTarget = layout?.taps.length ? layout.taps[tapIndex % layout.taps.length] ?? null : null;
22195
+ const Scene = step.Scene;
22196
+ return reactDom.createPortal(
22197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: overlayRef, className: "met-tutorial", role: "dialog", "aria-modal": "true", "aria-label": text, children: [
22198
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "met-tutorial-dim", "aria-hidden": "true", children: [
22199
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("mask", { id: maskId, children: [
22200
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", fill: "white" }),
22201
+ holes.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx(
22202
+ "rect",
22203
+ {
22204
+ x: r.x - HOLE_PAD,
22205
+ y: r.y - HOLE_PAD,
22206
+ width: r.width + HOLE_PAD * 2,
22207
+ height: r.height + HOLE_PAD * 2,
22208
+ rx: HOLE_RADIUS,
22209
+ fill: "black"
22210
+ },
22211
+ i
22212
+ ))
22213
+ ] }) }),
22214
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", mask: `url(#${maskId})` })
22215
+ ] }),
22216
+ holes.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx(
22217
+ "div",
22218
+ {
22219
+ className: "met-tutorial-glow",
22220
+ style: {
22221
+ left: r.x - HOLE_PAD,
22222
+ top: r.y - HOLE_PAD,
22223
+ width: r.width + HOLE_PAD * 2,
22224
+ height: r.height + HOLE_PAD * 2,
22225
+ borderRadius: HOLE_RADIUS
22226
+ }
22227
+ },
22228
+ i
22229
+ )),
22230
+ Scene && layout && /* @__PURE__ */ jsxRuntime.jsx(Scene, { ctx, spotlight: layout.spotlight, taps: layout.taps, tapCount }),
22231
+ tapTarget && /* @__PURE__ */ jsxRuntime.jsx(Hand, { target: tapTarget, tapKey: `${stepIndex}-${tapIndex}` }),
22232
+ layout && /* @__PURE__ */ jsxRuntime.jsx(
22233
+ Bubble,
22234
+ {
22235
+ layout,
22236
+ text,
22237
+ stepIndex,
22238
+ stepCount: steps.length,
22239
+ nextLabel: options.nextLabel ?? DEFAULT_NEXT_LABEL,
22240
+ onSpeak: options.onSpeak && (options.canSpeak?.(step.id) ?? true) ? () => optionsRef.current.onSpeak?.(step.id, textRef.current) : void 0,
22241
+ onNext: next,
22242
+ nextRef
22243
+ }
22244
+ )
22245
+ ] }),
22246
+ document.body
22247
+ );
22248
+ };
22249
+ const Hand = ({ target, tapKey }) => {
22250
+ const c = centerOf(target);
22251
+ return /* @__PURE__ */ jsxRuntime.jsx(
22252
+ "div",
22253
+ {
22254
+ className: "met-tutorial-hand",
22255
+ style: {
22256
+ width: HAND_W,
22257
+ height: HAND_H,
22258
+ transform: `translate(${c.x - TUTORIAL_HAND_TIP.x * HAND_W}px, ${c.y - TUTORIAL_HAND_TIP.y * HAND_H}px)`
22259
+ },
22260
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "met-tutorial-hand-press", children: [
22261
+ /* @__PURE__ */ jsxRuntime.jsx(
22262
+ "span",
22263
+ {
22264
+ className: "met-tutorial-ripple",
22265
+ style: { left: TUTORIAL_HAND_TIP.x * HAND_W, top: TUTORIAL_HAND_TIP.y * HAND_H }
22266
+ }
22267
+ ),
22268
+ /* @__PURE__ */ jsxRuntime.jsx(LionHand, {})
22269
+ ] }, tapKey)
22270
+ }
22271
+ );
22272
+ };
22273
+ const Bubble = ({ layout, text, stepIndex, stepCount, nextLabel, onSpeak, onNext, nextRef }) => {
22274
+ const { anchor, root, vw, vh } = layout;
22275
+ const clampLeft = (x) => Math.min(Math.max(EDGE, x), Math.max(EDGE, vw - BUBBLE_W - EDGE));
22276
+ const style = anchor ? { left: clampLeft(anchor.x), bottom: Math.max(EDGE, vh - anchor.y + BUBBLE_GAP), width: BUBBLE_W } : { left: clampLeft((root?.x ?? 0) + EDGE * 2), top: (root?.y ?? 0) + EDGE * 2, width: BUBBLE_W };
22277
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `met-tutorial-bubble${anchor ? " met-tutorial-bubble--anchored" : ""}`, style, children: [
22278
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "met-tutorial-text", dir: "auto", children: text }),
22279
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "met-tutorial-actions", children: [
22280
+ onSpeak && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "met-tutorial-speak", "aria-label": "🔊", onClick: onSpeak, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", width: "24", height: "24", "aria-hidden": "true", children: [
22281
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9v6h4l5 4V5L8 9H4z", fill: "currentColor" }),
22282
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
22283
+ ] }) }),
22284
+ /* @__PURE__ */ jsxRuntime.jsx("button", { ref: nextRef, type: "button", className: "met-tutorial-next", onClick: onNext, children: nextLabel })
22285
+ ] }),
22286
+ stepCount > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "met-tutorial-dots", "aria-hidden": "true", children: Array.from({ length: stepCount }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: i === stepIndex ? "is-current" : i < stepIndex ? "is-done" : "" }, i)) })
22287
+ ] });
22288
+ };
22289
+ const DEMO_KEYS = ["2", "5", "8"];
22290
+ const DEFAULT_SLOTS = 3;
22291
+ const MAX_SLOTS = 6;
22292
+ const STAND_IN_CHECK_SIZE = 52;
22293
+ const STAND_IN_CHECK_GAP = 20;
22294
+ function keypad(ctx) {
22295
+ return ctx.root.querySelector('[data-tutorial="keypad"]');
22296
+ }
22297
+ function key(ctx, k) {
22298
+ return ctx.root.querySelector(`[data-tutorial="keypad"] [data-key="${k}"]`);
22299
+ }
22300
+ function answerBox(ctx) {
22301
+ const inputs = getTextInputs(ctx.root);
22302
+ return inputs.find(canTakeInput) ?? inputs[0] ?? null;
22303
+ }
22304
+ function answerSlots(ctx) {
22305
+ const id = answerBox(ctx)?.id.replace(/-input$/, "");
22306
+ const max = id ? getMaxInputLength(ctx.json, id) : void 0;
22307
+ return Math.min(MAX_SLOTS, Math.max(1, max ?? DEFAULT_SLOTS));
22308
+ }
22309
+ function checkTarget(ctx) {
22310
+ if (ctx.submitTarget) return ctx.submitTarget;
22311
+ const box = answerBox(ctx)?.getBoundingClientRect();
22312
+ if (!box) return null;
22313
+ return new DOMRect(
22314
+ box.right + STAND_IN_CHECK_GAP,
22315
+ box.top + box.height / 2 - STAND_IN_CHECK_SIZE / 2,
22316
+ STAND_IN_CHECK_SIZE,
22317
+ STAND_IN_CHECK_SIZE
22318
+ );
22319
+ }
22320
+ const demoKeys = (ctx) => DEMO_KEYS.map((k) => key(ctx, k));
22321
+ const StandInFill = ({ box, slots, filled }) => /* @__PURE__ */ jsxRuntime.jsx(
22322
+ "div",
22323
+ {
22324
+ className: "met-tutorial-fill",
22325
+ style: { left: box.x, top: box.y, width: box.width, height: box.height },
22326
+ children: Array.from({ length: slots }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: i < filled ? "met-tutorial-fill-dot" : "met-tutorial-fill-slot", children: i < filled ? "●" : "_" }, i))
22327
+ }
22328
+ );
22329
+ const center = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
22330
+ const HopScene = ({ ctx, spotlight, taps, tapCount }) => {
22331
+ const box = spotlight[1];
22332
+ if (!box) return null;
22333
+ const slots = answerSlots(ctx);
22334
+ const filled = tapCount === 0 ? 0 : (tapCount - 1) % slots + 1;
22335
+ const from = tapCount > 0 ? taps[(tapCount - 1) % taps.length] : null;
22336
+ let arc = null;
22337
+ if (from) {
22338
+ const a = center(from);
22339
+ const b = center(box);
22340
+ const lift = Math.max(40, Math.hypot(b.x - a.x, b.y - a.y) * 0.35);
22341
+ arc = `M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${Math.min(a.y, b.y) - lift} ${b.x} ${b.y}`;
22342
+ }
22343
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22344
+ arc && /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "met-tutorial-arc", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: arc }) }),
22345
+ arc && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "met-tutorial-hop", style: { offsetPath: `path('${arc}')` } }, tapCount),
22346
+ /* @__PURE__ */ jsxRuntime.jsx(StandInFill, { box, slots, filled })
22347
+ ] });
22348
+ };
22349
+ const CheckScene = ({ ctx, spotlight }) => {
22350
+ const box = spotlight[0];
22351
+ const check = spotlight[1];
22352
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
22353
+ box && /* @__PURE__ */ jsxRuntime.jsx(StandInFill, { box, slots: answerSlots(ctx), filled: answerSlots(ctx) }),
22354
+ check && !ctx.submitTarget && /* @__PURE__ */ jsxRuntime.jsx(
22355
+ "div",
22356
+ {
22357
+ className: "met-tutorial-stand-in-check",
22358
+ style: { left: check.x, top: check.y, width: check.width, height: check.height },
22359
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "✓" })
22360
+ }
22361
+ )
22362
+ ] });
22363
+ };
22364
+ const keypadTutorial = {
22365
+ id: "keypad",
22366
+ appliesTo: (json) => json.widgets[NUMERIC_KEYPAD_WIDGET_ID]?.type === JSON_TYPES.NUMERIC_KEYPAD,
22367
+ steps: [
22368
+ {
22369
+ id: "keypad.tap-number",
22370
+ text: "اضغط على رقم",
22371
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22372
+ taps: demoKeys
22373
+ },
22374
+ {
22375
+ id: "keypad.hop",
22376
+ text: "يظهر الرقم في الخانة",
22377
+ spotlight: (ctx) => [keypad(ctx), answerBox(ctx)],
22378
+ taps: demoKeys,
22379
+ Scene: HopScene
22380
+ },
22381
+ {
22382
+ id: "keypad.tap-check",
22383
+ text: "اضغط على زر التحقق",
22384
+ spotlight: (ctx) => [answerBox(ctx), checkTarget(ctx)],
22385
+ taps: (ctx) => [checkTarget(ctx)],
22386
+ Scene: CheckScene
22387
+ }
22388
+ ]
22389
+ };
22390
+ const WIDGET_TUTORIALS = [keypadTutorial];
22391
+ function findWidgetTutorial(json) {
22392
+ return WIDGET_TUTORIALS.find((t) => t.appliesTo(json)) ?? null;
22393
+ }
21187
22394
  const TOOL_ASPECT_RATIO = {
21188
22395
  ruler: RULER_ASPECT_RATIO,
21189
22396
  protractor: PROTRACTOR_ASPECT_RATIO,
@@ -21222,6 +22429,11 @@ function buildInitialValues(json) {
21222
22429
  function toBaseId(widgetId) {
21223
22430
  return parseFractionKey(widgetId)?.widgetId ?? widgetId;
21224
22431
  }
22432
+ function hasJustFilledBox(json, key2, previous, value) {
22433
+ const maxLength = getMaxInputLength(json, key2);
22434
+ if (maxLength === void 0) return false;
22435
+ return String(previous ?? "").length < maxLength && String(value).length >= maxLength;
22436
+ }
21225
22437
  function buildInitialValuesSignature(json) {
21226
22438
  return JSON.stringify({
21227
22439
  widgets: json.widgets,
@@ -21271,6 +22483,27 @@ function buildRevealedValues(json) {
21271
22483
  }
21272
22484
  return revealed;
21273
22485
  }
22486
+ function buildRevealedDropSources(json, revealedValues) {
22487
+ const pool = [];
22488
+ const bank = json.widgets[DRAG_OPTIONS_WIDGET_ID];
22489
+ if (bank?.type === JSON_TYPES.DRAG_OPTIONS) {
22490
+ bank.options.forEach((value, idx) => pool.push({ optionId: `drag-option-${idx}`, value: String(value) }));
22491
+ }
22492
+ for (const [widgetId, widget] of Object.entries(json.widgets)) {
22493
+ if (widget.type === JSON_TYPES.INLINE_DRAG_OPTION) pool.push({ optionId: widgetId, value: widget.value });
22494
+ }
22495
+ const sources = /* @__PURE__ */ new Map();
22496
+ if (pool.length === 0) return sources;
22497
+ const used = /* @__PURE__ */ new Set();
22498
+ for (const [inputId, value] of Object.entries(revealedValues)) {
22499
+ if (typeof value !== "string" || value === "") continue;
22500
+ const match = pool.find((o) => !used.has(o.optionId) && normalize(o.value) === normalize(value));
22501
+ if (!match) continue;
22502
+ used.add(match.optionId);
22503
+ sources.set(inputId, match.optionId);
22504
+ }
22505
+ return sources;
22506
+ }
21274
22507
  const ExerciseRenderer = React.forwardRef(({
21275
22508
  json,
21276
22509
  onSubmit,
@@ -21293,6 +22526,8 @@ const ExerciseRenderer = React.forwardRef(({
21293
22526
  const rootRef = React.useRef(null);
21294
22527
  const [resetKey, setResetKey] = React.useState(0);
21295
22528
  const [revealed, setRevealed] = React.useState(false);
22529
+ const tutorial = React.useMemo(() => findWidgetTutorial(json), [json]);
22530
+ const [tutorialRun, setTutorialRun] = React.useState(null);
21296
22531
  const themeStyle = React.useMemo(() => {
21297
22532
  const style = {};
21298
22533
  if (theme?.correct) style["--exercise-correct"] = theme.correct;
@@ -21313,6 +22548,7 @@ const ExerciseRenderer = React.forwardRef(({
21313
22548
  setResetKey((k) => k + 1);
21314
22549
  setLoadedValuesSignature(initialValuesSignature);
21315
22550
  touchedRef.current = /* @__PURE__ */ new Set();
22551
+ setTutorialRun(null);
21316
22552
  }, [json, initialValuesSignature, loadedValuesSignature]);
21317
22553
  const locked = validationDetails !== null || externallyLocked;
21318
22554
  const lastResultRef = React.useRef(null);
@@ -21396,7 +22632,7 @@ const ExerciseRenderer = React.forwardRef(({
21396
22632
  const next = { ...valuesRef.current, ...revealedValues };
21397
22633
  valuesRef.current = next;
21398
22634
  setValues(next);
21399
- setDroppedSources(/* @__PURE__ */ new Map());
22635
+ setDroppedSources(buildRevealedDropSources(json, revealedValues));
21400
22636
  setValidationDetails(details);
21401
22637
  setRevealed(true);
21402
22638
  setResetKey((k) => k + 1);
@@ -21404,20 +22640,29 @@ const ExerciseRenderer = React.forwardRef(({
21404
22640
  focusFirstInput: () => {
21405
22641
  const container = rootRef.current ?? document.querySelector(".math-exercise-renderer");
21406
22642
  if (!container) return;
21407
- const candidates = Array.from(
21408
- container.querySelectorAll('input[type="text"], input[type="number"], textarea, select')
21409
- );
21410
- const canTakeInput = (el) => !el.readOnly && !el.disabled;
22643
+ const candidates = getTextInputs(container);
21411
22644
  const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
21412
22645
  target?.focus();
21413
- }
21414
- }), [json, values, onSubmit, playSound, locked, initialValuesSignature]);
22646
+ },
22647
+ hasTutorial: () => tutorial !== null,
22648
+ startTutorial: (options = {}) => {
22649
+ if (!tutorial || !rootRef.current) return false;
22650
+ setTutorialRun((prev) => ({ options, run: (prev?.run ?? 0) + 1 }));
22651
+ return true;
22652
+ },
22653
+ stopTutorial: () => setTutorialRun(null)
22654
+ }), [json, values, onSubmit, playSound, locked, initialValuesSignature, tutorial]);
22655
+ const focusNextInput = (currentKey) => {
22656
+ if (!rootRef.current) return;
22657
+ findNextInput(rootRef.current, currentKey)?.focus();
22658
+ };
21415
22659
  const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
21416
22660
  if (locked) return;
21417
22661
  if (!silent) {
21418
22662
  setRevealed(false);
21419
22663
  touchedRef.current.add(toBaseId(widgetId));
21420
22664
  }
22665
+ const previous = valuesRef.current[widgetId];
21421
22666
  const next = { ...valuesRef.current, [widgetId]: value };
21422
22667
  valuesRef.current = next;
21423
22668
  setValues(next);
@@ -21431,6 +22676,9 @@ const ExerciseRenderer = React.forwardRef(({
21431
22676
  return next2;
21432
22677
  });
21433
22678
  onChange?.(next);
22679
+ if (!fromDrop && !silent && hasJustFilledBox(json, widgetId, previous, value)) {
22680
+ focusNextInput(widgetId);
22681
+ }
21434
22682
  };
21435
22683
  const handleSubmit = () => {
21436
22684
  runSubmit();
@@ -21545,7 +22793,7 @@ const ExerciseRenderer = React.forwardRef(({
21545
22793
  const rulerWidget = json.widgets[RULER_WIDGET_ID];
21546
22794
  const protractorWidget = json.widgets[PROTRACTOR_WIDGET_ID];
21547
22795
  const setSquareWidget = json.widgets[SET_SQUARE_WIDGET_ID];
21548
- const numericKeypadWidget = json.widgets["_numeric_keypad"];
22796
+ const numericKeypadWidget = json.widgets[NUMERIC_KEYPAD_WIDGET_ID];
21549
22797
  const { tools, toolsMinHeight } = React.useMemo(() => {
21550
22798
  const list = [];
21551
22799
  const PAD = 12;
@@ -21564,7 +22812,7 @@ const ExerciseRenderer = React.forwardRef(({
21564
22812
  if (setSquareWidget) add2("set-square-1", "set-square", Math.round(getSize(setSquareWidget) * 160));
21565
22813
  return { tools: list, toolsMinHeight: maxH };
21566
22814
  }, [rulerWidget, protractorWidget, setSquareWidget]);
21567
- return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
22815
+ return /* @__PURE__ */ jsxRuntime.jsx(SoundPlayerProvider, { play: playSound, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseLockProvider, { locked, children: /* @__PURE__ */ jsxRuntime.jsx(ExerciseDndProvider, { droppedSources, exerciseKey: loadedValuesSignature, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: `math-exercise-renderer ${revealed ? "revealed" : ""} ${locked ? "locked" : ""} ${className}`, style: themeStyle, children: [
21568
22816
  /* @__PURE__ */ jsxRuntime.jsxs(
21569
22817
  "div",
21570
22818
  {
@@ -21660,7 +22908,21 @@ const ExerciseRenderer = React.forwardRef(({
21660
22908
  }
21661
22909
  }
21662
22910
  ) }),
21663
- showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" })
22911
+ showSubmitButton && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "exercise-submit-btn", disabled: locked, onClick: handleSubmit, children: "Check Answer" }),
22912
+ tutorial && tutorialRun && rootRef.current && /* @__PURE__ */ jsxRuntime.jsx(
22913
+ TutorialOverlay,
22914
+ {
22915
+ tutorial,
22916
+ root: rootRef.current,
22917
+ json,
22918
+ options: tutorialRun.options,
22919
+ onClose: (finished) => {
22920
+ setTutorialRun(null);
22921
+ if (finished) tutorialRun.options.onDone?.();
22922
+ }
22923
+ },
22924
+ tutorialRun.run
22925
+ )
21664
22926
  ] }) }) }) });
21665
22927
  });
21666
22928
  ExerciseRenderer.displayName = "ExerciseRenderer";
@@ -21716,10 +22978,16 @@ exports.ChartBarJSONSchema = ChartBarJSONSchema;
21716
22978
  exports.ChartBarSchema = ChartBarSchema;
21717
22979
  exports.ChartBarWidgetJSONSchema = ChartBarWidgetJSONSchema;
21718
22980
  exports.ChartBlockSchema = ChartBlockSchema;
22981
+ exports.ChartGroupJSONSchema = ChartGroupJSONSchema;
22982
+ exports.ChartGroupSchema = ChartGroupSchema;
21719
22983
  exports.ChartItemSchema = ChartItemSchema;
21720
22984
  exports.ChartPictoJSONSchema = ChartPictoJSONSchema;
21721
22985
  exports.ChartPictoSchema = ChartPictoSchema;
21722
22986
  exports.ChartPictoWidgetJSONSchema = ChartPictoWidgetJSONSchema;
22987
+ exports.ChartSeriesBarJSONSchema = ChartSeriesBarJSONSchema;
22988
+ exports.ChartSeriesBarSchema = ChartSeriesBarSchema;
22989
+ exports.ChartSeriesJSONSchema = ChartSeriesJSONSchema;
22990
+ exports.ChartSeriesSchema = ChartSeriesSchema;
21723
22991
  exports.ClockInlineSchema = ClockInlineSchema;
21724
22992
  exports.ClockItemSchema = ClockItemSchema;
21725
22993
  exports.ColorTextInlineSchema = ColorTextInlineSchema;
@@ -21753,11 +23021,13 @@ exports.GridSymmetryJSONSchema = GridSymmetryJSONSchema;
21753
23021
  exports.GroupItemSchema = GroupItemSchema;
21754
23022
  exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
21755
23023
  exports.HighlightedTextSchema = HighlightedTextSchema;
23024
+ exports.INLINE_INPUT_MAX_WIDTH = INLINE_INPUT_MAX_WIDTH;
21756
23025
  exports.ImageInlineSchema = ImageInlineSchema;
21757
23026
  exports.ImageItemSchema = ImageItemSchema;
21758
23027
  exports.InlineDragOptionSchema = InlineDragOptionSchema;
21759
23028
  exports.InlineInputJSONSchema = InlineInputJSONSchema;
21760
23029
  exports.InlineInputWidgetSchema = InlineInputWidgetSchema;
23030
+ exports.InlineInputWidthSchema = InlineInputWidthSchema;
21761
23031
  exports.InlineNodeSchema = InlineNodeSchema;
21762
23032
  exports.JSON_TYPES = JSON_TYPES;
21763
23033
  exports.LayoutItemSchema = LayoutItemSchema;
@@ -21831,9 +23101,11 @@ exports.VopRowItemSchema = VopRowItemSchema;
21831
23101
  exports.VopRowSchema = VopRowSchema;
21832
23102
  exports.VopSeparatorItemSchema = VopSeparatorItemSchema;
21833
23103
  exports.VopSeparatorSchema = VopSeparatorSchema;
23104
+ exports.WIDGET_TUTORIALS = WIDGET_TUTORIALS;
21834
23105
  exports.WidgetJSONSchema = WidgetJSONSchema;
21835
23106
  exports.WidgetRefSchema = WidgetRefSchema;
21836
23107
  exports.WidgetRenderer = WidgetRenderer;
23108
+ exports.alignGroupBarsToSeries = alignGroupBarsToSeries;
21837
23109
  exports.compile = compile;
21838
23110
  exports.compileWithAST = compileWithAST;
21839
23111
  exports.computeGridSymmetryCellsToFill = computeGridSymmetryCellsToFill;
@@ -21842,7 +23114,10 @@ exports.createBoldText = createBoldText;
21842
23114
  exports.createChartAxis = createChartAxis;
21843
23115
  exports.createChartBar = createChartBar;
21844
23116
  exports.createChartBlock = createChartBlock;
23117
+ exports.createChartGroup = createChartGroup;
21845
23118
  exports.createChartPicto = createChartPicto;
23119
+ exports.createChartSeries = createChartSeries;
23120
+ exports.createChartSeriesBar = createChartSeriesBar;
21846
23121
  exports.createColorText = createColorText;
21847
23122
  exports.createDisplayClock = createDisplayClock;
21848
23123
  exports.createDragOptions = createDragOptions;
@@ -21886,6 +23161,7 @@ exports.createVopSeparator = createVopSeparator;
21886
23161
  exports.evaluate = evaluate;
21887
23162
  exports.extractVariables = extractVariables;
21888
23163
  exports.findValidCombination = findValidCombination;
23164
+ exports.findWidgetTutorial = findWidgetTutorial;
21889
23165
  exports.getAnswerableInputs = getAnswerableInputs;
21890
23166
  exports.getFractionKey = getFractionKey;
21891
23167
  exports.getMissingInputs = getMissingInputs;
@@ -21895,9 +23171,12 @@ exports.isChartAxis = isChartAxis;
21895
23171
  exports.isChartBar = isChartBar;
21896
23172
  exports.isChartBarWidgetJSON = isChartBarWidgetJSON;
21897
23173
  exports.isChartBlock = isChartBlock;
23174
+ exports.isChartGroup = isChartGroup;
21898
23175
  exports.isChartItem = isChartItem;
21899
23176
  exports.isChartPicto = isChartPicto;
21900
23177
  exports.isChartPictoWidgetJSON = isChartPictoWidgetJSON;
23178
+ exports.isChartSeries = isChartSeries;
23179
+ exports.isChartSeriesBar = isChartSeriesBar;
21901
23180
  exports.isClock = isClock;
21902
23181
  exports.isClockItem = isClockItem;
21903
23182
  exports.isColorText = isColorText;
@@ -21984,7 +23263,8 @@ exports.validateExpression = validateExpression;
21984
23263
  exports.validateInputFormats = validateInputFormats;
21985
23264
  exports.validateMixed = validateMixed;
21986
23265
  exports.validateSimple = validateSimple;
23266
+ exports.validateUniqueIds = validateUniqueIds;
21987
23267
  exports.validateVisualFractionCapacity = validateVisualFractionCapacity;
21988
23268
  exports.validateWidgetJSON = validateWidgetJSON;
21989
23269
  exports.validateWidgetJSONSafe = validateWidgetJSONSafe;
21990
- //# sourceMappingURL=index-D-QbiBn5.js.map
23270
+ //# sourceMappingURL=index-C3qDr71I.js.map