svelte-analog-clock 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018–2026 Wojciech Maj
4
+ Copyright (c) 2026 Adhi Ravishankar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # svelte-analog-clock
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An analog clock for your Svelte 5 app. Svelte port of [React-Clock](https://github.com/wojtekmaj/react-clock).
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install svelte-analog-clock
9
+ ```
10
+
11
+ Requires Svelte 5 or later.
12
+
13
+ ## Usage
14
+
15
+ ```svelte
16
+ <script lang="ts">
17
+ import Clock from 'svelte-analog-clock';
18
+ </script>
19
+
20
+ <Clock value={new Date()} />
21
+ ```
22
+
23
+ Styles are imported automatically. Elements use BEM class names starting with `svelte-clock`.
24
+
25
+ ## Props
26
+
27
+ | Prop name | Description | Default value |
28
+ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
29
+ | class | Class name(s) added along with `"svelte-clock"`. | n/a |
30
+ | formatHour | `(locale, hour) => string`, used for numbers rendered with `renderNumbers`. | `Intl.NumberFormat` |
31
+ | hourHandLength, minuteHandLength, secondHandLength | Hand lengths, in percents of the clock radius. | `50`, `70`, `90` |
32
+ | hourHandOppositeLength, minuteHandOppositeLength, secondHandOppositeLength | Lengths of the tails of the hands, in percents of the clock radius. | `10` |
33
+ | hourHandWidth, minuteHandWidth, secondHandWidth | Hand widths, in pixels. | `4`, `2`, `1` |
34
+ | hourMarksLength, minuteMarksLength | Mark lengths, in percents of the clock radius. | `10`, `6` |
35
+ | hourMarksWidth, minuteMarksWidth | Mark widths, in pixels. | `3`, `1` |
36
+ | locale | Locale used to format hour numbers. | User's browser settings |
37
+ | renderHourMarks, renderMinuteMarks | Whether to render marks. | `true` |
38
+ | renderMinuteHand, renderSecondHand | Whether to render hands. | `true` |
39
+ | renderNumbers | Whether to render hour numbers. | `false` |
40
+ | size | Clock size, in pixels. | `150` |
41
+ | useMillisecondPrecision | Whether the second hand moves smoothly. Only applies when `value` is `undefined`. | `false` |
42
+ | value | A `Date` or a time string such as `"22:15:30"`. `undefined` shows the current time and keeps ticking; `null` renders no hands. | current time |
43
+
44
+ ## Part of a monorepo
45
+
46
+ `svelte-analog-clock` is part of the [svelte-datetime-pickers monorepo](https://github.com/adhiravishankar/svelte-datetime-pickers). See the [live demo and Storybook](https://adhiravishankar.github.io/svelte-datetime-pickers/).
47
+
48
+ ## License
49
+
50
+ The MIT License.
package/dist/Clock.css ADDED
@@ -0,0 +1,62 @@
1
+ .svelte-clock {
2
+ display: block;
3
+ position: relative;
4
+ }
5
+
6
+ .svelte-clock,
7
+ .svelte-clock *,
8
+ .svelte-clock *:before,
9
+ .svelte-clock *:after {
10
+ -moz-box-sizing: border-box;
11
+ -webkit-box-sizing: border-box;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .svelte-clock__face {
16
+ position: absolute;
17
+ top: 0;
18
+ bottom: 0;
19
+ left: 0;
20
+ right: 0;
21
+ border: 1px solid black;
22
+ border-radius: 50%;
23
+ }
24
+
25
+ .svelte-clock__hand {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ left: 50%;
30
+ right: 50%;
31
+ }
32
+
33
+ .svelte-clock__hand__body {
34
+ position: absolute;
35
+ background-color: black;
36
+ transform: translateX(-50%);
37
+ }
38
+
39
+ .svelte-clock__second-hand__body {
40
+ background-color: red;
41
+ }
42
+
43
+ .svelte-clock__mark {
44
+ position: absolute;
45
+ top: 0;
46
+ bottom: 0;
47
+ left: 50%;
48
+ right: 50%;
49
+ }
50
+
51
+ .svelte-clock__mark__body {
52
+ position: absolute;
53
+ background-color: black;
54
+ transform: translateX(-50%);
55
+ }
56
+
57
+ .svelte-clock__mark__number {
58
+ position: absolute;
59
+ left: -40px;
60
+ width: 80px;
61
+ text-align: center;
62
+ }
@@ -0,0 +1,329 @@
1
+ <script lang="ts">
2
+ import './Clock.css';
3
+
4
+ import type { ClassValue } from 'svelte/elements';
5
+
6
+ type ClockProps = {
7
+ /**
8
+ * Class name(s) that will be added along with `"svelte-clock"` to the main `<time>` element.
9
+ */
10
+ class?: ClassValue;
11
+ /**
12
+ * Function that formats hour numbers rendered when `renderNumbers` is set. Receives the locale and an hour from 1 to 12.
13
+ */
14
+ formatHour?: (locale: string | undefined, hour: number) => string;
15
+ /**
16
+ * Hour hand length, in percents of the clock radius.
17
+ *
18
+ * @default 50
19
+ */
20
+ hourHandLength?: number;
21
+ /**
22
+ * Hour hand opposite length, in percents of the clock radius.
23
+ *
24
+ * @default 10
25
+ */
26
+ hourHandOppositeLength?: number;
27
+ /**
28
+ * Hour hand width, in pixels.
29
+ *
30
+ * @default 4
31
+ */
32
+ hourHandWidth?: number;
33
+ /**
34
+ * Hour marks length, in percents of the clock radius.
35
+ *
36
+ * @default 10
37
+ */
38
+ hourMarksLength?: number;
39
+ /**
40
+ * Hour marks width, in pixels.
41
+ *
42
+ * @default 3
43
+ */
44
+ hourMarksWidth?: number;
45
+ /**
46
+ * Locale used to format hour numbers.
47
+ *
48
+ * @example 'hu-HU'
49
+ */
50
+ locale?: string;
51
+ /**
52
+ * Minute hand length, in percents of the clock radius.
53
+ *
54
+ * @default 70
55
+ */
56
+ minuteHandLength?: number;
57
+ /**
58
+ * Minute hand opposite length, in percents of the clock radius.
59
+ *
60
+ * @default 10
61
+ */
62
+ minuteHandOppositeLength?: number;
63
+ /**
64
+ * Minute hand width, in pixels.
65
+ *
66
+ * @default 2
67
+ */
68
+ minuteHandWidth?: number;
69
+ /**
70
+ * Minute marks length, in percents of the clock radius.
71
+ *
72
+ * @default 6
73
+ */
74
+ minuteMarksLength?: number;
75
+ /**
76
+ * Minute marks width, in pixels.
77
+ *
78
+ * @default 1
79
+ */
80
+ minuteMarksWidth?: number;
81
+ /**
82
+ * Whether hour marks should be rendered.
83
+ *
84
+ * @default true
85
+ */
86
+ renderHourMarks?: boolean;
87
+ /**
88
+ * Whether the minute hand should be rendered.
89
+ *
90
+ * @default true
91
+ */
92
+ renderMinuteHand?: boolean;
93
+ /**
94
+ * Whether minute marks should be rendered.
95
+ *
96
+ * @default true
97
+ */
98
+ renderMinuteMarks?: boolean;
99
+ /**
100
+ * Whether hour numbers should be rendered.
101
+ *
102
+ * @default false
103
+ */
104
+ renderNumbers?: boolean;
105
+ /**
106
+ * Whether the second hand should be rendered.
107
+ *
108
+ * @default true
109
+ */
110
+ renderSecondHand?: boolean;
111
+ /**
112
+ * Second hand length, in percents of the clock radius.
113
+ *
114
+ * @default 90
115
+ */
116
+ secondHandLength?: number;
117
+ /**
118
+ * Second hand opposite length, in percents of the clock radius.
119
+ *
120
+ * @default 10
121
+ */
122
+ secondHandOppositeLength?: number;
123
+ /**
124
+ * Second hand width, in pixels.
125
+ *
126
+ * @default 1
127
+ */
128
+ secondHandWidth?: number;
129
+ /**
130
+ * Clock size, in pixels.
131
+ *
132
+ * @default 150
133
+ */
134
+ size?: number;
135
+ /**
136
+ * Whether the second hand should move smoothly. Only applies when `value` is `undefined`.
137
+ *
138
+ * @default false
139
+ */
140
+ useMillisecondPrecision?: boolean;
141
+ /**
142
+ * Time to display. When `undefined`, the clock shows the current time and keeps ticking. When `null`, no hands are rendered.
143
+ *
144
+ * @example new Date()
145
+ * @example '22:15:00'
146
+ */
147
+ value?: Date | string | null;
148
+ };
149
+
150
+ let {
151
+ class: className,
152
+ formatHour = (locale, hour) => new Intl.NumberFormat(locale).format(hour),
153
+ hourHandLength = 50,
154
+ hourHandOppositeLength = 10,
155
+ hourHandWidth = 4,
156
+ hourMarksLength = 10,
157
+ hourMarksWidth = 3,
158
+ locale,
159
+ minuteHandLength = 70,
160
+ minuteHandOppositeLength = 10,
161
+ minuteHandWidth = 2,
162
+ minuteMarksLength = 6,
163
+ minuteMarksWidth = 1,
164
+ renderHourMarks = true,
165
+ renderMinuteHand = true,
166
+ renderMinuteMarks = true,
167
+ renderNumbers = false,
168
+ renderSecondHand = true,
169
+ secondHandLength = 90,
170
+ secondHandOppositeLength = 10,
171
+ secondHandWidth = 1,
172
+ size = 150,
173
+ useMillisecondPrecision = false,
174
+ value,
175
+ }: ClockProps = $props();
176
+
177
+ const hourMarks = Array.from({ length: 12 }, (_, index) => index);
178
+ const minuteMarks = Array.from({ length: 60 }, (_, index) => index).filter(
179
+ (index) => index % 5 !== 0,
180
+ );
181
+
182
+ let now = $state(new Date());
183
+
184
+ $effect(() => {
185
+ if (value !== undefined) {
186
+ return;
187
+ }
188
+
189
+ now = new Date();
190
+
191
+ if (useMillisecondPrecision) {
192
+ let frame = requestAnimationFrame(function tick() {
193
+ now = new Date();
194
+ frame = requestAnimationFrame(tick);
195
+ });
196
+
197
+ return () => cancelAnimationFrame(frame);
198
+ }
199
+
200
+ const interval = setInterval(() => {
201
+ now = new Date();
202
+ }, 1000);
203
+
204
+ return () => clearInterval(interval);
205
+ });
206
+
207
+ function parseTime(input: Date | string): Date | null {
208
+ if (input instanceof Date) {
209
+ return Number.isNaN(input.getTime()) ? null : input;
210
+ }
211
+
212
+ // Time-only strings, e.g. "22:15" or "22:15:30"
213
+ const match = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(input);
214
+
215
+ if (match) {
216
+ const date = new Date();
217
+ date.setHours(
218
+ Number(match[1]),
219
+ Number(match[2]),
220
+ Number(match[3] ?? 0),
221
+ Number((match[4] ?? '0').padEnd(3, '0').slice(0, 3)),
222
+ );
223
+ return date;
224
+ }
225
+
226
+ const date = new Date(input);
227
+ return Number.isNaN(date.getTime()) ? null : date;
228
+ }
229
+
230
+ const date = $derived(value === undefined ? now : value === null ? null : parseTime(value));
231
+
232
+ const angles = $derived.by(() => {
233
+ if (!date) {
234
+ return null;
235
+ }
236
+
237
+ const hours = date.getHours() % 12;
238
+ const minutes = date.getMinutes();
239
+ const seconds = date.getSeconds();
240
+ const milliseconds = useMillisecondPrecision ? date.getMilliseconds() : 0;
241
+
242
+ return {
243
+ hour: hours * 30 + minutes * 0.5 + seconds / 120,
244
+ minute: minutes * 6 + seconds * 0.1,
245
+ second: seconds * 6 + milliseconds * 0.006,
246
+ };
247
+ });
248
+
249
+ const datetime = $derived(date ? date.toISOString() : undefined);
250
+ </script>
251
+
252
+ {#snippet hand(
253
+ name: string,
254
+ angle: number,
255
+ length: number,
256
+ oppositeLength: number,
257
+ width: number,
258
+ )}
259
+ <div class="svelte-clock__hand svelte-clock__{name}-hand" style:transform="rotate({angle}deg)">
260
+ <div
261
+ class="svelte-clock__hand__body svelte-clock__{name}-hand__body"
262
+ style:width="{width}px"
263
+ style:top="{50 - length / 2}%"
264
+ style:bottom="{50 - oppositeLength / 2}%"
265
+ ></div>
266
+ </div>
267
+ {/snippet}
268
+
269
+ <time class={['svelte-clock', className]} {datetime} style:width="{size}px" style:height="{size}px">
270
+ <div class="svelte-clock__face">
271
+ {#if renderMinuteMarks}
272
+ {#each minuteMarks as index (index)}
273
+ <div class="svelte-clock__mark svelte-clock__minute-mark" style:transform="rotate({index * 6}deg)">
274
+ <div
275
+ class="svelte-clock__mark__body svelte-clock__minute-mark__body"
276
+ style:width="{minuteMarksWidth}px"
277
+ style:bottom="{100 - minuteMarksLength / 2}%"
278
+ ></div>
279
+ </div>
280
+ {/each}
281
+ {/if}
282
+ {#if renderHourMarks}
283
+ {#each hourMarks as index (index)}
284
+ <div class="svelte-clock__mark svelte-clock__hour-mark" style:transform="rotate({index * 30}deg)">
285
+ <div
286
+ class="svelte-clock__mark__body svelte-clock__hour-mark__body"
287
+ style:width="{hourMarksWidth}px"
288
+ style:bottom="{100 - hourMarksLength / 2}%"
289
+ ></div>
290
+ </div>
291
+ {/each}
292
+ {/if}
293
+ {#if renderNumbers}
294
+ {#each hourMarks as index (index)}
295
+ {@const hourNumber = index === 0 ? 12 : index}
296
+ <div class="svelte-clock__mark" style:transform="rotate({index * 30}deg)">
297
+ <div
298
+ class="svelte-clock__mark__number"
299
+ style:top="{hourMarksLength}%"
300
+ style:transform="rotate({-index * 30}deg)"
301
+ >
302
+ {formatHour(locale, hourNumber)}
303
+ </div>
304
+ </div>
305
+ {/each}
306
+ {/if}
307
+ </div>
308
+ {#if angles}
309
+ {@render hand('hour', angles.hour, hourHandLength, hourHandOppositeLength, hourHandWidth)}
310
+ {#if renderMinuteHand}
311
+ {@render hand(
312
+ 'minute',
313
+ angles.minute,
314
+ minuteHandLength,
315
+ minuteHandOppositeLength,
316
+ minuteHandWidth,
317
+ )}
318
+ {/if}
319
+ {#if renderSecondHand}
320
+ {@render hand(
321
+ 'second',
322
+ angles.second,
323
+ secondHandLength,
324
+ secondHandOppositeLength,
325
+ secondHandWidth,
326
+ )}
327
+ {/if}
328
+ {/if}
329
+ </time>
@@ -0,0 +1,148 @@
1
+ import './Clock.css';
2
+ import type { ClassValue } from 'svelte/elements';
3
+ type ClockProps = {
4
+ /**
5
+ * Class name(s) that will be added along with `"svelte-clock"` to the main `<time>` element.
6
+ */
7
+ class?: ClassValue;
8
+ /**
9
+ * Function that formats hour numbers rendered when `renderNumbers` is set. Receives the locale and an hour from 1 to 12.
10
+ */
11
+ formatHour?: (locale: string | undefined, hour: number) => string;
12
+ /**
13
+ * Hour hand length, in percents of the clock radius.
14
+ *
15
+ * @default 50
16
+ */
17
+ hourHandLength?: number;
18
+ /**
19
+ * Hour hand opposite length, in percents of the clock radius.
20
+ *
21
+ * @default 10
22
+ */
23
+ hourHandOppositeLength?: number;
24
+ /**
25
+ * Hour hand width, in pixels.
26
+ *
27
+ * @default 4
28
+ */
29
+ hourHandWidth?: number;
30
+ /**
31
+ * Hour marks length, in percents of the clock radius.
32
+ *
33
+ * @default 10
34
+ */
35
+ hourMarksLength?: number;
36
+ /**
37
+ * Hour marks width, in pixels.
38
+ *
39
+ * @default 3
40
+ */
41
+ hourMarksWidth?: number;
42
+ /**
43
+ * Locale used to format hour numbers.
44
+ *
45
+ * @example 'hu-HU'
46
+ */
47
+ locale?: string;
48
+ /**
49
+ * Minute hand length, in percents of the clock radius.
50
+ *
51
+ * @default 70
52
+ */
53
+ minuteHandLength?: number;
54
+ /**
55
+ * Minute hand opposite length, in percents of the clock radius.
56
+ *
57
+ * @default 10
58
+ */
59
+ minuteHandOppositeLength?: number;
60
+ /**
61
+ * Minute hand width, in pixels.
62
+ *
63
+ * @default 2
64
+ */
65
+ minuteHandWidth?: number;
66
+ /**
67
+ * Minute marks length, in percents of the clock radius.
68
+ *
69
+ * @default 6
70
+ */
71
+ minuteMarksLength?: number;
72
+ /**
73
+ * Minute marks width, in pixels.
74
+ *
75
+ * @default 1
76
+ */
77
+ minuteMarksWidth?: number;
78
+ /**
79
+ * Whether hour marks should be rendered.
80
+ *
81
+ * @default true
82
+ */
83
+ renderHourMarks?: boolean;
84
+ /**
85
+ * Whether the minute hand should be rendered.
86
+ *
87
+ * @default true
88
+ */
89
+ renderMinuteHand?: boolean;
90
+ /**
91
+ * Whether minute marks should be rendered.
92
+ *
93
+ * @default true
94
+ */
95
+ renderMinuteMarks?: boolean;
96
+ /**
97
+ * Whether hour numbers should be rendered.
98
+ *
99
+ * @default false
100
+ */
101
+ renderNumbers?: boolean;
102
+ /**
103
+ * Whether the second hand should be rendered.
104
+ *
105
+ * @default true
106
+ */
107
+ renderSecondHand?: boolean;
108
+ /**
109
+ * Second hand length, in percents of the clock radius.
110
+ *
111
+ * @default 90
112
+ */
113
+ secondHandLength?: number;
114
+ /**
115
+ * Second hand opposite length, in percents of the clock radius.
116
+ *
117
+ * @default 10
118
+ */
119
+ secondHandOppositeLength?: number;
120
+ /**
121
+ * Second hand width, in pixels.
122
+ *
123
+ * @default 1
124
+ */
125
+ secondHandWidth?: number;
126
+ /**
127
+ * Clock size, in pixels.
128
+ *
129
+ * @default 150
130
+ */
131
+ size?: number;
132
+ /**
133
+ * Whether the second hand should move smoothly. Only applies when `value` is `undefined`.
134
+ *
135
+ * @default false
136
+ */
137
+ useMillisecondPrecision?: boolean;
138
+ /**
139
+ * Time to display. When `undefined`, the clock shows the current time and keeps ticking. When `null`, no hands are rendered.
140
+ *
141
+ * @example new Date()
142
+ * @example '22:15:00'
143
+ */
144
+ value?: Date | string | null;
145
+ };
146
+ declare const Clock: import("svelte").Component<ClockProps, {}, "">;
147
+ type Clock = ReturnType<typeof Clock>;
148
+ export default Clock;
@@ -0,0 +1,5 @@
1
+ import Clock from './Clock.svelte';
2
+ import type { ComponentProps } from 'svelte';
3
+ export type ClockProps = ComponentProps<typeof Clock>;
4
+ export { Clock };
5
+ export default Clock;
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Clock from './Clock.svelte';
2
+ export { Clock };
3
+ export default Clock;
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "svelte-analog-clock",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "An analog clock for your Svelte app.",
5
+ "keywords": [
6
+ "analog-clock",
7
+ "clock",
8
+ "svelte",
9
+ "time"
10
+ ],
11
+ "license": "MIT",
12
+ "author": {
13
+ "name": "Adhi Ravishankar",
14
+ "email": "adhiravishankar@gmail.com"
15
+ },
16
+ "files": [
17
+ "dist/**/*",
18
+ "src/**/*",
19
+ "!**/*.spec.*"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": [
23
+ "*.css",
24
+ "*.svelte"
25
+ ],
26
+ "types": "./dist/index.d.ts",
27
+ "svelte": "./dist/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "svelte": "./dist/index.js",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./dist/*.css": "./dist/*.css"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "scripts": {
41
+ "build": "svelte-package --input src --output dist",
42
+ "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
43
+ "format": "vp fmt --check",
44
+ "lint": "vp lint",
45
+ "prepack": "npm run clean && npm run build",
46
+ "test": "npm run lint && npm run tsc && npm run format && npm run unit",
47
+ "tsc": "svelte-check --tsconfig ./tsconfig.json",
48
+ "unit": "vp test",
49
+ "watch": "svelte-package --input src --output dist --watch"
50
+ },
51
+ "devDependencies": {
52
+ "@sveltejs/package": "^3.0.1",
53
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
54
+ "@vitest/browser-playwright": "5.0.5",
55
+ "playwright": "^1.64.1",
56
+ "svelte": "^5.57.2",
57
+ "svelte-check": "^4.7.7",
58
+ "typescript": "^6.0.3",
59
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
60
+ "vite-plus": "1.4.0",
61
+ "vitest": "5.0.5",
62
+ "vitest-browser-svelte": "^3.1.0"
63
+ },
64
+ "peerDependencies": {
65
+ "svelte": "^5.0.0"
66
+ }
67
+ }
package/src/Clock.css ADDED
@@ -0,0 +1,62 @@
1
+ .svelte-clock {
2
+ display: block;
3
+ position: relative;
4
+ }
5
+
6
+ .svelte-clock,
7
+ .svelte-clock *,
8
+ .svelte-clock *:before,
9
+ .svelte-clock *:after {
10
+ -moz-box-sizing: border-box;
11
+ -webkit-box-sizing: border-box;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .svelte-clock__face {
16
+ position: absolute;
17
+ top: 0;
18
+ bottom: 0;
19
+ left: 0;
20
+ right: 0;
21
+ border: 1px solid black;
22
+ border-radius: 50%;
23
+ }
24
+
25
+ .svelte-clock__hand {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ left: 50%;
30
+ right: 50%;
31
+ }
32
+
33
+ .svelte-clock__hand__body {
34
+ position: absolute;
35
+ background-color: black;
36
+ transform: translateX(-50%);
37
+ }
38
+
39
+ .svelte-clock__second-hand__body {
40
+ background-color: red;
41
+ }
42
+
43
+ .svelte-clock__mark {
44
+ position: absolute;
45
+ top: 0;
46
+ bottom: 0;
47
+ left: 50%;
48
+ right: 50%;
49
+ }
50
+
51
+ .svelte-clock__mark__body {
52
+ position: absolute;
53
+ background-color: black;
54
+ transform: translateX(-50%);
55
+ }
56
+
57
+ .svelte-clock__mark__number {
58
+ position: absolute;
59
+ left: -40px;
60
+ width: 80px;
61
+ text-align: center;
62
+ }
@@ -0,0 +1,329 @@
1
+ <script lang="ts">
2
+ import './Clock.css';
3
+
4
+ import type { ClassValue } from 'svelte/elements';
5
+
6
+ type ClockProps = {
7
+ /**
8
+ * Class name(s) that will be added along with `"svelte-clock"` to the main `<time>` element.
9
+ */
10
+ class?: ClassValue;
11
+ /**
12
+ * Function that formats hour numbers rendered when `renderNumbers` is set. Receives the locale and an hour from 1 to 12.
13
+ */
14
+ formatHour?: (locale: string | undefined, hour: number) => string;
15
+ /**
16
+ * Hour hand length, in percents of the clock radius.
17
+ *
18
+ * @default 50
19
+ */
20
+ hourHandLength?: number;
21
+ /**
22
+ * Hour hand opposite length, in percents of the clock radius.
23
+ *
24
+ * @default 10
25
+ */
26
+ hourHandOppositeLength?: number;
27
+ /**
28
+ * Hour hand width, in pixels.
29
+ *
30
+ * @default 4
31
+ */
32
+ hourHandWidth?: number;
33
+ /**
34
+ * Hour marks length, in percents of the clock radius.
35
+ *
36
+ * @default 10
37
+ */
38
+ hourMarksLength?: number;
39
+ /**
40
+ * Hour marks width, in pixels.
41
+ *
42
+ * @default 3
43
+ */
44
+ hourMarksWidth?: number;
45
+ /**
46
+ * Locale used to format hour numbers.
47
+ *
48
+ * @example 'hu-HU'
49
+ */
50
+ locale?: string;
51
+ /**
52
+ * Minute hand length, in percents of the clock radius.
53
+ *
54
+ * @default 70
55
+ */
56
+ minuteHandLength?: number;
57
+ /**
58
+ * Minute hand opposite length, in percents of the clock radius.
59
+ *
60
+ * @default 10
61
+ */
62
+ minuteHandOppositeLength?: number;
63
+ /**
64
+ * Minute hand width, in pixels.
65
+ *
66
+ * @default 2
67
+ */
68
+ minuteHandWidth?: number;
69
+ /**
70
+ * Minute marks length, in percents of the clock radius.
71
+ *
72
+ * @default 6
73
+ */
74
+ minuteMarksLength?: number;
75
+ /**
76
+ * Minute marks width, in pixels.
77
+ *
78
+ * @default 1
79
+ */
80
+ minuteMarksWidth?: number;
81
+ /**
82
+ * Whether hour marks should be rendered.
83
+ *
84
+ * @default true
85
+ */
86
+ renderHourMarks?: boolean;
87
+ /**
88
+ * Whether the minute hand should be rendered.
89
+ *
90
+ * @default true
91
+ */
92
+ renderMinuteHand?: boolean;
93
+ /**
94
+ * Whether minute marks should be rendered.
95
+ *
96
+ * @default true
97
+ */
98
+ renderMinuteMarks?: boolean;
99
+ /**
100
+ * Whether hour numbers should be rendered.
101
+ *
102
+ * @default false
103
+ */
104
+ renderNumbers?: boolean;
105
+ /**
106
+ * Whether the second hand should be rendered.
107
+ *
108
+ * @default true
109
+ */
110
+ renderSecondHand?: boolean;
111
+ /**
112
+ * Second hand length, in percents of the clock radius.
113
+ *
114
+ * @default 90
115
+ */
116
+ secondHandLength?: number;
117
+ /**
118
+ * Second hand opposite length, in percents of the clock radius.
119
+ *
120
+ * @default 10
121
+ */
122
+ secondHandOppositeLength?: number;
123
+ /**
124
+ * Second hand width, in pixels.
125
+ *
126
+ * @default 1
127
+ */
128
+ secondHandWidth?: number;
129
+ /**
130
+ * Clock size, in pixels.
131
+ *
132
+ * @default 150
133
+ */
134
+ size?: number;
135
+ /**
136
+ * Whether the second hand should move smoothly. Only applies when `value` is `undefined`.
137
+ *
138
+ * @default false
139
+ */
140
+ useMillisecondPrecision?: boolean;
141
+ /**
142
+ * Time to display. When `undefined`, the clock shows the current time and keeps ticking. When `null`, no hands are rendered.
143
+ *
144
+ * @example new Date()
145
+ * @example '22:15:00'
146
+ */
147
+ value?: Date | string | null;
148
+ };
149
+
150
+ let {
151
+ class: className,
152
+ formatHour = (locale, hour) => new Intl.NumberFormat(locale).format(hour),
153
+ hourHandLength = 50,
154
+ hourHandOppositeLength = 10,
155
+ hourHandWidth = 4,
156
+ hourMarksLength = 10,
157
+ hourMarksWidth = 3,
158
+ locale,
159
+ minuteHandLength = 70,
160
+ minuteHandOppositeLength = 10,
161
+ minuteHandWidth = 2,
162
+ minuteMarksLength = 6,
163
+ minuteMarksWidth = 1,
164
+ renderHourMarks = true,
165
+ renderMinuteHand = true,
166
+ renderMinuteMarks = true,
167
+ renderNumbers = false,
168
+ renderSecondHand = true,
169
+ secondHandLength = 90,
170
+ secondHandOppositeLength = 10,
171
+ secondHandWidth = 1,
172
+ size = 150,
173
+ useMillisecondPrecision = false,
174
+ value,
175
+ }: ClockProps = $props();
176
+
177
+ const hourMarks = Array.from({ length: 12 }, (_, index) => index);
178
+ const minuteMarks = Array.from({ length: 60 }, (_, index) => index).filter(
179
+ (index) => index % 5 !== 0,
180
+ );
181
+
182
+ let now = $state(new Date());
183
+
184
+ $effect(() => {
185
+ if (value !== undefined) {
186
+ return;
187
+ }
188
+
189
+ now = new Date();
190
+
191
+ if (useMillisecondPrecision) {
192
+ let frame = requestAnimationFrame(function tick() {
193
+ now = new Date();
194
+ frame = requestAnimationFrame(tick);
195
+ });
196
+
197
+ return () => cancelAnimationFrame(frame);
198
+ }
199
+
200
+ const interval = setInterval(() => {
201
+ now = new Date();
202
+ }, 1000);
203
+
204
+ return () => clearInterval(interval);
205
+ });
206
+
207
+ function parseTime(input: Date | string): Date | null {
208
+ if (input instanceof Date) {
209
+ return Number.isNaN(input.getTime()) ? null : input;
210
+ }
211
+
212
+ // Time-only strings, e.g. "22:15" or "22:15:30"
213
+ const match = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(input);
214
+
215
+ if (match) {
216
+ const date = new Date();
217
+ date.setHours(
218
+ Number(match[1]),
219
+ Number(match[2]),
220
+ Number(match[3] ?? 0),
221
+ Number((match[4] ?? '0').padEnd(3, '0').slice(0, 3)),
222
+ );
223
+ return date;
224
+ }
225
+
226
+ const date = new Date(input);
227
+ return Number.isNaN(date.getTime()) ? null : date;
228
+ }
229
+
230
+ const date = $derived(value === undefined ? now : value === null ? null : parseTime(value));
231
+
232
+ const angles = $derived.by(() => {
233
+ if (!date) {
234
+ return null;
235
+ }
236
+
237
+ const hours = date.getHours() % 12;
238
+ const minutes = date.getMinutes();
239
+ const seconds = date.getSeconds();
240
+ const milliseconds = useMillisecondPrecision ? date.getMilliseconds() : 0;
241
+
242
+ return {
243
+ hour: hours * 30 + minutes * 0.5 + seconds / 120,
244
+ minute: minutes * 6 + seconds * 0.1,
245
+ second: seconds * 6 + milliseconds * 0.006,
246
+ };
247
+ });
248
+
249
+ const datetime = $derived(date ? date.toISOString() : undefined);
250
+ </script>
251
+
252
+ {#snippet hand(
253
+ name: string,
254
+ angle: number,
255
+ length: number,
256
+ oppositeLength: number,
257
+ width: number,
258
+ )}
259
+ <div class="svelte-clock__hand svelte-clock__{name}-hand" style:transform="rotate({angle}deg)">
260
+ <div
261
+ class="svelte-clock__hand__body svelte-clock__{name}-hand__body"
262
+ style:width="{width}px"
263
+ style:top="{50 - length / 2}%"
264
+ style:bottom="{50 - oppositeLength / 2}%"
265
+ ></div>
266
+ </div>
267
+ {/snippet}
268
+
269
+ <time class={['svelte-clock', className]} {datetime} style:width="{size}px" style:height="{size}px">
270
+ <div class="svelte-clock__face">
271
+ {#if renderMinuteMarks}
272
+ {#each minuteMarks as index (index)}
273
+ <div class="svelte-clock__mark svelte-clock__minute-mark" style:transform="rotate({index * 6}deg)">
274
+ <div
275
+ class="svelte-clock__mark__body svelte-clock__minute-mark__body"
276
+ style:width="{minuteMarksWidth}px"
277
+ style:bottom="{100 - minuteMarksLength / 2}%"
278
+ ></div>
279
+ </div>
280
+ {/each}
281
+ {/if}
282
+ {#if renderHourMarks}
283
+ {#each hourMarks as index (index)}
284
+ <div class="svelte-clock__mark svelte-clock__hour-mark" style:transform="rotate({index * 30}deg)">
285
+ <div
286
+ class="svelte-clock__mark__body svelte-clock__hour-mark__body"
287
+ style:width="{hourMarksWidth}px"
288
+ style:bottom="{100 - hourMarksLength / 2}%"
289
+ ></div>
290
+ </div>
291
+ {/each}
292
+ {/if}
293
+ {#if renderNumbers}
294
+ {#each hourMarks as index (index)}
295
+ {@const hourNumber = index === 0 ? 12 : index}
296
+ <div class="svelte-clock__mark" style:transform="rotate({index * 30}deg)">
297
+ <div
298
+ class="svelte-clock__mark__number"
299
+ style:top="{hourMarksLength}%"
300
+ style:transform="rotate({-index * 30}deg)"
301
+ >
302
+ {formatHour(locale, hourNumber)}
303
+ </div>
304
+ </div>
305
+ {/each}
306
+ {/if}
307
+ </div>
308
+ {#if angles}
309
+ {@render hand('hour', angles.hour, hourHandLength, hourHandOppositeLength, hourHandWidth)}
310
+ {#if renderMinuteHand}
311
+ {@render hand(
312
+ 'minute',
313
+ angles.minute,
314
+ minuteHandLength,
315
+ minuteHandOppositeLength,
316
+ minuteHandWidth,
317
+ )}
318
+ {/if}
319
+ {#if renderSecondHand}
320
+ {@render hand(
321
+ 'second',
322
+ angles.second,
323
+ secondHandLength,
324
+ secondHandOppositeLength,
325
+ secondHandWidth,
326
+ )}
327
+ {/if}
328
+ {/if}
329
+ </time>
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import Clock from './Clock.svelte';
2
+
3
+ import type { ComponentProps } from 'svelte';
4
+
5
+ export type ClockProps = ComponentProps<typeof Clock>;
6
+ export { Clock };
7
+
8
+ export default Clock;