@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.d.ts +1930 -15
- package/dist/index.js +426 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +426 -3
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +189 -0
- package/package.json +1 -1
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
|
}
|
|
@@ -16173,6 +16340,236 @@ const NumberLineRenderer = ({
|
|
|
16173
16340
|
}
|
|
16174
16341
|
);
|
|
16175
16342
|
};
|
|
16343
|
+
function parseTime(s) {
|
|
16344
|
+
const parts = s.split(":");
|
|
16345
|
+
return { h: Number(parts[0]), m: Number(parts[1]) };
|
|
16346
|
+
}
|
|
16347
|
+
function formatTime(h, m) {
|
|
16348
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
16349
|
+
}
|
|
16350
|
+
function formatDisplay(h24, m, format) {
|
|
16351
|
+
if (format === "24h") return formatTime(h24, m);
|
|
16352
|
+
const period = h24 >= 12 ? "PM" : "AM";
|
|
16353
|
+
const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
|
|
16354
|
+
return `${String(h12).padStart(2, "0")}:${String(m).padStart(2, "0")} ${period}`;
|
|
16355
|
+
}
|
|
16356
|
+
const CLOCK_SIZE = 160;
|
|
16357
|
+
const INTERACTIVE_SCALE = 1.2;
|
|
16358
|
+
const CENTER = CLOCK_SIZE / 2;
|
|
16359
|
+
const FACE_RADIUS = CENTER - 6;
|
|
16360
|
+
const HOUR_HAND = FACE_RADIUS * 0.45;
|
|
16361
|
+
const MINUTE_HAND = FACE_RADIUS * 0.65;
|
|
16362
|
+
const HOUR_LABELS = Array.from({ length: 12 }, (_, i) => i + 1);
|
|
16363
|
+
const M_SHAFT = Math.round(MINUTE_HAND * 0.049);
|
|
16364
|
+
const M_ARROW = Math.round(MINUTE_HAND * 0.156);
|
|
16365
|
+
const M_TIP = Math.round(MINUTE_HAND * 0.279);
|
|
16366
|
+
const M_TAIL = Math.round(MINUTE_HAND * 0.056);
|
|
16367
|
+
const H_SHAFT = Math.round(HOUR_HAND * 0.074);
|
|
16368
|
+
const H_ARROW = Math.round(HOUR_HAND * 0.246);
|
|
16369
|
+
const H_TIP = Math.round(HOUR_HAND * 0.369);
|
|
16370
|
+
const H_TAIL = Math.round(HOUR_HAND * 0.082);
|
|
16371
|
+
const ClockRenderer = ({
|
|
16372
|
+
item,
|
|
16373
|
+
value,
|
|
16374
|
+
validationStatus,
|
|
16375
|
+
onChange
|
|
16376
|
+
}) => {
|
|
16377
|
+
const format = item.format ?? "24h";
|
|
16378
|
+
const interactive = item.mode === "interactive";
|
|
16379
|
+
const snap = interactive && "snap" in item ? item.snap ?? 1 : 1;
|
|
16380
|
+
const initial = parseTime(interactive && value ? value : item.value);
|
|
16381
|
+
const [hours, setHours] = React.useState(initial.h);
|
|
16382
|
+
const [minutes, setMinutes] = React.useState(initial.m);
|
|
16383
|
+
React.useEffect(() => {
|
|
16384
|
+
if (interactive && value) {
|
|
16385
|
+
const t = parseTime(value);
|
|
16386
|
+
setHours(t.h);
|
|
16387
|
+
setMinutes(t.m);
|
|
16388
|
+
}
|
|
16389
|
+
}, [value, interactive]);
|
|
16390
|
+
const svgRef = React.useRef(null);
|
|
16391
|
+
const draggingRef = React.useRef(null);
|
|
16392
|
+
const updateFromPointer = React.useCallback((clientX, clientY) => {
|
|
16393
|
+
const svg = svgRef.current;
|
|
16394
|
+
if (!svg || !draggingRef.current) return;
|
|
16395
|
+
const rect = svg.getBoundingClientRect();
|
|
16396
|
+
const dx = clientX - rect.left - CENTER;
|
|
16397
|
+
const dy = clientY - rect.top - CENTER;
|
|
16398
|
+
const angle = (Math.atan2(dx, -dy) + 2 * Math.PI) % (2 * Math.PI);
|
|
16399
|
+
if (draggingRef.current === "minute") {
|
|
16400
|
+
const rawMin = angle / (2 * Math.PI) * 60;
|
|
16401
|
+
const snapped = Math.round(rawMin / snap) * snap % 60;
|
|
16402
|
+
setMinutes(snapped);
|
|
16403
|
+
if (item.mode === "interactive" && onChange) {
|
|
16404
|
+
onChange(item.name, formatTime(hours, snapped));
|
|
16405
|
+
}
|
|
16406
|
+
} else {
|
|
16407
|
+
const rawHour12 = angle / (2 * Math.PI) * 12;
|
|
16408
|
+
const rounded = Math.round(rawHour12) % 12;
|
|
16409
|
+
const isPm = hours >= 12;
|
|
16410
|
+
const newH = (rounded === 0 ? 0 : rounded) + (isPm ? 12 : 0);
|
|
16411
|
+
const h24 = newH % 24;
|
|
16412
|
+
setHours(h24);
|
|
16413
|
+
if (item.mode === "interactive" && onChange) {
|
|
16414
|
+
onChange(item.name, formatTime(h24, minutes));
|
|
16415
|
+
}
|
|
16416
|
+
}
|
|
16417
|
+
}, [snap, hours, minutes, item.mode, item.name, onChange]);
|
|
16418
|
+
const onPointerDown = (hand) => (e) => {
|
|
16419
|
+
if (!interactive) return;
|
|
16420
|
+
e.preventDefault();
|
|
16421
|
+
e.target.setPointerCapture?.(e.pointerId);
|
|
16422
|
+
draggingRef.current = hand;
|
|
16423
|
+
updateFromPointer(e.clientX, e.clientY);
|
|
16424
|
+
};
|
|
16425
|
+
const onPointerMove = (e) => {
|
|
16426
|
+
if (!interactive || !draggingRef.current) return;
|
|
16427
|
+
updateFromPointer(e.clientX, e.clientY);
|
|
16428
|
+
};
|
|
16429
|
+
const onPointerUp = (e) => {
|
|
16430
|
+
if (!interactive) return;
|
|
16431
|
+
e.target.releasePointerCapture?.(e.pointerId);
|
|
16432
|
+
draggingRef.current = null;
|
|
16433
|
+
};
|
|
16434
|
+
const hourAngleRad = React.useMemo(() => {
|
|
16435
|
+
const h12 = hours % 12;
|
|
16436
|
+
return (h12 + minutes / 60) / 12 * 2 * Math.PI;
|
|
16437
|
+
}, [hours, minutes]);
|
|
16438
|
+
const minuteAngleRad = React.useMemo(() => minutes / 60 * 2 * Math.PI, [minutes]);
|
|
16439
|
+
const hourX = CENTER + Math.sin(hourAngleRad) * HOUR_HAND;
|
|
16440
|
+
const hourY = CENTER - Math.cos(hourAngleRad) * HOUR_HAND;
|
|
16441
|
+
const minuteX = CENTER + Math.sin(minuteAngleRad) * MINUTE_HAND;
|
|
16442
|
+
const minuteY = CENTER - Math.cos(minuteAngleRad) * MINUTE_HAND;
|
|
16443
|
+
const hourDeg = hourAngleRad * 180 / Math.PI;
|
|
16444
|
+
const minuteDeg = minuteAngleRad * 180 / Math.PI;
|
|
16445
|
+
const scale = interactive ? INTERACTIVE_SCALE : 1;
|
|
16446
|
+
const svgSize = CLOCK_SIZE * scale;
|
|
16447
|
+
const ringSize = FACE_RADIUS * 2 * scale;
|
|
16448
|
+
const svgOffset = -6 * scale;
|
|
16449
|
+
const statusClass = validationStatus ? `clock-${validationStatus}` : "";
|
|
16450
|
+
if (item.clockType === "digital") {
|
|
16451
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `clock clock-digital ${statusClass}`, dir: "ltr", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "clock-digital-wrapper", children: [
|
|
16452
|
+
/* @__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: [
|
|
16453
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M0 8C0 3.58172 3.58172 0 8 0H14C18.4183 0 22 3.58172 22 8V9H0V8Z", fill: "#AE9CFF" }),
|
|
16454
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M63 8C63 3.58172 66.5817 0 71 0H77C81.4183 0 85 3.58172 85 8V9H63V8Z", fill: "#AE9CFF" })
|
|
16455
|
+
] }),
|
|
16456
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-digital-display", children: formatDisplay(hours, minutes, format) })
|
|
16457
|
+
] }) });
|
|
16458
|
+
}
|
|
16459
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `clock clock-analog ${interactive ? "clock-interactive" : "clock-display"} ${statusClass}`, dir: "ltr", children: [
|
|
16460
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-face-ring", style: { width: ringSize, height: ringSize }, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
16461
|
+
"svg",
|
|
16462
|
+
{
|
|
16463
|
+
ref: svgRef,
|
|
16464
|
+
width: svgSize,
|
|
16465
|
+
height: svgSize,
|
|
16466
|
+
viewBox: `0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`,
|
|
16467
|
+
style: { top: svgOffset, left: svgOffset },
|
|
16468
|
+
onPointerMove,
|
|
16469
|
+
onPointerUp,
|
|
16470
|
+
onPointerCancel: onPointerUp,
|
|
16471
|
+
className: "clock-svg",
|
|
16472
|
+
children: [
|
|
16473
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16474
|
+
"circle",
|
|
16475
|
+
{
|
|
16476
|
+
cx: CENTER,
|
|
16477
|
+
cy: CENTER,
|
|
16478
|
+
r: FACE_RADIUS,
|
|
16479
|
+
className: "clock-face"
|
|
16480
|
+
}
|
|
16481
|
+
),
|
|
16482
|
+
Array.from({ length: 12 }).map((_, i) => {
|
|
16483
|
+
const a = i / 12 * 2 * Math.PI;
|
|
16484
|
+
const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 8);
|
|
16485
|
+
const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 8);
|
|
16486
|
+
const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
|
|
16487
|
+
const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
|
|
16488
|
+
return /* @__PURE__ */ jsxRuntime.jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-hour" }, `t${i}`);
|
|
16489
|
+
}),
|
|
16490
|
+
Array.from({ length: 60 }).map((_, i) => {
|
|
16491
|
+
if (i % 5 === 0) return null;
|
|
16492
|
+
const a = i / 60 * 2 * Math.PI;
|
|
16493
|
+
const x1 = CENTER + Math.sin(a) * (FACE_RADIUS - 4);
|
|
16494
|
+
const y1 = CENTER - Math.cos(a) * (FACE_RADIUS - 4);
|
|
16495
|
+
const x2 = CENTER + Math.sin(a) * FACE_RADIUS;
|
|
16496
|
+
const y2 = CENTER - Math.cos(a) * FACE_RADIUS;
|
|
16497
|
+
return /* @__PURE__ */ jsxRuntime.jsx("line", { x1, y1, x2, y2, className: "clock-tick clock-tick-minute" }, `m${i}`);
|
|
16498
|
+
}),
|
|
16499
|
+
HOUR_LABELS.map((n) => {
|
|
16500
|
+
const a = n / 12 * 2 * Math.PI;
|
|
16501
|
+
const r = FACE_RADIUS - 20;
|
|
16502
|
+
const x = CENTER + Math.sin(a) * r;
|
|
16503
|
+
const y = CENTER - Math.cos(a) * r;
|
|
16504
|
+
return /* @__PURE__ */ jsxRuntime.jsx("text", { x, y, className: "clock-number", textAnchor: "middle", dominantBaseline: "central", children: n }, `l${n}`);
|
|
16505
|
+
}),
|
|
16506
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16507
|
+
"polygon",
|
|
16508
|
+
{
|
|
16509
|
+
transform: `translate(${CENTER},${CENTER}) rotate(${hourDeg})`,
|
|
16510
|
+
points: [
|
|
16511
|
+
`0,${-HOUR_HAND}`,
|
|
16512
|
+
`${H_ARROW},${-(HOUR_HAND - H_TIP)}`,
|
|
16513
|
+
`${H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
|
|
16514
|
+
`${H_SHAFT},${H_TAIL}`,
|
|
16515
|
+
`${-H_SHAFT},${H_TAIL}`,
|
|
16516
|
+
`${-H_SHAFT},${-(HOUR_HAND - H_TIP)}`,
|
|
16517
|
+
`${-H_ARROW},${-(HOUR_HAND - H_TIP)}`
|
|
16518
|
+
].join(" "),
|
|
16519
|
+
className: "clock-arrow clock-arrow-hour",
|
|
16520
|
+
style: { pointerEvents: "none" }
|
|
16521
|
+
}
|
|
16522
|
+
),
|
|
16523
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16524
|
+
"polygon",
|
|
16525
|
+
{
|
|
16526
|
+
transform: `translate(${CENTER},${CENTER}) rotate(${minuteDeg})`,
|
|
16527
|
+
points: [
|
|
16528
|
+
`0,${-MINUTE_HAND}`,
|
|
16529
|
+
`${M_ARROW},${-(MINUTE_HAND - M_TIP)}`,
|
|
16530
|
+
`${M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
|
|
16531
|
+
`${M_SHAFT},${M_TAIL}`,
|
|
16532
|
+
`${-M_SHAFT},${M_TAIL}`,
|
|
16533
|
+
`${-M_SHAFT},${-(MINUTE_HAND - M_TIP)}`,
|
|
16534
|
+
`${-M_ARROW},${-(MINUTE_HAND - M_TIP)}`
|
|
16535
|
+
].join(" "),
|
|
16536
|
+
className: "clock-arrow clock-arrow-minute",
|
|
16537
|
+
style: { pointerEvents: "none" }
|
|
16538
|
+
}
|
|
16539
|
+
),
|
|
16540
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16541
|
+
"line",
|
|
16542
|
+
{
|
|
16543
|
+
x1: CENTER,
|
|
16544
|
+
y1: CENTER,
|
|
16545
|
+
x2: hourX,
|
|
16546
|
+
y2: hourY,
|
|
16547
|
+
className: "clock-hand clock-hand-hour",
|
|
16548
|
+
stroke: "transparent",
|
|
16549
|
+
strokeWidth: 20,
|
|
16550
|
+
onPointerDown: onPointerDown("hour")
|
|
16551
|
+
}
|
|
16552
|
+
),
|
|
16553
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16554
|
+
"line",
|
|
16555
|
+
{
|
|
16556
|
+
x1: CENTER,
|
|
16557
|
+
y1: CENTER,
|
|
16558
|
+
x2: minuteX,
|
|
16559
|
+
y2: minuteY,
|
|
16560
|
+
className: "clock-hand clock-hand-minute",
|
|
16561
|
+
stroke: "transparent",
|
|
16562
|
+
strokeWidth: 16,
|
|
16563
|
+
onPointerDown: onPointerDown("minute")
|
|
16564
|
+
}
|
|
16565
|
+
),
|
|
16566
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: CENTER, cy: CENTER, r: 4, className: "clock-center" })
|
|
16567
|
+
]
|
|
16568
|
+
}
|
|
16569
|
+
) }),
|
|
16570
|
+
format === "12h" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
|
|
16571
|
+
] });
|
|
16572
|
+
};
|
|
16176
16573
|
function isTextItem(item) {
|
|
16177
16574
|
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
16575
|
}
|
|
@@ -16477,6 +16874,22 @@ const LayoutRenderer = ({
|
|
|
16477
16874
|
}
|
|
16478
16875
|
);
|
|
16479
16876
|
}
|
|
16877
|
+
case JSON_TYPES.CLOCK: {
|
|
16878
|
+
const clockItem = item;
|
|
16879
|
+
const widgetName = clockItem.mode === "interactive" ? clockItem.name : void 0;
|
|
16880
|
+
const validationStatus = widgetName ? toValidationStatus(validationDetails?.[widgetName]?.isCorrect) : null;
|
|
16881
|
+
const rawValue = widgetName ? values[widgetName] : void 0;
|
|
16882
|
+
const value = typeof rawValue === "string" ? rawValue : void 0;
|
|
16883
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16884
|
+
ClockRenderer,
|
|
16885
|
+
{
|
|
16886
|
+
item: clockItem,
|
|
16887
|
+
value,
|
|
16888
|
+
validationStatus,
|
|
16889
|
+
onChange
|
|
16890
|
+
}
|
|
16891
|
+
);
|
|
16892
|
+
}
|
|
16480
16893
|
default:
|
|
16481
16894
|
return null;
|
|
16482
16895
|
}
|
|
@@ -16800,6 +17213,12 @@ function compileWithAST(dsl) {
|
|
|
16800
17213
|
exports.AST_TYPES = AST_TYPES;
|
|
16801
17214
|
exports.BoldTextInlineSchema = BoldTextInlineSchema;
|
|
16802
17215
|
exports.BoldTextSchema = BoldTextSchema;
|
|
17216
|
+
exports.CLOCK_FORMATS = CLOCK_FORMATS;
|
|
17217
|
+
exports.CLOCK_MODES = CLOCK_MODES;
|
|
17218
|
+
exports.CLOCK_TIME_REGEX = CLOCK_TIME_REGEX;
|
|
17219
|
+
exports.CLOCK_TYPES = CLOCK_TYPES;
|
|
17220
|
+
exports.ClockInlineSchema = ClockInlineSchema;
|
|
17221
|
+
exports.ClockItemSchema = ClockItemSchema;
|
|
16803
17222
|
exports.ColorTextInlineSchema = ColorTextInlineSchema;
|
|
16804
17223
|
exports.ColorTextSchema = ColorTextSchema;
|
|
16805
17224
|
exports.ContentBlockSchema = ContentBlockSchema;
|
|
@@ -16894,6 +17313,7 @@ exports.compile = compile;
|
|
|
16894
17313
|
exports.compileWithAST = compileWithAST;
|
|
16895
17314
|
exports.createBoldText = createBoldText;
|
|
16896
17315
|
exports.createColorText = createColorText;
|
|
17316
|
+
exports.createDisplayClock = createDisplayClock;
|
|
16897
17317
|
exports.createDragOptions = createDragOptions;
|
|
16898
17318
|
exports.createExerciseDocument = createExerciseDocument;
|
|
16899
17319
|
exports.createExpressionValidation = createExpressionValidation;
|
|
@@ -16902,6 +17322,7 @@ exports.createFreeText = createFreeText;
|
|
|
16902
17322
|
exports.createHighlightedText = createHighlightedText;
|
|
16903
17323
|
exports.createImage = createImage;
|
|
16904
17324
|
exports.createInlineDragOption = createInlineDragOption;
|
|
17325
|
+
exports.createInteractiveClock = createInteractiveClock;
|
|
16905
17326
|
exports.createLineBlock = createLineBlock;
|
|
16906
17327
|
exports.createMatcherBlock = createMatcherBlock;
|
|
16907
17328
|
exports.createMatcherItem = createMatcherItem;
|
|
@@ -16932,6 +17353,8 @@ exports.findValidCombination = findValidCombination;
|
|
|
16932
17353
|
exports.getFractionKey = getFractionKey;
|
|
16933
17354
|
exports.getMissingInputs = getMissingInputs;
|
|
16934
17355
|
exports.isBoldText = isBoldText;
|
|
17356
|
+
exports.isClock = isClock;
|
|
17357
|
+
exports.isClockItem = isClockItem;
|
|
16935
17358
|
exports.isColorText = isColorText;
|
|
16936
17359
|
exports.isDragOptions = isDragOptions;
|
|
16937
17360
|
exports.isDragOptionsJSON = isDragOptionsJSON;
|