noph-ui 0.39.1 → 0.40.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 (62) hide show
  1. package/dist/checkbox/Checkbox.svelte +5 -4
  2. package/dist/checkbox/types.d.ts +3 -1
  3. package/dist/date-picker/DockedDatePicker.svelte +199 -174
  4. package/dist/date-picker/DockedDateTimePicker.svelte +232 -208
  5. package/dist/date-picker/types.d.ts +11 -6
  6. package/dist/form-reset.d.ts +1 -0
  7. package/dist/form-reset.js +7 -0
  8. package/dist/icons/index.d.ts +0 -20
  9. package/dist/icons/index.js +0 -20
  10. package/dist/select/Select.svelte +12 -0
  11. package/dist/slider/Slider.svelte.d.ts +1 -1
  12. package/package.json +108 -104
  13. package/dist/icons/AccountCircleIcon.svelte +0 -15
  14. package/dist/icons/AccountCircleIcon.svelte.d.ts +0 -7
  15. package/dist/icons/ArrowBackIcon.svelte +0 -3
  16. package/dist/icons/ArrowBackIcon.svelte.d.ts +0 -26
  17. package/dist/icons/ArrowForwardIcon.svelte +0 -3
  18. package/dist/icons/ArrowForwardIcon.svelte.d.ts +0 -26
  19. package/dist/icons/BreakingNewsIcon.svelte +0 -5
  20. package/dist/icons/BreakingNewsIcon.svelte.d.ts +0 -26
  21. package/dist/icons/BrightnessMediumIcon.svelte +0 -5
  22. package/dist/icons/BrightnessMediumIcon.svelte.d.ts +0 -26
  23. package/dist/icons/ChatIcon.svelte +0 -5
  24. package/dist/icons/ChatIcon.svelte.d.ts +0 -26
  25. package/dist/icons/CopyContentIcon.svelte +0 -5
  26. package/dist/icons/CopyContentIcon.svelte.d.ts +0 -26
  27. package/dist/icons/DarkModeIcon.svelte +0 -5
  28. package/dist/icons/DarkModeIcon.svelte.d.ts +0 -26
  29. package/dist/icons/DashboardIcon.svelte +0 -5
  30. package/dist/icons/DashboardIcon.svelte.d.ts +0 -26
  31. package/dist/icons/DownloadIcon.svelte +0 -5
  32. package/dist/icons/DownloadIcon.svelte.d.ts +0 -26
  33. package/dist/icons/ErrorIcon.svelte +0 -5
  34. package/dist/icons/ErrorIcon.svelte.d.ts +0 -26
  35. package/dist/icons/GroupsIcon.svelte +0 -5
  36. package/dist/icons/GroupsIcon.svelte.d.ts +0 -26
  37. package/dist/icons/LanguageIcon.svelte +0 -5
  38. package/dist/icons/LanguageIcon.svelte.d.ts +0 -26
  39. package/dist/icons/LightModeIcon.svelte +0 -5
  40. package/dist/icons/LightModeIcon.svelte.d.ts +0 -26
  41. package/dist/icons/LogoutIcon.svelte +0 -5
  42. package/dist/icons/LogoutIcon.svelte.d.ts +0 -26
  43. package/dist/icons/MenuIcon.svelte +0 -3
  44. package/dist/icons/MenuIcon.svelte.d.ts +0 -26
  45. package/dist/icons/OpenInNewIcon.svelte +0 -10
  46. package/dist/icons/OpenInNewIcon.svelte.d.ts +0 -26
  47. package/dist/icons/PaletteIcon.svelte +0 -5
  48. package/dist/icons/PaletteIcon.svelte.d.ts +0 -26
  49. package/dist/icons/PersonIcon.svelte +0 -5
  50. package/dist/icons/PersonIcon.svelte.d.ts +0 -26
  51. package/dist/icons/SearchIcon.svelte +0 -10
  52. package/dist/icons/SearchIcon.svelte.d.ts +0 -26
  53. package/dist/icons/SendIcon.svelte +0 -10
  54. package/dist/icons/SendIcon.svelte.d.ts +0 -26
  55. package/dist/icons/SettingsIcon.svelte +0 -5
  56. package/dist/icons/SettingsIcon.svelte.d.ts +0 -26
  57. package/dist/icons/SportsGymnasticsIcon.svelte +0 -5
  58. package/dist/icons/SportsGymnasticsIcon.svelte.d.ts +0 -26
  59. package/dist/icons/VisibilityIcon.svelte +0 -10
  60. package/dist/icons/VisibilityIcon.svelte.d.ts +0 -26
  61. package/dist/icons/VisibilityOffIcon.svelte +0 -10
  62. package/dist/icons/VisibilityOffIcon.svelte.d.ts +0 -26
@@ -7,13 +7,14 @@
7
7
  checked = $bindable(),
8
8
  element = $bindable(),
9
9
  group = $bindable(),
10
- error = false,
10
+ issues,
11
11
  style,
12
12
  value,
13
13
  ...attributes
14
14
  }: CheckboxProps = $props()
15
15
 
16
16
  let inputEl: HTMLInputElement | undefined = $state()
17
+ let hasError = $derived(!!issues?.length)
17
18
 
18
19
  let groupValue = $derived(
19
20
  typeof value === 'string' || typeof value === 'number' ? value : undefined,
@@ -39,7 +40,7 @@
39
40
 
40
41
  <div
41
42
  {style}
42
- class={['np-container', error && !attributes.disabled && 'np-error', attributes.class]}
43
+ class={['np-container', hasError && !attributes.disabled && 'np-error', attributes.class]}
43
44
  bind:this={element}
44
45
  >
45
46
  <div class="np-outline"></div>
@@ -62,7 +63,7 @@
62
63
  bind:this={inputEl}
63
64
  checked={groupChecked}
64
65
  {onchange}
65
- aria-invalid={error ? 'true' : undefined}
66
+ aria-invalid={hasError ? 'true' : undefined}
66
67
  aria-checked={indeterminate ? 'mixed' : undefined}
67
68
  />
68
69
  {:else}
@@ -74,7 +75,7 @@
74
75
  bind:indeterminate
75
76
  bind:checked
76
77
  bind:this={inputEl}
77
- aria-invalid={error ? 'true' : undefined}
78
+ aria-invalid={hasError ? 'true' : undefined}
78
79
  aria-checked={indeterminate ? 'mixed' : undefined}
79
80
  />
80
81
  {/if}
@@ -2,5 +2,7 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  export interface CheckboxProps extends Omit<HTMLInputAttributes, 'type'> {
3
3
  element?: HTMLElement;
4
4
  group?: (string | number)[] | null;
5
- error?: boolean;
5
+ issues?: {
6
+ message: string;
7
+ }[];
6
8
  }
@@ -4,6 +4,7 @@
4
4
  import Divider from '../divider/Divider.svelte'
5
5
  import CalendarToday from '../icons/CalendarToday.svelte'
6
6
  import Menu from '../menu/Menu.svelte'
7
+ import { onFormReset } from '../form-reset.js'
7
8
  import TextField from '../text-field/TextField.svelte'
8
9
  import { onMount, tick } from 'svelte'
9
10
  import Calendar from './Calendar.svelte'
@@ -32,6 +33,11 @@
32
33
 
33
34
  let {
34
35
  value = $bindable(),
36
+ defaultValue,
37
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- absorbed on purpose
38
+ type,
39
+ 'aria-invalid': ariaInvalid,
40
+ style,
35
41
  displayMonth = $bindable(),
36
42
  open = $bindable(false),
37
43
  element = $bindable(),
@@ -65,6 +71,8 @@
65
71
  invalidDateMessage = 'Enter a valid date.',
66
72
  selectedDateLabel = 'selected',
67
73
  onchange,
74
+ oninput,
75
+ onblur,
68
76
  ...attributes
69
77
  }: DockedDatePickerProps = $props()
70
78
 
@@ -72,23 +80,27 @@
72
80
 
73
81
  const uid = $props.id()
74
82
 
75
- let anchorElement = $state<HTMLDivElement>()
76
83
  let menuElement = $state<HTMLDivElement>()
77
84
  let mode = $state<'days' | 'months' | 'years'>('days')
78
85
  let listMode = $state<'months' | 'years' | undefined>(undefined)
79
86
  let pending = $state<ISODate | undefined>(undefined)
80
87
  let focusedDay = $state<Date | undefined>(undefined)
81
88
  let inputElement = $state<HTMLInputElement | HTMLTextAreaElement>()
89
+ let valueInput = $state<HTMLInputElement>()
82
90
 
83
91
  let typed = $state.raw<{ source: ISODate | null | undefined; text: string } | undefined>()
84
92
  let invalidText = $state<string | undefined>()
85
93
  let monthBeforeOpen: ISODate | undefined
86
94
 
95
+ let current = $derived<ISODate | undefined>(
96
+ typeof value === 'string' ? value : typeof defaultValue === 'string' ? defaultValue : undefined,
97
+ )
98
+
87
99
  let todayValue = $derived(getToday())
88
100
  let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
89
101
  let minDate = $derived(parseISODate(min))
90
102
  let maxDate = $derived(parseISODate(max))
91
- let committed = $derived(parseISODate(value))
103
+ let committed = $derived(parseISODate(current))
92
104
  let pendingDate = $derived(open ? parseISODate(pending) : committed)
93
105
  let mounted = $state(false)
94
106
 
@@ -122,7 +134,7 @@
122
134
  return localeKnown ? formatDate(committed, locale) : toISODate(committed)
123
135
  }
124
136
 
125
- let text = $derived(typed && typed.source === value ? typed.text : formatValue())
137
+ let text = $derived(typed && typed.source === current ? typed.text : formatValue())
126
138
 
127
139
  let inputInvalid = $derived(invalidText !== undefined && invalidText === text)
128
140
 
@@ -167,11 +179,25 @@
167
179
  }
168
180
 
169
181
  const commit = (next: ISODate | undefined) => {
170
- if (value === next) return
182
+ if (current === next) return
171
183
  value = next
172
184
  onchange?.(next)
173
185
  }
174
186
 
187
+ const restoreDefault = () => {
188
+ const next = typeof defaultValue === 'string' ? defaultValue : undefined
189
+ if (valueInput) valueInput.value = next ?? ''
190
+ queueMicrotask(() => {
191
+ typed = undefined
192
+ invalidText = undefined
193
+ pending = undefined
194
+ value = next
195
+ text = formatValue()
196
+ })
197
+ }
198
+
199
+ $effect(() => onFormReset(valueInput, restoreDefault))
200
+
175
201
  const isShowing = () => !!menuElement?.matches(':popover-open')
176
202
 
177
203
  const openPicker = () => {
@@ -200,7 +226,7 @@
200
226
  const closePicker = (restoreFocus = true) => {
201
227
  menuElement?.hidePopover()
202
228
  if (restoreFocus) {
203
- anchorElement?.querySelector<HTMLElement>('input')?.focus()
229
+ element?.querySelector<HTMLElement>('input')?.focus()
204
230
  }
205
231
  }
206
232
 
@@ -230,7 +256,7 @@
230
256
  commit(parsed ? toISODate(parsed) : undefined)
231
257
  if (parsed) setMonth(parsed)
232
258
  // Recorded after the commit, so that it is remembered against the value it just produced.
233
- typed = { source: value, text: next }
259
+ typed = { source: current, text: next }
234
260
  invalidText = undefined
235
261
  }
236
262
 
@@ -266,182 +292,181 @@
266
292
  }
267
293
  </script>
268
294
 
269
- <div {...attributes} bind:this={element} class={['np-docked-date-picker', attributes.class]}>
270
- <div class="np-docked-date-picker-anchor" bind:this={anchorElement} style:anchor-name="--{uid}">
271
- <TextField
272
- {label}
273
- {variant}
274
- {issues}
295
+ <TextField
296
+ {...attributes}
297
+ class={['np-docked-date-picker', attributes.class]}
298
+ style={`anchor-name:--${uid};${style ?? ''}`}
299
+ {label}
300
+ {variant}
301
+ {issues}
302
+ {disabled}
303
+ {readonly}
304
+ {required}
305
+ {noAsterisk}
306
+ {autocomplete}
307
+ supportingText={supportingText ?? pattern}
308
+ bind:value={text}
309
+ bind:element
310
+ bind:inputElement
311
+ aria-invalid={inputInvalid || issues?.length || ariaInvalid === true || ariaInvalid === 'true'
312
+ ? 'true'
313
+ : undefined}
314
+ oninput={(event) => {
315
+ handleInput(event)
316
+ oninput?.(event)
317
+ }}
318
+ onblur={(event) => {
319
+ handleBlur()
320
+ onblur?.(event)
321
+ }}
322
+ >
323
+ {#snippet end()}
324
+ <IconButton
325
+ type="button"
326
+ aria-label={openCalendarLabel}
327
+ aria-expanded={open}
328
+ aria-haspopup="dialog"
329
+ aria-controls={open ? `${uid}-calendar` : undefined}
275
330
  {disabled}
276
- {readonly}
277
- {required}
278
- {noAsterisk}
279
- {autocomplete}
280
- supportingText={supportingText ?? pattern}
281
- bind:value={text}
282
- bind:inputElement
283
- aria-invalid={inputInvalid || issues?.length ? 'true' : undefined}
284
- oninput={handleInput}
285
- onblur={handleBlur}
331
+ onclick={() => {
332
+ if (open) closePicker()
333
+ else openPicker()
334
+ }}
286
335
  >
287
- {#snippet end()}
288
- <IconButton
289
- type="button"
290
- aria-label={openCalendarLabel}
291
- aria-expanded={open}
292
- aria-haspopup="dialog"
293
- aria-controls={open ? `${uid}-calendar` : undefined}
294
- {disabled}
295
- onclick={() => {
296
- if (open) closePicker()
297
- else openPicker()
298
- }}
299
- >
300
- <CalendarToday />
301
- </IconButton>
302
- {/snippet}
303
- </TextField>
304
- <input
305
- class="np-docked-date-picker-value"
306
- type="hidden"
307
- value={value ?? ''}
308
- {name}
309
- {form}
310
- {disabled}
311
- />
312
- </div>
313
-
314
- <Menu
315
- id="{uid}-calendar"
316
- role="dialog"
317
- aria-label={label}
318
- aria-modal="false"
319
- class="np-docked-date-picker-menu"
320
- style={`position-anchor:--${uid};`}
321
- --np-menu-justify-self="none"
322
- --np-menu-position-area="bottom span-right"
323
- --np-menu-over-anchor-position-area="span-all span-right"
324
- --np-menu-margin="0"
325
- --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
326
- --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
327
- anchor={anchorElement}
328
- bind:element={menuElement}
329
- bind:open
330
- ontoggle={({ newState }) => {
331
- if (newState === 'open') {
332
- monthBeforeOpen = displayMonth
333
- pending = value ?? undefined
334
- focusCalendarWhenReady()
335
- }
336
- if (newState === 'closed') {
337
- setMode('days')
338
- listMode = undefined
339
- focusedDay = undefined
340
- displayMonth = monthBeforeOpen
341
- }
342
- }}
343
- >
344
- {#if open}
345
- <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
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
- <div class="np-docked-date-picker-views">
371
- <div class="np-docked-date-picker-body" inert={mode !== 'days'}>
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>
336
+ <CalendarToday />
337
+ </IconButton>
338
+ {/snippet}
339
+ </TextField>
340
+ <input
341
+ bind:this={valueInput}
342
+ class="np-docked-date-picker-value"
343
+ type="hidden"
344
+ value={current ?? ''}
345
+ {name}
346
+ {form}
347
+ {disabled}
348
+ />
349
+
350
+ <Menu
351
+ id="{uid}-calendar"
352
+ role="dialog"
353
+ aria-label={label}
354
+ aria-modal="false"
355
+ class="np-docked-date-picker-menu"
356
+ style={`position-anchor:--${uid};`}
357
+ --np-menu-justify-self="none"
358
+ --np-menu-position-area="bottom span-right"
359
+ --np-menu-over-anchor-position-area="span-all span-right"
360
+ --np-menu-margin="0"
361
+ --np-menu-container-color="var(--np-docked-date-picker-container-color, var(--np-color-surface-container-high))"
362
+ --np-menu-container-shape="var(--np-docked-date-picker-container-shape, var(--np-shape-corner-large))"
363
+ anchor={element}
364
+ bind:element={menuElement}
365
+ bind:open
366
+ ontoggle={({ newState }) => {
367
+ if (newState === 'open') {
368
+ monthBeforeOpen = displayMonth
369
+ pending = current
370
+ focusCalendarWhenReady()
371
+ }
372
+ if (newState === 'closed') {
373
+ setMode('days')
374
+ listMode = undefined
375
+ focusedDay = undefined
376
+ displayMonth = monthBeforeOpen
377
+ }
378
+ }}
379
+ >
380
+ {#if open}
381
+ <div class="np-docked-date-picker-container" style:--np-calendar-rows={rowCount}>
382
+ <CalendarHeader
383
+ {mode}
384
+ monthLabel={shortMonthNames[monthDate.getMonth()]}
385
+ yearLabel={`${monthDate.getFullYear()}`}
386
+ {canPreviousMonth}
387
+ {canNextMonth}
388
+ {canPreviousYear}
389
+ {canNextYear}
390
+ {previousMonthLabel}
391
+ {nextMonthLabel}
392
+ {previousYearLabel}
393
+ {nextYearLabel}
394
+ {selectMonthLabel}
395
+ {selectYearLabel}
396
+ monthListId="{uid}-months"
397
+ yearListId="{uid}-years"
398
+ onpreviousmonth={() => setMonth(addMonths(monthDate, -1))}
399
+ onnextmonth={() => setMonth(addMonths(monthDate, 1))}
400
+ onpreviousyear={() => setMonth(addMonths(monthDate, -12))}
401
+ onnextyear={() => setMonth(addMonths(monthDate, 12))}
402
+ ontogglemonths={() => setMode(mode === 'months' ? 'days' : 'months')}
403
+ ontoggleyears={() => setMode(mode === 'years' ? 'days' : 'years')}
404
+ />
405
+
406
+ <div class="np-docked-date-picker-views">
407
+ <div class="np-docked-date-picker-body" inert={mode !== 'days'}>
408
+ <Calendar
409
+ month={monthDate}
410
+ selected={pendingDate}
411
+ min={minDate}
412
+ max={maxDate}
413
+ {locale}
414
+ firstDayOfWeek={week}
415
+ {isDateEnabled}
416
+ {adjacentMonthDays}
417
+ dynamicRows
418
+ todayDate={todayValue}
419
+ focusedDate={focusedDay}
420
+ selectedLabel={selectedDateLabel}
421
+ onselect={selectDay}
422
+ onmonthstep={(delta) => setMonth(addMonths(monthDate, delta))}
423
+ onfocusday={(date) => {
424
+ focusedDay = date
425
+ if (
426
+ date.getMonth() !== monthDate.getMonth() ||
427
+ date.getFullYear() !== monthDate.getFullYear()
428
+ ) {
429
+ setMonth(date)
430
+ }
431
+ }}
432
+ />
433
+ </div>
398
434
 
399
- {#if listMode}
400
- <div class={['np-docked-date-picker-menu-view', mode !== 'days' && 'open']}>
401
- <div class="np-docked-date-picker-menu-shade">
402
- <Divider --np-divider-color="var(--np-color-outline-variant)" />
403
- {#if listMode === 'months'}
404
- <SelectionList
405
- id="{uid}-months"
406
- aria-label={selectMonthLabel}
407
- options={monthOptions}
408
- value={monthDate.getMonth()}
409
- onselect={chooseMonth}
410
- />
411
- {:else}
412
- <SelectionList
413
- id="{uid}-years"
414
- aria-label={selectYearLabel}
415
- options={yearOptions}
416
- value={monthDate.getFullYear()}
417
- onselect={chooseYear}
418
- />
419
- {/if}
420
- </div>
435
+ {#if listMode}
436
+ <div class={['np-docked-date-picker-menu-view', mode !== 'days' && 'open']}>
437
+ <div class="np-docked-date-picker-menu-shade">
438
+ <Divider --np-divider-color="var(--np-color-outline-variant)" />
439
+ {#if listMode === 'months'}
440
+ <SelectionList
441
+ id="{uid}-months"
442
+ aria-label={selectMonthLabel}
443
+ options={monthOptions}
444
+ value={monthDate.getMonth()}
445
+ onselect={chooseMonth}
446
+ />
447
+ {:else}
448
+ <SelectionList
449
+ id="{uid}-years"
450
+ aria-label={selectYearLabel}
451
+ options={yearOptions}
452
+ value={monthDate.getFullYear()}
453
+ onselect={chooseYear}
454
+ />
455
+ {/if}
421
456
  </div>
422
- {/if}
423
- </div>
457
+ </div>
458
+ {/if}
459
+ </div>
424
460
 
425
- <div class="np-docked-date-picker-actions">
426
- <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
427
- <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
428
- </div>
461
+ <div class="np-docked-date-picker-actions">
462
+ <Button type="button" variant="text" onclick={() => closePicker()}>{cancelLabel}</Button>
463
+ <Button type="button" variant="text" onclick={confirm}>{confirmLabel}</Button>
429
464
  </div>
430
- {/if}
431
- </Menu>
432
- </div>
465
+ </div>
466
+ {/if}
467
+ </Menu>
433
468
 
434
469
  <style>
435
- .np-docked-date-picker {
436
- display: inline-flex;
437
- flex-direction: column;
438
- }
439
-
440
- .np-docked-date-picker-anchor {
441
- display: inline-flex;
442
- position: relative;
443
- }
444
-
445
470
  :global(.np-docked-date-picker-menu .np-menu) {
446
471
  padding: 0;
447
472
  }