@mathscan/math-exercise-engine 1.1.4 → 1.1.5

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.mjs CHANGED
@@ -27,6 +27,8 @@ const AST_TYPES = {
27
27
  FRACTION: "Fraction",
28
28
  /** Inline image element */
29
29
  IMAGE: "Image",
30
+ /** Clock widget — analog/digital, display or interactive */
31
+ CLOCK: "Clock",
30
32
  // ═══════════════════════════════════════════════════════════════════════════
31
33
  // Blocks
32
34
  // ═══════════════════════════════════════════════════════════════════════════
@@ -112,6 +114,8 @@ const JSON_TYPES = {
112
114
  IMAGE: "image",
113
115
  /** Reference to a widget */
114
116
  WIDGET_REF: "widget-ref",
117
+ /** Clock widget (inline): analog or digital, display or interactive */
118
+ CLOCK: "clock",
115
119
  /** Line break element */
116
120
  LINEBREAK: "linebreak",
117
121
  /** Grouped layout items */
@@ -315,6 +319,10 @@ const COUNTER = createToken({
315
319
  name: "COUNTER",
316
320
  pattern: /COUNTER/
317
321
  });
322
+ const CLOCK = createToken({
323
+ name: "CLOCK",
324
+ pattern: /CLOCK/
325
+ });
318
326
  const HIGHLIGHT = createToken({
319
327
  name: "HIGHLIGHT",
320
328
  pattern: /HIGHLIGHT(?!_TEXT)/
@@ -486,6 +494,7 @@ const allTokens = [
486
494
  BOTTOM_ROW,
487
495
  CELL,
488
496
  COUNTER,
497
+ CLOCK,
489
498
  HIGHLIGHT,
490
499
  VISUAL_FRACTION,
491
500
  FRACTION,
@@ -1159,9 +1168,17 @@ class MathExerciseParser extends CstParser {
1159
1168
  // W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
1160
1169
  { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
1161
1170
  // INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
1162
- { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) }
1171
+ { GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) },
1172
+ // CLOCK (inline clock widget)
1173
+ { ALT: () => this.SUBRULE(this.clock) }
1163
1174
  ]);
1164
1175
  });
1176
+ this.clock = this.RULE("clock", () => {
1177
+ this.CONSUME(CLOCK);
1178
+ this.CONSUME(LBracket);
1179
+ this.SUBRULE(this.parameterList);
1180
+ this.CONSUME(RBracket);
1181
+ });
1165
1182
  this.textHighlighter = this.RULE("textHighlighter", () => {
1166
1183
  this.OR([
1167
1184
  { ALT: () => this.CONSUME(TEXT_HIGHLIGHTER) },
@@ -1519,6 +1536,33 @@ const InlineDragOptionSchema = z.object({
1519
1536
  type: z.literal(AST_TYPES.INLINE_DRAG_OPTION),
1520
1537
  value: z.string().min(1)
1521
1538
  });
1539
+ const CLOCK_TYPES = ["analog", "digital"];
1540
+ const CLOCK_MODES = ["display", "interactive"];
1541
+ const CLOCK_FORMATS = ["12h", "24h"];
1542
+ const CLOCK_TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
1543
+ const TimeStringSchema = z.string().regex(CLOCK_TIME_REGEX, 'must be "HH:MM" in 24h format');
1544
+ const DisplayClockInlineSchema = z.object({
1545
+ type: z.literal(AST_TYPES.CLOCK),
1546
+ mode: z.literal("display"),
1547
+ clockType: z.enum(CLOCK_TYPES),
1548
+ value: TimeStringSchema,
1549
+ format: z.enum(CLOCK_FORMATS).optional(),
1550
+ name: z.string().optional()
1551
+ });
1552
+ const InteractiveClockInlineSchema = z.object({
1553
+ type: z.literal(AST_TYPES.CLOCK),
1554
+ mode: z.literal("interactive"),
1555
+ clockType: z.literal("analog"),
1556
+ value: TimeStringSchema,
1557
+ answer: TimeStringSchema,
1558
+ format: z.enum(CLOCK_FORMATS).optional(),
1559
+ snap: z.number().int().min(1).max(60).optional(),
1560
+ name: z.string()
1561
+ });
1562
+ const ClockInlineSchema = z.discriminatedUnion("mode", [
1563
+ DisplayClockInlineSchema,
1564
+ InteractiveClockInlineSchema
1565
+ ]);
1522
1566
  const InlineNodeSchema = z.union([
1523
1567
  FreeTextInlineSchema,
1524
1568
  SpaceInlineSchema,
@@ -1529,7 +1573,8 @@ const InlineNodeSchema = z.union([
1529
1573
  FractionInlineSchema,
1530
1574
  ImageInlineSchema,
1531
1575
  VisualFractionInlineSchema,
1532
- InlineDragOptionSchema
1576
+ InlineDragOptionSchema,
1577
+ ClockInlineSchema
1533
1578
  ]);
1534
1579
  function createFreeText(value) {
1535
1580
  return FreeTextInlineSchema.parse({
@@ -1622,6 +1667,21 @@ function createInlineDragOption(value) {
1622
1667
  value
1623
1668
  });
1624
1669
  }
1670
+ function createDisplayClock(params) {
1671
+ return ClockInlineSchema.parse({
1672
+ type: AST_TYPES.CLOCK,
1673
+ mode: "display",
1674
+ ...params
1675
+ });
1676
+ }
1677
+ function createInteractiveClock(params) {
1678
+ return ClockInlineSchema.parse({
1679
+ type: AST_TYPES.CLOCK,
1680
+ mode: "interactive",
1681
+ clockType: "analog",
1682
+ ...params
1683
+ });
1684
+ }
1625
1685
  function isFreeText(value) {
1626
1686
  return FreeTextInlineSchema.safeParse(value).success;
1627
1687
  }
@@ -1637,6 +1697,9 @@ function isFraction(value) {
1637
1697
  function isImage(value) {
1638
1698
  return ImageInlineSchema.safeParse(value).success;
1639
1699
  }
1700
+ function isClock(value) {
1701
+ return ClockInlineSchema.safeParse(value).success;
1702
+ }
1640
1703
  function isInlineNode(value) {
1641
1704
  return InlineNodeSchema.safeParse(value).success;
1642
1705
  }
@@ -2683,8 +2746,66 @@ class NaturalDSLVisitor extends BaseCstVisitor {
2683
2746
  if (ctx.inlineDragOption) {
2684
2747
  return this.visit(ctx.inlineDragOption);
2685
2748
  }
2749
+ if (ctx.clock) {
2750
+ return this.visit(ctx.clock);
2751
+ }
2686
2752
  throw new Error("Unknown inline element");
2687
2753
  }
2754
+ // Manual checks below duplicate what ClockInlineSchema.parse() enforces, but produce
2755
+ // DSL-friendly error messages (e.g. 'CLOCK: value is required') instead of Zod paths.
2756
+ clock(ctx) {
2757
+ const params = this.visit(ctx.parameterList);
2758
+ const mode = params.mode ?? "display";
2759
+ if (!CLOCK_MODES.includes(mode)) {
2760
+ throw new Error(`CLOCK: mode must be one of ${CLOCK_MODES.join(", ")} (got "${mode}")`);
2761
+ }
2762
+ const clockType = params.type;
2763
+ if (!clockType) {
2764
+ throw new Error('CLOCK: type is required ("analog" or "digital")');
2765
+ }
2766
+ if (!CLOCK_TYPES.includes(clockType)) {
2767
+ throw new Error(`CLOCK: type must be one of ${CLOCK_TYPES.join(", ")} (got "${clockType}")`);
2768
+ }
2769
+ const value = params.value;
2770
+ if (!value) {
2771
+ throw new Error('CLOCK: value is required ("HH:MM" 24h format)');
2772
+ }
2773
+ if (!CLOCK_TIME_REGEX.test(value)) {
2774
+ throw new Error(`CLOCK: value must be "HH:MM" in 24h format (got "${value}")`);
2775
+ }
2776
+ const format = params.format;
2777
+ if (format !== void 0 && !CLOCK_FORMATS.includes(format)) {
2778
+ throw new Error(`CLOCK: format must be one of ${CLOCK_FORMATS.join(", ")} (got "${format}")`);
2779
+ }
2780
+ const name = params.name;
2781
+ const answer = params.answer;
2782
+ const snap = params.snap;
2783
+ if (mode === "display") {
2784
+ if (answer !== void 0) {
2785
+ throw new Error('CLOCK: "answer" is only valid when mode="interactive"');
2786
+ }
2787
+ if (snap !== void 0) {
2788
+ throw new Error('CLOCK: "snap" is only valid when mode="interactive"');
2789
+ }
2790
+ return createDisplayClock({ clockType, value, format, name });
2791
+ }
2792
+ if (clockType !== "analog") {
2793
+ throw new Error('CLOCK: interactive mode requires type="analog" (digital clocks cannot be interactive)');
2794
+ }
2795
+ if (answer === void 0) {
2796
+ throw new Error('CLOCK: interactive mode requires "answer"');
2797
+ }
2798
+ if (!CLOCK_TIME_REGEX.test(answer)) {
2799
+ throw new Error(`CLOCK: answer must be "HH:MM" in 24h format (got "${answer}")`);
2800
+ }
2801
+ if (!name) {
2802
+ throw new Error('CLOCK: interactive mode requires "name" (for answer validation)');
2803
+ }
2804
+ if (snap !== void 0 && (!Number.isInteger(snap) || snap < 1 || snap > 60)) {
2805
+ throw new Error(`CLOCK: snap must be an integer in 1..60 (got ${snap})`);
2806
+ }
2807
+ return createInteractiveClock({ name, value, answer, format, snap });
2808
+ }
2688
2809
  textHighlighter(ctx) {
2689
2810
  const params = this.visit(ctx.parameterList);
2690
2811
  const value = params.value;
@@ -2944,6 +3065,31 @@ const OperatorItemSchema = z.object({
2944
3065
  type: z.literal(JSON_TYPES.OPERATOR),
2945
3066
  operator: OperatorSchema
2946
3067
  });
3068
+ const ClockTimeSchema = z.string().regex(CLOCK_TIME_REGEX);
3069
+ const ClockFormatSchema = z.enum(["12h", "24h"]);
3070
+ const ClockDisplayItemSchema = z.object({
3071
+ id: z.string(),
3072
+ type: z.literal(JSON_TYPES.CLOCK),
3073
+ mode: z.literal("display"),
3074
+ clockType: z.enum(["analog", "digital"]),
3075
+ value: ClockTimeSchema,
3076
+ format: ClockFormatSchema.optional(),
3077
+ name: z.string().optional()
3078
+ });
3079
+ const ClockInteractiveItemSchema = z.object({
3080
+ id: z.string(),
3081
+ type: z.literal(JSON_TYPES.CLOCK),
3082
+ mode: z.literal("interactive"),
3083
+ clockType: z.literal("analog"),
3084
+ value: ClockTimeSchema,
3085
+ format: ClockFormatSchema.optional(),
3086
+ snap: z.number().int().min(1).max(60).optional(),
3087
+ name: z.string()
3088
+ });
3089
+ const ClockItemSchema = z.discriminatedUnion("mode", [
3090
+ ClockDisplayItemSchema,
3091
+ ClockInteractiveItemSchema
3092
+ ]);
2947
3093
  const LayoutItemSchema = z.lazy(
2948
3094
  () => z.union([
2949
3095
  QuestionTextSchema,
@@ -2959,7 +3105,8 @@ const LayoutItemSchema = z.lazy(
2959
3105
  OperatorItemSchema,
2960
3106
  TableItemSchema,
2961
3107
  NumberLineItemSchema,
2962
- VerticalOperationItemSchema
3108
+ VerticalOperationItemSchema,
3109
+ ClockItemSchema
2963
3110
  ])
2964
3111
  );
2965
3112
  const GroupItemSchema = z.object({
@@ -3278,6 +3425,9 @@ function isTableCellItem(item) {
3278
3425
  function isVerticalOperationItem(item) {
3279
3426
  return item.type === JSON_TYPES.VERTICAL_OPERATION;
3280
3427
  }
3428
+ function isClockItem(item) {
3429
+ return item.type === JSON_TYPES.CLOCK;
3430
+ }
3281
3431
  function isInlineInputJSON(widget) {
3282
3432
  return widget.type === JSON_TYPES.INLINE_INPUT;
3283
3433
  }
@@ -3398,6 +3548,8 @@ function toJSON(ast) {
3398
3548
  if (inline.answer !== void 0) {
3399
3549
  answers[inline.name] = inline.answer;
3400
3550
  }
3551
+ } else if (inline.type === AST_TYPES.CLOCK && inline.mode === "interactive") {
3552
+ answers[inline.name] = inline.answer;
3401
3553
  }
3402
3554
  if (inline.type === AST_TYPES.FRACTION) {
3403
3555
  widgets[inline.name] = {
@@ -3781,6 +3933,21 @@ function transformInline(inline, idGenerator, inlineDragIds) {
3781
3933
  type: JSON_TYPES.WIDGET_REF,
3782
3934
  widgetId: inlineDragIds.get(inline)
3783
3935
  };
3936
+ case AST_TYPES.CLOCK: {
3937
+ const base = {
3938
+ id: idGenerator.generate(JSON_TYPES.CLOCK),
3939
+ type: JSON_TYPES.CLOCK,
3940
+ mode: inline.mode,
3941
+ clockType: inline.clockType,
3942
+ value: inline.value,
3943
+ ...inline.format !== void 0 && { format: inline.format },
3944
+ ...inline.name !== void 0 && { name: inline.name }
3945
+ };
3946
+ if (inline.mode === "interactive") {
3947
+ return { ...base, ...inline.snap !== void 0 && { snap: inline.snap } };
3948
+ }
3949
+ return base;
3950
+ }
3784
3951
  default:
3785
3952
  throw new Error(`Unknown inline type: ${inline.type}`);
3786
3953
  }
@@ -16171,6 +16338,236 @@ const NumberLineRenderer = ({
16171
16338
  }
16172
16339
  );
16173
16340
  };
16341
+ function parseTime(s) {
16342
+ const parts = s.split(":");
16343
+ return { h: Number(parts[0]), m: Number(parts[1]) };
16344
+ }
16345
+ function formatTime(h, m) {
16346
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
16347
+ }
16348
+ function formatDisplay(h24, m, format) {
16349
+ if (format === "24h") return formatTime(h24, m);
16350
+ const period = h24 >= 12 ? "PM" : "AM";
16351
+ const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
16352
+ return `${String(h12).padStart(2, "0")}:${String(m).padStart(2, "0")} ${period}`;
16353
+ }
16354
+ const CLOCK_SIZE = 160;
16355
+ const INTERACTIVE_SCALE = 1.2;
16356
+ const CENTER = CLOCK_SIZE / 2;
16357
+ const FACE_RADIUS = CENTER - 6;
16358
+ const HOUR_HAND = FACE_RADIUS * 0.45;
16359
+ const MINUTE_HAND = FACE_RADIUS * 0.65;
16360
+ const HOUR_LABELS = Array.from({ length: 12 }, (_, i) => i + 1);
16361
+ const M_SHAFT = Math.round(MINUTE_HAND * 0.049);
16362
+ const M_ARROW = Math.round(MINUTE_HAND * 0.156);
16363
+ const M_TIP = Math.round(MINUTE_HAND * 0.279);
16364
+ const M_TAIL = Math.round(MINUTE_HAND * 0.056);
16365
+ const H_SHAFT = Math.round(HOUR_HAND * 0.074);
16366
+ const H_ARROW = Math.round(HOUR_HAND * 0.246);
16367
+ const H_TIP = Math.round(HOUR_HAND * 0.369);
16368
+ const H_TAIL = Math.round(HOUR_HAND * 0.082);
16369
+ const ClockRenderer = ({
16370
+ item,
16371
+ value,
16372
+ validationStatus,
16373
+ onChange
16374
+ }) => {
16375
+ const format = item.format ?? "24h";
16376
+ const interactive = item.mode === "interactive";
16377
+ const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
16378
+ const initial = parseTime(interactive && value ? value : item.value);
16379
+ const [hours, setHours] = useState(initial.h);
16380
+ const [minutes, setMinutes] = useState(initial.m);
16381
+ useEffect(() => {
16382
+ if (interactive && value) {
16383
+ const t = parseTime(value);
16384
+ setHours(t.h);
16385
+ setMinutes(t.m);
16386
+ }
16387
+ }, [value, interactive]);
16388
+ const svgRef = useRef(null);
16389
+ const draggingRef = useRef(null);
16390
+ const updateFromPointer = useCallback((clientX, clientY) => {
16391
+ const svg = svgRef.current;
16392
+ if (!svg || !draggingRef.current) return;
16393
+ const rect = svg.getBoundingClientRect();
16394
+ const dx = clientX - rect.left - CENTER;
16395
+ const dy = clientY - rect.top - CENTER;
16396
+ const angle = (Math.atan2(dx, -dy) + 2 * Math.PI) % (2 * Math.PI);
16397
+ if (draggingRef.current === "minute") {
16398
+ const rawMin = angle / (2 * Math.PI) * 60;
16399
+ const snapped = Math.round(rawMin / snap) * snap % 60;
16400
+ setMinutes(snapped);
16401
+ if (item.mode === "interactive" && onChange) {
16402
+ onChange(item.name, formatTime(hours, snapped));
16403
+ }
16404
+ } else {
16405
+ const rawHour12 = angle / (2 * Math.PI) * 12;
16406
+ const rounded = Math.round(rawHour12) % 12;
16407
+ const isPm = hours >= 12;
16408
+ const newH = (rounded === 0 ? 0 : rounded) + (isPm ? 12 : 0);
16409
+ const h24 = newH % 24;
16410
+ setHours(h24);
16411
+ if (item.mode === "interactive" && onChange) {
16412
+ onChange(item.name, formatTime(h24, minutes));
16413
+ }
16414
+ }
16415
+ }, [snap, hours, minutes, item.mode, item.name, onChange]);
16416
+ const onPointerDown = (hand) => (e) => {
16417
+ if (!interactive) return;
16418
+ e.preventDefault();
16419
+ e.target.setPointerCapture?.(e.pointerId);
16420
+ draggingRef.current = hand;
16421
+ updateFromPointer(e.clientX, e.clientY);
16422
+ };
16423
+ const onPointerMove = (e) => {
16424
+ if (!interactive || !draggingRef.current) return;
16425
+ updateFromPointer(e.clientX, e.clientY);
16426
+ };
16427
+ const onPointerUp = (e) => {
16428
+ if (!interactive) return;
16429
+ e.target.releasePointerCapture?.(e.pointerId);
16430
+ draggingRef.current = null;
16431
+ };
16432
+ const hourAngleRad = useMemo(() => {
16433
+ const h12 = hours % 12;
16434
+ return (h12 + minutes / 60) / 12 * 2 * Math.PI;
16435
+ }, [hours, minutes]);
16436
+ const minuteAngleRad = useMemo(() => minutes / 60 * 2 * Math.PI, [minutes]);
16437
+ const hourX = CENTER + Math.sin(hourAngleRad) * HOUR_HAND;
16438
+ const hourY = CENTER - Math.cos(hourAngleRad) * HOUR_HAND;
16439
+ const minuteX = CENTER + Math.sin(minuteAngleRad) * MINUTE_HAND;
16440
+ const minuteY = CENTER - Math.cos(minuteAngleRad) * MINUTE_HAND;
16441
+ const hourDeg = hourAngleRad * 180 / Math.PI;
16442
+ const minuteDeg = minuteAngleRad * 180 / Math.PI;
16443
+ const scale = interactive ? INTERACTIVE_SCALE : 1;
16444
+ const svgSize = CLOCK_SIZE * scale;
16445
+ const ringSize = FACE_RADIUS * 2 * scale;
16446
+ const svgOffset = -6 * scale;
16447
+ const statusClass = validationStatus ? `clock-${validationStatus}` : "";
16448
+ if (item.clockType === "digital") {
16449
+ return /* @__PURE__ */ jsx("span", { className: `clock clock-digital ${statusClass}`, dir: "ltr", children: /* @__PURE__ */ jsxs("span", { className: "clock-digital-wrapper", children: [
16450
+ /* @__PURE__ */ 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: [
16451
+ /* @__PURE__ */ jsx("path", { d: "M0 8C0 3.58172 3.58172 0 8 0H14C18.4183 0 22 3.58172 22 8V9H0V8Z", fill: "#AE9CFF" }),
16452
+ /* @__PURE__ */ jsx("path", { d: "M63 8C63 3.58172 66.5817 0 71 0H77C81.4183 0 85 3.58172 85 8V9H63V8Z", fill: "#AE9CFF" })
16453
+ ] }),
16454
+ /* @__PURE__ */ jsx("span", { className: "clock-digital-display", children: formatDisplay(hours, minutes, format) })
16455
+ ] }) });
16456
+ }
16457
+ return /* @__PURE__ */ jsxs("span", { className: `clock clock-analog ${interactive ? "clock-interactive" : "clock-display"} ${statusClass}`, dir: "ltr", children: [
16458
+ /* @__PURE__ */ jsx("span", { className: "clock-face-ring", style: { width: ringSize, height: ringSize }, children: /* @__PURE__ */ jsxs(
16459
+ "svg",
16460
+ {
16461
+ ref: svgRef,
16462
+ width: svgSize,
16463
+ height: svgSize,
16464
+ viewBox: `0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`,
16465
+ style: { top: svgOffset, left: svgOffset },
16466
+ onPointerMove,
16467
+ onPointerUp,
16468
+ onPointerCancel: onPointerUp,
16469
+ className: "clock-svg",
16470
+ children: [
16471
+ /* @__PURE__ */ jsx(
16472
+ "circle",
16473
+ {
16474
+ cx: CENTER,
16475
+ cy: CENTER,
16476
+ r: FACE_RADIUS,
16477
+ className: "clock-face"
16478
+ }
16479
+ ),
16480
+ Array.from({ length: 12 }).map((_, i) => {
16481
+ const a = i / 12 * 2 * Math.PI;
16482
+ const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 8);
16483
+ const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 8);
16484
+ const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
16485
+ const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
16486
+ return /* @__PURE__ */ jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-hour" }, `t${i}`);
16487
+ }),
16488
+ Array.from({ length: 60 }).map((_, i) => {
16489
+ if (i % 5 === 0) return null;
16490
+ const a = i / 60 * 2 * Math.PI;
16491
+ const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 4);
16492
+ const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 4);
16493
+ const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
16494
+ const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
16495
+ return /* @__PURE__ */ jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-minute" }, `m${i}`);
16496
+ }),
16497
+ HOUR_LABELS.map((n) => {
16498
+ const a = n / 12 * 2 * Math.PI;
16499
+ const r = FACE_RADIUS - 20;
16500
+ const x = CENTER + Math.sin(a) * r;
16501
+ const y = CENTER - Math.cos(a) * r;
16502
+ return /* @__PURE__ */ jsx("text", { x, y, className: "clock-number", textAnchor: "middle", dominantBaseline: "central", children: n }, `l${n}`);
16503
+ }),
16504
+ /* @__PURE__ */ jsx(
16505
+ "polygon",
16506
+ {
16507
+ transform: `translate(${CENTER},${CENTER}) rotate(${hourDeg})`,
16508
+ points: [
16509
+ `0,${-HOUR_HAND}`,
16510
+ `${H_ARROW},${-(HOUR_HAND - H_TIP)}`,
16511
+ `${H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
16512
+ `${H_SHAFT},${H_TAIL}`,
16513
+ `${-H_SHAFT},${H_TAIL}`,
16514
+ `${-H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
16515
+ `${-H_ARROW},${-(HOUR_HAND - H_TIP)}`
16516
+ ].join(" "),
16517
+ className: "clock-arrow clock-arrow-hour",
16518
+ style: { pointerEvents: "none" }
16519
+ }
16520
+ ),
16521
+ /* @__PURE__ */ jsx(
16522
+ "polygon",
16523
+ {
16524
+ transform: `translate(${CENTER},${CENTER}) rotate(${minuteDeg})`,
16525
+ points: [
16526
+ `0,${-MINUTE_HAND}`,
16527
+ `${M_ARROW},${-(MINUTE_HAND - M_TIP)}`,
16528
+ `${M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
16529
+ `${M_SHAFT},${M_TAIL}`,
16530
+ `${-M_SHAFT},${M_TAIL}`,
16531
+ `${-M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
16532
+ `${-M_ARROW},${-(MINUTE_HAND - M_TIP)}`
16533
+ ].join(" "),
16534
+ className: "clock-arrow clock-arrow-minute",
16535
+ style: { pointerEvents: "none" }
16536
+ }
16537
+ ),
16538
+ /* @__PURE__ */ jsx(
16539
+ "line",
16540
+ {
16541
+ x1: CENTER,
16542
+ y1: CENTER,
16543
+ x2: hourX,
16544
+ y2: hourY,
16545
+ className: "clock-hand clock-hand-hour",
16546
+ stroke: "transparent",
16547
+ strokeWidth: 20,
16548
+ onPointerDown: onPointerDown("hour")
16549
+ }
16550
+ ),
16551
+ /* @__PURE__ */ jsx(
16552
+ "line",
16553
+ {
16554
+ x1: CENTER,
16555
+ y1: CENTER,
16556
+ x2: minuteX,
16557
+ y2: minuteY,
16558
+ className: "clock-hand clock-hand-minute",
16559
+ stroke: "transparent",
16560
+ strokeWidth: 16,
16561
+ onPointerDown: onPointerDown("minute")
16562
+ }
16563
+ ),
16564
+ /* @__PURE__ */ jsx("circle", { cx: CENTER, cy: CENTER, r: 4, className: "clock-center" })
16565
+ ]
16566
+ }
16567
+ ) }),
16568
+ format === "12h" && /* @__PURE__ */ jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
16569
+ ] });
16570
+ };
16174
16571
  function isTextItem(item) {
16175
16572
  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;
16176
16573
  }
@@ -16475,6 +16872,22 @@ const LayoutRenderer = ({
16475
16872
  }
16476
16873
  );
16477
16874
  }
16875
+ case JSON_TYPES.CLOCK: {
16876
+ const clockItem = item;
16877
+ const widgetName = clockItem.mode === "interactive" ? clockItem.name : void 0;
16878
+ const validationStatus = widgetName ? toValidationStatus(validationDetails?.[widgetName]?.isCorrect) : null;
16879
+ const rawValue = widgetName ? values[widgetName] : void 0;
16880
+ const value = typeof rawValue === "string" ? rawValue : void 0;
16881
+ return /* @__PURE__ */ jsx(
16882
+ ClockRenderer,
16883
+ {
16884
+ item: clockItem,
16885
+ value,
16886
+ validationStatus,
16887
+ onChange
16888
+ }
16889
+ );
16890
+ }
16478
16891
  default:
16479
16892
  return null;
16480
16893
  }
@@ -16799,6 +17212,12 @@ export {
16799
17212
  AST_TYPES,
16800
17213
  BoldTextInlineSchema,
16801
17214
  BoldTextSchema,
17215
+ CLOCK_FORMATS,
17216
+ CLOCK_MODES,
17217
+ CLOCK_TIME_REGEX,
17218
+ CLOCK_TYPES,
17219
+ ClockInlineSchema,
17220
+ ClockItemSchema,
16802
17221
  ColorTextInlineSchema,
16803
17222
  ColorTextSchema,
16804
17223
  ContentBlockSchema,
@@ -16893,6 +17312,7 @@ export {
16893
17312
  compileWithAST,
16894
17313
  createBoldText,
16895
17314
  createColorText,
17315
+ createDisplayClock,
16896
17316
  createDragOptions,
16897
17317
  createExerciseDocument,
16898
17318
  createExpressionValidation,
@@ -16901,6 +17321,7 @@ export {
16901
17321
  createHighlightedText,
16902
17322
  createImage,
16903
17323
  createInlineDragOption,
17324
+ createInteractiveClock,
16904
17325
  createLineBlock,
16905
17326
  createMatcherBlock,
16906
17327
  createMatcherItem,
@@ -16931,6 +17352,8 @@ export {
16931
17352
  getFractionKey,
16932
17353
  getMissingInputs,
16933
17354
  isBoldText,
17355
+ isClock,
17356
+ isClockItem,
16934
17357
  isColorText,
16935
17358
  isDragOptions,
16936
17359
  isDragOptionsJSON,