@gfazioli/mantine-clock 1.0.0

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.
@@ -0,0 +1,571 @@
1
+ 'use client';
2
+ import dayjs from 'dayjs';
3
+ import timezonePlugin from 'dayjs/plugin/timezone';
4
+ import utc from 'dayjs/plugin/utc';
5
+ import React, { useState, useRef, useEffect } from 'react';
6
+ import { createVarsResolver, px, getSize, parseThemeColor, factory, useProps, useStyles, Box, Text } from '@mantine/core';
7
+ import classes from './Clock.module.css.mjs';
8
+
9
+ dayjs.extend(utc);
10
+ dayjs.extend(timezonePlugin);
11
+ const defaultProps = {
12
+ size: 400,
13
+ hourHandSize: 0.017,
14
+ minuteHandSize: 0.011,
15
+ secondHandSize: 6e-3,
16
+ hourHandLength: 0.4,
17
+ minuteHandLength: 0.57,
18
+ secondHandLength: 0.68,
19
+ secondHandOpacity: 1,
20
+ minuteHandOpacity: 1,
21
+ hourHandOpacity: 1,
22
+ running: true
23
+ };
24
+ const defaultClockSizes = {
25
+ xs: 100,
26
+ sm: 200,
27
+ md: 400,
28
+ lg: 480,
29
+ xl: 512
30
+ };
31
+ const varsResolver = createVarsResolver(
32
+ (theme, {
33
+ size,
34
+ color,
35
+ hourTicksColor,
36
+ hourTicksOpacity,
37
+ minuteTicksColor,
38
+ minuteTicksOpacity,
39
+ primaryNumbersColor,
40
+ primaryNumbersOpacity,
41
+ secondaryNumbersColor,
42
+ secondaryNumbersOpacity,
43
+ secondHandColor,
44
+ minuteHandColor,
45
+ hourHandColor
46
+ }) => {
47
+ const sizeValue = size || "md";
48
+ const clockSize = typeof sizeValue === "string" && sizeValue in defaultClockSizes ? defaultClockSizes[sizeValue] : sizeValue;
49
+ const effectiveSize = Math.round(px(getSize(clockSize, "clock-size")));
50
+ return {
51
+ root: {
52
+ "--clock-size": `${effectiveSize}px`,
53
+ "--clock-color": parseThemeColor({
54
+ color: color || "",
55
+ theme
56
+ }).value,
57
+ "--clock-hour-ticks-color": parseThemeColor({
58
+ color: hourTicksColor || "",
59
+ theme
60
+ }).value,
61
+ "--clock-hour-ticks-opacity": (Math.round((hourTicksOpacity || 1) * 100) / 100).toString(),
62
+ "--clock-minute-ticks-color": parseThemeColor({
63
+ color: minuteTicksColor || "",
64
+ theme
65
+ }).value,
66
+ "--clock-minute-ticks-opacity": (Math.round((minuteTicksOpacity || 1) * 100) / 100).toString(),
67
+ "--clock-primary-numbers-color": parseThemeColor({
68
+ color: primaryNumbersColor || "",
69
+ theme
70
+ }).value,
71
+ "--clock-primary-numbers-opacity": (Math.round((primaryNumbersOpacity || 1) * 100) / 100).toString(),
72
+ "--clock-secondary-numbers-color": parseThemeColor({
73
+ color: secondaryNumbersColor || "",
74
+ theme
75
+ }).value,
76
+ "--clock-secondary-numbers-opacity": (Math.round((secondaryNumbersOpacity || 1) * 100) / 100).toString(),
77
+ "--clock-second-hand-color": parseThemeColor({
78
+ color: secondHandColor || "",
79
+ theme
80
+ }).value,
81
+ "--clock-minute-hand-color": parseThemeColor({
82
+ color: minuteHandColor || "",
83
+ theme
84
+ }).value,
85
+ "--clock-hour-hand-color": parseThemeColor({
86
+ color: hourHandColor || "",
87
+ theme
88
+ }).value
89
+ }
90
+ };
91
+ }
92
+ );
93
+ const parseTimeValue = (value) => {
94
+ if (!value) {
95
+ return null;
96
+ }
97
+ if (value instanceof Date) {
98
+ return value;
99
+ }
100
+ if (dayjs.isDayjs(value)) {
101
+ return value.toDate();
102
+ }
103
+ if (typeof value === "string") {
104
+ const timeRegex = /^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/;
105
+ const match = value.match(timeRegex);
106
+ if (match) {
107
+ const hours = parseInt(match[1], 10);
108
+ const minutes = parseInt(match[2], 10);
109
+ const seconds = parseInt(match[3] || "0", 10);
110
+ const date = /* @__PURE__ */ new Date();
111
+ date.setHours(hours, minutes, seconds, 0);
112
+ return date;
113
+ }
114
+ const parsed = new Date(value);
115
+ if (!isNaN(parsed.getTime())) {
116
+ return parsed;
117
+ }
118
+ }
119
+ return null;
120
+ };
121
+ const RealClock = (props) => {
122
+ const {
123
+ time,
124
+ timezone: timezone2,
125
+ getStyles,
126
+ effectiveSize,
127
+ hourHandSize,
128
+ minuteHandSize,
129
+ secondHandSize,
130
+ hourHandLength,
131
+ minuteHandLength,
132
+ secondHandLength,
133
+ secondHandBehavior,
134
+ secondHandOpacity,
135
+ minuteHandOpacity,
136
+ hourHandOpacity,
137
+ hourTicksOpacity,
138
+ minuteTicksOpacity,
139
+ primaryNumbersOpacity,
140
+ secondaryNumbersOpacity,
141
+ hourNumbersDistance = 0.75,
142
+ // Default distance for hour numbers
143
+ primaryNumbersProps,
144
+ secondaryNumbersProps
145
+ } = props;
146
+ const timezoneTime = timezone2 && timezone2 !== "" ? dayjs(time).tz(timezone2) : dayjs(time);
147
+ const hours = timezoneTime.hour() % 12;
148
+ const minutes = timezoneTime.minute();
149
+ const seconds = timezoneTime.second();
150
+ const milliseconds = timezoneTime.millisecond();
151
+ const hourAngle = hours * 30 + minutes * 0.5;
152
+ const minuteAngle = minutes * 6;
153
+ let secondAngle = 0;
154
+ switch (secondHandBehavior) {
155
+ case "tick":
156
+ secondAngle = seconds * 6;
157
+ break;
158
+ case "tick-half":
159
+ secondAngle = (seconds + Math.floor(milliseconds / 500) * 0.5) * 6;
160
+ break;
161
+ case "tick-high-freq":
162
+ secondAngle = (seconds + Math.floor(milliseconds / 125) * 0.125) * 6;
163
+ break;
164
+ case "smooth":
165
+ default:
166
+ secondAngle = (seconds + milliseconds / 1e3) * 6;
167
+ break;
168
+ }
169
+ const size = effectiveSize;
170
+ const clockRadius = Math.round(size / 2);
171
+ const numberRadius = Math.round(clockRadius * hourNumbersDistance);
172
+ const calculatedHourHandLength = Math.round(
173
+ clockRadius * (hourHandLength ?? defaultProps.hourHandLength)
174
+ );
175
+ const calculatedMinuteHandLength = Math.round(
176
+ clockRadius * (minuteHandLength ?? defaultProps.minuteHandLength)
177
+ );
178
+ const calculatedSecondHandLength = Math.round(
179
+ clockRadius * (secondHandLength ?? defaultProps.secondHandLength)
180
+ );
181
+ const centerSize = Math.round(size * 0.034);
182
+ const tickOffset = Math.round(size * 0.028);
183
+ return /* @__PURE__ */ React.createElement(Box, { ...getStyles("clockContainer") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("glassWrapper") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("clockFace") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("hourMarks") }, hourTicksOpacity !== 0 && Array.from({ length: 12 }, (_, i) => /* @__PURE__ */ React.createElement(
184
+ Box,
185
+ {
186
+ key: `hour-tick-${i}`,
187
+ ...getStyles("hourTick", {
188
+ style: {
189
+ top: tickOffset,
190
+ left: "50%",
191
+ transformOrigin: `50% ${clockRadius - tickOffset}px`,
192
+ transform: `translateX(-50%) rotate(${i * 30}deg)`
193
+ }
194
+ })
195
+ }
196
+ )), minuteTicksOpacity !== 0 && Array.from({ length: 60 }, (_, i) => {
197
+ if (i % 5 === 0) {
198
+ return null;
199
+ }
200
+ return /* @__PURE__ */ React.createElement(
201
+ Box,
202
+ {
203
+ key: `minute-tick-${i}`,
204
+ ...getStyles("minuteTick", {
205
+ style: {
206
+ top: tickOffset,
207
+ left: "50%",
208
+ transformOrigin: `50% ${clockRadius - tickOffset}px`,
209
+ transform: `translateX(-50%) rotate(${i * 6}deg)`
210
+ }
211
+ })
212
+ }
213
+ );
214
+ }), primaryNumbersOpacity !== 0 && [12, 3, 6, 9].map((num) => {
215
+ const i = num === 12 ? 0 : num;
216
+ const angle = (i * 30 - 90) * (Math.PI / 180);
217
+ const x = Math.round(clockRadius + Math.cos(angle) * numberRadius);
218
+ const y = Math.round(clockRadius + Math.sin(angle) * numberRadius);
219
+ return /* @__PURE__ */ React.createElement(
220
+ Text,
221
+ {
222
+ key: `primary-number-${num}`,
223
+ ...primaryNumbersProps,
224
+ ...getStyles("primaryNumber", {
225
+ className: getStyles("number").className,
226
+ style: {
227
+ left: x,
228
+ top: y
229
+ }
230
+ })
231
+ },
232
+ num
233
+ );
234
+ }), secondaryNumbersOpacity !== 0 && [1, 2, 4, 5, 7, 8, 10, 11].map((num) => {
235
+ const i = num;
236
+ const angle = (i * 30 - 90) * (Math.PI / 180);
237
+ const x = Math.round(clockRadius + Math.cos(angle) * numberRadius);
238
+ const y = Math.round(clockRadius + Math.sin(angle) * numberRadius);
239
+ return /* @__PURE__ */ React.createElement(
240
+ Text,
241
+ {
242
+ key: `secondary-number-${num}`,
243
+ ...secondaryNumbersProps,
244
+ ...getStyles("secondaryNumber", {
245
+ className: getStyles("number").className,
246
+ style: {
247
+ left: x,
248
+ top: y
249
+ }
250
+ })
251
+ },
252
+ num
253
+ );
254
+ })), (hourHandOpacity ?? defaultProps.hourHandOpacity) !== 0 && /* @__PURE__ */ React.createElement(
255
+ Box,
256
+ {
257
+ ...getStyles("hand", {
258
+ className: getStyles("hourHand").className,
259
+ style: {
260
+ width: Math.round(size * (hourHandSize ?? defaultProps.hourHandSize) * 100) / 100,
261
+ height: calculatedHourHandLength,
262
+ opacity: Math.round((hourHandOpacity ?? defaultProps.hourHandOpacity) * 100) / 100,
263
+ bottom: clockRadius,
264
+ left: clockRadius,
265
+ marginLeft: Math.round(-(size * (hourHandSize ?? defaultProps.hourHandSize)) / 2 * 100) / 100,
266
+ borderRadius: `${Math.round(size * (hourHandSize ?? defaultProps.hourHandSize) * 100) / 100}px`,
267
+ transform: `rotate(${Math.round(hourAngle * 100) / 100}deg)`
268
+ }
269
+ })
270
+ }
271
+ ), (minuteHandOpacity ?? defaultProps.minuteHandOpacity) !== 0 && /* @__PURE__ */ React.createElement(
272
+ Box,
273
+ {
274
+ ...getStyles("hand", {
275
+ className: getStyles("minuteHand").className,
276
+ style: {
277
+ width: Math.round(size * (minuteHandSize ?? defaultProps.minuteHandSize) * 100) / 100,
278
+ height: calculatedMinuteHandLength,
279
+ opacity: Math.round((minuteHandOpacity ?? defaultProps.minuteHandOpacity) * 100) / 100,
280
+ bottom: clockRadius,
281
+ left: clockRadius,
282
+ marginLeft: Math.round(
283
+ -(size * (minuteHandSize ?? defaultProps.minuteHandSize)) / 2 * 100
284
+ ) / 100,
285
+ borderRadius: `${Math.round(size * (minuteHandSize ?? defaultProps.minuteHandSize) * 100) / 100}px`,
286
+ transform: `rotate(${Math.round(minuteAngle * 100) / 100}deg)`
287
+ }
288
+ })
289
+ }
290
+ ), (secondHandOpacity ?? defaultProps.secondHandOpacity) !== 0 && /* @__PURE__ */ React.createElement(
291
+ Box,
292
+ {
293
+ ...getStyles("secondHandContainer", {
294
+ style: {
295
+ width: Math.round(size * (secondHandSize ?? defaultProps.secondHandSize) * 100) / 100,
296
+ height: calculatedSecondHandLength,
297
+ top: clockRadius - calculatedSecondHandLength,
298
+ left: clockRadius,
299
+ marginLeft: Math.round(
300
+ -(size * (secondHandSize ?? defaultProps.secondHandSize)) / 2 * 100
301
+ ) / 100,
302
+ transformOrigin: `${Math.round(size * (secondHandSize ?? defaultProps.secondHandSize) / 2 * 100) / 100}px ${calculatedSecondHandLength}px`,
303
+ transform: `rotate(${Math.round(secondAngle * 100) / 100}deg)`
304
+ }
305
+ })
306
+ },
307
+ /* @__PURE__ */ React.createElement(
308
+ Box,
309
+ {
310
+ ...getStyles("secondHand", {
311
+ style: {
312
+ width: Math.round(size * (secondHandSize ?? defaultProps.secondHandSize) * 100) / 100,
313
+ height: calculatedSecondHandLength,
314
+ opacity: Math.round((secondHandOpacity ?? defaultProps.secondHandOpacity) * 100) / 100
315
+ }
316
+ })
317
+ }
318
+ ),
319
+ /* @__PURE__ */ React.createElement(
320
+ Box,
321
+ {
322
+ ...getStyles("secondHandCounterweight", {
323
+ style: {
324
+ width: Math.round(size * 6e-3 * 3 * 100) / 100,
325
+ opacity: Math.round((secondHandOpacity ?? defaultProps.secondHandOpacity) * 100) / 100,
326
+ left: Math.round(
327
+ size * (secondHandSize ?? defaultProps.secondHandSize) / 2 - size * 6e-3 * 3 / 2
328
+ )
329
+ }
330
+ })
331
+ }
332
+ )
333
+ ), /* @__PURE__ */ React.createElement(Box, { ...getStyles("centerBlur") }), /* @__PURE__ */ React.createElement(
334
+ Box,
335
+ {
336
+ ...getStyles("centerDot", {
337
+ style: {
338
+ width: centerSize,
339
+ height: centerSize,
340
+ opacity: Math.round((secondHandOpacity ?? defaultProps.secondHandOpacity) * 100) / 100,
341
+ top: Math.round(clockRadius - centerSize / 2),
342
+ left: Math.round(clockRadius - centerSize / 2)
343
+ }
344
+ })
345
+ }
346
+ ))));
347
+ };
348
+ const Clock = factory((_props, ref) => {
349
+ const props = useProps("Clock", defaultProps, _props);
350
+ const [time, setTime] = useState(/* @__PURE__ */ new Date());
351
+ const [hasMounted, setHasMounted] = useState(false);
352
+ const intervalRef = useRef(null);
353
+ const startTimeRef = useRef(null);
354
+ const realStartTimeRef = useRef(null);
355
+ const {
356
+ // Clock-specific props that should not be passed to DOM
357
+ size,
358
+ color,
359
+ hourTicksColor,
360
+ hourTicksOpacity,
361
+ minuteTicksColor,
362
+ minuteTicksOpacity,
363
+ primaryNumbersColor,
364
+ primaryNumbersOpacity,
365
+ secondaryNumbersColor,
366
+ secondaryNumbersOpacity,
367
+ secondHandBehavior,
368
+ secondHandColor,
369
+ secondHandOpacity,
370
+ secondHandLength,
371
+ secondHandSize,
372
+ minuteHandColor,
373
+ minuteHandOpacity,
374
+ minuteHandSize,
375
+ minuteHandLength,
376
+ hourHandColor,
377
+ hourHandOpacity,
378
+ hourHandSize,
379
+ hourHandLength,
380
+ hourTicksOpacity: _hourTicksOpacity,
381
+ minuteTicksOpacity: _minuteTicksOpacity,
382
+ hourNumbersDistance,
383
+ primaryNumbersProps,
384
+ secondaryNumbersProps,
385
+ timezone: timezone2,
386
+ running,
387
+ value,
388
+ // Styles API props
389
+ classNames,
390
+ style,
391
+ styles,
392
+ unstyled,
393
+ vars,
394
+ className,
395
+ ...others
396
+ } = props;
397
+ const getStyles = useStyles({
398
+ name: "Clock",
399
+ props,
400
+ classes,
401
+ className,
402
+ style,
403
+ classNames,
404
+ styles,
405
+ unstyled,
406
+ vars,
407
+ varsResolver
408
+ });
409
+ const effectiveSize = Math.round(
410
+ px(
411
+ getSize(
412
+ typeof size === "string" && size in defaultClockSizes ? defaultClockSizes[size] : size || defaultProps.size,
413
+ "clock-size"
414
+ )
415
+ )
416
+ );
417
+ useEffect(() => {
418
+ setHasMounted(true);
419
+ }, []);
420
+ const getEffectiveTime = () => {
421
+ const parsedValue = parseTimeValue(value);
422
+ if (!running) {
423
+ if (parsedValue) {
424
+ return parsedValue;
425
+ }
426
+ return time;
427
+ }
428
+ if (parsedValue && startTimeRef.current && realStartTimeRef.current) {
429
+ const now = /* @__PURE__ */ new Date();
430
+ const elapsed = now.getTime() - realStartTimeRef.current.getTime();
431
+ return new Date(startTimeRef.current.getTime() + elapsed);
432
+ }
433
+ return time;
434
+ };
435
+ useEffect(() => {
436
+ if (intervalRef.current) {
437
+ clearInterval(intervalRef.current);
438
+ intervalRef.current = null;
439
+ }
440
+ if (!running) {
441
+ return;
442
+ }
443
+ const parsedValue = parseTimeValue(value);
444
+ if (parsedValue) {
445
+ startTimeRef.current = parsedValue;
446
+ realStartTimeRef.current = /* @__PURE__ */ new Date();
447
+ } else {
448
+ startTimeRef.current = null;
449
+ realStartTimeRef.current = null;
450
+ }
451
+ let interval = 1e3;
452
+ switch (secondHandBehavior) {
453
+ case "smooth":
454
+ interval = 16;
455
+ break;
456
+ case "tick-half":
457
+ interval = 500;
458
+ break;
459
+ case "tick-high-freq":
460
+ interval = 125;
461
+ break;
462
+ case "tick":
463
+ default:
464
+ interval = 1e3;
465
+ break;
466
+ }
467
+ const updateTime = () => {
468
+ setTime(/* @__PURE__ */ new Date());
469
+ };
470
+ updateTime();
471
+ intervalRef.current = setInterval(updateTime, interval);
472
+ return () => {
473
+ if (intervalRef.current) {
474
+ clearInterval(intervalRef.current);
475
+ }
476
+ };
477
+ }, [running, value, secondHandBehavior, secondHandOpacity]);
478
+ if (!hasMounted) {
479
+ return /* @__PURE__ */ React.createElement(Box, { ...getStyles("root"), ref, ...others }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("clockContainer") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("glassWrapper") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("clockFace") }, /* @__PURE__ */ React.createElement(Box, { ...getStyles("hourMarks") }, (hourTicksOpacity || 1) !== 0 && Array.from({ length: 12 }, (_, i) => /* @__PURE__ */ React.createElement(
480
+ Box,
481
+ {
482
+ key: `hour-tick-${i}`,
483
+ ...getStyles("hourTick", {
484
+ style: {
485
+ top: Math.round(effectiveSize * 0.028),
486
+ left: "50%",
487
+ transformOrigin: `50% ${Math.round(effectiveSize / 2) - Math.round(effectiveSize * 0.028)}px`,
488
+ transform: `translateX(-50%) rotate(${i * 30}deg)`
489
+ }
490
+ })
491
+ }
492
+ )), (minuteTicksOpacity || 1) !== 0 && Array.from({ length: 60 }, (_, i) => {
493
+ if (i % 5 === 0) {
494
+ return null;
495
+ }
496
+ return /* @__PURE__ */ React.createElement(
497
+ Box,
498
+ {
499
+ key: `minute-tick-${i}`,
500
+ ...getStyles("minuteTick", {
501
+ style: {
502
+ top: Math.round(effectiveSize * 0.028),
503
+ left: "50%",
504
+ transformOrigin: `50% ${Math.round(effectiveSize / 2) - Math.round(effectiveSize * 0.028)}px`,
505
+ transform: `translateX(-50%) rotate(${i * 6}deg)`
506
+ }
507
+ })
508
+ }
509
+ );
510
+ }), (primaryNumbersOpacity || 1) !== 0 && [12, 3, 6, 9].map((num) => {
511
+ const i = num === 12 ? 0 : num;
512
+ const angle = (i * 30 - 90) * (Math.PI / 180);
513
+ const clockRadius = Math.round(effectiveSize / 2);
514
+ const numberRadius = Math.round(clockRadius * (hourNumbersDistance || 0.75));
515
+ const x = Math.round(clockRadius + Math.cos(angle) * numberRadius);
516
+ const y = Math.round(clockRadius + Math.sin(angle) * numberRadius);
517
+ return /* @__PURE__ */ React.createElement(
518
+ Text,
519
+ {
520
+ key: `primary-number-${num}`,
521
+ ...primaryNumbersProps,
522
+ ...getStyles("primaryNumber", {
523
+ className: getStyles("number").className,
524
+ style: {
525
+ left: x,
526
+ top: y
527
+ }
528
+ })
529
+ },
530
+ num
531
+ );
532
+ }), (secondaryNumbersOpacity || 1) !== 0 && [1, 2, 4, 5, 7, 8, 10, 11].map((num) => {
533
+ const i = num;
534
+ const angle = (i * 30 - 90) * (Math.PI / 180);
535
+ const clockRadius = Math.round(effectiveSize / 2);
536
+ const numberRadius = Math.round(clockRadius * (hourNumbersDistance || 0.75));
537
+ const x = Math.round(clockRadius + Math.cos(angle) * numberRadius);
538
+ const y = Math.round(clockRadius + Math.sin(angle) * numberRadius);
539
+ return /* @__PURE__ */ React.createElement(
540
+ Text,
541
+ {
542
+ key: `secondary-number-${num}`,
543
+ ...secondaryNumbersProps,
544
+ ...getStyles("secondaryNumber", {
545
+ className: getStyles("number").className,
546
+ style: {
547
+ left: x,
548
+ top: y
549
+ }
550
+ })
551
+ },
552
+ num
553
+ );
554
+ }))))));
555
+ }
556
+ const effectiveTime = getEffectiveTime();
557
+ return /* @__PURE__ */ React.createElement(Box, { ...getStyles("root"), ref, ...others }, /* @__PURE__ */ React.createElement(
558
+ RealClock,
559
+ {
560
+ time: effectiveTime,
561
+ getStyles,
562
+ effectiveSize,
563
+ ...props
564
+ }
565
+ ));
566
+ });
567
+ Clock.classes = classes;
568
+ Clock.displayName = "Clock";
569
+
570
+ export { Clock, defaultProps };
571
+ //# sourceMappingURL=Clock.mjs.map