noph-ui 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +14 -13
  2. package/dist/autocomplete/AutoComplete.svelte +1 -1
  3. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  4. package/dist/autocomplete/types.d.ts +1 -1
  5. package/dist/button/Button.svelte +1 -2
  6. package/dist/button/IconButton.svelte +1 -1
  7. package/dist/button/SegmentedButton.svelte +1 -1
  8. package/dist/card/Card.svelte +1 -1
  9. package/dist/checkbox/Checkbox.svelte +1 -1
  10. package/dist/chip/AssistChip.svelte +1 -1
  11. package/dist/chip/ChipSet.svelte +1 -1
  12. package/dist/chip/FilterChip.svelte +1 -1
  13. package/dist/chip/FilterChip.svelte.d.ts +1 -1
  14. package/dist/chip/InputChip.svelte +1 -3
  15. package/dist/chip/types.d.ts +1 -1
  16. package/dist/date-picker/Calendar.svelte +487 -0
  17. package/dist/date-picker/Calendar.svelte.d.ts +4 -0
  18. package/dist/date-picker/CalendarHeader.svelte +229 -0
  19. package/dist/date-picker/CalendarHeader.svelte.d.ts +27 -0
  20. package/dist/date-picker/DatePickerDialog.svelte +481 -0
  21. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -0
  22. package/dist/date-picker/DateRangePicker.svelte +351 -0
  23. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -0
  24. package/dist/date-picker/DockedDatePicker.svelte +518 -5
  25. package/dist/date-picker/DockedDatePicker.svelte.d.ts +3 -17
  26. package/dist/date-picker/SelectionList.svelte +148 -0
  27. package/dist/date-picker/SelectionList.svelte.d.ts +14 -0
  28. package/dist/date-picker/YearGrid.svelte +195 -0
  29. package/dist/date-picker/YearGrid.svelte.d.ts +11 -0
  30. package/dist/date-picker/dateUtils.d.ts +41 -0
  31. package/dist/date-picker/dateUtils.js +153 -0
  32. package/dist/date-picker/enterAnimation.d.ts +1 -0
  33. package/dist/date-picker/enterAnimation.js +10 -0
  34. package/dist/date-picker/index.d.ts +6 -0
  35. package/dist/date-picker/index.js +6 -0
  36. package/dist/date-picker/types.d.ts +107 -0
  37. package/dist/date-picker/types.js +1 -0
  38. package/dist/dialog/Dialog.svelte +17 -12
  39. package/dist/dialog/types.d.ts +1 -1
  40. package/dist/divider/Divider.svelte +1 -1
  41. package/dist/icons/ArrowDropDownIcon.svelte +3 -0
  42. package/dist/icons/ArrowDropDownIcon.svelte.d.ts +26 -0
  43. package/dist/icons/ChevronLeftIcon.svelte +3 -0
  44. package/dist/icons/ChevronLeftIcon.svelte.d.ts +26 -0
  45. package/dist/icons/ChevronRightIcon.svelte +3 -0
  46. package/dist/icons/ChevronRightIcon.svelte.d.ts +26 -0
  47. package/dist/icons/EditCalendarIcon.svelte +5 -0
  48. package/dist/icons/EditCalendarIcon.svelte.d.ts +26 -0
  49. package/dist/icons/index.d.ts +5 -1
  50. package/dist/icons/index.js +5 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +2 -0
  53. package/dist/list/Item.svelte +1 -1
  54. package/dist/list/List.svelte +1 -1
  55. package/dist/list/ListItem.svelte +1 -1
  56. package/dist/loading-indicator/LoadingIndicator.svelte +135 -0
  57. package/dist/loading-indicator/LoadingIndicator.svelte.d.ts +4 -0
  58. package/dist/loading-indicator/index.d.ts +1 -0
  59. package/dist/loading-indicator/index.js +1 -0
  60. package/dist/loading-indicator/shapes.d.ts +6 -0
  61. package/dist/loading-indicator/shapes.js +49 -0
  62. package/dist/loading-indicator/types.d.ts +7 -0
  63. package/dist/loading-indicator/types.js +1 -0
  64. package/dist/menu/Menu.svelte +3 -3
  65. package/dist/menu/MenuItem.svelte +1 -1
  66. package/dist/menu/types.d.ts +1 -1
  67. package/dist/navigation-drawer/NavigationDrawer.svelte +1 -1
  68. package/dist/navigation-drawer/NavigationDrawerItem.svelte +2 -2
  69. package/dist/navigation-rail/NavigationRail.svelte +1 -1
  70. package/dist/navigation-rail/NavigationRailItem.svelte +1 -1
  71. package/dist/progress/CircularProgress.svelte +1 -1
  72. package/dist/progress/LinearProgress.svelte +24 -7
  73. package/dist/radio/Radio.svelte +1 -1
  74. package/dist/ripple/Ripple.svelte +1 -1
  75. package/dist/select/Option.svelte +0 -1
  76. package/dist/select/Select.svelte +1 -1
  77. package/dist/select/Select.svelte.d.ts +1 -1
  78. package/dist/select/VirtualList.svelte +3 -4
  79. package/dist/slider/Slider.svelte +83 -19
  80. package/dist/slider/Slider.svelte.d.ts +1 -1
  81. package/dist/snackbar/Snackbar.svelte +3 -3
  82. package/dist/switch/Switch.svelte +1 -1
  83. package/dist/tabs/Tab.svelte +1 -1
  84. package/dist/tabs/Tabs.svelte +1 -1
  85. package/dist/tabs/Tabs.svelte.d.ts +1 -1
  86. package/dist/text-field/TextField.svelte +3 -3
  87. package/dist/text-field/TextField.svelte.d.ts +1 -1
  88. package/dist/tooltip/Tooltip.svelte +1 -1
  89. package/dist/types.d.ts +2 -0
  90. package/dist/types.js +23 -21
  91. package/package.json +14 -11
@@ -0,0 +1,481 @@
1
+ <script lang="ts">
2
+ import Button from '../button/Button.svelte'
3
+ import IconButton from '../button/IconButton.svelte'
4
+ import Dialog from '../dialog/Dialog.svelte'
5
+ import Divider from '../divider/Divider.svelte'
6
+ import ArrowDropDownIcon from '../icons/ArrowDropDownIcon.svelte'
7
+ import CalendarToday from '../icons/CalendarToday.svelte'
8
+ import ChevronLeftIcon from '../icons/ChevronLeftIcon.svelte'
9
+ import ChevronRightIcon from '../icons/ChevronRightIcon.svelte'
10
+ import EditCalendarIcon from '../icons/EditCalendarIcon.svelte'
11
+ import TextField from '../text-field/TextField.svelte'
12
+ import { tick } from 'svelte'
13
+ import Calendar from './Calendar.svelte'
14
+ import YearGrid from './YearGrid.svelte'
15
+ import {
16
+ addMonths,
17
+ clampMonthInYear,
18
+ compareDays,
19
+ DEFAULT_YEAR_RANGE,
20
+ formatDate,
21
+ formatDateMedium,
22
+ formatMonthYear,
23
+ getDatePattern,
24
+ getFirstDayOfWeek,
25
+ isWithin,
26
+ parseDateInput,
27
+ parseISODate,
28
+ startOfMonth,
29
+ toISODate,
30
+ today as getToday,
31
+ } from './dateUtils.js'
32
+ import { afterTwoFrames } from './enterAnimation.js'
33
+ import type { DatePickerDialogProps, ISODate } from './types.js'
34
+
35
+ let {
36
+ value = $bindable(),
37
+ displayMonth = $bindable(),
38
+ open = $bindable(false),
39
+ element = $bindable(),
40
+ locale,
41
+ firstDayOfWeek,
42
+ min,
43
+ max,
44
+ yearRange = DEFAULT_YEAR_RANGE,
45
+ isDateEnabled,
46
+ adjacentMonthDays = false,
47
+ title = 'Select date',
48
+ headline,
49
+ label = 'Date',
50
+ supportingText,
51
+ name,
52
+ form,
53
+ modeToggle = false,
54
+ cancelLabel = 'Cancel',
55
+ confirmLabel = 'OK',
56
+ nextMonthLabel = 'Next month',
57
+ previousMonthLabel = 'Previous month',
58
+ selectYearLabel = 'Select year',
59
+ calendarModeLabel = 'Switch to calendar mode',
60
+ inputModeLabel = 'Switch to text input mode',
61
+ selectedDateLabel = 'selected',
62
+ onchange,
63
+ onconfirm,
64
+ oncancel,
65
+ ...attributes
66
+ }: DatePickerDialogProps = $props()
67
+
68
+ const uid = $props.id()
69
+
70
+ let mode = $state<'calendar' | 'input'>('calendar')
71
+ let yearPickerVisible = $state(false)
72
+ let pending = $state<ISODate | undefined>(undefined)
73
+ let focusedDay = $state<Date | undefined>(undefined)
74
+ let text = $state('')
75
+ let inputInvalid = $state(false)
76
+
77
+ let confirming = false
78
+ let monthBeforeOpen: ISODate | undefined
79
+
80
+ let todayValue = $derived(getToday())
81
+ let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
82
+ let minDate = $derived(parseISODate(min))
83
+ let maxDate = $derived(parseISODate(max))
84
+ let pendingDate = $derived(parseISODate(pending))
85
+ let pattern = $derived(getDatePattern(locale))
86
+ let monthDate = $derived(
87
+ parseISODate(displayMonth) ?? startOfMonth(pendingDate ?? parseISODate(value) ?? todayValue),
88
+ )
89
+ let headlineText = $derived(
90
+ headline ?? (pendingDate ? formatDateMedium(pendingDate, locale) : title),
91
+ )
92
+
93
+ let canPreviousMonth = $derived(
94
+ !minDate ||
95
+ compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth(), 0), minDate) >= 0,
96
+ )
97
+ let canNextMonth = $derived(
98
+ !maxDate ||
99
+ compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 1), maxDate) <= 0,
100
+ )
101
+
102
+ let canConfirm = $derived(
103
+ !pendingDate ||
104
+ (isWithin(pendingDate, minDate, maxDate) && (isDateEnabled?.(pendingDate) ?? true)),
105
+ )
106
+
107
+ let modeEnter = $state<'' | 'to-input' | 'to-calendar'>('')
108
+ let yearEnter = $state(false)
109
+ let cancelModeEnter: (() => void) | undefined
110
+ let cancelYearEnter: (() => void) | undefined
111
+
112
+ const setMode = (next: 'calendar' | 'input') => {
113
+ if (mode === next) return
114
+ mode = next
115
+ cancelModeEnter?.()
116
+ modeEnter = next === 'input' ? 'to-input' : 'to-calendar'
117
+ cancelModeEnter = afterTwoFrames(() => (modeEnter = ''))
118
+ }
119
+
120
+ const showYearPicker = (next: boolean) => {
121
+ yearPickerVisible = next
122
+ if (!next) return
123
+ cancelYearEnter?.()
124
+ yearEnter = true
125
+ cancelYearEnter = afterTwoFrames(() => (yearEnter = false))
126
+ }
127
+
128
+ const isOpen = () => !!element?.matches(':popover-open')
129
+
130
+ $effect(() => {
131
+ if (open && !isOpen()) element?.showPopover()
132
+ if (!open && isOpen()) element?.hidePopover()
133
+ })
134
+
135
+ const setMonth = (date: Date) => {
136
+ displayMonth = toISODate(startOfMonth(date))
137
+ }
138
+
139
+ const cancel = () => {
140
+ open = false
141
+ }
142
+
143
+ const confirm = () => {
144
+ confirming = true
145
+ value = pending
146
+ onchange?.(pending)
147
+ onconfirm?.(pending)
148
+ open = false
149
+ }
150
+
151
+ const focusCalendarWhenReady = async () => {
152
+ await tick()
153
+ element?.querySelector<HTMLElement>('.np-calendar-day[tabindex="0"]')?.focus()
154
+ }
155
+
156
+ const syncText = () => {
157
+ text = pendingDate ? formatDate(pendingDate, locale) : ''
158
+ inputInvalid = false
159
+ }
160
+
161
+ const usableDate = (typed: string) => {
162
+ const parsed = parseDateInput(typed, locale)
163
+ if (!parsed || !isWithin(parsed, minDate, maxDate)) return undefined
164
+ return (isDateEnabled?.(parsed) ?? true) ? parsed : undefined
165
+ }
166
+
167
+ const handleInput = (event: Event) => {
168
+ text = (event.currentTarget as HTMLInputElement).value
169
+ inputInvalid = false
170
+ const parsed = usableDate(text)
171
+ pending = parsed ? toISODate(parsed) : undefined
172
+ if (parsed) setMonth(parsed)
173
+ }
174
+
175
+ const handleBlur = () => {
176
+ if (!text.trim()) {
177
+ inputInvalid = false
178
+ return
179
+ }
180
+ const parsed = usableDate(text)
181
+ inputInvalid = !parsed
182
+ if (parsed) text = formatDate(parsed, locale)
183
+ }
184
+ </script>
185
+
186
+ <Dialog
187
+ {...attributes}
188
+ bind:element
189
+ aria-label={title}
190
+ class={['np-date-picker-dialog', attributes.class]}
191
+ --np-dialog-container-width="fit-content"
192
+ --np-dialog-container-min-width="0"
193
+ --np-dialog-padding="0"
194
+ --np-dialog-container-color="var(--np-date-picker-dialog-container-color, var(--np-color-surface-container-high))"
195
+ --np-dialog-container-shape="var(--np-date-picker-dialog-container-shape, var(--np-shape-corner-extra-large))"
196
+ ontoggle={(event) => {
197
+ const nowOpen = event.newState === 'open'
198
+ open = nowOpen
199
+ if (nowOpen) {
200
+ monthBeforeOpen = displayMonth
201
+ confirming = false
202
+ pending = value ?? undefined
203
+ setMode('calendar')
204
+ showYearPicker(false)
205
+ focusedDay = undefined
206
+ syncText()
207
+ focusCalendarWhenReady()
208
+ } else {
209
+ displayMonth = monthBeforeOpen
210
+ if (!confirming) oncancel?.()
211
+ confirming = false
212
+ }
213
+ }}
214
+ >
215
+ <input class="np-date-picker-dialog-value" type="hidden" value={value ?? ''} {name} {form} />
216
+ {#if open}
217
+ <div class="np-date-picker-dialog-content">
218
+ <div class="np-date-picker-dialog-header">
219
+ <div class="np-date-picker-dialog-header-text">
220
+ <span class="np-date-picker-dialog-title">{title}</span>
221
+ <h2 class="np-date-picker-dialog-headline">{headlineText}</h2>
222
+ </div>
223
+ {#if modeToggle}
224
+ <IconButton
225
+ type="button"
226
+ aria-label={mode === 'calendar' ? inputModeLabel : calendarModeLabel}
227
+ onclick={() => {
228
+ // The calendar moves `pending` on without touching the field, so the text has to
229
+ // catch up before it is shown again. Otherwise the field contradicts the headline
230
+ // and OK confirms a date the user never saw there.
231
+ if (mode === 'calendar') syncText()
232
+ setMode(mode === 'calendar' ? 'input' : 'calendar')
233
+ showYearPicker(false)
234
+ }}
235
+ >
236
+ {#if mode === 'calendar'}
237
+ <EditCalendarIcon />
238
+ {:else}
239
+ <CalendarToday />
240
+ {/if}
241
+ </IconButton>
242
+ {/if}
243
+ </div>
244
+ <Divider />
245
+
246
+ {#if mode === 'input'}
247
+ <div class={['np-date-picker-dialog-input', modeEnter && modeEnter]}>
248
+ <TextField
249
+ {label}
250
+ supportingText={supportingText ?? pattern}
251
+ value={text}
252
+ aria-invalid={inputInvalid ? 'true' : undefined}
253
+ oninput={handleInput}
254
+ onblur={handleBlur}
255
+ />
256
+ </div>
257
+ {:else}
258
+ <div class="np-date-picker-dialog-navigation">
259
+ <button
260
+ type="button"
261
+ class="np-date-picker-dialog-year-button"
262
+ aria-label="{selectYearLabel}, {formatMonthYear(monthDate, locale)}"
263
+ aria-expanded={yearPickerVisible}
264
+ aria-controls={yearPickerVisible ? `${uid}-years` : undefined}
265
+ onclick={() => showYearPicker(!yearPickerVisible)}
266
+ >
267
+ <span aria-live="polite">{formatMonthYear(monthDate, locale)}</span>
268
+ <span class={['np-date-picker-dialog-caret', yearPickerVisible && 'expanded']}>
269
+ <ArrowDropDownIcon />
270
+ </span>
271
+ </button>
272
+ {#if !yearPickerVisible}
273
+ <div class="np-date-picker-dialog-arrows">
274
+ <IconButton
275
+ type="button"
276
+ aria-label={previousMonthLabel}
277
+ disabled={!canPreviousMonth}
278
+ onclick={() => setMonth(addMonths(monthDate, -1))}
279
+ >
280
+ <ChevronLeftIcon />
281
+ </IconButton>
282
+ <IconButton
283
+ type="button"
284
+ aria-label={nextMonthLabel}
285
+ disabled={!canNextMonth}
286
+ onclick={() => setMonth(addMonths(monthDate, 1))}
287
+ >
288
+ <ChevronRightIcon />
289
+ </IconButton>
290
+ </div>
291
+ {/if}
292
+ </div>
293
+
294
+ {#if yearPickerVisible}
295
+ <YearGrid
296
+ class={yearEnter ? 'entering' : undefined}
297
+ id="{uid}-years"
298
+ aria-label={selectYearLabel}
299
+ {yearRange}
300
+ {minDate}
301
+ {maxDate}
302
+ value={monthDate.getFullYear()}
303
+ onselect={(year) => {
304
+ setMonth(
305
+ new Date(year, clampMonthInYear(year, monthDate.getMonth(), minDate, maxDate), 1),
306
+ )
307
+ showYearPicker(false)
308
+ }}
309
+ />
310
+ <Divider />
311
+ {:else}
312
+ <div class={['np-date-picker-dialog-calendar', modeEnter && modeEnter]}>
313
+ <Calendar
314
+ month={monthDate}
315
+ selected={pendingDate}
316
+ min={minDate}
317
+ max={maxDate}
318
+ {locale}
319
+ firstDayOfWeek={week}
320
+ {isDateEnabled}
321
+ {adjacentMonthDays}
322
+ todayDate={todayValue}
323
+ focusedDate={focusedDay}
324
+ selectedLabel={selectedDateLabel}
325
+ onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
326
+ onselect={(date) => {
327
+ pending = toISODate(date)
328
+ focusedDay = date
329
+ }}
330
+ onfocusday={(date) => {
331
+ focusedDay = date
332
+ if (
333
+ date.getMonth() !== monthDate.getMonth() ||
334
+ date.getFullYear() !== monthDate.getFullYear()
335
+ ) {
336
+ setMonth(date)
337
+ }
338
+ }}
339
+ />
340
+ </div>
341
+ {/if}
342
+ {/if}
343
+ </div>
344
+ {/if}
345
+
346
+ {#snippet actions()}
347
+ <Button type="button" variant="text" onclick={cancel}>{cancelLabel}</Button>
348
+ <Button type="button" variant="text" disabled={!canConfirm} onclick={confirm}>
349
+ {confirmLabel}
350
+ </Button>
351
+ {/snippet}
352
+ </Dialog>
353
+
354
+ <style>
355
+ .np-date-picker-dialog-content {
356
+ width: var(--np-date-picker-dialog-container-width, 22.5rem);
357
+ max-width: 100%;
358
+ }
359
+
360
+ :global(.np-date-picker-dialog .np-dialog-actions) {
361
+ margin-top: 0;
362
+ padding: 0.5rem 0.75rem 0.75rem;
363
+ }
364
+
365
+ .np-date-picker-dialog-header {
366
+ box-sizing: border-box;
367
+ height: 7.5rem;
368
+ display: flex;
369
+ align-items: flex-end;
370
+ justify-content: space-between;
371
+ gap: 0.5rem;
372
+ padding: 1rem 0.75rem 0.75rem 1.5rem;
373
+ color: var(--np-color-on-surface-variant);
374
+ }
375
+
376
+ .np-date-picker-dialog-header-text {
377
+ display: flex;
378
+ flex-direction: column;
379
+ gap: 2rem;
380
+ min-width: 0;
381
+ }
382
+
383
+ .np-date-picker-dialog-title {
384
+ font-size: 0.875rem;
385
+ line-height: 1.25rem;
386
+ font-weight: 500;
387
+ }
388
+
389
+ .np-date-picker-dialog-headline {
390
+ margin: 0;
391
+ font-size: 2rem;
392
+ line-height: 2.5rem;
393
+ font-weight: 400;
394
+ overflow: hidden;
395
+ text-overflow: ellipsis;
396
+ white-space: nowrap;
397
+ }
398
+
399
+ .np-date-picker-dialog-navigation {
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: space-between;
403
+ height: 3.5rem;
404
+ padding-inline: 0.75rem;
405
+ color: var(--np-color-on-surface-variant);
406
+ }
407
+
408
+ .np-date-picker-dialog-arrows {
409
+ display: flex;
410
+ }
411
+
412
+ .np-date-picker-dialog-year-button {
413
+ font: inherit;
414
+ -webkit-tap-highlight-color: transparent;
415
+ background: none;
416
+ border: 0;
417
+ color: inherit;
418
+ cursor: pointer;
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 0.25rem;
422
+ height: 2.25rem;
423
+ padding-inline: 0.75rem;
424
+ border-radius: var(--np-shape-corner-full);
425
+ font-size: 0.875rem;
426
+ line-height: 1.25rem;
427
+ }
428
+
429
+ .np-date-picker-dialog-year-button:focus-visible {
430
+ outline: 3px solid var(--np-color-secondary);
431
+ outline-offset: 2px;
432
+ }
433
+
434
+ .np-date-picker-dialog-caret {
435
+ display: flex;
436
+ transition: rotate var(--np-motion-standard-fast-effects);
437
+ }
438
+
439
+ .np-date-picker-dialog-caret.expanded {
440
+ rotate: 180deg;
441
+ }
442
+
443
+ :global(.np-date-picker-dialog-caret svg) {
444
+ width: 1.25rem;
445
+ height: 1.25rem;
446
+ fill: currentColor;
447
+ }
448
+
449
+ .np-date-picker-dialog-calendar {
450
+ padding-inline: 0.75rem;
451
+ }
452
+
453
+ .np-date-picker-dialog-input {
454
+ padding: 1rem 1.5rem 0;
455
+ }
456
+
457
+ .np-date-picker-dialog-content {
458
+ overflow: hidden;
459
+ }
460
+
461
+ .np-date-picker-dialog-input.to-input {
462
+ transform: translateY(100%);
463
+ opacity: 0;
464
+ transition: none;
465
+ }
466
+
467
+ .np-date-picker-dialog-calendar.to-calendar {
468
+ transform: translateY(-3rem);
469
+ opacity: 0;
470
+ transition: none;
471
+ }
472
+
473
+ @media (prefers-reduced-motion: no-preference) {
474
+ .np-date-picker-dialog-input,
475
+ .np-date-picker-dialog-calendar {
476
+ transition:
477
+ transform var(--np-motion-expressive-default-spatial),
478
+ opacity var(--np-motion-expressive-default-effects);
479
+ }
480
+ }
481
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { DatePickerDialogProps } from './types.ts';
2
+ declare const DatePickerDialog: import("svelte").Component<DatePickerDialogProps, {}, "value" | "element" | "displayMonth" | "open">;
3
+ type DatePickerDialog = ReturnType<typeof DatePickerDialog>;
4
+ export default DatePickerDialog;