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