noph-ui 0.52.0 → 0.53.1

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 (104) hide show
  1. package/dist/animation.d.ts +1 -13
  2. package/dist/animation.js +18 -13
  3. package/dist/button/Button.svelte +2 -14
  4. package/dist/button/IconButton.svelte +5 -18
  5. package/dist/button/SegmentedButton.svelte +8 -11
  6. package/dist/card/Card.svelte +3 -19
  7. package/dist/card/types.d.ts +12 -2
  8. package/dist/carousel/Carousel.svelte +77 -14
  9. package/dist/carousel/CarouselItem.svelte +26 -14
  10. package/dist/carousel/carouselMorph.js +1 -1
  11. package/dist/carousel/carouselStrategy.js +25 -27
  12. package/dist/checkbox/Checkbox.svelte +1 -8
  13. package/dist/chip/FilterChip.svelte +1 -13
  14. package/dist/date-picker/Calendar.svelte +31 -30
  15. package/dist/date-picker/Calendar.svelte.d.ts +2 -0
  16. package/dist/date-picker/CalendarHeader.svelte +84 -62
  17. package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
  18. package/dist/date-picker/DatePickerDialog.svelte +134 -75
  19. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
  20. package/dist/date-picker/DateRangePicker.svelte +226 -78
  21. package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
  22. package/dist/date-picker/DockedDatePicker.svelte +76 -464
  23. package/dist/date-picker/DockedDatePicker.svelte.d.ts +2 -2
  24. package/dist/date-picker/DockedDateTimePicker.svelte +128 -525
  25. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +2 -2
  26. package/dist/date-picker/DockedPickerField.svelte +221 -0
  27. package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
  28. package/dist/date-picker/MonthPanel.svelte +291 -0
  29. package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
  30. package/dist/date-picker/SelectionList.svelte +7 -6
  31. package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
  32. package/dist/date-picker/TimeColumn.svelte +2 -6
  33. package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
  34. package/dist/date-picker/YearGrid.svelte +2 -11
  35. package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
  36. package/dist/date-picker/dateUtils.d.ts +4 -0
  37. package/dist/date-picker/dateUtils.js +15 -88
  38. package/dist/date-picker/exitVisibility.svelte.js +13 -6
  39. package/dist/date-picker/formatters.d.ts +16 -0
  40. package/dist/date-picker/formatters.js +48 -15
  41. package/dist/date-picker/internalTypes.d.ts +59 -1
  42. package/dist/date-picker/shortcuts.d.ts +11 -0
  43. package/dist/date-picker/shortcuts.js +40 -0
  44. package/dist/date-picker/timeUtils.d.ts +8 -3
  45. package/dist/date-picker/timeUtils.js +33 -51
  46. package/dist/date-picker/types.d.ts +22 -0
  47. package/dist/icons/Icon.svelte +1 -4
  48. package/dist/internal/exit.css +0 -14
  49. package/dist/internal/field.css +337 -0
  50. package/dist/internal/focus-ring.css +23 -8
  51. package/dist/internal/pop-in.css +19 -0
  52. package/dist/internal/visually-hidden.css +11 -0
  53. package/dist/list/Item.svelte +32 -18
  54. package/dist/menu/Menu.svelte +6 -25
  55. package/dist/menu/Menu.svelte.d.ts +1 -0
  56. package/dist/navigation-bar/NavigationBarItem.svelte +6 -30
  57. package/dist/navigation-drawer/NavigationDrawerItem.svelte +4 -11
  58. package/dist/navigation-rail/NavigationRail.svelte +2 -3
  59. package/dist/navigation-rail/NavigationRailItem.svelte +2 -13
  60. package/dist/press.svelte.d.ts +0 -1
  61. package/dist/press.svelte.js +0 -11
  62. package/dist/progress/CircularProgress.svelte +26 -81
  63. package/dist/progress/CircularProgress.svelte.d.ts +1 -0
  64. package/dist/progress/LinearProgress.svelte +19 -151
  65. package/dist/progress/LinearProgress.svelte.d.ts +1 -0
  66. package/dist/progress/four-color.css +34 -0
  67. package/dist/radio/Radio.svelte +6 -9
  68. package/dist/ripple/Ripple.svelte +8 -16
  69. package/dist/search/Search.svelte +2 -9
  70. package/dist/search/Search.svelte.d.ts +1 -0
  71. package/dist/select/Check.svelte +8 -31
  72. package/dist/select/Check.svelte.d.ts +1 -1
  73. package/dist/select/NativeSelect.svelte +97 -366
  74. package/dist/select/NativeSelect.svelte.d.ts +1 -0
  75. package/dist/select/Option.svelte +1 -9
  76. package/dist/select/Select.svelte +139 -746
  77. package/dist/select/Select.svelte.d.ts +1 -0
  78. package/dist/select/VirtualList.svelte +0 -1
  79. package/dist/sheet/Sheet.svelte +11 -22
  80. package/dist/slider/Slider.svelte +13 -37
  81. package/dist/slider/Slider.svelte.d.ts +1 -0
  82. package/dist/switch/Switch.svelte +2 -8
  83. package/dist/tabs/Tab.svelte +2 -4
  84. package/dist/text-field/TextField.svelte +81 -609
  85. package/dist/text-field/TextField.svelte.d.ts +1 -0
  86. package/dist/time-picker/ClockDial.svelte +8 -25
  87. package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
  88. package/dist/time-picker/DockedTimePicker.svelte +102 -244
  89. package/dist/time-picker/DockedTimePicker.svelte.d.ts +2 -2
  90. package/dist/time-picker/PeriodSelector.svelte +8 -7
  91. package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
  92. package/dist/time-picker/TimeInputs.svelte +13 -2
  93. package/dist/time-picker/TimePickerDialog.svelte +32 -26
  94. package/dist/time-picker/TimePickerDialog.svelte.d.ts +2 -2
  95. package/dist/time-picker/TimePickerPanel.svelte +26 -4
  96. package/dist/time-picker/TimeSelectors.svelte +4 -9
  97. package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
  98. package/dist/time-picker/internalTypes.d.ts +2 -0
  99. package/dist/time-picker/timePickerState.svelte.js +3 -9
  100. package/dist/tooltip/RichTooltip.svelte +2 -23
  101. package/dist/tooltip/RichTooltip.svelte.d.ts +1 -0
  102. package/dist/tooltip/Tooltip.svelte +2 -24
  103. package/dist/tooltip/Tooltip.svelte.d.ts +1 -0
  104. package/package.json +12 -12
@@ -1,28 +1,34 @@
1
1
  <script lang="ts">
2
+ import { afterTwoFrames, morphSize } from '../animation.js'
2
3
  import Button from '../button/Button.svelte'
3
4
  import IconButton from '../button/IconButton.svelte'
4
5
  import Dialog from '../dialog/Dialog.svelte'
5
6
  import Divider from '../divider/Divider.svelte'
7
+ import CalendarToday from '../icons/CalendarToday.svelte'
6
8
  import CloseIcon from '../icons/CloseIcon.svelte'
7
- import { syncOpenEffect } from '../popover.svelte.js'
8
- import { tick } from 'svelte'
9
+ import EditCalendarIcon from '../icons/EditCalendarIcon.svelte'
10
+ import TextField from '../text-field/TextField.svelte'
11
+ import { flushSync, tick } from 'svelte'
9
12
  import Calendar from './Calendar.svelte'
10
13
  import { exitVisibility } from './exitVisibility.svelte.js'
11
14
  import {
12
15
  clampDate,
13
16
  compareDays,
14
17
  DEFAULT_YEAR_RANGE,
15
- formatDateMedium,
18
+ formatDate,
19
+ formatMonthDay,
20
+ getDatePattern,
16
21
  getFirstDayOfWeek,
17
22
  getWeekdayLabels,
18
23
  isWithin,
24
+ parseDateInput,
19
25
  parseISODate,
20
26
  startOfDay,
21
27
  startOfMonth,
22
28
  toISODate,
23
29
  today as getToday,
24
30
  } from './dateUtils.js'
25
- import type { DateRange, DateRangePickerProps } from './types.js'
31
+ import type { DateRange, DatePickerMode, DateRangePickerProps } from './types.js'
26
32
 
27
33
  let {
28
34
  value = $bindable({}),
@@ -42,6 +48,10 @@
42
48
  form,
43
49
  startLabel = 'Start date',
44
50
  endLabel = 'End date',
51
+ mode = $bindable<DatePickerMode>('calendar'),
52
+ modeToggle = true,
53
+ calendarModeLabel = 'Switch to calendar mode',
54
+ inputModeLabel = 'Switch to text input mode',
45
55
  cancelLabel = 'Cancel',
46
56
  confirmLabel = 'Save',
47
57
  selectedDateLabel = 'selected',
@@ -116,20 +126,10 @@
116
126
  if (target && scroller) scroller.scrollTop = target.offsetTop
117
127
  }
118
128
 
119
- export const show = () => {
120
- if (element && !element.open) element.showModal()
121
- }
122
-
123
- export const close = () => {
124
- if (element?.open) element.close()
125
- }
129
+ let dialog = $state<Dialog>()
126
130
 
127
- syncOpenEffect(
128
- () => element,
129
- () => open,
130
- show,
131
- close,
132
- )
131
+ export const show = () => dialog?.show()
132
+ export const close = () => dialog?.close()
133
133
 
134
134
  let prepending = false
135
135
 
@@ -167,10 +167,6 @@
167
167
  if (index > lastMonth) lastMonth = index
168
168
  }
169
169
 
170
- const cancel = () => {
171
- open = false
172
- }
173
-
174
170
  const confirm = () => {
175
171
  confirming = true
176
172
  value = pending
@@ -179,26 +175,92 @@
179
175
  open = false
180
176
  }
181
177
 
182
- let headlineStart = $derived(startDate ? formatDateMedium(startDate, locale) : startLabel)
183
- let headlineEnd = $derived(endDate ? formatDateMedium(endDate, locale) : endLabel)
178
+ // The input mode types the range into two fields, read as they are typed and formatted once
179
+ // left or entered. An end before the start does not count.
180
+ let startText = $state('')
181
+ let endText = $state('')
182
+ let startInvalid = $state(false)
183
+ let endInvalid = $state(false)
184
+ let pattern = $derived(getDatePattern(locale))
185
+
186
+ const usableDate = (typed: string) => {
187
+ const parsed = parseDateInput(typed, locale)
188
+ return parsed && isSelectable(parsed) ? parsed : undefined
189
+ }
190
+
191
+ const syncText = () => {
192
+ startText = startDate ? formatDate(startDate, locale) : ''
193
+ endText = endDate ? formatDate(endDate, locale) : ''
194
+ startInvalid = false
195
+ endInvalid = false
196
+ }
197
+
198
+ const readTyped = () => {
199
+ const start = usableDate(startText)
200
+ const end = usableDate(endText)
201
+ pending = {
202
+ start: start && toISODate(start),
203
+ end: start && end && compareDays(end, start) >= 0 ? toISODate(end) : undefined,
204
+ }
205
+ }
206
+
207
+ const settleTyped = () => {
208
+ startInvalid = startText.trim() !== '' && !startDate
209
+ endInvalid = endText.trim() !== '' && !endDate
210
+ if (startDate) startText = formatDate(startDate, locale)
211
+ if (endDate) endText = formatDate(endDate, locale)
212
+ }
213
+
214
+ // The fields slide in on a switch to input mode, not when the picker opens in it.
215
+ let inputEnter = $state(false)
216
+ let cancelInputEnter: (() => void) | undefined
217
+
218
+ let content = $state<HTMLElement>()
219
+
220
+ const setMode = (next: DatePickerMode) => {
221
+ cancelInputEnter?.()
222
+ morphSize(
223
+ content,
224
+ () =>
225
+ flushSync(() => {
226
+ mode = next
227
+ inputEnter = next === 'input'
228
+ if (next === 'input') syncText()
229
+ else resetWindow()
230
+ }),
231
+ '--np-motion-expressive-default-spatial',
232
+ )
233
+ if (next === 'input') cancelInputEnter = afterTwoFrames(() => (inputEnter = false))
234
+ else scrollToAnchor()
235
+ }
236
+
237
+ let headlineStart = $derived(startDate ? formatMonthDay(startDate, locale) : startLabel)
238
+ let headlineEnd = $derived(endDate ? formatMonthDay(endDate, locale) : endLabel)
184
239
  </script>
185
240
 
186
241
  <Dialog
187
242
  role="dialog"
188
243
  {...attributes}
244
+ bind:this={dialog}
189
245
  bind:element
246
+ bind:open
190
247
  aria-label={title}
191
248
  class={['np-date-range-picker', attributes.class]}
192
249
  ontoggle={(event) => {
193
- const nowOpen = event.newState === 'open'
194
- open = nowOpen
195
- if (nowOpen) {
250
+ if (event.newState === 'open') {
196
251
  exit.show()
197
252
  confirming = false
198
253
  pending = { ...value }
199
254
  focusedDay = undefined
200
- resetWindow()
201
- scrollToAnchor()
255
+ if (mode === 'input') {
256
+ syncText()
257
+ tick().then(() =>
258
+ element?.querySelector<HTMLInputElement>('.np-date-range-picker-inputs input')?.focus(),
259
+ )
260
+ } else {
261
+ resetWindow()
262
+ scrollToAnchor()
263
+ }
202
264
  } else {
203
265
  if (!confirming) oncancel?.()
204
266
  confirming = false
@@ -215,72 +277,117 @@
215
277
  {form}
216
278
  />
217
279
  {#if exit.visible}
218
- <div class="np-date-range-picker-content">
280
+ <div class="np-date-range-picker-content" bind:this={content}>
219
281
  <div class="np-date-range-picker-header">
220
282
  <div class="np-date-range-picker-top-bar">
221
- <IconButton type="button" aria-label={cancelLabel} onclick={cancel}>
283
+ <IconButton type="button" aria-label={cancelLabel} onclick={close}>
222
284
  <CloseIcon />
223
285
  </IconButton>
224
286
  <Button type="button" variant="text" disabled={!canConfirm} onclick={confirm}>
225
287
  {confirmLabel}
226
288
  </Button>
227
289
  </div>
228
- <div class="np-date-range-picker-header-text">
229
- <span class="np-date-range-picker-title">{title}</span>
230
- <h2 class="np-date-range-picker-headline">
231
- {#if headline}
232
- <span>{headline}</span>
233
- {:else}
234
- <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
235
- <span aria-hidden="true">–</span>
236
- <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
237
- {/if}
238
- </h2>
290
+ <div class="np-date-range-picker-header-row">
291
+ <div class="np-date-range-picker-header-text">
292
+ <span class="np-date-range-picker-title">{title}</span>
293
+ <h2 class="np-date-range-picker-headline">
294
+ {#if headline}
295
+ <span>{headline}</span>
296
+ {:else}
297
+ <span class={[!startDate && 'placeholder']}>{headlineStart}</span>
298
+ <span aria-hidden="true">–</span>
299
+ <span class={[!endDate && 'placeholder']}>{headlineEnd}</span>
300
+ {/if}
301
+ </h2>
302
+ </div>
303
+ {#if modeToggle}
304
+ <IconButton
305
+ type="button"
306
+ aria-label={mode === 'calendar' ? inputModeLabel : calendarModeLabel}
307
+ onclick={() => setMode(mode === 'calendar' ? 'input' : 'calendar')}
308
+ >
309
+ {#if mode === 'calendar'}
310
+ <EditCalendarIcon />
311
+ {:else}
312
+ <CalendarToday />
313
+ {/if}
314
+ </IconButton>
315
+ {/if}
239
316
  </div>
240
317
  </div>
241
318
  <Divider />
242
319
 
243
- <div class="np-date-range-picker-weekdays" aria-hidden="true">
244
- {#each weekdayLabels as weekday (weekday.long)}
245
- <span>{weekday.narrow}</span>
246
- {/each}
247
- </div>
320
+ {#if mode === 'input'}
321
+ <div class={['np-date-range-picker-inputs', inputEnter && 'entering']}>
322
+ {#snippet field(label: string, text: string, invalid: boolean, start: boolean)}
323
+ <TextField
324
+ {label}
325
+ supportingText={pattern}
326
+ value={text}
327
+ aria-invalid={invalid ? 'true' : undefined}
328
+ oninput={(event) => {
329
+ const typed = (event.currentTarget as HTMLInputElement).value
330
+ if (start) {
331
+ startText = typed
332
+ startInvalid = false
333
+ } else {
334
+ endText = typed
335
+ endInvalid = false
336
+ }
337
+ readTyped()
338
+ }}
339
+ onblur={settleTyped}
340
+ onkeydown={(event) => {
341
+ if (event.key === 'Enter') settleTyped()
342
+ }}
343
+ />
344
+ {/snippet}
345
+ {@render field(startLabel, startText, startInvalid, true)}
346
+ {@render field(endLabel, endText, endInvalid, false)}
347
+ </div>
348
+ {:else}
349
+ <div class="np-date-range-picker-weekdays" aria-hidden="true">
350
+ {#each weekdayLabels as weekday (weekday.long)}
351
+ <span>{weekday.narrow}</span>
352
+ {/each}
353
+ </div>
248
354
 
249
- <div class="np-date-range-picker-months" bind:this={scroller} onscroll={handleScroll}>
250
- {#each months as month (month.getTime())}
251
- <Calendar
252
- {month}
253
- data-month={monthIndex(month)}
254
- rangeStart={startDate}
255
- rangeEnd={endDate}
256
- min={minDate}
257
- max={maxDate}
258
- {locale}
259
- firstDayOfWeek={week}
260
- {isDateEnabled}
261
- {adjacentMonthDays}
262
- weekdays={false}
263
- dynamicRows
264
- todayDate={todayValue}
265
- focusedDate={focusedDay}
266
- selectedLabel={selectedDateLabel}
267
- {tabStopDate}
268
- focusRoot={scroller}
269
- onselect={selectDay}
270
- onfocusday={focusDay}
271
- >
272
- {#snippet monthSubhead(monthName)}
273
- <div class="np-date-range-picker-subhead">{monthName}</div>
274
- {/snippet}
275
- </Calendar>
276
- {/each}
277
- </div>
355
+ <div class="np-date-range-picker-months" bind:this={scroller} onscroll={handleScroll}>
356
+ {#each months as month (month.getTime())}
357
+ <Calendar
358
+ {month}
359
+ data-month={monthIndex(month)}
360
+ rangeStart={startDate}
361
+ rangeEnd={endDate}
362
+ min={minDate}
363
+ max={maxDate}
364
+ {locale}
365
+ firstDayOfWeek={week}
366
+ {isDateEnabled}
367
+ {adjacentMonthDays}
368
+ weekdays={false}
369
+ dynamicRows
370
+ todayDate={todayValue}
371
+ focusedDate={focusedDay}
372
+ selectedLabel={selectedDateLabel}
373
+ {tabStopDate}
374
+ focusRoot={scroller}
375
+ onselect={selectDay}
376
+ onfocusday={focusDay}
377
+ >
378
+ {#snippet monthSubhead(monthName)}
379
+ <div class="np-date-range-picker-subhead">{monthName}</div>
380
+ {/snippet}
381
+ </Calendar>
382
+ {/each}
383
+ </div>
384
+ {/if}
278
385
  </div>
279
386
  {/if}
280
387
 
281
388
  {#snippet actions()}
282
389
  {#if exit.visible}
283
- <Button type="button" variant="text" onclick={cancel}>{cancelLabel}</Button>
390
+ <Button type="button" variant="text" onclick={close}>{cancelLabel}</Button>
284
391
  <Button type="button" variant="text" disabled={!canConfirm} onclick={confirm}>
285
392
  {confirmLabel}
286
393
  </Button>
@@ -349,12 +456,47 @@
349
456
  color: var(--np-color-on-surface-variant);
350
457
  }
351
458
 
352
- .np-date-range-picker-top-bar {
459
+ .np-date-range-picker-top-bar,
460
+ .np-date-range-picker-header-row {
353
461
  display: flex;
354
462
  align-items: center;
355
463
  justify-content: space-between;
356
464
  }
357
465
 
466
+ .np-date-range-picker-header-row {
467
+ align-items: flex-end;
468
+ gap: 0.5rem;
469
+ }
470
+
471
+ .np-date-range-picker-inputs {
472
+ box-sizing: border-box;
473
+ display: flex;
474
+ gap: 1rem;
475
+ max-width: var(--np-date-range-picker-content-width, 25.5rem);
476
+ width: 100%;
477
+ margin-inline: auto;
478
+ padding: 1rem 1rem 0;
479
+ }
480
+
481
+ .np-date-range-picker-inputs > :global(.np-text-field) {
482
+ flex: 1;
483
+ min-width: 0;
484
+ }
485
+
486
+ .np-date-range-picker-inputs.entering {
487
+ transform: translateY(1.5rem);
488
+ opacity: 0;
489
+ transition: none;
490
+ }
491
+
492
+ @media (prefers-reduced-motion: no-preference) {
493
+ .np-date-range-picker-inputs {
494
+ transition:
495
+ transform var(--np-motion-expressive-default-spatial),
496
+ opacity var(--np-motion-expressive-default-effects);
497
+ }
498
+ }
499
+
358
500
  .np-date-range-picker-header-text {
359
501
  display: flex;
360
502
  flex-direction: column;
@@ -399,6 +541,9 @@
399
541
  min-height: 0;
400
542
  overflow-y: auto;
401
543
  overscroll-behavior: contain;
544
+ /* Months added above are made up for in script, in every browser alike. Scroll anchoring
545
+ would make up for them a second time. */
546
+ overflow-anchor: none;
402
547
  padding-inline: 0.75rem;
403
548
  scrollbar-color: var(--np-color-on-surface-variant) transparent;
404
549
  scrollbar-width: thin;
@@ -420,7 +565,6 @@
420
565
  --np-dialog-container-width: fit-content;
421
566
  --np-dialog-inset: 2rem 1rem;
422
567
  --np-dialog-max-height: calc(100dvh - 3rem);
423
- --np-dialog-elevation: var(--np-elevation-3);
424
568
  --np-dialog-container-shape: var(
425
569
  --np-date-range-picker-container-shape,
426
570
  var(--np-shape-corner-extra-large)
@@ -446,6 +590,10 @@
446
590
  display: none;
447
591
  }
448
592
 
593
+ .np-date-range-picker-inputs {
594
+ padding-inline: 1.5rem;
595
+ }
596
+
449
597
  .np-date-range-picker-header {
450
598
  min-height: 7.5rem;
451
599
  justify-content: flex-end;
@@ -1,7 +1,7 @@
1
1
  import type { DateRangePickerProps } from './types.js';
2
2
  declare const DateRangePicker: import("svelte").Component<DateRangePickerProps, {
3
- show: () => void;
4
- close: () => void;
5
- }, "open" | "element" | "value">;
3
+ show: () => any;
4
+ close: () => any;
5
+ }, "open" | "element" | "value" | "mode">;
6
6
  type DateRangePicker = ReturnType<typeof DateRangePicker>;
7
7
  export default DateRangePicker;