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
@@ -1,11 +1,524 @@
1
1
  <script lang="ts">
2
+ import Button from '../button/Button.svelte'
2
3
  import IconButton from '../button/IconButton.svelte'
4
+ import Divider from '../divider/Divider.svelte'
3
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
+ import Menu from '../menu/Menu.svelte'
4
7
  import TextField from '../text-field/TextField.svelte'
8
+ import { onMount, tick } from 'svelte'
9
+ import Calendar from './Calendar.svelte'
10
+ import CalendarHeader from './CalendarHeader.svelte'
11
+ import SelectionList from './SelectionList.svelte'
12
+ import {
13
+ addMonths,
14
+ clampMonthInYear,
15
+ compareDays,
16
+ DEFAULT_YEAR_RANGE,
17
+ formatDate,
18
+ getDatePattern,
19
+ getFirstDayOfWeek,
20
+ getMonthNames,
21
+ getWeekRowCount,
22
+ isMonthWithin,
23
+ isWithin,
24
+ isYearWithin,
25
+ parseDateInput,
26
+ parseISODate,
27
+ startOfMonth,
28
+ toISODate,
29
+ today as getToday,
30
+ } from './dateUtils.js'
31
+ import { afterTwoFrames } from './enterAnimation.js'
32
+ import type { DockedDatePickerProps, ISODate } from './types.js'
33
+
34
+ let {
35
+ value = $bindable(),
36
+ displayMonth = $bindable(),
37
+ open = $bindable(false),
38
+ element = $bindable(),
39
+ locale,
40
+ firstDayOfWeek,
41
+ min,
42
+ max,
43
+ yearRange = DEFAULT_YEAR_RANGE,
44
+ isDateEnabled,
45
+ adjacentMonthDays = false,
46
+ label = 'Date',
47
+ supportingText,
48
+ issues,
49
+ variant = 'outlined',
50
+ name,
51
+ form,
52
+ required,
53
+ disabled,
54
+ readonly,
55
+ noAsterisk,
56
+ autocomplete = 'off',
57
+ cancelLabel = 'Cancel',
58
+ confirmLabel = 'OK',
59
+ nextMonthLabel = 'Next month',
60
+ previousMonthLabel = 'Previous month',
61
+ nextYearLabel = 'Next year',
62
+ previousYearLabel = 'Previous year',
63
+ selectMonthLabel = 'Select month',
64
+ selectYearLabel = 'Select year',
65
+ openCalendarLabel = 'Show date picker',
66
+ invalidDateMessage = 'Enter a valid date.',
67
+ selectedDateLabel = 'selected',
68
+ onchange,
69
+ ...attributes
70
+ }: DockedDatePickerProps = $props()
71
+
72
+ const ISO_PATTERN = 'YYYY-MM-DD'
73
+
74
+ const uid = $props.id()
75
+
76
+ let anchorElement = $state<HTMLDivElement>()
77
+ let menuElement = $state<HTMLDivElement>()
78
+ let mode = $state<'days' | 'months' | 'years'>('days')
79
+ let pending = $state<ISODate | undefined>(undefined)
80
+ let focusedDay = $state<Date | undefined>(undefined)
81
+ let inputElement = $state<HTMLInputElement | HTMLTextAreaElement>()
82
+
83
+ let typed = $state.raw<{ source: ISODate | null | undefined; text: string } | undefined>()
84
+ let invalidText = $state<string | undefined>()
85
+ let monthBeforeOpen: ISODate | undefined
86
+
87
+ let todayValue = $derived(getToday())
88
+ let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
89
+ let minDate = $derived(parseISODate(min))
90
+ let maxDate = $derived(parseISODate(max))
91
+ let committed = $derived(parseISODate(value))
92
+ let pendingDate = $derived(open ? parseISODate(pending) : committed)
93
+ let mounted = $state(false)
94
+
95
+ onMount(() => {
96
+ mounted = true
97
+ })
98
+
99
+ let localeKnown = $derived(locale !== undefined || mounted)
100
+ let pattern = $derived(localeKnown ? getDatePattern(locale) : ISO_PATTERN)
101
+ let monthDate = $derived(
102
+ parseISODate(displayMonth) ?? startOfMonth(pendingDate ?? committed ?? todayValue),
103
+ )
104
+ let monthNames = $derived(getMonthNames(locale, 'long'))
105
+ let shortMonthNames = $derived(getMonthNames(locale, 'short'))
106
+ let rowCount = $derived(getWeekRowCount(monthDate, week))
107
+
108
+ let viewEnter = $state(false)
109
+ let cancelViewEnter: (() => void) | undefined
110
+
111
+ const setMode = (next: 'days' | 'months' | 'years') => {
112
+ if (mode === next) return
113
+ mode = next
114
+ cancelViewEnter?.()
115
+ viewEnter = true
116
+ cancelViewEnter = afterTwoFrames(() => (viewEnter = false))
117
+ }
118
+
119
+ const formatValue = () => {
120
+ if (!committed) return ''
121
+ return localeKnown ? formatDate(committed, locale) : toISODate(committed)
122
+ }
123
+
124
+ let text = $derived(typed && typed.source === value ? typed.text : formatValue())
125
+
126
+ let inputInvalid = $derived(invalidText !== undefined && invalidText === text)
127
+
128
+ let monthOptions = $derived(
129
+ monthNames.map((monthName, index) => ({
130
+ value: index,
131
+ label: monthName,
132
+ disabled: !isMonthWithin(monthDate.getFullYear(), index, minDate, maxDate),
133
+ })),
134
+ )
135
+
136
+ let yearOptions = $derived(
137
+ Array.from({ length: yearRange[1] - yearRange[0] + 1 }, (_, index) => {
138
+ const year = yearRange[0] + index
139
+ return {
140
+ value: year,
141
+ label: `${year}`,
142
+ disabled: !isYearWithin(year, minDate, maxDate),
143
+ }
144
+ }),
145
+ )
146
+
147
+ let canPreviousMonth = $derived(
148
+ !minDate ||
149
+ compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth(), 0), minDate) >= 0,
150
+ )
151
+ let canNextMonth = $derived(
152
+ !maxDate ||
153
+ compareDays(new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 1), maxDate) <= 0,
154
+ )
155
+ let canPreviousYear = $derived(
156
+ monthDate.getFullYear() > yearRange[0] &&
157
+ (!minDate || compareDays(addMonths(monthDate, -12), startOfMonth(minDate)) >= 0),
158
+ )
159
+ let canNextYear = $derived(
160
+ monthDate.getFullYear() < yearRange[1] &&
161
+ (!maxDate || compareDays(addMonths(monthDate, 12), startOfMonth(maxDate)) <= 0),
162
+ )
163
+
164
+ const setMonth = (date: Date) => {
165
+ displayMonth = toISODate(startOfMonth(date))
166
+ }
167
+
168
+ const commit = (next: ISODate | undefined) => {
169
+ if (value === next) return
170
+ value = next
171
+ onchange?.(next)
172
+ }
173
+
174
+ const isShowing = () => !!menuElement?.matches(':popover-open')
175
+
176
+ const openPicker = () => {
177
+ if (isShowing()) return
178
+ if (disabled || readonly) {
179
+ open = false
180
+ return
181
+ }
182
+ menuElement?.showPopover()
183
+ }
184
+
185
+ const focusCalendarWhenReady = async () => {
186
+ await tick()
187
+ focusCalendar()
188
+ }
189
+
190
+ $effect(() => {
191
+ if (open) openPicker()
192
+ else if (isShowing()) menuElement?.hidePopover()
193
+ })
194
+
195
+ const focusCalendar = () => {
196
+ menuElement?.querySelector<HTMLElement>('.np-calendar-day[tabindex="0"]')?.focus()
197
+ }
198
+
199
+ const closePicker = (restoreFocus = true) => {
200
+ menuElement?.hidePopover()
201
+ if (restoreFocus) {
202
+ anchorElement?.querySelector<HTMLElement>('input')?.focus()
203
+ }
204
+ }
205
+
206
+ const selectDay = (date: Date) => {
207
+ pending = toISODate(date)
208
+ setMonth(date)
209
+ focusedDay = date
210
+ }
211
+
212
+ const confirm = () => {
213
+ typed = undefined
214
+ invalidText = undefined
215
+ commit(pending)
216
+ closePicker()
217
+ }
218
+
219
+ const usableDate = (typed: string) => {
220
+ const parsed = parseDateInput(typed, locale)
221
+ if (!parsed) return undefined
222
+ if (!isWithin(parsed, minDate, maxDate)) return undefined
223
+ return (isDateEnabled?.(parsed) ?? true) ? parsed : undefined
224
+ }
225
+
226
+ const handleInput = (event: Event) => {
227
+ const next = (event.currentTarget as HTMLInputElement).value
228
+ const parsed = usableDate(next)
229
+ commit(parsed ? toISODate(parsed) : undefined)
230
+ if (parsed) setMonth(parsed)
231
+ // Recorded after the commit, so that it is remembered against the value it just produced.
232
+ typed = { source: value, text: next }
233
+ invalidText = undefined
234
+ }
235
+
236
+ const handleBlur = () => {
237
+ if (!text.trim()) {
238
+ invalidText = undefined
239
+ return
240
+ }
241
+ const parsed = usableDate(text)
242
+ invalidText = parsed ? undefined : text
243
+ // Leaving the field tidies a loose entry: dropping what was typed falls back to the
244
+ // canonical formatting of the value it produced.
245
+ if (parsed) typed = undefined
246
+ }
247
+
248
+ $effect(() => {
249
+ const input = inputElement
250
+ if (!input) return
251
+ const unusable = text.trim() !== '' && !usableDate(text)
252
+ input.setCustomValidity(unusable ? invalidDateMessage : '')
253
+ })
254
+
255
+ const chooseMonth = (month: number) => {
256
+ setMonth(new Date(monthDate.getFullYear(), month, 1))
257
+ setMode('days')
258
+ }
259
+
260
+ const chooseYear = (year: number) => {
261
+ // A year is offered as soon as any of its days is in range, so keeping the month the list was
262
+ // opened from can land on a month where nothing at all is selectable.
263
+ setMonth(new Date(year, clampMonthInYear(year, monthDate.getMonth(), minDate, maxDate), 1))
264
+ setMode('days')
265
+ }
5
266
  </script>
6
267
 
7
- <TextField label="Date" supportingText="MM/DD/YYYY">
8
- {#snippet end()}
9
- <IconButton><CalendarToday /></IconButton>
10
- {/snippet}
11
- </TextField>
268
+ <div {...attributes} bind:this={element} class={['np-docked-date-picker', attributes.class]}>
269
+ <div class="np-docked-date-picker-anchor" bind:this={anchorElement} style:anchor-name="--{uid}">
270
+ <TextField
271
+ {label}
272
+ {variant}
273
+ {issues}
274
+ {disabled}
275
+ {readonly}
276
+ {required}
277
+ {noAsterisk}
278
+ {autocomplete}
279
+ supportingText={supportingText ?? pattern}
280
+ bind:value={text}
281
+ bind:inputElement
282
+ aria-invalid={inputInvalid || issues?.length ? 'true' : undefined}
283
+ oninput={handleInput}
284
+ onblur={handleBlur}
285
+ >
286
+ {#snippet end()}
287
+ <IconButton
288
+ type="button"
289
+ aria-label={openCalendarLabel}
290
+ aria-expanded={open}
291
+ aria-haspopup="dialog"
292
+ aria-controls={open ? `${uid}-calendar` : undefined}
293
+ {disabled}
294
+ onclick={() => {
295
+ if (open) closePicker()
296
+ else openPicker()
297
+ }}
298
+ >
299
+ <CalendarToday />
300
+ </IconButton>
301
+ {/snippet}
302
+ </TextField>
303
+ <input
304
+ class="np-docked-date-picker-value"
305
+ type="hidden"
306
+ value={value ?? ''}
307
+ {name}
308
+ {form}
309
+ {disabled}
310
+ />
311
+ </div>
312
+
313
+ <Menu
314
+ id="{uid}-calendar"
315
+ role="dialog"
316
+ aria-label={label}
317
+ aria-modal="false"
318
+ class="np-docked-date-picker-menu"
319
+ style={`position-anchor:--${uid};`}
320
+ --np-menu-justify-self="none"
321
+ --np-menu-position-area="bottom span-right"
322
+ --np-menu-margin="0"
323
+ --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
324
+ --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
325
+ anchor={anchorElement}
326
+ bind:element={menuElement}
327
+ bind:open
328
+ ontoggle={({ newState }) => {
329
+ if (newState === 'open') {
330
+ monthBeforeOpen = displayMonth
331
+ pending = value ?? undefined
332
+ focusCalendarWhenReady()
333
+ }
334
+ if (newState === 'closed') {
335
+ setMode('days')
336
+ focusedDay = undefined
337
+ displayMonth = monthBeforeOpen
338
+ }
339
+ }}
340
+ >
341
+ {#if open}
342
+ <div
343
+ class={['np-docked-date-picker-container', mode !== 'days' && 'menu']}
344
+ style:--np-calendar-rows={rowCount}
345
+ >
346
+ <CalendarHeader
347
+ {mode}
348
+ monthLabel={shortMonthNames[monthDate.getMonth()]}
349
+ yearLabel={`${monthDate.getFullYear()}`}
350
+ {canPreviousMonth}
351
+ {canNextMonth}
352
+ {canPreviousYear}
353
+ {canNextYear}
354
+ {previousMonthLabel}
355
+ {nextMonthLabel}
356
+ {previousYearLabel}
357
+ {nextYearLabel}
358
+ {selectMonthLabel}
359
+ {selectYearLabel}
360
+ monthListId="{uid}-months"
361
+ yearListId="{uid}-years"
362
+ onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
363
+ onnextmonth={() => setMonth(addMonths(monthDate, 1))}
364
+ onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
365
+ onnextyear={() => setMonth(addMonths(monthDate, 12))}
366
+ ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
367
+ ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
368
+ />
369
+
370
+ {#if mode === 'days'}
371
+ <div class={['np-docked-date-picker-body', viewEnter && 'entering']}>
372
+ <Calendar
373
+ month={monthDate}
374
+ selected={pendingDate}
375
+ min={minDate}
376
+ max={maxDate}
377
+ {locale}
378
+ firstDayOfWeek={week}
379
+ {isDateEnabled}
380
+ {adjacentMonthDays}
381
+ dynamicRows
382
+ todayDate={todayValue}
383
+ focusedDate={focusedDay}
384
+ selectedLabel={selectedDateLabel}
385
+ onselect={selectDay}
386
+ onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
387
+ onfocusday={(date) => {
388
+ focusedDay = date
389
+ if (
390
+ date.getMonth() !== monthDate.getMonth() ||
391
+ date.getFullYear() !== monthDate.getFullYear()
392
+ ) {
393
+ setMonth(date)
394
+ }
395
+ }}
396
+ />
397
+ </div>
398
+ <div class="np-docked-date-picker-actions">
399
+ <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button
400
+ >
401
+ <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
402
+ </div>
403
+ {:else}
404
+ <div class={['np-docked-date-picker-menu-view', viewEnter && 'entering']}>
405
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
406
+ {#if mode === 'months'}
407
+ <SelectionList
408
+ id="{uid}-months"
409
+ aria-label={selectMonthLabel}
410
+ options={monthOptions}
411
+ value={monthDate.getMonth()}
412
+ onselect={chooseMonth}
413
+ />
414
+ {:else}
415
+ <SelectionList
416
+ id="{uid}-years"
417
+ aria-label={selectYearLabel}
418
+ options={yearOptions}
419
+ value={monthDate.getFullYear()}
420
+ onselect={chooseYear}
421
+ />
422
+ {/if}
423
+ </div>
424
+ {/if}
425
+ </div>
426
+ {/if}
427
+ </Menu>
428
+ </div>
429
+
430
+ <style>
431
+ .np-docked-date-picker {
432
+ display: inline-flex;
433
+ flex-direction: column;
434
+ }
435
+
436
+ .np-docked-date-picker-anchor {
437
+ display: inline-flex;
438
+ position: relative;
439
+ }
440
+
441
+ :global(.np-docked-date-picker-menu .np-menu) {
442
+ padding: 0;
443
+ }
444
+
445
+ .np-docked-date-picker-container {
446
+ box-sizing: border-box;
447
+ display: flex;
448
+ flex-direction: column;
449
+ width: var(--np-docked-date-picker-container-width, 22.5rem);
450
+ max-width: 100%;
451
+ padding-top: 1.25rem;
452
+ color: var(--np-color-on-surface);
453
+ }
454
+
455
+ .np-docked-date-picker-container :global(.np-date-picker-header) {
456
+ margin-inline: 0.75rem;
457
+ }
458
+
459
+ .np-docked-date-picker-body {
460
+ padding-top: 1.875rem;
461
+ padding-inline: 0.75rem;
462
+ }
463
+
464
+ .np-docked-date-picker-actions {
465
+ display: flex;
466
+ justify-content: flex-end;
467
+ align-items: center;
468
+ gap: 0.5rem;
469
+ margin-top: auto;
470
+ margin-inline: 0.75rem;
471
+ padding-top: 0.5rem;
472
+ padding-bottom: 0.75rem;
473
+ }
474
+
475
+ :global(.np-docked-date-picker-actions .np-button) {
476
+ height: 2.25rem;
477
+ }
478
+
479
+ .np-docked-date-picker-container.menu {
480
+ padding-bottom: 1.25rem;
481
+ }
482
+
483
+ .np-docked-date-picker-menu-view {
484
+ display: flex;
485
+ flex-direction: column;
486
+ overflow: hidden;
487
+ margin-top: 1.875rem;
488
+ height: calc(2.5rem + var(--np-calendar-rows) * 3rem);
489
+ }
490
+
491
+ .np-docked-date-picker-menu-view :global(.np-date-picker-selection-list) {
492
+ flex: 1;
493
+ min-height: 0;
494
+ }
495
+
496
+ .np-docked-date-picker-menu-view.entering {
497
+ height: 0;
498
+ margin-top: 0;
499
+ opacity: 0.6;
500
+ transition: none;
501
+ }
502
+
503
+ .np-docked-date-picker-body.entering {
504
+ opacity: 0.6;
505
+ transition: none;
506
+ }
507
+
508
+ @media (prefers-reduced-motion: no-preference) {
509
+ .np-docked-date-picker-menu-view {
510
+ transition:
511
+ height var(--np-motion-expressive-default-effects),
512
+ margin-top var(--np-motion-expressive-default-effects),
513
+ opacity var(--np-motion-expressive-default-effects);
514
+ }
515
+
516
+ .np-docked-date-picker-body {
517
+ transition: opacity var(--np-motion-expressive-default-effects);
518
+ }
519
+ }
520
+
521
+ .np-docked-date-picker-container :global(.np-divider) {
522
+ flex: none;
523
+ }
524
+ </style>
@@ -1,18 +1,4 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const DockedDatePicker: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type DockedDatePicker = InstanceType<typeof DockedDatePicker>;
1
+ import type { DockedDatePickerProps } from './types.ts';
2
+ declare const DockedDatePicker: import("svelte").Component<DockedDatePickerProps, {}, "value" | "element" | "displayMonth" | "open">;
3
+ type DockedDatePicker = ReturnType<typeof DockedDatePicker>;
18
4
  export default DockedDatePicker;
@@ -0,0 +1,148 @@
1
+ <script lang="ts">
2
+ import CheckIcon from '../icons/CheckIcon.svelte'
3
+ import { arrowKeyNav } from '../keyboard-nav.js'
4
+ import Ripple from '../ripple/Ripple.svelte'
5
+ import type { Attachment } from 'svelte/attachments'
6
+ import type { HTMLAttributes } from 'svelte/elements'
7
+
8
+ interface SelectionOption {
9
+ value: number
10
+ label: string
11
+ disabled?: boolean
12
+ }
13
+
14
+ interface SelectionListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
15
+ options: SelectionOption[]
16
+ value?: number
17
+ onselect?: (value: number) => void
18
+ }
19
+
20
+ let { options, value, onselect, ...attributes }: SelectionListProps = $props()
21
+
22
+ const handleKeydown = arrowKeyNav('[role="option"]')
23
+
24
+ let tabStopValue = $derived.by(() => {
25
+ if (options.some((option) => option.value === value)) return value
26
+ const offered = options.filter((option) => !option.disabled)
27
+ const pool = offered.length ? offered : options
28
+ if (!pool.length) return undefined
29
+ if (value === undefined) return pool[0].value
30
+ return pool.reduce((nearest, option) =>
31
+ Math.abs(option.value - value) < Math.abs(nearest.value - value) ? option : nearest,
32
+ ).value
33
+ })
34
+
35
+ const centerInList: Attachment<HTMLElement> = (element) => {
36
+ const list = element.closest<HTMLElement>('.np-date-picker-selection-list')
37
+ if (!list) return
38
+ list.scrollTop = element.offsetTop - list.clientHeight / 2 + element.offsetHeight / 2
39
+ }
40
+ </script>
41
+
42
+ <div
43
+ {...attributes}
44
+ role="listbox"
45
+ tabindex="-1"
46
+ class={['np-date-picker-selection-list', attributes.class]}
47
+ onkeydown={handleKeydown}
48
+ >
49
+ {#each options as option (option.value)}
50
+ {@const selected = option.value === value}
51
+ <button
52
+ {@attach selected && centerInList}
53
+ type="button"
54
+ role="option"
55
+ aria-selected={selected}
56
+ aria-disabled={option.disabled}
57
+ tabindex={option.value === tabStopValue ? 0 : -1}
58
+ class={['np-date-picker-selection-option', selected && 'selected']}
59
+ onclick={() => {
60
+ if (option.disabled) return
61
+ onselect?.(option.value)
62
+ }}
63
+ >
64
+ <span class="np-date-picker-selection-check" aria-hidden="true">
65
+ {#if selected}
66
+ <CheckIcon />
67
+ {/if}
68
+ </span>
69
+ <span class="np-date-picker-selection-label">{option.label}</span>
70
+ {#if !option.disabled}
71
+ <Ripple />
72
+ {/if}
73
+ </button>
74
+ {/each}
75
+ </div>
76
+
77
+ <style>
78
+ .np-date-picker-selection-list {
79
+ position: relative;
80
+ overflow-y: auto;
81
+ overscroll-behavior: contain;
82
+ flex: 1;
83
+ min-height: 0;
84
+ scrollbar-color: var(--np-color-surface-container-high) transparent;
85
+ scrollbar-width: thin;
86
+ }
87
+
88
+ .np-date-picker-selection-list:focus-visible {
89
+ outline: none;
90
+ }
91
+
92
+ .np-date-picker-selection-option {
93
+ font: inherit;
94
+ -webkit-tap-highlight-color: transparent;
95
+ background: none;
96
+ border: 0;
97
+ margin: 0;
98
+ width: 100%;
99
+ box-sizing: border-box;
100
+ height: 3rem;
101
+ padding-inline: 1rem;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 1rem;
105
+ cursor: pointer;
106
+ position: relative;
107
+ text-align: start;
108
+ color: var(--np-color-on-surface);
109
+ }
110
+
111
+ .np-date-picker-selection-option.selected {
112
+ background-color: var(
113
+ --np-date-picker-menu-selected-container-color,
114
+ var(--np-color-surface-variant)
115
+ );
116
+ }
117
+
118
+ .np-date-picker-selection-option[aria-disabled='true'] {
119
+ cursor: default;
120
+ color: color-mix(in srgb, var(--np-color-on-surface) 38%, transparent);
121
+ }
122
+
123
+ .np-date-picker-selection-check {
124
+ display: flex;
125
+ width: 1.5rem;
126
+ height: 1.5rem;
127
+ flex: none;
128
+ }
129
+
130
+ :global(.np-date-picker-selection-check svg) {
131
+ width: 1.5rem;
132
+ height: 1.5rem;
133
+ fill: currentColor;
134
+ }
135
+
136
+ .np-date-picker-selection-label {
137
+ font-size: 1rem;
138
+ line-height: 1.5rem;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ white-space: nowrap;
142
+ }
143
+
144
+ .np-date-picker-selection-option:focus-visible {
145
+ outline: 3px solid var(--np-color-secondary);
146
+ outline-offset: -3px;
147
+ }
148
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface SelectionOption {
3
+ value: number;
4
+ label: string;
5
+ disabled?: boolean;
6
+ }
7
+ interface SelectionListProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
8
+ options: SelectionOption[];
9
+ value?: number;
10
+ onselect?: (value: number) => void;
11
+ }
12
+ declare const SelectionList: import("svelte").Component<SelectionListProps, {}, "">;
13
+ type SelectionList = ReturnType<typeof SelectionList>;
14
+ export default SelectionList;