@mathscan/math-exercise-engine 1.1.4 → 1.1.6

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.
package/dist/index.js CHANGED
@@ -29,6 +29,8 @@ const AST_TYPES = {
29
29
  FRACTION: "Fraction",
30
30
  /** Inline image element */
31
31
  IMAGE: "Image",
32
+ /** Clock widget — analog/digital, display or interactive */
33
+ CLOCK: "Clock",
32
34
  // ═══════════════════════════════════════════════════════════════════════════
33
35
  // Blocks
34
36
  // ═══════════════════════════════════════════════════════════════════════════
@@ -114,6 +116,8 @@ const JSON_TYPES = {
114
116
  IMAGE: "image",
115
117
  /** Reference to a widget */
116
118
  WIDGET_REF: "widget-ref",
119
+ /** Clock widget (inline): analog or digital, display or interactive */
120
+ CLOCK: "clock",
117
121
  /** Line break element */
118
122
  LINEBREAK: "linebreak",
119
123
  /** Grouped layout items */
@@ -317,6 +321,10 @@ const COUNTER = chevrotain.createToken({
317
321
  name: "COUNTER",
318
322
  pattern: /COUNTER/
319
323
  });
324
+ const CLOCK = chevrotain.createToken({
325
+ name: "CLOCK",
326
+ pattern: /CLOCK/
327
+ });
320
328
  const HIGHLIGHT = chevrotain.createToken({
321
329
  name: "HIGHLIGHT",
322
330
  pattern: /HIGHLIGHT(?!_TEXT)/
@@ -488,6 +496,7 @@ const allTokens = [
488
496
  BOTTOM_ROW,
489
497
  CELL,
490
498
  COUNTER,
499
+ CLOCK,
491
500
  HIGHLIGHT,
492
501
  VISUAL_FRACTION,
493
502
  FRACTION,
@@ -1161,9 +1170,17 @@ class MathExerciseParser extends chevrotain.CstParser {
1161
1170
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
1162
1171
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
1163
1172
  // INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
1164
- { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) }
1173
+ { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) },
1174
+ // CLOCK (inline clock widget)
1175
+ { ALT: () => this.SUBRULE(this.clock) }
1165
1176
  ]);
1166
1177
  });
1178
+ this.clock = this.RULE("clock", () => {
1179
+ this.CONSUME(CLOCK);
1180
+ this.CONSUME(LBracket);
1181
+ this.SUBRULE(this.parameterList);
1182
+ this.CONSUME(RBracket);
1183
+ });
1167
1184
  this.textHighlighter = this.RULE("textHighlighter", () => {
1168
1185
  this.OR([
1169
1186
  { ALT: () => this.CONSUME(TEXT_HIGHLIGHTER) },
@@ -1521,6 +1538,33 @@ const InlineDragOptionSchema = zod.z.object({
1521
1538
  type: zod.z.literal(AST_TYPES.INLINE_DRAG_OPTION),
1522
1539
  value: zod.z.string().min(1)
1523
1540
  });
1541
+ const CLOCK_TYPES = ["analog", "digital"];
1542
+ const CLOCK_MODES = ["display", "interactive"];
1543
+ const CLOCK_FORMATS = ["12h", "24h"];
1544
+ const CLOCK_TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
1545
+ const TimeStringSchema = zod.z.string().regex(CLOCK_TIME_REGEX, 'must be "HH:MM" in 24h format');
1546
+ const DisplayClockInlineSchema = zod.z.object({
1547
+ type: zod.z.literal(AST_TYPES.CLOCK),
1548
+ mode: zod.z.literal("display"),
1549
+ clockType: zod.z.enum(CLOCK_TYPES),
1550
+ value: TimeStringSchema,
1551
+ format: zod.z.enum(CLOCK_FORMATS).optional(),
1552
+ name: zod.z.string().optional()
1553
+ });
1554
+ const InteractiveClockInlineSchema = zod.z.object({
1555
+ type: zod.z.literal(AST_TYPES.CLOCK),
1556
+ mode: zod.z.literal("interactive"),
1557
+ clockType: zod.z.literal("analog"),
1558
+ value: TimeStringSchema,
1559
+ answer: TimeStringSchema,
1560
+ format: zod.z.enum(CLOCK_FORMATS).optional(),
1561
+ snap: zod.z.number().int().min(1).max(60).optional(),
1562
+ name: zod.z.string()
1563
+ });
1564
+ const ClockInlineSchema = zod.z.discriminatedUnion("mode", [
1565
+ DisplayClockInlineSchema,
1566
+ InteractiveClockInlineSchema
1567
+ ]);
1524
1568
  const InlineNodeSchema = zod.z.union([
1525
1569
  FreeTextInlineSchema,
1526
1570
  SpaceInlineSchema,
@@ -1531,7 +1575,8 @@ const InlineNodeSchema = zod.z.union([
1531
1575
  FractionInlineSchema,
1532
1576
  ImageInlineSchema,
1533
1577
  VisualFractionInlineSchema,
1534
- InlineDragOptionSchema
1578
+ InlineDragOptionSchema,
1579
+ ClockInlineSchema
1535
1580
  ]);
1536
1581
  function createFreeText(value) {
1537
1582
  return FreeTextInlineSchema.parse({
@@ -1624,6 +1669,21 @@ function createInlineDragOption(value) {
1624
1669
  value
1625
1670
  });
1626
1671
  }
1672
+ function createDisplayClock(params) {
1673
+ return ClockInlineSchema.parse({
1674
+ type: AST_TYPES.CLOCK,
1675
+ mode: "display",
1676
+ ...params
1677
+ });
1678
+ }
1679
+ function createInteractiveClock(params) {
1680
+ return ClockInlineSchema.parse({
1681
+ type: AST_TYPES.CLOCK,
1682
+ mode: "interactive",
1683
+ clockType: "analog",
1684
+ ...params
1685
+ });
1686
+ }
1627
1687
  function isFreeText(value) {
1628
1688
  return FreeTextInlineSchema.safeParse(value).success;
1629
1689
  }
@@ -1639,6 +1699,9 @@ function isFraction(value) {
1639
1699
  function isImage(value) {
1640
1700
  return ImageInlineSchema.safeParse(value).success;
1641
1701
  }
1702
+ function isClock(value) {
1703
+ return ClockInlineSchema.safeParse(value).success;
1704
+ }
1642
1705
  function isInlineNode(value) {
1643
1706
  return InlineNodeSchema.safeParse(value).success;
1644
1707
  }
@@ -2685,8 +2748,66 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2685
2748
  if (ctx.inlineDragOption) {
2686
2749
  return this.visit(ctx.inlineDragOption);
2687
2750
  }
2751
+ if (ctx.clock) {
2752
+ return this.visit(ctx.clock);
2753
+ }
2688
2754
  throw new Error("Unknown inline element");
2689
2755
  }
2756
+ // Manual checks below duplicate what ClockInlineSchema.parse() enforces, but produce
2757
+ // DSL-friendly error messages (e.g. 'CLOCK: value is required') instead of Zod paths.
2758
+ clock(ctx) {
2759
+ const params = this.visit(ctx.parameterList);
2760
+ const mode = params.mode ?? "display";
2761
+ if (!CLOCK_MODES.includes(mode)) {
2762
+ throw new Error(`CLOCK: mode must be one of ${CLOCK_MODES.join(", ")} (got "${mode}")`);
2763
+ }
2764
+ const clockType = params.type;
2765
+ if (!clockType) {
2766
+ throw new Error('CLOCK: type is required ("analog" or "digital")');
2767
+ }
2768
+ if (!CLOCK_TYPES.includes(clockType)) {
2769
+ throw new Error(`CLOCK: type must be one of ${CLOCK_TYPES.join(", ")} (got "${clockType}")`);
2770
+ }
2771
+ const value = params.value;
2772
+ if (!value) {
2773
+ throw new Error('CLOCK: value is required ("HH:MM" 24h format)');
2774
+ }
2775
+ if (!CLOCK_TIME_REGEX.test(value)) {
2776
+ throw new Error(`CLOCK: value must be "HH:MM" in 24h format (got "${value}")`);
2777
+ }
2778
+ const format = params.format;
2779
+ if (format !== void 0 && !CLOCK_FORMATS.includes(format)) {
2780
+ throw new Error(`CLOCK: format must be one of ${CLOCK_FORMATS.join(", ")} (got "${format}")`);
2781
+ }
2782
+ const name = params.name;
2783
+ const answer = params.answer;
2784
+ const snap = params.snap;
2785
+ if (mode === "display") {
2786
+ if (answer !== void 0) {
2787
+ throw new Error('CLOCK: "answer" is only valid when mode="interactive"');
2788
+ }
2789
+ if (snap !== void 0) {
2790
+ throw new Error('CLOCK: "snap" is only valid when mode="interactive"');
2791
+ }
2792
+ return createDisplayClock({ clockType, value, format, name });
2793
+ }
2794
+ if (clockType !== "analog") {
2795
+ throw new Error('CLOCK: interactive mode requires type="analog" (digital clocks cannot be interactive)');
2796
+ }
2797
+ if (answer === void 0) {
2798
+ throw new Error('CLOCK: interactive mode requires "answer"');
2799
+ }
2800
+ if (!CLOCK_TIME_REGEX.test(answer)) {
2801
+ throw new Error(`CLOCK: answer must be "HH:MM" in 24h format (got "${answer}")`);
2802
+ }
2803
+ if (!name) {
2804
+ throw new Error('CLOCK: interactive mode requires "name" (for answer validation)');
2805
+ }
2806
+ if (snap !== void 0 && (!Number.isInteger(snap) || snap < 1 || snap > 60)) {
2807
+ throw new Error(`CLOCK: snap must be an integer in 1..60 (got ${snap})`);
2808
+ }
2809
+ return createInteractiveClock({ name, value, answer, format, snap });
2810
+ }
2690
2811
  textHighlighter(ctx) {
2691
2812
  const params = this.visit(ctx.parameterList);
2692
2813
  const value = params.value;
@@ -2946,6 +3067,31 @@ const OperatorItemSchema = zod.z.object({
2946
3067
  type: zod.z.literal(JSON_TYPES.OPERATOR),
2947
3068
  operator: OperatorSchema
2948
3069
  });
3070
+ const ClockTimeSchema = zod.z.string().regex(CLOCK_TIME_REGEX);
3071
+ const ClockFormatSchema = zod.z.enum(["12h", "24h"]);
3072
+ const ClockDisplayItemSchema = zod.z.object({
3073
+ id: zod.z.string(),
3074
+ type: zod.z.literal(JSON_TYPES.CLOCK),
3075
+ mode: zod.z.literal("display"),
3076
+ clockType: zod.z.enum(["analog", "digital"]),
3077
+ value: ClockTimeSchema,
3078
+ format: ClockFormatSchema.optional(),
3079
+ name: zod.z.string().optional()
3080
+ });
3081
+ const ClockInteractiveItemSchema = zod.z.object({
3082
+ id: zod.z.string(),
3083
+ type: zod.z.literal(JSON_TYPES.CLOCK),
3084
+ mode: zod.z.literal("interactive"),
3085
+ clockType: zod.z.literal("analog"),
3086
+ value: ClockTimeSchema,
3087
+ format: ClockFormatSchema.optional(),
3088
+ snap: zod.z.number().int().min(1).max(60).optional(),
3089
+ name: zod.z.string()
3090
+ });
3091
+ const ClockItemSchema = zod.z.discriminatedUnion("mode", [
3092
+ ClockDisplayItemSchema,
3093
+ ClockInteractiveItemSchema
3094
+ ]);
2949
3095
  const LayoutItemSchema = zod.z.lazy(
2950
3096
  () => zod.z.union([
2951
3097
  QuestionTextSchema,
@@ -2961,7 +3107,8 @@ const LayoutItemSchema = zod.z.lazy(
2961
3107
  OperatorItemSchema,
2962
3108
  TableItemSchema,
2963
3109
  NumberLineItemSchema,
2964
- VerticalOperationItemSchema
3110
+ VerticalOperationItemSchema,
3111
+ ClockItemSchema
2965
3112
  ])
2966
3113
  );
2967
3114
  const GroupItemSchema = zod.z.object({
@@ -3280,6 +3427,9 @@ function isTableCellItem(item) {
3280
3427
  function isVerticalOperationItem(item) {
3281
3428
  return item.type === JSON_TYPES.VERTICAL_OPERATION;
3282
3429
  }
3430
+ function isClockItem(item) {
3431
+ return item.type === JSON_TYPES.CLOCK;
3432
+ }
3283
3433
  function isInlineInputJSON(widget) {
3284
3434
  return widget.type === JSON_TYPES.INLINE_INPUT;
3285
3435
  }
@@ -3400,6 +3550,8 @@ function toJSON(ast) {
3400
3550
  if (inline.answer !== void 0) {
3401
3551
  answers[inline.name] = inline.answer;
3402
3552
  }
3553
+ } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
3554
+ answers[inline.name] = inline.answer;
3403
3555
  }
3404
3556
  if (inline.type === AST_TYPES.FRACTION) {
3405
3557
  widgets[inline.name] = {
@@ -3783,6 +3935,21 @@ function transformInline(inline, idGenerator, inlineDragIds) {
3783
3935
  type: JSON_TYPES.WIDGET_REF,
3784
3936
  widgetId: inlineDragIds.get(inline)
3785
3937
  };
3938
+ case AST_TYPES.CLOCK: {
3939
+ const base = {
3940
+ id: idGenerator.generate(JSON_TYPES.CLOCK),
3941
+ type: JSON_TYPES.CLOCK,
3942
+ mode: inline.mode,
3943
+ clockType: inline.clockType,
3944
+ value: inline.value,
3945
+ ...inline.format !== void 0 && { format: inline.format },
3946
+ ...inline.name !== void 0 && { name: inline.name }
3947
+ };
3948
+ if (inline.mode === "interactive") {
3949
+ return { ...base, ...inline.snap !== void 0 && { snap: inline.snap } };
3950
+ }
3951
+ return base;
3952
+ }
3786
3953
  default:
3787
3954
  throw new Error(`Unknown inline type: ${inline.type}`);
3788
3955
  }
@@ -14008,12 +14175,13 @@ const SingleDragOption = ({
14008
14175
  const PillBackground = ({
14009
14176
  width = "100%",
14010
14177
  minHeight = "72px",
14011
- fill = "#FFE7B4",
14178
+ fill,
14012
14179
  children,
14013
14180
  className = "",
14014
14181
  style,
14015
14182
  ...rest
14016
14183
  }) => {
14184
+ const pillVarStyle = fill ? { "--exercise-pill-bg": fill } : {};
14017
14185
  return /* @__PURE__ */ jsxRuntime.jsxs(
14018
14186
  "div",
14019
14187
  {
@@ -14025,6 +14193,7 @@ const PillBackground = ({
14025
14193
  justifyContent: "center",
14026
14194
  width,
14027
14195
  minHeight,
14196
+ ...pillVarStyle,
14028
14197
  ...style
14029
14198
  },
14030
14199
  ...rest,
@@ -14046,8 +14215,8 @@ const PillBackground = ({
14046
14215
  children: /* @__PURE__ */ jsxRuntime.jsx(
14047
14216
  "path",
14048
14217
  {
14049
- d: "M439.707 0C457.071 0 472.52 8.196 482.398 20.9297C498.608 26.3308 510 39.1028 510 54C510 68.8966 498.608 81.667 482.399 87.0684C472.521 99.803 457.072 108 439.707 108H70.8975C53.653 108 38.2956 99.9172 28.4092 87.333C11.7719 82.0918 0.00025108 69.1432 0 54C0 38.8562 11.7721 25.906 28.4102 20.665C38.2966 8.08186 53.6537 0 70.8975 0H439.707Z",
14050
- fill
14218
+ className: "pill-bg-path",
14219
+ d: "M439.707 0C457.071 0 472.52 8.196 482.398 20.9297C498.608 26.3308 510 39.1028 510 54C510 68.8966 498.608 81.667 482.399 87.0684C472.521 99.803 457.072 108 439.707 108H70.8975C53.653 108 38.2956 99.9172 28.4092 87.333C11.7719 82.0918 0.00025108 69.1432 0 54C0 38.8562 11.7721 25.906 28.4102 20.665C38.2966 8.08186 53.6537 0 70.8975 0H439.707Z"
14051
14220
  }
14052
14221
  )
14053
14222
  }
@@ -16173,6 +16342,236 @@ const NumberLineRenderer = ({
16173
16342
  }
16174
16343
  );
16175
16344
  };
16345
+ function parseTime(s) {
16346
+ const parts = s.split(":");
16347
+ return { h: Number(parts[0]), m: Number(parts[1]) };
16348
+ }
16349
+ function formatTime(h, m) {
16350
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
16351
+ }
16352
+ function formatDisplay(h24, m, format) {
16353
+ if (format === "24h") return formatTime(h24, m);
16354
+ const period = h24 >= 12 ? "PM" : "AM";
16355
+ const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
16356
+ return `${String(h12).padStart(2, "0")}:${String(m).padStart(2, "0")} ${period}`;
16357
+ }
16358
+ const CLOCK_SIZE = 160;
16359
+ const INTERACTIVE_SCALE = 1.2;
16360
+ const CENTER = CLOCK_SIZE / 2;
16361
+ const FACE_RADIUS = CENTER - 6;
16362
+ const HOUR_HAND = FACE_RADIUS * 0.45;
16363
+ const MINUTE_HAND = FACE_RADIUS * 0.65;
16364
+ const HOUR_LABELS = Array.from({ length: 12 }, (_, i) => i + 1);
16365
+ const M_SHAFT = Math.round(MINUTE_HAND * 0.049);
16366
+ const M_ARROW = Math.round(MINUTE_HAND * 0.156);
16367
+ const M_TIP = Math.round(MINUTE_HAND * 0.279);
16368
+ const M_TAIL = Math.round(MINUTE_HAND * 0.056);
16369
+ const H_SHAFT = Math.round(HOUR_HAND * 0.074);
16370
+ const H_ARROW = Math.round(HOUR_HAND * 0.246);
16371
+ const H_TIP = Math.round(HOUR_HAND * 0.369);
16372
+ const H_TAIL = Math.round(HOUR_HAND * 0.082);
16373
+ const ClockRenderer = ({
16374
+ item,
16375
+ value,
16376
+ validationStatus,
16377
+ onChange
16378
+ }) => {
16379
+ const format = item.format ?? "24h";
16380
+ const interactive = item.mode === "interactive";
16381
+ const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
16382
+ const initial = parseTime(interactive && value ? value : item.value);
16383
+ const [hours, setHours] = React.useState(initial.h);
16384
+ const [minutes, setMinutes] = React.useState(initial.m);
16385
+ React.useEffect(() => {
16386
+ if (interactive && value) {
16387
+ const t = parseTime(value);
16388
+ setHours(t.h);
16389
+ setMinutes(t.m);
16390
+ }
16391
+ }, [value, interactive]);
16392
+ const svgRef = React.useRef(null);
16393
+ const draggingRef = React.useRef(null);
16394
+ const updateFromPointer = React.useCallback((clientX, clientY) => {
16395
+ const svg = svgRef.current;
16396
+ if (!svg || !draggingRef.current) return;
16397
+ const rect = svg.getBoundingClientRect();
16398
+ const dx = clientX - rect.left - CENTER;
16399
+ const dy = clientY - rect.top - CENTER;
16400
+ const angle = (Math.atan2(dx, -dy) + 2 * Math.PI) % (2 * Math.PI);
16401
+ if (draggingRef.current === "minute") {
16402
+ const rawMin = angle / (2 * Math.PI) * 60;
16403
+ const snapped = Math.round(rawMin / snap) * snap % 60;
16404
+ setMinutes(snapped);
16405
+ if (item.mode === "interactive" && onChange) {
16406
+ onChange(item.name, formatTime(hours, snapped));
16407
+ }
16408
+ } else {
16409
+ const rawHour12 = angle / (2 * Math.PI) * 12;
16410
+ const rounded = Math.round(rawHour12) % 12;
16411
+ const isPm = hours >= 12;
16412
+ const newH = (rounded === 0 ? 0 : rounded) + (isPm ? 12 : 0);
16413
+ const h24 = newH % 24;
16414
+ setHours(h24);
16415
+ if (item.mode === "interactive" && onChange) {
16416
+ onChange(item.name, formatTime(h24, minutes));
16417
+ }
16418
+ }
16419
+ }, [snap, hours, minutes, item.mode, item.name, onChange]);
16420
+ const onPointerDown = (hand) => (e) => {
16421
+ if (!interactive) return;
16422
+ e.preventDefault();
16423
+ e.target.setPointerCapture?.(e.pointerId);
16424
+ draggingRef.current = hand;
16425
+ updateFromPointer(e.clientX, e.clientY);
16426
+ };
16427
+ const onPointerMove = (e) => {
16428
+ if (!interactive || !draggingRef.current) return;
16429
+ updateFromPointer(e.clientX, e.clientY);
16430
+ };
16431
+ const onPointerUp = (e) => {
16432
+ if (!interactive) return;
16433
+ e.target.releasePointerCapture?.(e.pointerId);
16434
+ draggingRef.current = null;
16435
+ };
16436
+ const hourAngleRad = React.useMemo(() => {
16437
+ const h12 = hours % 12;
16438
+ return (h12 + minutes / 60) / 12 * 2 * Math.PI;
16439
+ }, [hours, minutes]);
16440
+ const minuteAngleRad = React.useMemo(() => minutes / 60 * 2 * Math.PI, [minutes]);
16441
+ const hourX = CENTER + Math.sin(hourAngleRad) * HOUR_HAND;
16442
+ const hourY = CENTER - Math.cos(hourAngleRad) * HOUR_HAND;
16443
+ const minuteX = CENTER + Math.sin(minuteAngleRad) * MINUTE_HAND;
16444
+ const minuteY = CENTER - Math.cos(minuteAngleRad) * MINUTE_HAND;
16445
+ const hourDeg = hourAngleRad * 180 / Math.PI;
16446
+ const minuteDeg = minuteAngleRad * 180 / Math.PI;
16447
+ const scale = interactive ? INTERACTIVE_SCALE : 1;
16448
+ const svgSize = CLOCK_SIZE * scale;
16449
+ const ringSize = FACE_RADIUS * 2 * scale;
16450
+ const svgOffset = -6 * scale;
16451
+ const statusClass = validationStatus ? `clock-${validationStatus}` : "";
16452
+ if (item.clockType === "digital") {
16453
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `clock clock-digital ${statusClass}`, dir: "ltr", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "clock-digital-wrapper", children: [
16454
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "clock-digital-tabs", width: "85", height: "9", viewBox: "0 0 85 9", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
16455
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M0 8C0 3.58172 3.58172 0 8 0H14C18.4183 0 22 3.58172 22 8V9H0V8Z", fill: "#AE9CFF" }),
16456
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M63 8C63 3.58172 66.5817 0 71 0H77C81.4183 0 85 3.58172 85 8V9H63V8Z", fill: "#AE9CFF" })
16457
+ ] }),
16458
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-digital-display", children: formatDisplay(hours, minutes, format) })
16459
+ ] }) });
16460
+ }
16461
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `clock clock-analog ${interactive ? "clock-interactive" : "clock-display"} ${statusClass}`, dir: "ltr", children: [
16462
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-face-ring", style: { width: ringSize, height: ringSize }, children: /* @__PURE__ */ jsxRuntime.jsxs(
16463
+ "svg",
16464
+ {
16465
+ ref: svgRef,
16466
+ width: svgSize,
16467
+ height: svgSize,
16468
+ viewBox: `0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`,
16469
+ style: { top: svgOffset, left: svgOffset },
16470
+ onPointerMove,
16471
+ onPointerUp,
16472
+ onPointerCancel: onPointerUp,
16473
+ className: "clock-svg",
16474
+ children: [
16475
+ /* @__PURE__ */ jsxRuntime.jsx(
16476
+ "circle",
16477
+ {
16478
+ cx: CENTER,
16479
+ cy: CENTER,
16480
+ r: FACE_RADIUS,
16481
+ className: "clock-face"
16482
+ }
16483
+ ),
16484
+ Array.from({ length: 12 }).map((_, i) => {
16485
+ const a = i / 12 * 2 * Math.PI;
16486
+ const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 8);
16487
+ const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 8);
16488
+ const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
16489
+ const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
16490
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-hour" }, `t${i}`);
16491
+ }),
16492
+ Array.from({ length: 60 }).map((_, i) => {
16493
+ if (i % 5 === 0) return null;
16494
+ const a = i / 60 * 2 * Math.PI;
16495
+ const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 4);
16496
+ const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 4);
16497
+ const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
16498
+ const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
16499
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-minute" }, `m${i}`);
16500
+ }),
16501
+ HOUR_LABELS.map((n) => {
16502
+ const a = n / 12 * 2 * Math.PI;
16503
+ const r = FACE_RADIUS - 20;
16504
+ const x = CENTER + Math.sin(a) * r;
16505
+ const y = CENTER - Math.cos(a) * r;
16506
+ return /* @__PURE__ */ jsxRuntime.jsx("text", { x, y, className: "clock-number", textAnchor: "middle", dominantBaseline: "central", children: n }, `l${n}`);
16507
+ }),
16508
+ /* @__PURE__ */ jsxRuntime.jsx(
16509
+ "polygon",
16510
+ {
16511
+ transform: `translate(${CENTER},${CENTER}) rotate(${hourDeg})`,
16512
+ points: [
16513
+ `0,${-HOUR_HAND}`,
16514
+ `${H_ARROW},${-(HOUR_HAND - H_TIP)}`,
16515
+ `${H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
16516
+ `${H_SHAFT},${H_TAIL}`,
16517
+ `${-H_SHAFT},${H_TAIL}`,
16518
+ `${-H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
16519
+ `${-H_ARROW},${-(HOUR_HAND - H_TIP)}`
16520
+ ].join(" "),
16521
+ className: "clock-arrow clock-arrow-hour",
16522
+ style: { pointerEvents: "none" }
16523
+ }
16524
+ ),
16525
+ /* @__PURE__ */ jsxRuntime.jsx(
16526
+ "polygon",
16527
+ {
16528
+ transform: `translate(${CENTER},${CENTER}) rotate(${minuteDeg})`,
16529
+ points: [
16530
+ `0,${-MINUTE_HAND}`,
16531
+ `${M_ARROW},${-(MINUTE_HAND - M_TIP)}`,
16532
+ `${M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
16533
+ `${M_SHAFT},${M_TAIL}`,
16534
+ `${-M_SHAFT},${M_TAIL}`,
16535
+ `${-M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
16536
+ `${-M_ARROW},${-(MINUTE_HAND - M_TIP)}`
16537
+ ].join(" "),
16538
+ className: "clock-arrow clock-arrow-minute",
16539
+ style: { pointerEvents: "none" }
16540
+ }
16541
+ ),
16542
+ /* @__PURE__ */ jsxRuntime.jsx(
16543
+ "line",
16544
+ {
16545
+ x1: CENTER,
16546
+ y1: CENTER,
16547
+ x2: hourX,
16548
+ y2: hourY,
16549
+ className: "clock-hand clock-hand-hour",
16550
+ stroke: "transparent",
16551
+ strokeWidth: 20,
16552
+ onPointerDown: onPointerDown("hour")
16553
+ }
16554
+ ),
16555
+ /* @__PURE__ */ jsxRuntime.jsx(
16556
+ "line",
16557
+ {
16558
+ x1: CENTER,
16559
+ y1: CENTER,
16560
+ x2: minuteX,
16561
+ y2: minuteY,
16562
+ className: "clock-hand clock-hand-minute",
16563
+ stroke: "transparent",
16564
+ strokeWidth: 16,
16565
+ onPointerDown: onPointerDown("minute")
16566
+ }
16567
+ ),
16568
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: CENTER, cy: CENTER, r: 4, className: "clock-center" })
16569
+ ]
16570
+ }
16571
+ ) }),
16572
+ format === "12h" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
16573
+ ] });
16574
+ };
16176
16575
  function isTextItem(item) {
16177
16576
  return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
16178
16577
  }
@@ -16477,6 +16876,22 @@ const LayoutRenderer = ({
16477
16876
  }
16478
16877
  );
16479
16878
  }
16879
+ case JSON_TYPES.CLOCK: {
16880
+ const clockItem = item;
16881
+ const widgetName = clockItem.mode === "interactive" ? clockItem.name : void 0;
16882
+ const validationStatus = widgetName ? toValidationStatus(validationDetails?.[widgetName]?.isCorrect) : null;
16883
+ const rawValue = widgetName ? values[widgetName] : void 0;
16884
+ const value = typeof rawValue === "string" ? rawValue : void 0;
16885
+ return /* @__PURE__ */ jsxRuntime.jsx(
16886
+ ClockRenderer,
16887
+ {
16888
+ item: clockItem,
16889
+ value,
16890
+ validationStatus,
16891
+ onChange
16892
+ }
16893
+ );
16894
+ }
16480
16895
  default:
16481
16896
  return null;
16482
16897
  }
@@ -16800,6 +17215,12 @@ function compileWithAST(dsl) {
16800
17215
  exports.AST_TYPES = AST_TYPES;
16801
17216
  exports.BoldTextInlineSchema = BoldTextInlineSchema;
16802
17217
  exports.BoldTextSchema = BoldTextSchema;
17218
+ exports.CLOCK_FORMATS = CLOCK_FORMATS;
17219
+ exports.CLOCK_MODES = CLOCK_MODES;
17220
+ exports.CLOCK_TIME_REGEX = CLOCK_TIME_REGEX;
17221
+ exports.CLOCK_TYPES = CLOCK_TYPES;
17222
+ exports.ClockInlineSchema = ClockInlineSchema;
17223
+ exports.ClockItemSchema = ClockItemSchema;
16803
17224
  exports.ColorTextInlineSchema = ColorTextInlineSchema;
16804
17225
  exports.ColorTextSchema = ColorTextSchema;
16805
17226
  exports.ContentBlockSchema = ContentBlockSchema;
@@ -16894,6 +17315,7 @@ exports.compile = compile;
16894
17315
  exports.compileWithAST = compileWithAST;
16895
17316
  exports.createBoldText = createBoldText;
16896
17317
  exports.createColorText = createColorText;
17318
+ exports.createDisplayClock = createDisplayClock;
16897
17319
  exports.createDragOptions = createDragOptions;
16898
17320
  exports.createExerciseDocument = createExerciseDocument;
16899
17321
  exports.createExpressionValidation = createExpressionValidation;
@@ -16902,6 +17324,7 @@ exports.createFreeText = createFreeText;
16902
17324
  exports.createHighlightedText = createHighlightedText;
16903
17325
  exports.createImage = createImage;
16904
17326
  exports.createInlineDragOption = createInlineDragOption;
17327
+ exports.createInteractiveClock = createInteractiveClock;
16905
17328
  exports.createLineBlock = createLineBlock;
16906
17329
  exports.createMatcherBlock = createMatcherBlock;
16907
17330
  exports.createMatcherItem = createMatcherItem;
@@ -16932,6 +17355,8 @@ exports.findValidCombination = findValidCombination;
16932
17355
  exports.getFractionKey = getFractionKey;
16933
17356
  exports.getMissingInputs = getMissingInputs;
16934
17357
  exports.isBoldText = isBoldText;
17358
+ exports.isClock = isClock;
17359
+ exports.isClockItem = isClockItem;
16935
17360
  exports.isColorText = isColorText;
16936
17361
  exports.isDragOptions = isDragOptions;
16937
17362
  exports.isDragOptionsJSON = isDragOptionsJSON;