svelte-comp 1.3.5 → 1.3.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.
Files changed (46) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -101
  3. package/dist/App.svelte +1046 -1046
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -234
  6. package/dist/app.d.ts +10 -10
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -185
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/CheckBox.svelte +210 -210
  14. package/dist/lib/CodeView.svelte +308 -308
  15. package/dist/lib/ColorPicker.svelte +159 -159
  16. package/dist/lib/ContextMenu.svelte +328 -328
  17. package/dist/lib/DatePicker.svelte +246 -246
  18. package/dist/lib/Dialog.svelte +233 -233
  19. package/dist/lib/Field.svelte +299 -299
  20. package/dist/lib/FilePicker.svelte +295 -295
  21. package/dist/lib/Form.svelte +438 -438
  22. package/dist/lib/Hamburger.svelte +217 -217
  23. package/dist/lib/InstallPWA.svelte +94 -94
  24. package/dist/lib/Menu.svelte +623 -623
  25. package/dist/lib/NoticeBase.svelte +140 -140
  26. package/dist/lib/PaginatedCard.svelte +73 -73
  27. package/dist/lib/Pagination.svelte +119 -119
  28. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  29. package/dist/lib/ProgressBar.svelte +141 -141
  30. package/dist/lib/ProgressCircle.svelte +190 -190
  31. package/dist/lib/Radio.svelte +189 -189
  32. package/dist/lib/SearchInput.svelte +104 -104
  33. package/dist/lib/Select.svelte +524 -524
  34. package/dist/lib/Slider.svelte +253 -253
  35. package/dist/lib/Splitter.svelte +159 -159
  36. package/dist/lib/Switch.svelte +168 -168
  37. package/dist/lib/Table.svelte +299 -299
  38. package/dist/lib/Tabs.svelte +213 -213
  39. package/dist/lib/ThemeToggle.svelte +128 -128
  40. package/dist/lib/TimePicker.svelte +312 -312
  41. package/dist/lib/TimePickerNew.svelte +634 -634
  42. package/dist/lib/Toast.svelte +123 -123
  43. package/dist/lib/Tooltip.svelte +110 -110
  44. package/dist/lib/Topbar.svelte +112 -112
  45. package/dist/styles.css +234 -234
  46. package/package.json +52 -52
@@ -1,384 +1,384 @@
1
- <!-- src/lib/Calendar.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component Calendar
5
- * @description Monthly calendar grid with navigation and date selection.
6
- *
7
- * @prop value {string | null} - Selected date in ISO `YYYY-MM-DD` (bindable)
8
- * @default null
9
- *
10
- * @prop min {string} - Minimum selectable date (ISO `YYYY-MM-DD`)
11
- *
12
- * @prop max {string} - Maximum selectable date (ISO `YYYY-MM-DD`)
13
- *
14
- * @prop locale {string} - Locale used for month/day labels
15
- * @default "en-US"
16
- *
17
- * @prop weekStartsOn {0|1|2|3|4|5|6} - First day of week (0=Sun ... 6=Sat)
18
- * @default 1
19
- *
20
- * @prop showOutsideDays {boolean} - Render days from adjacent months
21
- * @default true
22
- *
23
- * @prop disabled {boolean} - Disables selection and navigation
24
- * @default false
25
- *
26
- * @prop onChange {(value: string | null) => void} - Fired on date selection
27
- *
28
- * @prop class {string} - Additional classes for the root wrapper
29
- * @default ""
30
- */
31
- import type { HTMLAttributes } from "svelte/elements";
32
- import { cx } from "../utils";
33
- import { TEXT } from "./types";
34
-
35
- type Props = HTMLAttributes<HTMLDivElement> & {
36
- value?: string | null;
37
- min?: string;
38
- max?: string;
39
- locale?: string;
40
- weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
41
- showOutsideDays?: boolean;
42
- disabled?: boolean;
43
- onChange?: (value: string | null) => void;
44
- class?: string;
45
- };
46
-
47
- let {
48
- value = $bindable<string | null>(null),
49
- min = "1926-01-01",
50
- max,
51
- locale = "en-US",
52
- weekStartsOn = 1,
53
- showOutsideDays = true,
54
- disabled = false,
55
- onChange,
56
- class: externalClass = "",
57
- ...rest
58
- }: Props = $props();
59
-
60
- type ViewMode = "days" | "months" | "years";
61
-
62
- function startOfDay(date: Date) {
63
- return new Date(date.getFullYear(), date.getMonth(), date.getDate());
64
- }
65
-
66
- function parseIso(value: string | null | undefined): Date | null {
67
- if (!value) return null;
68
- const [y, m, d] = value.split("-").map((part) => Number(part));
69
- if (!y || !m || !d) return null;
70
- const date = new Date(y, m - 1, d);
71
- return Number.isNaN(date.getTime()) ? null : date;
72
- }
73
-
74
- function toIso(date: Date) {
75
- const y = date.getFullYear();
76
- const m = String(date.getMonth() + 1).padStart(2, "0");
77
- const d = String(date.getDate()).padStart(2, "0");
78
- return `${y}-${m}-${d}`;
79
- }
80
-
81
- function isSameDay(a: Date, b: Date) {
82
- return (
83
- a.getFullYear() === b.getFullYear() &&
84
- a.getMonth() === b.getMonth() &&
85
- a.getDate() === b.getDate()
86
- );
87
- }
88
-
89
- let internalValue = $state<string | null>(value ?? null);
90
- let lastValue = $state<string | null>(value ?? null);
91
- let viewDate = $state(startOfDay(new Date()));
92
- let viewMode = $state<ViewMode>("days");
93
-
94
- const selectedDate = $derived(parseIso(internalValue));
95
- const minDate = $derived(parseIso(min));
96
- const maxDate = $derived(parseIso(max));
97
- const today = $derived(startOfDay(new Date()));
98
-
99
- $effect(() => {
100
- const next = value ?? null;
101
- if (next !== lastValue) {
102
- lastValue = next;
103
- internalValue = next;
104
- }
105
- });
106
-
107
- $effect(() => {
108
- const selected = parseIso(internalValue);
109
- if (!selected) return;
110
- viewDate = new Date(selected.getFullYear(), selected.getMonth(), 1);
111
- });
112
-
113
- const monthLabel = $derived(
114
- new Intl.DateTimeFormat(locale, { month: "long" }).format(viewDate)
115
- );
116
-
117
- const yearLabel = $derived(String(viewDate.getFullYear()));
118
-
119
- const weekdayLabels = $derived.by(() => {
120
- const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
121
- const labels: string[] = [];
122
- for (let i = 0; i < 7; i += 1) {
123
- const offset = (weekStartsOn + i) % 7;
124
- const date = new Date(2023, 0, 1 + offset);
125
- labels.push(formatter.format(date));
126
- }
127
- return labels;
128
- });
129
-
130
- const dayLabelFormatter = $derived.by(
131
- () =>
132
- new Intl.DateTimeFormat(locale, {
133
- weekday: "long",
134
- year: "numeric",
135
- month: "long",
136
- day: "numeric",
137
- })
138
- );
139
-
140
- const monthOptions = $derived.by(() => {
141
- const formatter = new Intl.DateTimeFormat(locale, { month: "short" });
142
- return Array.from({ length: 12 }, (_, idx) => ({
143
- index: idx,
144
- label: formatter.format(new Date(2024, idx, 1)),
145
- }));
146
- });
147
-
148
- const yearRangeStart = $derived(() => {
149
- const year = viewDate.getFullYear();
150
- return year - (year % 12);
151
- });
152
-
153
- const yearOptions = $derived.by(() =>
154
- Array.from({ length: 12 }, (_, idx) => yearRangeStart() + idx)
155
- );
156
-
157
- type DayCell = {
158
- date: Date;
159
- iso: string;
160
- inMonth: boolean;
161
- isToday: boolean;
162
- isSelected: boolean;
163
- isDisabled: boolean;
164
- };
165
-
166
- const days = $derived.by(() => {
167
- const year = viewDate.getFullYear();
168
- const month = viewDate.getMonth();
169
- const first = new Date(year, month, 1);
170
- const startOffset = (first.getDay() - weekStartsOn + 7) % 7;
171
- const cells: DayCell[] = [];
172
- for (let i = 0; i < 42; i += 1) {
173
- const date = new Date(year, month, 1 - startOffset + i);
174
- const day = startOfDay(date);
175
- const inMonth = day.getMonth() === month;
176
- const isToday = isSameDay(day, today);
177
- const isSelected = selectedDate ? isSameDay(day, selectedDate) : false;
178
- const beforeMin = minDate ? day < startOfDay(minDate) : false;
179
- const afterMax = maxDate ? day > startOfDay(maxDate) : false;
180
- const isDisabled =
181
- disabled || beforeMin || afterMax || (!showOutsideDays && !inMonth);
182
- cells.push({
183
- date: day,
184
- iso: toIso(day),
185
- inMonth,
186
- isToday,
187
- isSelected,
188
- isDisabled,
189
- });
190
- }
191
- return cells;
192
- });
193
-
194
- function handleShift(delta: number) {
195
- if (disabled) return;
196
- if (viewMode === "days") {
197
- viewDate = new Date(
198
- viewDate.getFullYear(),
199
- viewDate.getMonth() + delta,
200
- 1
201
- );
202
- } else if (viewMode === "months") {
203
- viewDate = new Date(viewDate.getFullYear() + delta, viewDate.getMonth(), 1);
204
- } else {
205
- viewDate = new Date(
206
- viewDate.getFullYear() + delta * 12,
207
- viewDate.getMonth(),
208
- 1
209
- );
210
- }
211
- }
212
-
213
- function handleWheel(event: WheelEvent) {
214
- if (disabled || viewMode !== "days") return;
215
- if (event.deltaY === 0) return;
216
- event.preventDefault();
217
- handleShift(event.deltaY > 0 ? 1 : -1);
218
- }
219
-
220
- function selectDay(cell: DayCell) {
221
- if (cell.isDisabled) return;
222
- internalValue = cell.iso;
223
- value = cell.iso;
224
- onChange?.(cell.iso);
225
- }
226
-
227
- function selectMonth(index: number) {
228
- if (disabled) return;
229
- viewDate = new Date(viewDate.getFullYear(), index, 1);
230
- viewMode = "days";
231
- }
232
-
233
- function selectYear(year: number) {
234
- if (disabled) return;
235
- viewDate = new Date(year, viewDate.getMonth(), 1);
236
- viewMode = "months";
237
- }
238
-
239
- const wrapperClass = $derived(cx("w-full", externalClass));
240
-
241
- const headerButtonBase =
242
- "px-1 py-0.5 rounded-[var(--radius-sm)] text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed text-[var(--text-xs)] transition-none";
243
-
244
- const arrowButtonBase =
245
- "inline-flex items-center justify-center rounded-[var(--radius-sm)] text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed h-[var(--cal-cell)] w-[var(--cal-cell)] transition-none";
246
-
247
- const dayButtonBase =
248
- "rounded-full flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] text-[var(--text-xs)] text-[var(--color-text-default)] transition-none";
249
- </script>
250
-
251
- <div
252
- class={cx(
253
- wrapperClass,
254
- "w-full h-full text-[var(--color-text-default)] [--cal-cell:clamp(16px,4vw,20px)] [--cal-gap:clamp(1px,0.6vw,3px)] [--cal-gap-lg:clamp(2px,0.9vw,6px)]"
255
- )}
256
- {...rest}
257
- >
258
- <div class="flex items-center justify-between mb-[var(--cal-gap-lg)]">
259
- <div class="flex items-center gap-1">
260
- <button
261
- type="button"
262
- class={cx(headerButtonBase, TEXT.sm)}
263
- onclick={() => (viewMode = viewMode === "months" ? "days" : "months")}
264
- disabled={disabled}
265
- >
266
- {monthLabel}
267
- </button>
268
- <button
269
- type="button"
270
- class={cx(headerButtonBase, TEXT.sm)}
271
- onclick={() => (viewMode = viewMode === "years" ? "days" : "years")}
272
- disabled={disabled}
273
- >
274
- {yearLabel}
275
- </button>
276
- </div>
277
-
278
- <div class="flex items-center gap-1">
279
- <button
280
- type="button"
281
- class={arrowButtonBase}
282
- aria-label="Previous"
283
- onclick={() => handleShift(-1)}
284
- disabled={disabled}
285
- >
286
-
287
- </button>
288
- <button
289
- type="button"
290
- class={arrowButtonBase}
291
- aria-label="Next"
292
- onclick={() => handleShift(1)}
293
- disabled={disabled}
294
- >
295
-
296
- </button>
297
- </div>
298
- </div>
299
-
300
- {#if viewMode === "days"}
301
- <div class={cx("grid grid-cols-7 gap-[var(--cal-gap)] text-center", TEXT.xs)}>
302
- {#each weekdayLabels as label, i (i)}
303
- <div class="py-[var(--cal-gap)] [color:var(--color-text-muted)]">
304
- {label}
305
- </div>
306
- {/each}
307
- </div>
308
-
309
- <div
310
- class="grid grid-cols-7 gap-[var(--cal-gap)] mt-[var(--cal-gap)]"
311
- onwheel={handleWheel}
312
- >
313
- {#each days as cell (cell.iso)}
314
- <button
315
- type="button"
316
- class={cx(
317
- dayButtonBase,
318
- "text-[var(--color-text-default)]",
319
- cell.isToday &&
320
- "bg-[var(--color-bg-primary)] text-[var(--color-text-default)] hover:brightness-110",
321
- cell.isSelected &&
322
- "border border-[var(--border-color-primary)]",
323
- !cell.inMonth && "opacity-60",
324
- cell.isDisabled &&
325
- "opacity-[var(--opacity-disabled)] cursor-not-allowed",
326
- !cell.isDisabled && "hover:bg-[var(--color-bg-hover)]",
327
- "w-[var(--cal-cell)] h-[var(--cal-cell)] justify-self-center"
328
- )}
329
- aria-pressed={cell.isSelected}
330
- aria-current={cell.isToday ? "date" : undefined}
331
- aria-label={dayLabelFormatter.format(cell.date)}
332
- disabled={cell.isDisabled}
333
- onclick={() => selectDay(cell)}
334
- >
335
- {#if cell.inMonth || showOutsideDays}
336
- {cell.date.getDate()}
337
- {/if}
338
- </button>
339
- {/each}
340
- </div>
341
- {:else if viewMode === "months"}
342
- <div class="grid grid-cols-4 gap-[var(--cal-gap-lg)]">
343
- {#each monthOptions as month (month.index)}
344
- <button
345
- type="button"
346
- class={cx(
347
- "rounded-[var(--radius-md)] text-center text-[var(--color-text-default)] w-[var(--cal-cell)] h-[var(--cal-cell)]",
348
- TEXT.xs,
349
- month.index === viewDate.getMonth() &&
350
- "bg-[var(--color-bg-primary)]",
351
- month.index !== viewDate.getMonth() &&
352
- "hover:bg-[var(--color-bg-hover)]",
353
- disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
354
- )}
355
- onclick={() => selectMonth(month.index)}
356
- disabled={disabled}
357
- >
358
- {month.label}
359
- </button>
360
- {/each}
361
- </div>
362
- {:else}
363
- <div class="grid grid-cols-4 gap-[var(--cal-gap-lg)]">
364
- {#each yearOptions as year (year)}
365
- <button
366
- type="button"
367
- class={cx(
368
- "rounded-[var(--radius-md)] text-center text-[var(--color-text-default)] w-[var(--cal-cell)] h-[var(--cal-cell)] transition-none",
369
- TEXT.xs,
370
- year === viewDate.getFullYear() &&
371
- "bg-[var(--color-bg-primary)]",
372
- year !== viewDate.getFullYear() &&
373
- "hover:bg-[var(--color-bg-hover)]",
374
- disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
375
- )}
376
- onclick={() => selectYear(year)}
377
- disabled={disabled}
378
- >
379
- {year}
380
- </button>
381
- {/each}
382
- </div>
383
- {/if}
384
- </div>
1
+ <!-- src/lib/Calendar.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component Calendar
5
+ * @description Monthly calendar grid with navigation and date selection.
6
+ *
7
+ * @prop value {string | null} - Selected date in ISO `YYYY-MM-DD` (bindable)
8
+ * @default null
9
+ *
10
+ * @prop min {string} - Minimum selectable date (ISO `YYYY-MM-DD`)
11
+ *
12
+ * @prop max {string} - Maximum selectable date (ISO `YYYY-MM-DD`)
13
+ *
14
+ * @prop locale {string} - Locale used for month/day labels
15
+ * @default "en-US"
16
+ *
17
+ * @prop weekStartsOn {0|1|2|3|4|5|6} - First day of week (0=Sun ... 6=Sat)
18
+ * @default 1
19
+ *
20
+ * @prop showOutsideDays {boolean} - Render days from adjacent months
21
+ * @default true
22
+ *
23
+ * @prop disabled {boolean} - Disables selection and navigation
24
+ * @default false
25
+ *
26
+ * @prop onChange {(value: string | null) => void} - Fired on date selection
27
+ *
28
+ * @prop class {string} - Additional classes for the root wrapper
29
+ * @default ""
30
+ */
31
+ import type { HTMLAttributes } from "svelte/elements";
32
+ import { cx } from "../utils";
33
+ import { TEXT } from "./types";
34
+
35
+ type Props = HTMLAttributes<HTMLDivElement> & {
36
+ value?: string | null;
37
+ min?: string;
38
+ max?: string;
39
+ locale?: string;
40
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
41
+ showOutsideDays?: boolean;
42
+ disabled?: boolean;
43
+ onChange?: (value: string | null) => void;
44
+ class?: string;
45
+ };
46
+
47
+ let {
48
+ value = $bindable<string | null>(null),
49
+ min = "1926-01-01",
50
+ max,
51
+ locale = "en-US",
52
+ weekStartsOn = 1,
53
+ showOutsideDays = true,
54
+ disabled = false,
55
+ onChange,
56
+ class: externalClass = "",
57
+ ...rest
58
+ }: Props = $props();
59
+
60
+ type ViewMode = "days" | "months" | "years";
61
+
62
+ function startOfDay(date: Date) {
63
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
64
+ }
65
+
66
+ function parseIso(value: string | null | undefined): Date | null {
67
+ if (!value) return null;
68
+ const [y, m, d] = value.split("-").map((part) => Number(part));
69
+ if (!y || !m || !d) return null;
70
+ const date = new Date(y, m - 1, d);
71
+ return Number.isNaN(date.getTime()) ? null : date;
72
+ }
73
+
74
+ function toIso(date: Date) {
75
+ const y = date.getFullYear();
76
+ const m = String(date.getMonth() + 1).padStart(2, "0");
77
+ const d = String(date.getDate()).padStart(2, "0");
78
+ return `${y}-${m}-${d}`;
79
+ }
80
+
81
+ function isSameDay(a: Date, b: Date) {
82
+ return (
83
+ a.getFullYear() === b.getFullYear() &&
84
+ a.getMonth() === b.getMonth() &&
85
+ a.getDate() === b.getDate()
86
+ );
87
+ }
88
+
89
+ let internalValue = $state<string | null>(value ?? null);
90
+ let lastValue = $state<string | null>(value ?? null);
91
+ let viewDate = $state(startOfDay(new Date()));
92
+ let viewMode = $state<ViewMode>("days");
93
+
94
+ const selectedDate = $derived(parseIso(internalValue));
95
+ const minDate = $derived(parseIso(min));
96
+ const maxDate = $derived(parseIso(max));
97
+ const today = $derived(startOfDay(new Date()));
98
+
99
+ $effect(() => {
100
+ const next = value ?? null;
101
+ if (next !== lastValue) {
102
+ lastValue = next;
103
+ internalValue = next;
104
+ }
105
+ });
106
+
107
+ $effect(() => {
108
+ const selected = parseIso(internalValue);
109
+ if (!selected) return;
110
+ viewDate = new Date(selected.getFullYear(), selected.getMonth(), 1);
111
+ });
112
+
113
+ const monthLabel = $derived(
114
+ new Intl.DateTimeFormat(locale, { month: "long" }).format(viewDate)
115
+ );
116
+
117
+ const yearLabel = $derived(String(viewDate.getFullYear()));
118
+
119
+ const weekdayLabels = $derived.by(() => {
120
+ const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
121
+ const labels: string[] = [];
122
+ for (let i = 0; i < 7; i += 1) {
123
+ const offset = (weekStartsOn + i) % 7;
124
+ const date = new Date(2023, 0, 1 + offset);
125
+ labels.push(formatter.format(date));
126
+ }
127
+ return labels;
128
+ });
129
+
130
+ const dayLabelFormatter = $derived.by(
131
+ () =>
132
+ new Intl.DateTimeFormat(locale, {
133
+ weekday: "long",
134
+ year: "numeric",
135
+ month: "long",
136
+ day: "numeric",
137
+ })
138
+ );
139
+
140
+ const monthOptions = $derived.by(() => {
141
+ const formatter = new Intl.DateTimeFormat(locale, { month: "short" });
142
+ return Array.from({ length: 12 }, (_, idx) => ({
143
+ index: idx,
144
+ label: formatter.format(new Date(2024, idx, 1)),
145
+ }));
146
+ });
147
+
148
+ const yearRangeStart = $derived(() => {
149
+ const year = viewDate.getFullYear();
150
+ return year - (year % 12);
151
+ });
152
+
153
+ const yearOptions = $derived.by(() =>
154
+ Array.from({ length: 12 }, (_, idx) => yearRangeStart() + idx)
155
+ );
156
+
157
+ type DayCell = {
158
+ date: Date;
159
+ iso: string;
160
+ inMonth: boolean;
161
+ isToday: boolean;
162
+ isSelected: boolean;
163
+ isDisabled: boolean;
164
+ };
165
+
166
+ const days = $derived.by(() => {
167
+ const year = viewDate.getFullYear();
168
+ const month = viewDate.getMonth();
169
+ const first = new Date(year, month, 1);
170
+ const startOffset = (first.getDay() - weekStartsOn + 7) % 7;
171
+ const cells: DayCell[] = [];
172
+ for (let i = 0; i < 42; i += 1) {
173
+ const date = new Date(year, month, 1 - startOffset + i);
174
+ const day = startOfDay(date);
175
+ const inMonth = day.getMonth() === month;
176
+ const isToday = isSameDay(day, today);
177
+ const isSelected = selectedDate ? isSameDay(day, selectedDate) : false;
178
+ const beforeMin = minDate ? day < startOfDay(minDate) : false;
179
+ const afterMax = maxDate ? day > startOfDay(maxDate) : false;
180
+ const isDisabled =
181
+ disabled || beforeMin || afterMax || (!showOutsideDays && !inMonth);
182
+ cells.push({
183
+ date: day,
184
+ iso: toIso(day),
185
+ inMonth,
186
+ isToday,
187
+ isSelected,
188
+ isDisabled,
189
+ });
190
+ }
191
+ return cells;
192
+ });
193
+
194
+ function handleShift(delta: number) {
195
+ if (disabled) return;
196
+ if (viewMode === "days") {
197
+ viewDate = new Date(
198
+ viewDate.getFullYear(),
199
+ viewDate.getMonth() + delta,
200
+ 1
201
+ );
202
+ } else if (viewMode === "months") {
203
+ viewDate = new Date(viewDate.getFullYear() + delta, viewDate.getMonth(), 1);
204
+ } else {
205
+ viewDate = new Date(
206
+ viewDate.getFullYear() + delta * 12,
207
+ viewDate.getMonth(),
208
+ 1
209
+ );
210
+ }
211
+ }
212
+
213
+ function handleWheel(event: WheelEvent) {
214
+ if (disabled || viewMode !== "days") return;
215
+ if (event.deltaY === 0) return;
216
+ event.preventDefault();
217
+ handleShift(event.deltaY > 0 ? 1 : -1);
218
+ }
219
+
220
+ function selectDay(cell: DayCell) {
221
+ if (cell.isDisabled) return;
222
+ internalValue = cell.iso;
223
+ value = cell.iso;
224
+ onChange?.(cell.iso);
225
+ }
226
+
227
+ function selectMonth(index: number) {
228
+ if (disabled) return;
229
+ viewDate = new Date(viewDate.getFullYear(), index, 1);
230
+ viewMode = "days";
231
+ }
232
+
233
+ function selectYear(year: number) {
234
+ if (disabled) return;
235
+ viewDate = new Date(year, viewDate.getMonth(), 1);
236
+ viewMode = "months";
237
+ }
238
+
239
+ const wrapperClass = $derived(cx("w-full", externalClass));
240
+
241
+ const headerButtonBase =
242
+ "px-1 py-0.5 rounded-[var(--radius-sm)] text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed text-[var(--text-xs)] transition-none";
243
+
244
+ const arrowButtonBase =
245
+ "inline-flex items-center justify-center rounded-[var(--radius-sm)] text-[var(--color-text-default)] hover:bg-[var(--color-bg-hover)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed h-[var(--cal-cell)] w-[var(--cal-cell)] transition-none";
246
+
247
+ const dayButtonBase =
248
+ "rounded-full flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] text-[var(--text-xs)] text-[var(--color-text-default)] transition-none";
249
+ </script>
250
+
251
+ <div
252
+ class={cx(
253
+ wrapperClass,
254
+ "w-full h-full text-[var(--color-text-default)] [--cal-cell:clamp(16px,4vw,20px)] [--cal-gap:clamp(1px,0.6vw,3px)] [--cal-gap-lg:clamp(2px,0.9vw,6px)]"
255
+ )}
256
+ {...rest}
257
+ >
258
+ <div class="flex items-center justify-between mb-[var(--cal-gap-lg)]">
259
+ <div class="flex items-center gap-1">
260
+ <button
261
+ type="button"
262
+ class={cx(headerButtonBase, TEXT.sm)}
263
+ onclick={() => (viewMode = viewMode === "months" ? "days" : "months")}
264
+ disabled={disabled}
265
+ >
266
+ {monthLabel}
267
+ </button>
268
+ <button
269
+ type="button"
270
+ class={cx(headerButtonBase, TEXT.sm)}
271
+ onclick={() => (viewMode = viewMode === "years" ? "days" : "years")}
272
+ disabled={disabled}
273
+ >
274
+ {yearLabel}
275
+ </button>
276
+ </div>
277
+
278
+ <div class="flex items-center gap-1">
279
+ <button
280
+ type="button"
281
+ class={arrowButtonBase}
282
+ aria-label="Previous"
283
+ onclick={() => handleShift(-1)}
284
+ disabled={disabled}
285
+ >
286
+
287
+ </button>
288
+ <button
289
+ type="button"
290
+ class={arrowButtonBase}
291
+ aria-label="Next"
292
+ onclick={() => handleShift(1)}
293
+ disabled={disabled}
294
+ >
295
+
296
+ </button>
297
+ </div>
298
+ </div>
299
+
300
+ {#if viewMode === "days"}
301
+ <div class={cx("grid grid-cols-7 gap-[var(--cal-gap)] text-center", TEXT.xs)}>
302
+ {#each weekdayLabels as label, i (i)}
303
+ <div class="py-[var(--cal-gap)] [color:var(--color-text-muted)]">
304
+ {label}
305
+ </div>
306
+ {/each}
307
+ </div>
308
+
309
+ <div
310
+ class="grid grid-cols-7 gap-[var(--cal-gap)] mt-[var(--cal-gap)]"
311
+ onwheel={handleWheel}
312
+ >
313
+ {#each days as cell (cell.iso)}
314
+ <button
315
+ type="button"
316
+ class={cx(
317
+ dayButtonBase,
318
+ "text-[var(--color-text-default)]",
319
+ cell.isToday &&
320
+ "bg-[var(--color-bg-primary)] text-[var(--color-text-default)] hover:brightness-110",
321
+ cell.isSelected &&
322
+ "border border-[var(--border-color-primary)]",
323
+ !cell.inMonth && "opacity-60",
324
+ cell.isDisabled &&
325
+ "opacity-[var(--opacity-disabled)] cursor-not-allowed",
326
+ !cell.isDisabled && "hover:bg-[var(--color-bg-hover)]",
327
+ "w-[var(--cal-cell)] h-[var(--cal-cell)] justify-self-center"
328
+ )}
329
+ aria-pressed={cell.isSelected}
330
+ aria-current={cell.isToday ? "date" : undefined}
331
+ aria-label={dayLabelFormatter.format(cell.date)}
332
+ disabled={cell.isDisabled}
333
+ onclick={() => selectDay(cell)}
334
+ >
335
+ {#if cell.inMonth || showOutsideDays}
336
+ {cell.date.getDate()}
337
+ {/if}
338
+ </button>
339
+ {/each}
340
+ </div>
341
+ {:else if viewMode === "months"}
342
+ <div class="grid grid-cols-4 gap-[var(--cal-gap-lg)]">
343
+ {#each monthOptions as month (month.index)}
344
+ <button
345
+ type="button"
346
+ class={cx(
347
+ "rounded-[var(--radius-md)] text-center text-[var(--color-text-default)] w-[var(--cal-cell)] h-[var(--cal-cell)]",
348
+ TEXT.xs,
349
+ month.index === viewDate.getMonth() &&
350
+ "bg-[var(--color-bg-primary)]",
351
+ month.index !== viewDate.getMonth() &&
352
+ "hover:bg-[var(--color-bg-hover)]",
353
+ disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
354
+ )}
355
+ onclick={() => selectMonth(month.index)}
356
+ disabled={disabled}
357
+ >
358
+ {month.label}
359
+ </button>
360
+ {/each}
361
+ </div>
362
+ {:else}
363
+ <div class="grid grid-cols-4 gap-[var(--cal-gap-lg)]">
364
+ {#each yearOptions as year (year)}
365
+ <button
366
+ type="button"
367
+ class={cx(
368
+ "rounded-[var(--radius-md)] text-center text-[var(--color-text-default)] w-[var(--cal-cell)] h-[var(--cal-cell)] transition-none",
369
+ TEXT.xs,
370
+ year === viewDate.getFullYear() &&
371
+ "bg-[var(--color-bg-primary)]",
372
+ year !== viewDate.getFullYear() &&
373
+ "hover:bg-[var(--color-bg-hover)]",
374
+ disabled && "opacity-[var(--opacity-disabled)] cursor-not-allowed"
375
+ )}
376
+ onclick={() => selectYear(year)}
377
+ disabled={disabled}
378
+ >
379
+ {year}
380
+ </button>
381
+ {/each}
382
+ </div>
383
+ {/if}
384
+ </div>