@placeholderco/placeholder-ui 1.0.3 → 1.0.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 (136) hide show
  1. package/LICENSE +26 -26
  2. package/README.md +179 -179
  3. package/dist/display/Alert.svelte +179 -179
  4. package/dist/display/Avatar.svelte +166 -166
  5. package/dist/display/LinkCollection.svelte +161 -161
  6. package/dist/display/Paper.svelte +118 -118
  7. package/dist/form/Autocomplete.svelte +223 -191
  8. package/dist/form/Autocomplete.svelte.d.ts +3 -1
  9. package/dist/form/AutocompleteMulti.svelte +356 -0
  10. package/dist/form/AutocompleteMulti.svelte.d.ts +28 -0
  11. package/dist/form/Checkbox.svelte +201 -201
  12. package/dist/form/Chips.svelte +128 -128
  13. package/dist/form/ComboBox.svelte +158 -158
  14. package/dist/form/ComboBox.svelte.d.ts +1 -1
  15. package/dist/form/ComboBoxItemBuilder.svelte +460 -460
  16. package/dist/form/ComboBoxMulti.svelte +197 -197
  17. package/dist/form/ComboBoxMulti.svelte.d.ts +1 -1
  18. package/dist/form/CronBuilder.svelte +693 -693
  19. package/dist/form/DatePicker.svelte +672 -672
  20. package/dist/form/DateTimePicker.svelte +712 -712
  21. package/dist/form/FileInput.svelte +235 -235
  22. package/dist/form/FormGroup.svelte +68 -68
  23. package/dist/form/Number.svelte +238 -238
  24. package/dist/form/PasswordInput.svelte +252 -252
  25. package/dist/form/RadioGroup.svelte +210 -210
  26. package/dist/form/Rating.svelte +235 -235
  27. package/dist/form/SegmentedControl.svelte +149 -149
  28. package/dist/form/Select.svelte +590 -590
  29. package/dist/form/Select.svelte.d.ts +1 -1
  30. package/dist/form/SelectMulti.svelte +613 -613
  31. package/dist/form/SelectMulti.svelte.d.ts +1 -1
  32. package/dist/form/Slider.svelte +358 -358
  33. package/dist/form/Switch.svelte +147 -147
  34. package/dist/form/TextArea.svelte +148 -148
  35. package/dist/form/Textbox.svelte +228 -228
  36. package/dist/form/TimePicker.svelte +267 -267
  37. package/dist/icon/Icon.svelte +52 -52
  38. package/dist/icon/alert-octagon.svg +5 -5
  39. package/dist/icon/alert-triangle.svg +5 -5
  40. package/dist/icon/archive.svg +1 -1
  41. package/dist/icon/arrow-down.svg +1 -1
  42. package/dist/icon/arrow-left.svg +1 -1
  43. package/dist/icon/arrow-right.svg +1 -1
  44. package/dist/icon/arrow-up.svg +1 -1
  45. package/dist/icon/at.svg +1 -1
  46. package/dist/icon/bell.svg +1 -1
  47. package/dist/icon/bookmark.svg +1 -1
  48. package/dist/icon/calendar.svg +1 -1
  49. package/dist/icon/camera.svg +1 -1
  50. package/dist/icon/chart-bar.svg +1 -1
  51. package/dist/icon/chart-line.svg +1 -1
  52. package/dist/icon/chart-pie.svg +1 -1
  53. package/dist/icon/checkbox.svg +1 -1
  54. package/dist/icon/checklist.svg +1 -1
  55. package/dist/icon/circle-check.svg +1 -1
  56. package/dist/icon/circle-x.svg +1 -1
  57. package/dist/icon/clock.svg +1 -1
  58. package/dist/icon/credit-card.svg +1 -1
  59. package/dist/icon/dots-vertical.svg +1 -1
  60. package/dist/icon/dots.svg +1 -1
  61. package/dist/icon/external-link.svg +1 -1
  62. package/dist/icon/eye-off.svg +1 -1
  63. package/dist/icon/eye.svg +1 -1
  64. package/dist/icon/filter.svg +1 -1
  65. package/dist/icon/fingerprint.svg +1 -1
  66. package/dist/icon/flag.svg +1 -1
  67. package/dist/icon/heart.svg +1 -1
  68. package/dist/icon/home.svg +1 -1
  69. package/dist/icon/key.svg +1 -1
  70. package/dist/icon/list-check.svg +1 -1
  71. package/dist/icon/login.svg +1 -1
  72. package/dist/icon/logout.svg +1 -1
  73. package/dist/icon/map-pin.svg +1 -1
  74. package/dist/icon/maximize.svg +1 -1
  75. package/dist/icon/microphone.svg +1 -1
  76. package/dist/icon/minimize.svg +1 -1
  77. package/dist/icon/note.svg +1 -1
  78. package/dist/icon/player-pause.svg +1 -1
  79. package/dist/icon/printer.svg +1 -1
  80. package/dist/icon/qrcode.svg +1 -1
  81. package/dist/icon/send.svg +1 -1
  82. package/dist/icon/settings.svg +1 -1
  83. package/dist/icon/share.svg +1 -1
  84. package/dist/icon/shopping-cart.svg +1 -1
  85. package/dist/icon/sort-ascending.svg +1 -1
  86. package/dist/icon/sort-descending.svg +1 -1
  87. package/dist/icon/star.svg +1 -1
  88. package/dist/icon/tag.svg +1 -1
  89. package/dist/icon/trending-down.svg +1 -1
  90. package/dist/icon/trending-up.svg +1 -1
  91. package/dist/icon/upload.svg +1 -1
  92. package/dist/icon/volume-off.svg +1 -1
  93. package/dist/icon/volume.svg +1 -1
  94. package/dist/icon/world.svg +1 -1
  95. package/dist/icon/zoom-in.svg +1 -1
  96. package/dist/icon/zoom-out.svg +1 -1
  97. package/dist/index.d.ts +1 -0
  98. package/dist/index.js +1 -0
  99. package/dist/layout/AppShell.svelte +169 -169
  100. package/dist/layout/CustomNavbar.svelte +61 -61
  101. package/dist/layout/Navbar.svelte +206 -206
  102. package/dist/layout/NavbarItemDisplay.svelte +29 -29
  103. package/dist/layout/Sidenav.svelte +712 -712
  104. package/dist/styles/components.css +199 -199
  105. package/dist/styles/dark.css +146 -146
  106. package/dist/styles/index.css +116 -116
  107. package/dist/styles/reset.css +110 -110
  108. package/dist/styles/semantic.css +86 -86
  109. package/dist/styles/tokens.css +203 -197
  110. package/dist/styles/utilities.css +523 -523
  111. package/dist/ui/Accordion.svelte +289 -289
  112. package/dist/ui/ActionIcon.svelte +76 -76
  113. package/dist/ui/Badge.svelte +329 -279
  114. package/dist/ui/Breadcrumbs.svelte +131 -131
  115. package/dist/ui/Button.svelte +432 -370
  116. package/dist/ui/ButtonVariant.d.ts +1 -1
  117. package/dist/ui/Dialog.svelte +307 -307
  118. package/dist/ui/Drawer.svelte +524 -524
  119. package/dist/ui/Dropdown.svelte +97 -97
  120. package/dist/ui/Dropzone.svelte +122 -122
  121. package/dist/ui/Link.svelte +32 -32
  122. package/dist/ui/Loader.svelte +70 -70
  123. package/dist/ui/LoadingOverlay.svelte +53 -53
  124. package/dist/ui/Pagination.svelte +135 -135
  125. package/dist/ui/Popover.svelte +225 -225
  126. package/dist/ui/Progress.svelte +191 -191
  127. package/dist/ui/RingProgress.svelte +141 -141
  128. package/dist/ui/Skeleton.svelte +85 -85
  129. package/dist/ui/Stepper.svelte +355 -355
  130. package/dist/ui/Table.svelte +345 -345
  131. package/dist/ui/Tabs.svelte +146 -146
  132. package/dist/ui/ThemeSwitcher.svelte +39 -39
  133. package/dist/ui/Timeline.svelte +225 -225
  134. package/dist/ui/Toaster.svelte +6 -6
  135. package/dist/ui/Tooltip.svelte +434 -434
  136. package/package.json +14 -14
@@ -1,712 +1,712 @@
1
- <script lang="ts">
2
- import Textbox from "./Textbox.svelte";
3
- import { fade } from "svelte/transition";
4
- import dayjs from "dayjs";
5
- import { clickOutside } from "../actions/ClickOutside.js";
6
- import ActionIcon from "../ui/ActionIcon.svelte";
7
- import { iconChevronLeft, iconChevronRight, iconX } from "../icon/index.js";
8
- import TimePicker from "./TimePicker.svelte";
9
-
10
- interface Props {
11
- label?: string;
12
- disabled?: boolean;
13
- required?: boolean;
14
- containerClass?: string;
15
- value?: string | undefined;
16
- minDate?: string | undefined;
17
- maxDate?: string | undefined;
18
- onchange?: (date: string | undefined) => void;
19
- }
20
-
21
- let {
22
- label,
23
- disabled,
24
- required,
25
- containerClass,
26
- value = $bindable(undefined),
27
- minDate,
28
- maxDate,
29
- onchange,
30
- }: Props = $props();
31
-
32
- const days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
33
-
34
- // Parse min and max dates
35
- const minDateParsed = $derived(
36
- minDate ? dayjs(minDate).startOf("date") : undefined,
37
- );
38
- const maxDateParsed = $derived(
39
- maxDate ? dayjs(maxDate).startOf("date") : undefined,
40
- );
41
-
42
- type DisplayedDateColor = "normal" | "red" | "grey" | "disabled";
43
- type DisplayedDate = {
44
- date: dayjs.Dayjs;
45
- text: string;
46
- color: DisplayedDateColor;
47
- isSelected: boolean;
48
- isDisabled: boolean;
49
- };
50
-
51
- const todayDate = dayjs().startOf("date");
52
-
53
- let selectedDate: dayjs.Dayjs | undefined = $state(undefined);
54
- let selectedTime: string | undefined = $state(undefined);
55
-
56
- $effect(() => {
57
- if (value) {
58
- const date = dayjs(value);
59
- // Extract time if present BEFORE calling setDate
60
- if (date.isValid() && value.includes("T")) {
61
- selectedTime = date.format("HH:mm:ss");
62
- }
63
- setDate(date, false, true);
64
- } else {
65
- selectedTime = undefined;
66
- setDate(undefined, false, true);
67
- }
68
- if (selectedDate) {
69
- focusedMonth = selectedDate.startOf("month");
70
- }
71
- });
72
-
73
- let showRequiredRing = $state(false);
74
- // svelte-ignore state_referenced_locally
75
- let focusedMonth = $state((selectedDate ?? todayDate).startOf("month"));
76
-
77
- function startOfWeek(date: dayjs.Dayjs) {
78
- // We celebrate Monday as the one true start of the week in this household. NO exceptions
79
- return date.day(date.day() === 0 ? -6 : 1);
80
- }
81
-
82
- function endOfWeek(date: dayjs.Dayjs) {
83
- return date.day(date.day() === 6 ? 8 : 7);
84
- }
85
-
86
- function getFullWeekCount(month: dayjs.Dayjs) {
87
- const start = startOfWeek(month.date(1));
88
- const end = endOfWeek(month.date(month.daysInMonth()));
89
- const diff = end.diff(start, "day", true);
90
-
91
- return Math.ceil(diff / 7) * 7;
92
- }
93
-
94
- function isDateInRange(date: dayjs.Dayjs) {
95
- if (minDateParsed && date.isBefore(minDateParsed)) return false;
96
- if (maxDateParsed && date.isAfter(maxDateParsed)) return false;
97
- return true;
98
- }
99
-
100
- function calculateDays(
101
- selectedDate: dayjs.Dayjs | undefined,
102
- focusedMonth: dayjs.Dayjs,
103
- ) {
104
- // Fill out the entire 7 day grid
105
- const length = getFullWeekCount(focusedMonth);
106
-
107
- const displayedDates: DisplayedDate[] = [];
108
-
109
- const startDate = startOfWeek(focusedMonth);
110
-
111
- for (let i = 0; i < length; i++) {
112
- const date = startDate.add(i, "day");
113
-
114
- let color: DisplayedDateColor;
115
- let isDisabled = false;
116
-
117
- // Check if date is outside of min/max range
118
- if (!isDateInRange(date)) {
119
- color = "disabled";
120
- isDisabled = true;
121
- } else if (!date.isSame(focusedMonth, "month")) {
122
- color = "grey";
123
- } else if (date.day() == 0 || date.day() == 6) {
124
- color = "red";
125
- } else {
126
- color = "normal";
127
- }
128
-
129
- displayedDates.push({
130
- date,
131
- text: date.date().toString(),
132
- color,
133
- isSelected:
134
- selectedDate !== undefined &&
135
- selectedDate.isSame(date, "date"),
136
- isDisabled,
137
- });
138
- }
139
-
140
- return displayedDates;
141
- }
142
-
143
- function buildDayClasses(date: DisplayedDate) {
144
- let classes = [];
145
-
146
- if (date.isDisabled) {
147
- classes.push("disabled");
148
- } else if (date.color === "red") {
149
- classes.push("text-red");
150
- } else if (date.color === "grey") {
151
- classes.push("text-grey");
152
- }
153
-
154
- if (date.isSelected && !date.isDisabled) {
155
- classes.push("selected");
156
- } else {
157
- if (date.date.isSame(todayDate, "date") && !date.isDisabled) {
158
- classes.push("today");
159
- }
160
- }
161
-
162
- return classes.join(" ");
163
- }
164
-
165
- function setDate(
166
- newDate: dayjs.Dayjs | undefined,
167
- fromTextbox: boolean,
168
- fromValue: boolean,
169
- ) {
170
- if (newDate === undefined && selectedDate === undefined) return;
171
-
172
- // Check if the new date is within the allowed range
173
- if (newDate !== undefined && !isDateInRange(newDate)) {
174
- // Don't set the date if it's outside the allowed range
175
- return;
176
- }
177
-
178
- const isSameDate =
179
- newDate !== undefined &&
180
- selectedDate !== undefined &&
181
- newDate.isSame(selectedDate, "date");
182
-
183
- if (
184
- newDate === undefined ||
185
- !newDate.isValid() ||
186
- (isSameDate && !fromTextbox && !fromValue)
187
- ) {
188
- value = undefined;
189
- selectedDate = undefined;
190
- readableDate = "";
191
- return;
192
- } else if (isSameDate) {
193
- return;
194
- }
195
-
196
- selectedDate = newDate;
197
- focusedMonth = newDate.startOf("month");
198
-
199
- // Combine date and time
200
- let dateString = newDate.format("YYYY-MM-DD");
201
- let timeString = selectedTime || "00:00:00";
202
- value = `${dateString}T${timeString}`;
203
- value = dayjs(value).format("YYYY-MM-DDTHH:mm:ssZ");
204
- onchange?.(value);
205
-
206
- if (!fromTextbox)
207
- readableDate = dayjs(value).format("D MMMM YYYY - h:mma");
208
- }
209
-
210
- function incrementFocusedMonth(value: number) {
211
- focusedMonth = focusedMonth.add(value, "month");
212
- }
213
-
214
- // -----------------------------
215
- // Control code
216
-
217
- let textboxElement: HTMLElement | undefined = $state(undefined);
218
- let readableDate: string = $state("");
219
- let showCalendar = $state(false);
220
- let showYearPicker = $state(false);
221
- let dropdownPosition: "above" | "below" = $state("below");
222
-
223
- // Year picker: show a range of years centered around the focused year
224
- const yearRangeSize = 12;
225
- let yearRangeStart = $state(
226
- Math.floor(dayjs().year() / yearRangeSize) * yearRangeSize,
227
- );
228
-
229
- function getYearRange() {
230
- const years: number[] = [];
231
- for (let i = 0; i < yearRangeSize; i++) {
232
- years.push(yearRangeStart + i);
233
- }
234
- return years;
235
- }
236
-
237
- function selectYear(year: number) {
238
- focusedMonth = focusedMonth.year(year);
239
- showYearPicker = false;
240
- }
241
-
242
- function isYearInRange(year: number) {
243
- if (minDateParsed && year < minDateParsed.year()) return false;
244
- if (maxDateParsed && year > maxDateParsed.year()) return false;
245
- return true;
246
- }
247
-
248
- function incrementYearRange(delta: number) {
249
- yearRangeStart += delta * yearRangeSize;
250
- }
251
-
252
- function toggleYearPicker() {
253
- if (!showYearPicker) {
254
- // Center the range around the focused year
255
- yearRangeStart =
256
- Math.floor(focusedMonth.year() / yearRangeSize) * yearRangeSize;
257
- }
258
- showYearPicker = !showYearPicker;
259
- }
260
-
261
- function checkDropdownPosition() {
262
- if (!textboxElement) return;
263
-
264
- const rect = textboxElement.getBoundingClientRect();
265
- const dropdownHeight = 400; // Approximate height of the calendar + time picker dropdown
266
- const spaceBelow = window.innerHeight - rect.bottom;
267
- const spaceAbove = rect.top;
268
-
269
- // If not enough space below and more space above, position above
270
- if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
271
- dropdownPosition = "above";
272
- } else {
273
- dropdownPosition = "below";
274
- }
275
- }
276
-
277
- function onClickOutside(event: MouseEvent) {
278
- if (!showCalendar) return;
279
-
280
- const target = event.target as HTMLElement;
281
-
282
- if (!textboxElement?.contains(target)) {
283
- showCalendar = false;
284
- }
285
- }
286
-
287
- function onDateTextChange(event: Event) {
288
- const textbox = event.target as HTMLInputElement;
289
-
290
- const date = dayjs(textbox.value, ["D MMMM YYYY", "DD/MM/YYYY"]);
291
- if (date.isValid()) {
292
- setDate(date, true, false);
293
- }
294
- }
295
-
296
- function clearDate() {
297
- value = undefined;
298
- selectedDate = undefined;
299
- selectedTime = undefined;
300
- readableDate = "";
301
- onchange?.(undefined);
302
- }
303
-
304
- function handleDateChangeClick(
305
- event: MouseEvent,
306
- date: dayjs.Dayjs,
307
- isDisabled: boolean,
308
- ) {
309
- if (isDisabled) {
310
- event.stopPropagation();
311
- return;
312
- }
313
-
314
- setDate(date, false, false);
315
- showCalendar = false;
316
-
317
- event.stopPropagation();
318
- }
319
- </script>
320
-
321
- <div class="date-picker-container {containerClass}">
322
- <Textbox
323
- {label}
324
- placeholder="Select date and time"
325
- {disabled}
326
- {required}
327
- noAutocomplete
328
- bind:textboxElement
329
- value={readableDate}
330
- onfocus={() => {
331
- checkDropdownPosition();
332
- showCalendar = true;
333
- }}
334
- onkeyup={onDateTextChange}
335
- class={showRequiredRing ? "!border-required" : ""}
336
- >
337
- {#snippet right()}
338
- {#if readableDate && !disabled}
339
- <div class="clear-button">
340
- <ActionIcon
341
- variant="secondary-subtle"
342
- svg={iconX}
343
- size="0.75rem"
344
- onclick={(e: MouseEvent) => {
345
- clearDate();
346
- e.stopPropagation();
347
- }}
348
- />
349
- </div>
350
- {/if}
351
- {/snippet}
352
- <div class="date-picker">
353
- {#if showCalendar}
354
- <div
355
- class="calendar {dropdownPosition}"
356
- transition:fade={{ duration: 200 }}
357
- use:clickOutside={onClickOutside}
358
- >
359
- <div class="month-buttons">
360
- {#if showYearPicker}
361
- <ActionIcon
362
- svg={iconChevronLeft}
363
- size="1.5rem"
364
- onclick={() => incrementYearRange(-1)}
365
- />
366
- <button
367
- class="month-label clickable"
368
- onclick={toggleYearPicker}
369
- >
370
- {yearRangeStart} - {yearRangeStart +
371
- yearRangeSize -
372
- 1}
373
- </button>
374
- <ActionIcon
375
- svg={iconChevronRight}
376
- size="1.5rem"
377
- onclick={() => incrementYearRange(1)}
378
- />
379
- {:else}
380
- <ActionIcon
381
- svg={iconChevronLeft}
382
- size="1.5rem"
383
- onclick={() => incrementFocusedMonth(-1)}
384
- />
385
- <button
386
- class="month-label clickable"
387
- onclick={toggleYearPicker}
388
- >
389
- {focusedMonth.format("MMMM YYYY")}
390
- </button>
391
- <ActionIcon
392
- svg={iconChevronRight}
393
- size="1.5rem"
394
- onclick={() => incrementFocusedMonth(1)}
395
- />
396
- {/if}
397
- </div>
398
-
399
- {#if showYearPicker}
400
- <div class="year-grid">
401
- {#each getYearRange() as year}
402
- <button
403
- class="year-button"
404
- class:selected={focusedMonth.year() ===
405
- year}
406
- class:current-year={todayDate.year() ===
407
- year}
408
- class:disabled={!isYearInRange(year)}
409
- disabled={!isYearInRange(year)}
410
- onclick={() => selectYear(year)}
411
- >
412
- {year}
413
- </button>
414
- {/each}
415
- </div>
416
- {:else}
417
- <div class="days-header-bar">
418
- {#each days as day}
419
- <div class="day-label">
420
- {day}
421
- </div>
422
- {/each}
423
- </div>
424
- <div class="days-grid">
425
- {#each calculateDays(selectedDate, focusedMonth) as day, i (i)}
426
- <button
427
- class="day-button {buildDayClasses(day)}"
428
- disabled={day.isDisabled}
429
- onclick={(e) =>
430
- handleDateChangeClick(
431
- e,
432
- day.date,
433
- day.isDisabled,
434
- )}
435
- >
436
- {day.text}
437
- </button>
438
- {/each}
439
- </div>
440
- {/if}
441
-
442
- <TimePicker
443
- bind:value={selectedTime}
444
- {minDate}
445
- {maxDate}
446
- selectedDate={selectedDate?.format("YYYY-MM-DD")}
447
- onchange={(t) => {
448
- selectedTime = t!;
449
- if (selectedDate) {
450
- let dateString =
451
- selectedDate.format("YYYY-MM-DD");
452
- let timeString = selectedTime || "00:00:00";
453
- value = `${dateString}T${timeString}`;
454
- value = dayjs(value).format(
455
- "YYYY-MM-DDTHH:mm:ssZ",
456
- );
457
- onchange?.(value);
458
- readableDate = dayjs(value).format(
459
- "D MMMM YYYY - h:mma",
460
- );
461
- }
462
- }}
463
- />
464
- </div>
465
- {/if}
466
- </div>
467
- </Textbox>
468
- </div>
469
-
470
- <style>
471
- .date-picker {
472
- position: relative;
473
- }
474
-
475
- .clear-button {
476
- position: absolute;
477
- right: var(--pui-spacing-2);
478
- top: 50%;
479
- transform: translateY(-50%);
480
- line-height: 0;
481
- }
482
-
483
- .calendar {
484
- width: 18rem;
485
- text-align: center;
486
- position: absolute;
487
- z-index: var(--pui-z-dropdown);
488
- user-select: none;
489
- background-color: var(--pui-bg-surface-raised);
490
- border: 1px solid var(--pui-border-default);
491
- border-radius: var(--pui-radius-md);
492
- box-shadow: var(--pui-shadow-lg);
493
- padding: var(--pui-spacing-2);
494
- }
495
-
496
- .calendar.below {
497
- top: 0;
498
- }
499
-
500
- .calendar.above {
501
- bottom: 2.25rem;
502
- }
503
-
504
- .month-buttons {
505
- display: flex;
506
- justify-content: space-between;
507
- align-items: center;
508
- padding: var(--pui-spacing-2);
509
- }
510
-
511
- .month-label {
512
- font-weight: var(--pui-font-weight-semibold);
513
- color: var(--pui-text-primary);
514
- font-size: var(--pui-font-size-lg);
515
- background: none;
516
- border: none;
517
- cursor: default;
518
- }
519
-
520
- .month-label.clickable {
521
- cursor: pointer;
522
- padding: var(--pui-spacing-1) var(--pui-spacing-2);
523
- border-radius: var(--pui-radius-sm);
524
- transition: background-color var(--pui-transition-fast) var(--pui-ease-in-out);
525
- }
526
-
527
- .month-label.clickable:hover {
528
- background-color: var(--pui-bg-hover);
529
- }
530
-
531
- .year-grid {
532
- display: grid;
533
- grid-template-columns: repeat(3, 1fr);
534
- gap: var(--pui-spacing-2);
535
- margin-top: var(--pui-spacing-2);
536
- padding: var(--pui-spacing-2);
537
- }
538
-
539
- .year-button {
540
- color: var(--pui-text-primary);
541
- padding: var(--pui-spacing-3) var(--pui-spacing-2);
542
- border-radius: var(--pui-radius-md);
543
- border: none;
544
- background: transparent;
545
- cursor: pointer;
546
- font-size: var(--pui-font-size-sm);
547
- transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
548
- }
549
-
550
- .year-button:hover:not(.disabled) {
551
- background-color: var(--pui-bg-hover);
552
- color: var(--pui-color-primary);
553
- }
554
-
555
- .year-button.current-year {
556
- background-color: var(--pui-bg-active);
557
- color: var(--pui-color-primary);
558
- font-weight: var(--pui-font-weight-semibold);
559
- }
560
-
561
- .year-button.selected {
562
- background-color: var(--pui-color-primary);
563
- color: var(--pui-color-white);
564
- font-weight: var(--pui-font-weight-semibold);
565
- }
566
-
567
- .year-button.disabled {
568
- color: var(--pui-text-disabled) !important;
569
- background-color: transparent !important;
570
- cursor: not-allowed !important;
571
- opacity: 0.5;
572
- }
573
-
574
- .days-header-bar {
575
- display: grid;
576
- grid-template-columns: repeat(7, 1fr);
577
- margin-top: var(--pui-spacing-2);
578
- background-color: var(--pui-color-gray-100);
579
- border-radius: var(--pui-radius-sm);
580
- border: 1px solid var(--pui-border-default);
581
- }
582
-
583
- .day-label {
584
- color: var(--pui-color-gray-500);
585
- padding: var(--pui-spacing-2);
586
- font-size: var(--pui-font-size-xs);
587
- font-weight: var(--pui-font-weight-semibold);
588
- text-transform: uppercase;
589
- text-align: center;
590
- }
591
-
592
- .days-grid {
593
- display: grid;
594
- grid-template-columns: repeat(7, 1fr);
595
- gap: 2px;
596
- margin-top: var(--pui-spacing-2);
597
- }
598
-
599
- .day-button {
600
- color: var(--pui-text-primary);
601
- width: 2.25rem;
602
- height: 2.25rem;
603
- border-radius: var(--pui-radius-md);
604
- border: none;
605
- background: transparent;
606
- cursor: pointer;
607
- font-size: var(--pui-font-size-sm);
608
- transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
609
- justify-self: center;
610
- }
611
-
612
- .day-button:hover {
613
- background-color: var(--pui-bg-hover);
614
- color: var(--pui-color-primary);
615
- }
616
-
617
- .today {
618
- background-color: var(--pui-bg-active);
619
- color: var(--pui-color-primary);
620
- font-weight: var(--pui-font-weight-semibold);
621
- }
622
-
623
- .selected {
624
- background-color: var(--pui-color-primary);
625
- color: var(--pui-color-white);
626
- font-weight: var(--pui-font-weight-semibold);
627
- }
628
-
629
- .text-red {
630
- color: var(--pui-color-danger);
631
- }
632
-
633
- .text-grey {
634
- color: var(--pui-text-muted);
635
- }
636
-
637
- .disabled {
638
- color: var(--pui-text-disabled) !important;
639
- background-color: transparent !important;
640
- cursor: not-allowed !important;
641
- opacity: 0.5;
642
- }
643
-
644
- .disabled:hover {
645
- background-color: transparent !important;
646
- color: var(--pui-text-disabled) !important;
647
- }
648
-
649
- :global(.dark) {
650
- .days-header-bar {
651
- background-color: var(--pui-color-dark-200);
652
- }
653
-
654
- .day-label {
655
- color: var(--pui-color-gray-300);
656
- }
657
-
658
- .day-button:hover {
659
- background-color: var(--pui-bg-hover);
660
- color: var(--pui-color-secondary);
661
- }
662
-
663
- .today {
664
- background-color: var(--pui-bg-active);
665
- color: var(--pui-color-secondary);
666
- }
667
-
668
- .selected {
669
- background-color: var(--pui-color-secondary);
670
- color: var(--pui-color-primary);
671
- }
672
-
673
- .text-grey {
674
- color: var(--pui-color-gray-500);
675
- }
676
-
677
- .disabled {
678
- color: var(--pui-color-gray-500) !important;
679
- background-color: transparent !important;
680
- cursor: not-allowed !important;
681
- opacity: 0.5;
682
- }
683
-
684
- .disabled:hover {
685
- background-color: transparent !important;
686
- color: var(--pui-color-gray-500) !important;
687
- }
688
-
689
- .month-label.clickable:hover {
690
- background-color: var(--pui-bg-hover);
691
- }
692
-
693
- .year-button:hover:not(.disabled) {
694
- background-color: var(--pui-bg-hover);
695
- color: var(--pui-color-secondary);
696
- }
697
-
698
- .year-button.current-year {
699
- background-color: var(--pui-bg-active);
700
- color: var(--pui-color-secondary);
701
- }
702
-
703
- .year-button.selected {
704
- background-color: var(--pui-color-secondary);
705
- color: var(--pui-color-primary);
706
- }
707
-
708
- .year-button.disabled {
709
- color: var(--pui-color-gray-500) !important;
710
- }
711
- }
712
- </style>
1
+ <script lang="ts">
2
+ import Textbox from "./Textbox.svelte";
3
+ import { fade } from "svelte/transition";
4
+ import dayjs from "dayjs";
5
+ import { clickOutside } from "../actions/ClickOutside.js";
6
+ import ActionIcon from "../ui/ActionIcon.svelte";
7
+ import { iconChevronLeft, iconChevronRight, iconX } from "../icon/index.js";
8
+ import TimePicker from "./TimePicker.svelte";
9
+
10
+ interface Props {
11
+ label?: string;
12
+ disabled?: boolean;
13
+ required?: boolean;
14
+ containerClass?: string;
15
+ value?: string | undefined;
16
+ minDate?: string | undefined;
17
+ maxDate?: string | undefined;
18
+ onchange?: (date: string | undefined) => void;
19
+ }
20
+
21
+ let {
22
+ label,
23
+ disabled,
24
+ required,
25
+ containerClass,
26
+ value = $bindable(undefined),
27
+ minDate,
28
+ maxDate,
29
+ onchange,
30
+ }: Props = $props();
31
+
32
+ const days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
33
+
34
+ // Parse min and max dates
35
+ const minDateParsed = $derived(
36
+ minDate ? dayjs(minDate).startOf("date") : undefined,
37
+ );
38
+ const maxDateParsed = $derived(
39
+ maxDate ? dayjs(maxDate).startOf("date") : undefined,
40
+ );
41
+
42
+ type DisplayedDateColor = "normal" | "red" | "grey" | "disabled";
43
+ type DisplayedDate = {
44
+ date: dayjs.Dayjs;
45
+ text: string;
46
+ color: DisplayedDateColor;
47
+ isSelected: boolean;
48
+ isDisabled: boolean;
49
+ };
50
+
51
+ const todayDate = dayjs().startOf("date");
52
+
53
+ let selectedDate: dayjs.Dayjs | undefined = $state(undefined);
54
+ let selectedTime: string | undefined = $state(undefined);
55
+
56
+ $effect(() => {
57
+ if (value) {
58
+ const date = dayjs(value);
59
+ // Extract time if present BEFORE calling setDate
60
+ if (date.isValid() && value.includes("T")) {
61
+ selectedTime = date.format("HH:mm:ss");
62
+ }
63
+ setDate(date, false, true);
64
+ } else {
65
+ selectedTime = undefined;
66
+ setDate(undefined, false, true);
67
+ }
68
+ if (selectedDate) {
69
+ focusedMonth = selectedDate.startOf("month");
70
+ }
71
+ });
72
+
73
+ let showRequiredRing = $state(false);
74
+ // svelte-ignore state_referenced_locally
75
+ let focusedMonth = $state((selectedDate ?? todayDate).startOf("month"));
76
+
77
+ function startOfWeek(date: dayjs.Dayjs) {
78
+ // We celebrate Monday as the one true start of the week in this household. NO exceptions
79
+ return date.day(date.day() === 0 ? -6 : 1);
80
+ }
81
+
82
+ function endOfWeek(date: dayjs.Dayjs) {
83
+ return date.day(date.day() === 6 ? 8 : 7);
84
+ }
85
+
86
+ function getFullWeekCount(month: dayjs.Dayjs) {
87
+ const start = startOfWeek(month.date(1));
88
+ const end = endOfWeek(month.date(month.daysInMonth()));
89
+ const diff = end.diff(start, "day", true);
90
+
91
+ return Math.ceil(diff / 7) * 7;
92
+ }
93
+
94
+ function isDateInRange(date: dayjs.Dayjs) {
95
+ if (minDateParsed && date.isBefore(minDateParsed)) return false;
96
+ if (maxDateParsed && date.isAfter(maxDateParsed)) return false;
97
+ return true;
98
+ }
99
+
100
+ function calculateDays(
101
+ selectedDate: dayjs.Dayjs | undefined,
102
+ focusedMonth: dayjs.Dayjs,
103
+ ) {
104
+ // Fill out the entire 7 day grid
105
+ const length = getFullWeekCount(focusedMonth);
106
+
107
+ const displayedDates: DisplayedDate[] = [];
108
+
109
+ const startDate = startOfWeek(focusedMonth);
110
+
111
+ for (let i = 0; i < length; i++) {
112
+ const date = startDate.add(i, "day");
113
+
114
+ let color: DisplayedDateColor;
115
+ let isDisabled = false;
116
+
117
+ // Check if date is outside of min/max range
118
+ if (!isDateInRange(date)) {
119
+ color = "disabled";
120
+ isDisabled = true;
121
+ } else if (!date.isSame(focusedMonth, "month")) {
122
+ color = "grey";
123
+ } else if (date.day() == 0 || date.day() == 6) {
124
+ color = "red";
125
+ } else {
126
+ color = "normal";
127
+ }
128
+
129
+ displayedDates.push({
130
+ date,
131
+ text: date.date().toString(),
132
+ color,
133
+ isSelected:
134
+ selectedDate !== undefined &&
135
+ selectedDate.isSame(date, "date"),
136
+ isDisabled,
137
+ });
138
+ }
139
+
140
+ return displayedDates;
141
+ }
142
+
143
+ function buildDayClasses(date: DisplayedDate) {
144
+ let classes = [];
145
+
146
+ if (date.isDisabled) {
147
+ classes.push("disabled");
148
+ } else if (date.color === "red") {
149
+ classes.push("text-red");
150
+ } else if (date.color === "grey") {
151
+ classes.push("text-grey");
152
+ }
153
+
154
+ if (date.isSelected && !date.isDisabled) {
155
+ classes.push("selected");
156
+ } else {
157
+ if (date.date.isSame(todayDate, "date") && !date.isDisabled) {
158
+ classes.push("today");
159
+ }
160
+ }
161
+
162
+ return classes.join(" ");
163
+ }
164
+
165
+ function setDate(
166
+ newDate: dayjs.Dayjs | undefined,
167
+ fromTextbox: boolean,
168
+ fromValue: boolean,
169
+ ) {
170
+ if (newDate === undefined && selectedDate === undefined) return;
171
+
172
+ // Check if the new date is within the allowed range
173
+ if (newDate !== undefined && !isDateInRange(newDate)) {
174
+ // Don't set the date if it's outside the allowed range
175
+ return;
176
+ }
177
+
178
+ const isSameDate =
179
+ newDate !== undefined &&
180
+ selectedDate !== undefined &&
181
+ newDate.isSame(selectedDate, "date");
182
+
183
+ if (
184
+ newDate === undefined ||
185
+ !newDate.isValid() ||
186
+ (isSameDate && !fromTextbox && !fromValue)
187
+ ) {
188
+ value = undefined;
189
+ selectedDate = undefined;
190
+ readableDate = "";
191
+ return;
192
+ } else if (isSameDate) {
193
+ return;
194
+ }
195
+
196
+ selectedDate = newDate;
197
+ focusedMonth = newDate.startOf("month");
198
+
199
+ // Combine date and time
200
+ let dateString = newDate.format("YYYY-MM-DD");
201
+ let timeString = selectedTime || "00:00:00";
202
+ value = `${dateString}T${timeString}`;
203
+ value = dayjs(value).format("YYYY-MM-DDTHH:mm:ssZ");
204
+ onchange?.(value);
205
+
206
+ if (!fromTextbox)
207
+ readableDate = dayjs(value).format("D MMMM YYYY - h:mma");
208
+ }
209
+
210
+ function incrementFocusedMonth(value: number) {
211
+ focusedMonth = focusedMonth.add(value, "month");
212
+ }
213
+
214
+ // -----------------------------
215
+ // Control code
216
+
217
+ let textboxElement: HTMLElement | undefined = $state(undefined);
218
+ let readableDate: string = $state("");
219
+ let showCalendar = $state(false);
220
+ let showYearPicker = $state(false);
221
+ let dropdownPosition: "above" | "below" = $state("below");
222
+
223
+ // Year picker: show a range of years centered around the focused year
224
+ const yearRangeSize = 12;
225
+ let yearRangeStart = $state(
226
+ Math.floor(dayjs().year() / yearRangeSize) * yearRangeSize,
227
+ );
228
+
229
+ function getYearRange() {
230
+ const years: number[] = [];
231
+ for (let i = 0; i < yearRangeSize; i++) {
232
+ years.push(yearRangeStart + i);
233
+ }
234
+ return years;
235
+ }
236
+
237
+ function selectYear(year: number) {
238
+ focusedMonth = focusedMonth.year(year);
239
+ showYearPicker = false;
240
+ }
241
+
242
+ function isYearInRange(year: number) {
243
+ if (minDateParsed && year < minDateParsed.year()) return false;
244
+ if (maxDateParsed && year > maxDateParsed.year()) return false;
245
+ return true;
246
+ }
247
+
248
+ function incrementYearRange(delta: number) {
249
+ yearRangeStart += delta * yearRangeSize;
250
+ }
251
+
252
+ function toggleYearPicker() {
253
+ if (!showYearPicker) {
254
+ // Center the range around the focused year
255
+ yearRangeStart =
256
+ Math.floor(focusedMonth.year() / yearRangeSize) * yearRangeSize;
257
+ }
258
+ showYearPicker = !showYearPicker;
259
+ }
260
+
261
+ function checkDropdownPosition() {
262
+ if (!textboxElement) return;
263
+
264
+ const rect = textboxElement.getBoundingClientRect();
265
+ const dropdownHeight = 400; // Approximate height of the calendar + time picker dropdown
266
+ const spaceBelow = window.innerHeight - rect.bottom;
267
+ const spaceAbove = rect.top;
268
+
269
+ // If not enough space below and more space above, position above
270
+ if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
271
+ dropdownPosition = "above";
272
+ } else {
273
+ dropdownPosition = "below";
274
+ }
275
+ }
276
+
277
+ function onClickOutside(event: MouseEvent) {
278
+ if (!showCalendar) return;
279
+
280
+ const target = event.target as HTMLElement;
281
+
282
+ if (!textboxElement?.contains(target)) {
283
+ showCalendar = false;
284
+ }
285
+ }
286
+
287
+ function onDateTextChange(event: Event) {
288
+ const textbox = event.target as HTMLInputElement;
289
+
290
+ const date = dayjs(textbox.value, ["D MMMM YYYY", "DD/MM/YYYY"]);
291
+ if (date.isValid()) {
292
+ setDate(date, true, false);
293
+ }
294
+ }
295
+
296
+ function clearDate() {
297
+ value = undefined;
298
+ selectedDate = undefined;
299
+ selectedTime = undefined;
300
+ readableDate = "";
301
+ onchange?.(undefined);
302
+ }
303
+
304
+ function handleDateChangeClick(
305
+ event: MouseEvent,
306
+ date: dayjs.Dayjs,
307
+ isDisabled: boolean,
308
+ ) {
309
+ if (isDisabled) {
310
+ event.stopPropagation();
311
+ return;
312
+ }
313
+
314
+ setDate(date, false, false);
315
+ showCalendar = false;
316
+
317
+ event.stopPropagation();
318
+ }
319
+ </script>
320
+
321
+ <div class="date-picker-container {containerClass}">
322
+ <Textbox
323
+ {label}
324
+ placeholder="Select date and time"
325
+ {disabled}
326
+ {required}
327
+ noAutocomplete
328
+ bind:textboxElement
329
+ value={readableDate}
330
+ onfocus={() => {
331
+ checkDropdownPosition();
332
+ showCalendar = true;
333
+ }}
334
+ onkeyup={onDateTextChange}
335
+ class={showRequiredRing ? "!border-required" : ""}
336
+ >
337
+ {#snippet right()}
338
+ {#if readableDate && !disabled}
339
+ <div class="clear-button">
340
+ <ActionIcon
341
+ variant="secondary-subtle"
342
+ svg={iconX}
343
+ size="0.75rem"
344
+ onclick={(e: MouseEvent) => {
345
+ clearDate();
346
+ e.stopPropagation();
347
+ }}
348
+ />
349
+ </div>
350
+ {/if}
351
+ {/snippet}
352
+ <div class="date-picker">
353
+ {#if showCalendar}
354
+ <div
355
+ class="calendar {dropdownPosition}"
356
+ transition:fade={{ duration: 200 }}
357
+ use:clickOutside={onClickOutside}
358
+ >
359
+ <div class="month-buttons">
360
+ {#if showYearPicker}
361
+ <ActionIcon
362
+ svg={iconChevronLeft}
363
+ size="1.5rem"
364
+ onclick={() => incrementYearRange(-1)}
365
+ />
366
+ <button
367
+ class="month-label clickable"
368
+ onclick={toggleYearPicker}
369
+ >
370
+ {yearRangeStart} - {yearRangeStart +
371
+ yearRangeSize -
372
+ 1}
373
+ </button>
374
+ <ActionIcon
375
+ svg={iconChevronRight}
376
+ size="1.5rem"
377
+ onclick={() => incrementYearRange(1)}
378
+ />
379
+ {:else}
380
+ <ActionIcon
381
+ svg={iconChevronLeft}
382
+ size="1.5rem"
383
+ onclick={() => incrementFocusedMonth(-1)}
384
+ />
385
+ <button
386
+ class="month-label clickable"
387
+ onclick={toggleYearPicker}
388
+ >
389
+ {focusedMonth.format("MMMM YYYY")}
390
+ </button>
391
+ <ActionIcon
392
+ svg={iconChevronRight}
393
+ size="1.5rem"
394
+ onclick={() => incrementFocusedMonth(1)}
395
+ />
396
+ {/if}
397
+ </div>
398
+
399
+ {#if showYearPicker}
400
+ <div class="year-grid">
401
+ {#each getYearRange() as year}
402
+ <button
403
+ class="year-button"
404
+ class:selected={focusedMonth.year() ===
405
+ year}
406
+ class:current-year={todayDate.year() ===
407
+ year}
408
+ class:disabled={!isYearInRange(year)}
409
+ disabled={!isYearInRange(year)}
410
+ onclick={() => selectYear(year)}
411
+ >
412
+ {year}
413
+ </button>
414
+ {/each}
415
+ </div>
416
+ {:else}
417
+ <div class="days-header-bar">
418
+ {#each days as day}
419
+ <div class="day-label">
420
+ {day}
421
+ </div>
422
+ {/each}
423
+ </div>
424
+ <div class="days-grid">
425
+ {#each calculateDays(selectedDate, focusedMonth) as day, i (i)}
426
+ <button
427
+ class="day-button {buildDayClasses(day)}"
428
+ disabled={day.isDisabled}
429
+ onclick={(e) =>
430
+ handleDateChangeClick(
431
+ e,
432
+ day.date,
433
+ day.isDisabled,
434
+ )}
435
+ >
436
+ {day.text}
437
+ </button>
438
+ {/each}
439
+ </div>
440
+ {/if}
441
+
442
+ <TimePicker
443
+ bind:value={selectedTime}
444
+ {minDate}
445
+ {maxDate}
446
+ selectedDate={selectedDate?.format("YYYY-MM-DD")}
447
+ onchange={(t) => {
448
+ selectedTime = t!;
449
+ if (selectedDate) {
450
+ let dateString =
451
+ selectedDate.format("YYYY-MM-DD");
452
+ let timeString = selectedTime || "00:00:00";
453
+ value = `${dateString}T${timeString}`;
454
+ value = dayjs(value).format(
455
+ "YYYY-MM-DDTHH:mm:ssZ",
456
+ );
457
+ onchange?.(value);
458
+ readableDate = dayjs(value).format(
459
+ "D MMMM YYYY - h:mma",
460
+ );
461
+ }
462
+ }}
463
+ />
464
+ </div>
465
+ {/if}
466
+ </div>
467
+ </Textbox>
468
+ </div>
469
+
470
+ <style>
471
+ .date-picker {
472
+ position: relative;
473
+ }
474
+
475
+ .clear-button {
476
+ position: absolute;
477
+ right: var(--pui-spacing-2);
478
+ top: 50%;
479
+ transform: translateY(-50%);
480
+ line-height: 0;
481
+ }
482
+
483
+ .calendar {
484
+ width: 18rem;
485
+ text-align: center;
486
+ position: absolute;
487
+ z-index: var(--pui-z-dropdown);
488
+ user-select: none;
489
+ background-color: var(--pui-bg-surface-raised);
490
+ border: 1px solid var(--pui-border-default);
491
+ border-radius: var(--pui-radius-md);
492
+ box-shadow: var(--pui-shadow-lg);
493
+ padding: var(--pui-spacing-2);
494
+ }
495
+
496
+ .calendar.below {
497
+ top: 0;
498
+ }
499
+
500
+ .calendar.above {
501
+ bottom: 2.25rem;
502
+ }
503
+
504
+ .month-buttons {
505
+ display: flex;
506
+ justify-content: space-between;
507
+ align-items: center;
508
+ padding: var(--pui-spacing-2);
509
+ }
510
+
511
+ .month-label {
512
+ font-weight: var(--pui-font-weight-semibold);
513
+ color: var(--pui-text-primary);
514
+ font-size: var(--pui-font-size-lg);
515
+ background: none;
516
+ border: none;
517
+ cursor: default;
518
+ }
519
+
520
+ .month-label.clickable {
521
+ cursor: pointer;
522
+ padding: var(--pui-spacing-1) var(--pui-spacing-2);
523
+ border-radius: var(--pui-radius-sm);
524
+ transition: background-color var(--pui-transition-fast) var(--pui-ease-in-out);
525
+ }
526
+
527
+ .month-label.clickable:hover {
528
+ background-color: var(--pui-bg-hover);
529
+ }
530
+
531
+ .year-grid {
532
+ display: grid;
533
+ grid-template-columns: repeat(3, 1fr);
534
+ gap: var(--pui-spacing-2);
535
+ margin-top: var(--pui-spacing-2);
536
+ padding: var(--pui-spacing-2);
537
+ }
538
+
539
+ .year-button {
540
+ color: var(--pui-text-primary);
541
+ padding: var(--pui-spacing-3) var(--pui-spacing-2);
542
+ border-radius: var(--pui-radius-md);
543
+ border: none;
544
+ background: transparent;
545
+ cursor: pointer;
546
+ font-size: var(--pui-font-size-sm);
547
+ transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
548
+ }
549
+
550
+ .year-button:hover:not(.disabled) {
551
+ background-color: var(--pui-bg-hover);
552
+ color: var(--pui-color-primary);
553
+ }
554
+
555
+ .year-button.current-year {
556
+ background-color: var(--pui-bg-active);
557
+ color: var(--pui-color-primary);
558
+ font-weight: var(--pui-font-weight-semibold);
559
+ }
560
+
561
+ .year-button.selected {
562
+ background-color: var(--pui-color-primary);
563
+ color: var(--pui-color-white);
564
+ font-weight: var(--pui-font-weight-semibold);
565
+ }
566
+
567
+ .year-button.disabled {
568
+ color: var(--pui-text-disabled) !important;
569
+ background-color: transparent !important;
570
+ cursor: not-allowed !important;
571
+ opacity: 0.5;
572
+ }
573
+
574
+ .days-header-bar {
575
+ display: grid;
576
+ grid-template-columns: repeat(7, 1fr);
577
+ margin-top: var(--pui-spacing-2);
578
+ background-color: var(--pui-color-gray-100);
579
+ border-radius: var(--pui-radius-sm);
580
+ border: 1px solid var(--pui-border-default);
581
+ }
582
+
583
+ .day-label {
584
+ color: var(--pui-color-gray-500);
585
+ padding: var(--pui-spacing-2);
586
+ font-size: var(--pui-font-size-xs);
587
+ font-weight: var(--pui-font-weight-semibold);
588
+ text-transform: uppercase;
589
+ text-align: center;
590
+ }
591
+
592
+ .days-grid {
593
+ display: grid;
594
+ grid-template-columns: repeat(7, 1fr);
595
+ gap: 2px;
596
+ margin-top: var(--pui-spacing-2);
597
+ }
598
+
599
+ .day-button {
600
+ color: var(--pui-text-primary);
601
+ width: 2.25rem;
602
+ height: 2.25rem;
603
+ border-radius: var(--pui-radius-md);
604
+ border: none;
605
+ background: transparent;
606
+ cursor: pointer;
607
+ font-size: var(--pui-font-size-sm);
608
+ transition: all var(--pui-transition-fast) var(--pui-ease-in-out);
609
+ justify-self: center;
610
+ }
611
+
612
+ .day-button:hover {
613
+ background-color: var(--pui-bg-hover);
614
+ color: var(--pui-color-primary);
615
+ }
616
+
617
+ .today {
618
+ background-color: var(--pui-bg-active);
619
+ color: var(--pui-color-primary);
620
+ font-weight: var(--pui-font-weight-semibold);
621
+ }
622
+
623
+ .selected {
624
+ background-color: var(--pui-color-primary);
625
+ color: var(--pui-color-white);
626
+ font-weight: var(--pui-font-weight-semibold);
627
+ }
628
+
629
+ .text-red {
630
+ color: var(--pui-color-danger);
631
+ }
632
+
633
+ .text-grey {
634
+ color: var(--pui-text-muted);
635
+ }
636
+
637
+ .disabled {
638
+ color: var(--pui-text-disabled) !important;
639
+ background-color: transparent !important;
640
+ cursor: not-allowed !important;
641
+ opacity: 0.5;
642
+ }
643
+
644
+ .disabled:hover {
645
+ background-color: transparent !important;
646
+ color: var(--pui-text-disabled) !important;
647
+ }
648
+
649
+ :global(.dark) {
650
+ .days-header-bar {
651
+ background-color: var(--pui-color-dark-200);
652
+ }
653
+
654
+ .day-label {
655
+ color: var(--pui-color-gray-300);
656
+ }
657
+
658
+ .day-button:hover {
659
+ background-color: var(--pui-bg-hover);
660
+ color: var(--pui-color-secondary);
661
+ }
662
+
663
+ .today {
664
+ background-color: var(--pui-bg-active);
665
+ color: var(--pui-color-secondary);
666
+ }
667
+
668
+ .selected {
669
+ background-color: var(--pui-color-secondary);
670
+ color: var(--pui-color-primary);
671
+ }
672
+
673
+ .text-grey {
674
+ color: var(--pui-color-gray-500);
675
+ }
676
+
677
+ .disabled {
678
+ color: var(--pui-color-gray-500) !important;
679
+ background-color: transparent !important;
680
+ cursor: not-allowed !important;
681
+ opacity: 0.5;
682
+ }
683
+
684
+ .disabled:hover {
685
+ background-color: transparent !important;
686
+ color: var(--pui-color-gray-500) !important;
687
+ }
688
+
689
+ .month-label.clickable:hover {
690
+ background-color: var(--pui-bg-hover);
691
+ }
692
+
693
+ .year-button:hover:not(.disabled) {
694
+ background-color: var(--pui-bg-hover);
695
+ color: var(--pui-color-secondary);
696
+ }
697
+
698
+ .year-button.current-year {
699
+ background-color: var(--pui-bg-active);
700
+ color: var(--pui-color-secondary);
701
+ }
702
+
703
+ .year-button.selected {
704
+ background-color: var(--pui-color-secondary);
705
+ color: var(--pui-color-primary);
706
+ }
707
+
708
+ .year-button.disabled {
709
+ color: var(--pui-color-gray-500) !important;
710
+ }
711
+ }
712
+ </style>