@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.d.ts +1930 -15
- package/dist/index.js +431 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +431 -6
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +193 -0
- package/package.json +1 -1
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
|
}
|
|
@@ -14006,12 +14173,13 @@ const SingleDragOption = ({
|
|
|
14006
14173
|
const PillBackground = ({
|
|
14007
14174
|
width = "100%",
|
|
14008
14175
|
minHeight = "72px",
|
|
14009
|
-
fill
|
|
14176
|
+
fill,
|
|
14010
14177
|
children,
|
|
14011
14178
|
className = "",
|
|
14012
14179
|
style,
|
|
14013
14180
|
...rest
|
|
14014
14181
|
}) => {
|
|
14182
|
+
const pillVarStyle = fill ? { "--exercise-pill-bg": fill } : {};
|
|
14015
14183
|
return /* @__PURE__ */ jsxs(
|
|
14016
14184
|
"div",
|
|
14017
14185
|
{
|
|
@@ -14023,6 +14191,7 @@ const PillBackground = ({
|
|
|
14023
14191
|
justifyContent: "center",
|
|
14024
14192
|
width,
|
|
14025
14193
|
minHeight,
|
|
14194
|
+
...pillVarStyle,
|
|
14026
14195
|
...style
|
|
14027
14196
|
},
|
|
14028
14197
|
...rest,
|
|
@@ -14044,8 +14213,8 @@ const PillBackground = ({
|
|
|
14044
14213
|
children: /* @__PURE__ */ jsx(
|
|
14045
14214
|
"path",
|
|
14046
14215
|
{
|
|
14047
|
-
|
|
14048
|
-
|
|
14216
|
+
className: "pill-bg-path",
|
|
14217
|
+
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"
|
|
14049
14218
|
}
|
|
14050
14219
|
)
|
|
14051
14220
|
}
|
|
@@ -16171,6 +16340,236 @@ const NumberLineRenderer = ({
|
|
|
16171
16340
|
}
|
|
16172
16341
|
);
|
|
16173
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] = useState(initial.h);
|
|
16382
|
+
const [minutes, setMinutes] = useState(initial.m);
|
|
16383
|
+
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 = useRef(null);
|
|
16391
|
+
const draggingRef = useRef(null);
|
|
16392
|
+
const updateFromPointer = 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 = useMemo(() => {
|
|
16435
|
+
const h12 = hours % 12;
|
|
16436
|
+
return (h12 + minutes / 60) / 12 * 2 * Math.PI;
|
|
16437
|
+
}, [hours, minutes]);
|
|
16438
|
+
const minuteAngleRad = 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__ */ jsx("span", { className: `clock clock-digital ${statusClass}`, dir: "ltr", children: /* @__PURE__ */ jsxs("span", { className: "clock-digital-wrapper", children: [
|
|
16452
|
+
/* @__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: [
|
|
16453
|
+
/* @__PURE__ */ jsx("path", { d: "M0 8C0 3.58172 3.58172 0 8 0H14C18.4183 0 22 3.58172 22 8V9H0V8Z", fill: "#AE9CFF" }),
|
|
16454
|
+
/* @__PURE__ */ jsx("path", { d: "M63 8C63 3.58172 66.5817 0 71 0H77C81.4183 0 85 3.58172 85 8V9H63V8Z", fill: "#AE9CFF" })
|
|
16455
|
+
] }),
|
|
16456
|
+
/* @__PURE__ */ jsx("span", { className: "clock-digital-display", children: formatDisplay(hours, minutes, format) })
|
|
16457
|
+
] }) });
|
|
16458
|
+
}
|
|
16459
|
+
return /* @__PURE__ */ jsxs("span", { className: `clock clock-analog ${interactive ? "clock-interactive" : "clock-display"} ${statusClass}`, dir: "ltr", children: [
|
|
16460
|
+
/* @__PURE__ */ jsx("span", { className: "clock-face-ring", style: { width: ringSize, height: ringSize }, children: /* @__PURE__ */ 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__ */ 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__ */ 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__ */ 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__ */ jsx("text", { x, y, className: "clock-number", textAnchor: "middle", dominantBaseline: "central", children: n }, `l${n}`);
|
|
16505
|
+
}),
|
|
16506
|
+
/* @__PURE__ */ 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__ */ 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__ */ 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__ */ 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__ */ jsx("circle", { cx: CENTER, cy: CENTER, r: 4, className: "clock-center" })
|
|
16567
|
+
]
|
|
16568
|
+
}
|
|
16569
|
+
) }),
|
|
16570
|
+
format === "12h" && /* @__PURE__ */ jsx("span", { className: "clock-period-badge", children: hours >= 12 ? "PM" : "AM" })
|
|
16571
|
+
] });
|
|
16572
|
+
};
|
|
16174
16573
|
function isTextItem(item) {
|
|
16175
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;
|
|
16176
16575
|
}
|
|
@@ -16475,6 +16874,22 @@ const LayoutRenderer = ({
|
|
|
16475
16874
|
}
|
|
16476
16875
|
);
|
|
16477
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__ */ jsx(
|
|
16884
|
+
ClockRenderer,
|
|
16885
|
+
{
|
|
16886
|
+
item: clockItem,
|
|
16887
|
+
value,
|
|
16888
|
+
validationStatus,
|
|
16889
|
+
onChange
|
|
16890
|
+
}
|
|
16891
|
+
);
|
|
16892
|
+
}
|
|
16478
16893
|
default:
|
|
16479
16894
|
return null;
|
|
16480
16895
|
}
|
|
@@ -16799,6 +17214,12 @@ export {
|
|
|
16799
17214
|
AST_TYPES,
|
|
16800
17215
|
BoldTextInlineSchema,
|
|
16801
17216
|
BoldTextSchema,
|
|
17217
|
+
CLOCK_FORMATS,
|
|
17218
|
+
CLOCK_MODES,
|
|
17219
|
+
CLOCK_TIME_REGEX,
|
|
17220
|
+
CLOCK_TYPES,
|
|
17221
|
+
ClockInlineSchema,
|
|
17222
|
+
ClockItemSchema,
|
|
16802
17223
|
ColorTextInlineSchema,
|
|
16803
17224
|
ColorTextSchema,
|
|
16804
17225
|
ContentBlockSchema,
|
|
@@ -16893,6 +17314,7 @@ export {
|
|
|
16893
17314
|
compileWithAST,
|
|
16894
17315
|
createBoldText,
|
|
16895
17316
|
createColorText,
|
|
17317
|
+
createDisplayClock,
|
|
16896
17318
|
createDragOptions,
|
|
16897
17319
|
createExerciseDocument,
|
|
16898
17320
|
createExpressionValidation,
|
|
@@ -16901,6 +17323,7 @@ export {
|
|
|
16901
17323
|
createHighlightedText,
|
|
16902
17324
|
createImage,
|
|
16903
17325
|
createInlineDragOption,
|
|
17326
|
+
createInteractiveClock,
|
|
16904
17327
|
createLineBlock,
|
|
16905
17328
|
createMatcherBlock,
|
|
16906
17329
|
createMatcherItem,
|
|
@@ -16931,6 +17354,8 @@ export {
|
|
|
16931
17354
|
getFractionKey,
|
|
16932
17355
|
getMissingInputs,
|
|
16933
17356
|
isBoldText,
|
|
17357
|
+
isClock,
|
|
17358
|
+
isClockItem,
|
|
16934
17359
|
isColorText,
|
|
16935
17360
|
isDragOptions,
|
|
16936
17361
|
isDragOptionsJSON,
|