noph-ui 0.44.0 → 0.45.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 (136) hide show
  1. package/README.md +10 -1
  2. package/dist/animation.d.ts +2 -0
  3. package/dist/animation.js +33 -0
  4. package/dist/app-bar/AppBar.svelte +230 -0
  5. package/dist/app-bar/AppBar.svelte.d.ts +4 -0
  6. package/dist/app-bar/index.d.ts +1 -0
  7. package/dist/app-bar/index.js +1 -0
  8. package/dist/app-bar/types.d.ts +13 -0
  9. package/dist/app-bar/types.js +1 -0
  10. package/dist/autocomplete/AutoComplete.svelte +10 -11
  11. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  12. package/dist/autocomplete/types.d.ts +1 -1
  13. package/dist/badge/Badge.svelte +6 -1
  14. package/dist/badge/types.d.ts +0 -1
  15. package/dist/button/Button.svelte +5 -15
  16. package/dist/button/IconButton.svelte +5 -15
  17. package/dist/button/SegmentedButton.svelte +15 -1
  18. package/dist/button/SplitButton.svelte +75 -0
  19. package/dist/button/SplitButton.svelte.d.ts +4 -0
  20. package/dist/button/index.d.ts +1 -0
  21. package/dist/button/index.js +1 -0
  22. package/dist/button/types.d.ts +17 -4
  23. package/dist/card/types.d.ts +1 -1
  24. package/dist/checkbox/Checkbox.svelte +47 -58
  25. package/dist/chip/AssistChip.svelte +5 -5
  26. package/dist/chip/ChipSet.svelte +3 -11
  27. package/dist/chip/FilterChip.svelte +33 -27
  28. package/dist/chip/InputChip.svelte +13 -2
  29. package/dist/chip/SuggestionChip.svelte +29 -0
  30. package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
  31. package/dist/chip/index.d.ts +1 -0
  32. package/dist/chip/index.js +1 -0
  33. package/dist/chip/types.d.ts +16 -9
  34. package/dist/date-picker/Calendar.svelte +1 -1
  35. package/dist/date-picker/DatePickerDialog.svelte +23 -11
  36. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
  37. package/dist/date-picker/DateRangePicker.svelte +40 -10
  38. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
  39. package/dist/date-picker/DockedDatePicker.svelte +13 -10
  40. package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
  41. package/dist/date-picker/DockedDateTimePicker.svelte +13 -7
  42. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
  43. package/dist/date-picker/SelectionList.svelte +6 -32
  44. package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
  45. package/dist/date-picker/TimeColumn.svelte +4 -29
  46. package/dist/date-picker/exitVisibility.svelte.d.ts +5 -0
  47. package/dist/date-picker/exitVisibility.svelte.js +23 -0
  48. package/dist/date-picker/optionList.d.ts +4 -0
  49. package/dist/date-picker/optionList.js +31 -0
  50. package/dist/date-picker/types.d.ts +11 -4
  51. package/dist/dialog/Dialog.svelte +55 -68
  52. package/dist/dialog/Dialog.svelte.d.ts +3 -3
  53. package/dist/dialog/types.d.ts +4 -3
  54. package/dist/fab/ExtendedFab.svelte +255 -0
  55. package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
  56. package/dist/fab/Fab.svelte +211 -0
  57. package/dist/fab/Fab.svelte.d.ts +5 -0
  58. package/dist/fab/FabMenu.svelte +207 -0
  59. package/dist/fab/FabMenu.svelte.d.ts +7 -0
  60. package/dist/fab/index.d.ts +3 -0
  61. package/dist/fab/index.js +3 -0
  62. package/dist/fab/types.d.ts +34 -0
  63. package/dist/fab/types.js +1 -0
  64. package/dist/icons/ArrowBackIcon.svelte +13 -0
  65. package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
  66. package/dist/icons/ChevronDownIcon.svelte +3 -0
  67. package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
  68. package/dist/icons/SearchIcon.svelte +15 -0
  69. package/dist/icons/SearchIcon.svelte.d.ts +7 -0
  70. package/dist/icons/index.d.ts +3 -0
  71. package/dist/icons/index.js +3 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +6 -0
  74. package/dist/internal/focus-ring.css +12 -0
  75. package/dist/loading-indicator/shapes.js +0 -7
  76. package/dist/menu/Menu.svelte +12 -7
  77. package/dist/menu/Menu.svelte.d.ts +2 -2
  78. package/dist/menu/types.d.ts +0 -2
  79. package/dist/navigation-bar/NavigationBar.svelte +59 -0
  80. package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
  81. package/dist/navigation-bar/NavigationBarItem.svelte +239 -0
  82. package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
  83. package/dist/navigation-bar/index.d.ts +2 -0
  84. package/dist/navigation-bar/index.js +2 -0
  85. package/dist/navigation-bar/types.d.ts +19 -0
  86. package/dist/navigation-bar/types.js +1 -0
  87. package/dist/navigation-drawer/NavigationDrawer.svelte +107 -76
  88. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
  89. package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -3
  90. package/dist/navigation-drawer/types.d.ts +3 -1
  91. package/dist/navigation-rail/NavigationRailItem.svelte +16 -0
  92. package/dist/navigation-rail/types.d.ts +3 -0
  93. package/dist/popover.svelte.d.ts +5 -0
  94. package/dist/popover.svelte.js +22 -0
  95. package/dist/press.svelte.d.ts +6 -0
  96. package/dist/press.svelte.js +32 -0
  97. package/dist/radio/Radio.svelte +26 -7
  98. package/dist/radio/types.d.ts +3 -0
  99. package/dist/search/Search.svelte +391 -0
  100. package/dist/search/Search.svelte.d.ts +5 -0
  101. package/dist/search/index.d.ts +1 -0
  102. package/dist/search/index.js +1 -0
  103. package/dist/search/types.d.ts +19 -0
  104. package/dist/search/types.js +1 -0
  105. package/dist/select/Select.svelte +48 -87
  106. package/dist/sheet/Sheet.svelte +235 -0
  107. package/dist/sheet/Sheet.svelte.d.ts +7 -0
  108. package/dist/sheet/index.d.ts +1 -0
  109. package/dist/sheet/index.js +1 -0
  110. package/dist/sheet/types.d.ts +11 -0
  111. package/dist/sheet/types.js +1 -0
  112. package/dist/slider/Slider.svelte +118 -30
  113. package/dist/slider/types.d.ts +3 -0
  114. package/dist/snackbar/Snackbar.svelte +32 -35
  115. package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
  116. package/dist/snackbar/types.d.ts +3 -2
  117. package/dist/switch/Switch.svelte +51 -20
  118. package/dist/switch/types.d.ts +3 -0
  119. package/dist/tabs/Tab.svelte +4 -12
  120. package/dist/tabs/Tabs.svelte +5 -5
  121. package/dist/text-field/TextField.svelte +2 -4
  122. package/dist/toolbar/Toolbar.svelte +121 -0
  123. package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
  124. package/dist/toolbar/index.d.ts +1 -0
  125. package/dist/toolbar/index.js +1 -0
  126. package/dist/toolbar/types.d.ts +8 -0
  127. package/dist/toolbar/types.js +1 -0
  128. package/dist/tooltip/RichTooltip.svelte +18 -12
  129. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
  130. package/dist/tooltip/Tooltip.svelte +22 -11
  131. package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
  132. package/dist/types.d.ts +6 -0
  133. package/dist/types.js +6 -0
  134. package/package.json +13 -13
  135. package/dist/date-picker/enterAnimation.d.ts +0 -1
  136. package/dist/date-picker/enterAnimation.js +0 -10
@@ -74,7 +74,6 @@
74
74
  )
75
75
 
76
76
  let field = $state<HTMLDivElement>()
77
- let clientWidth = $state(0)
78
77
  let menuOpen = $state(false)
79
78
  let focusIndex = $state(-1)
80
79
  let pendingFocus = false
@@ -230,6 +229,30 @@
230
229
  }
231
230
  }
232
231
 
232
+ const optionKeydown = (event: KeyboardEvent, option: SelectOption) => {
233
+ const key = event.key
234
+ if (key === 'ArrowDown') {
235
+ event.preventDefault()
236
+ moveFocus(1)
237
+ } else if (key === 'ArrowUp') {
238
+ event.preventDefault()
239
+ moveFocus(-1)
240
+ } else if (key === 'Home') {
241
+ event.preventDefault()
242
+ focusEdge(true)
243
+ } else if (key === 'End') {
244
+ event.preventDefault()
245
+ focusEdge(false)
246
+ } else if (key === 'Enter' || key === ' ') {
247
+ event.preventDefault()
248
+ handleOptionSelect(event, option)
249
+ } else if (key === 'Tab') {
250
+ menuElement?.hidePopover()
251
+ } else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
252
+ performTypeahead(key)
253
+ }
254
+ }
255
+
233
256
  const performTypeahead = (char: string) => {
234
257
  const now = performance.now()
235
258
  if (now - lastTypeTime > 700) typeBuffer = ''
@@ -268,7 +291,7 @@
268
291
  : !label?.length
269
292
  ? '--top-space:1rem;--bottom-space:1rem;'
270
293
  : '') + style}
271
- class={['text-field', attributes.class]}
294
+ class={['np-text-field', attributes.class]}
272
295
  bind:this={element}
273
296
  >
274
297
  <!-- svelte-ignore a11y_autofocus -->
@@ -291,7 +314,6 @@
291
314
  aria-activedescendant={activeDescendantId}
292
315
  data-testid={attributes['data-testid']}
293
316
  bind:this={field}
294
- bind:clientWidth
295
317
  autofocus={disabled ? false : autofocus}
296
318
  onclick={(event) => {
297
319
  const target = event.target as HTMLElement
@@ -453,93 +475,32 @@
453
475
  </div>
454
476
 
455
477
  {#snippet item(option: SelectOption, index: number)}
456
- {#if Array.isArray(value) && multiple}
457
- <Item
458
- id="{uid}-opt-{index}"
459
- onclick={(event) => {
460
- handleOptionSelect(event, option)
461
- field?.focus()
462
- }}
463
- tabindex={-1}
464
- disabled={option.disabled}
465
- aria-disabled={option.disabled}
466
- role="option"
467
- onkeydown={(event) => {
468
- const key = event.key
469
- if (key === 'ArrowDown') {
470
- event.preventDefault()
471
- moveFocus(1)
472
- } else if (key === 'ArrowUp') {
473
- event.preventDefault()
474
- moveFocus(-1)
475
- } else if (key === 'Home') {
476
- event.preventDefault()
477
- focusEdge(true)
478
- } else if (key === 'End') {
479
- event.preventDefault()
480
- focusEdge(false)
481
- } else if (key === 'Enter' || key === ' ') {
482
- event.preventDefault()
483
- handleOptionSelect(event, option)
484
- } else if (key === 'Tab') {
485
- menuElement?.hidePopover()
486
- } else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
487
- performTypeahead(key)
488
- }
489
- }}
490
- variant="button"
491
- selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
492
- aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
493
- >{option.label}
494
- {#snippet start()}
495
- <Check disabled={option.disabled} checked={value.includes(option.value)} />
496
- {/snippet}
497
- </Item>
498
- {:else}
499
- <Item
500
- id="{uid}-opt-{index}"
501
- onclick={(event) => {
502
- handleOptionSelect(event, option)
503
- field?.focus()
504
- }}
505
- tabindex={-1}
506
- disabled={option.disabled}
507
- aria-disabled={option.disabled}
508
- role="option"
509
- onkeydown={(event) => {
510
- const key = event.key
511
- if (key === 'ArrowDown') {
512
- event.preventDefault()
513
- moveFocus(1)
514
- } else if (key === 'ArrowUp') {
515
- event.preventDefault()
516
- moveFocus(-1)
517
- } else if (key === 'Home') {
518
- event.preventDefault()
519
- focusEdge(true)
520
- } else if (key === 'End') {
521
- event.preventDefault()
522
- focusEdge(false)
523
- } else if (key === 'Enter' || key === ' ') {
524
- event.preventDefault()
525
- handleOptionSelect(event, option)
526
- } else if (key === 'Tab') {
527
- menuElement?.hidePopover()
528
- } else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
529
- performTypeahead(key)
530
- }
531
- }}
532
- variant="button"
533
- selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
534
- aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
535
- >{option.label}
536
- </Item>
537
- {/if}
478
+ {@const isSelected = selectedSet.has(option.value)}
479
+ {#snippet check()}
480
+ <Check disabled={option.disabled} checked={isSelected} />
481
+ {/snippet}
482
+ <Item
483
+ id="{uid}-opt-{index}"
484
+ onclick={(event) => {
485
+ handleOptionSelect(event, option)
486
+ field?.focus()
487
+ }}
488
+ tabindex={-1}
489
+ disabled={option.disabled}
490
+ aria-disabled={option.disabled}
491
+ role="option"
492
+ onkeydown={(event) => optionKeydown(event, option)}
493
+ variant="button"
494
+ selected={isSelected}
495
+ aria-selected={isSelected}
496
+ start={multiple ? check : undefined}
497
+ >{option.label}
498
+ </Item>
538
499
  {/snippet}
539
500
 
540
501
  <Menu
541
502
  id="listbox-{uid}"
542
- style={`position-anchor:--${uid};${widthProp}:${clientWidth}px`}
503
+ style={`position-anchor:--${uid};${widthProp}:anchor-size(width)`}
543
504
  role="listbox"
544
505
  aria-multiselectable={multiple}
545
506
  --np-menu-justify-self="none"
@@ -689,7 +650,7 @@
689
650
  height: 100%;
690
651
  position: relative;
691
652
  }
692
- .text-field {
653
+ .np-text-field {
693
654
  display: inline-flex;
694
655
  resize: both;
695
656
  text-align: start;
@@ -0,0 +1,235 @@
1
+ <script lang="ts">
2
+ import { syncOpenEffect } from '../popover.svelte.js'
3
+ import type { SheetProps } from './types.js'
4
+
5
+ let {
6
+ open = $bindable(false),
7
+ modal = true,
8
+ placement = 'bottom',
9
+ handle = true,
10
+ headline,
11
+ action,
12
+ children,
13
+ element = $bindable(),
14
+ onclose,
15
+ ontoggle,
16
+ ...attributes
17
+ }: SheetProps = $props()
18
+
19
+ const uid = $props.id()
20
+ const headlineId = `np-sheet-headline-${uid}`
21
+
22
+ export const show = () => {
23
+ if (!element || element.open) return
24
+ if (modal) element.showModal()
25
+ else element.show()
26
+ }
27
+
28
+ export const close = () => {
29
+ element?.close()
30
+ }
31
+
32
+ syncOpenEffect(
33
+ () => element,
34
+ () => open,
35
+ show,
36
+ close,
37
+ )
38
+ </script>
39
+
40
+ <dialog
41
+ {...attributes}
42
+ bind:this={element}
43
+ tabindex="-1"
44
+ closedby={modal ? 'any' : 'none'}
45
+ aria-labelledby={headline ? headlineId : attributes['aria-labelledby']}
46
+ class={['np-sheet', `np-sheet-${placement}`, modal && 'np-sheet-modal', attributes.class]}
47
+ onclose={(event) => {
48
+ open = false
49
+ onclose?.(event)
50
+ }}
51
+ ontoggle={(event) => {
52
+ open = event.newState === 'open'
53
+ if (event.newState === 'open') element?.focus()
54
+ ontoggle?.(event)
55
+ }}
56
+ onclick={(event) => {
57
+ if (modal && event.target === element) close()
58
+ }}
59
+ >
60
+ {#if handle && placement === 'bottom'}
61
+ <div class="np-sheet-handle" aria-hidden="true"></div>
62
+ {/if}
63
+ {#if headline || action}
64
+ <div class="np-sheet-header">
65
+ {#if headline}
66
+ <h2 id={headlineId} class="np-sheet-headline">{headline}</h2>
67
+ {/if}
68
+ {#if action}
69
+ <div class="np-sheet-action">{@render action()}</div>
70
+ {/if}
71
+ </div>
72
+ {/if}
73
+ <div class="np-sheet-content">
74
+ {@render children?.()}
75
+ </div>
76
+ </dialog>
77
+
78
+ <style>
79
+ .np-sheet {
80
+ position: fixed;
81
+ outline: none;
82
+ z-index: var(--np-sheet-z-index, 24);
83
+ box-sizing: border-box;
84
+ margin: 0;
85
+ padding: 0;
86
+ border: none;
87
+ max-width: none;
88
+ max-height: none;
89
+ display: flex;
90
+ flex-direction: column;
91
+ overflow: hidden;
92
+ background-color: var(--np-sheet-container-color, var(--np-color-surface-container-low));
93
+ color: var(--np-color-on-surface);
94
+ box-shadow: var(--np-sheet-elevation, var(--np-elevation-1));
95
+ }
96
+
97
+ .np-sheet:not([open]) {
98
+ display: none;
99
+ }
100
+
101
+ .np-sheet-bottom,
102
+ .np-sheet-top {
103
+ inset-inline: 0;
104
+ width: 100%;
105
+ max-height: var(--np-sheet-size, 50dvh);
106
+ }
107
+ .np-sheet-bottom {
108
+ inset-block-end: 0;
109
+ inset-block-start: auto;
110
+ border-start-start-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
111
+ border-start-end-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
112
+ }
113
+ .np-sheet-top {
114
+ inset-block-start: 0;
115
+ inset-block-end: auto;
116
+ border-end-start-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
117
+ border-end-end-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
118
+ }
119
+
120
+ .np-sheet-start,
121
+ .np-sheet-end {
122
+ inset-block: 0;
123
+ height: 100dvh;
124
+ width: var(--np-sheet-size, 20rem);
125
+ max-width: 100%;
126
+ }
127
+ .np-sheet-start {
128
+ inset-inline-start: 0;
129
+ inset-inline-end: auto;
130
+ border-start-end-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
131
+ border-end-end-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
132
+ }
133
+ .np-sheet-end {
134
+ inset-inline-end: 0;
135
+ inset-inline-start: auto;
136
+ border-start-start-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
137
+ border-end-start-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
138
+ }
139
+
140
+ .np-sheet-handle {
141
+ flex: none;
142
+ width: 2rem;
143
+ height: 0.25rem;
144
+ margin: 1rem auto 0;
145
+ border-radius: var(--np-shape-corner-full);
146
+ background-color: var(--np-sheet-handle-color, var(--np-color-outline-variant));
147
+ }
148
+
149
+ .np-sheet-header {
150
+ flex: none;
151
+ display: flex;
152
+ align-items: center;
153
+ gap: 0.5rem;
154
+ padding: 1rem 1.5rem 0;
155
+ }
156
+ .np-sheet-headline {
157
+ flex: 1;
158
+ margin: 0;
159
+ font-size: 1.5rem;
160
+ line-height: 2rem;
161
+ font-weight: 400;
162
+ }
163
+ .np-sheet-action {
164
+ flex: none;
165
+ display: flex;
166
+ }
167
+
168
+ .np-sheet-content {
169
+ flex: 1;
170
+ min-height: 0;
171
+ overflow-y: auto;
172
+ overscroll-behavior: contain;
173
+ padding: 1rem 1.5rem 1.5rem;
174
+ }
175
+
176
+ .np-sheet-modal::backdrop {
177
+ background-color: var(--np-color-scrim);
178
+ opacity: 0.32;
179
+ }
180
+
181
+ @media (prefers-reduced-motion: no-preference) {
182
+ .np-sheet {
183
+ transition:
184
+ translate var(--np-motion-expressive-default-spatial),
185
+ display var(--np-motion-expressive-default-spatial) allow-discrete,
186
+ overlay var(--np-motion-expressive-default-spatial) allow-discrete;
187
+ }
188
+ .np-sheet::backdrop {
189
+ transition:
190
+ opacity var(--np-motion-expressive-fast-effects),
191
+ display var(--np-motion-expressive-fast-effects) allow-discrete,
192
+ overlay var(--np-motion-expressive-fast-effects) allow-discrete;
193
+ }
194
+
195
+ .np-sheet-bottom {
196
+ translate: 0 100%;
197
+ }
198
+ .np-sheet-top {
199
+ translate: 0 -100%;
200
+ }
201
+ .np-sheet-start {
202
+ translate: -100% 0;
203
+ }
204
+ .np-sheet-end {
205
+ translate: 100% 0;
206
+ }
207
+ .np-sheet[open] {
208
+ translate: 0 0;
209
+ }
210
+ @starting-style {
211
+ .np-sheet-bottom[open] {
212
+ translate: 0 100%;
213
+ }
214
+ .np-sheet-top[open] {
215
+ translate: 0 -100%;
216
+ }
217
+ .np-sheet-start[open] {
218
+ translate: -100% 0;
219
+ }
220
+ .np-sheet-end[open] {
221
+ translate: 100% 0;
222
+ }
223
+ }
224
+
225
+ .np-sheet-modal::backdrop {
226
+ opacity: 0;
227
+ }
228
+ .np-sheet-modal[open]::backdrop {
229
+ opacity: 0.32;
230
+ @starting-style {
231
+ opacity: 0;
232
+ }
233
+ }
234
+ }
235
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { SheetProps } from './types.ts';
2
+ declare const Sheet: import("svelte").Component<SheetProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "open">;
6
+ type Sheet = ReturnType<typeof Sheet>;
7
+ export default Sheet;
@@ -0,0 +1 @@
1
+ export { default as Sheet } from './Sheet.svelte';
@@ -0,0 +1 @@
1
+ export { default as Sheet } from './Sheet.svelte';
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
+ export interface SheetProps extends Omit<HTMLDialogAttributes, 'open'> {
4
+ open?: boolean;
5
+ modal?: boolean;
6
+ placement?: 'bottom' | 'top' | 'start' | 'end';
7
+ handle?: boolean;
8
+ headline?: string;
9
+ action?: Snippet;
10
+ element?: HTMLDialogElement;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { popoverController, syncOpenEffect } from '../popover.svelte.js'
2
3
  import type { SliderProps } from './types.js'
3
4
 
4
5
  let {
@@ -21,9 +22,14 @@
21
22
  element = $bindable(),
22
23
  inputElement = $bindable(),
23
24
  endInputElement = $bindable(),
25
+ issues,
26
+ 'aria-invalid': ariaInvalid,
24
27
  ...attributes
25
28
  }: SliderProps = $props()
26
29
 
30
+ let hasError = $derived(!!issues?.length)
31
+ const uid = $props.id()
32
+
27
33
  const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
28
34
 
29
35
  let span = $derived(max - min || 1)
@@ -109,6 +115,24 @@
109
115
 
110
116
  const DRAG_THRESHOLD = 3
111
117
 
118
+ let startLabelElement = $state<HTMLDivElement>()
119
+ let endLabelElement = $state<HTMLDivElement>()
120
+ let startHandleElement = $state<HTMLDivElement>()
121
+ let endHandleElement = $state<HTMLDivElement>()
122
+ let hovered = $state<'start' | 'end' | null>(null)
123
+ let focused = $state<'start' | 'end' | null>(null)
124
+
125
+ let labelled = $derived(
126
+ disabled ? null : (dragging ?? hovered ?? (pointerFocused ? null : focused)),
127
+ )
128
+
129
+ const bindLabel = (getElement: () => HTMLDivElement | undefined, side: 'start' | 'end') => {
130
+ const controller = popoverController(getElement)
131
+ syncOpenEffect(getElement, () => labelled === side, controller.show, controller.close)
132
+ }
133
+ bindLabel(() => startLabelElement, 'start')
134
+ bindLabel(() => endLabelElement, 'end')
135
+
112
136
  $effect(() => {
113
137
  if (!icon || !activeLaneElement) return
114
138
  const lane = activeLaneElement
@@ -155,7 +179,24 @@
155
179
  }
156
180
  }
157
181
 
182
+ const HOVER_TOLERANCE = 12
183
+
184
+ const distanceTo = (handle: HTMLDivElement | undefined, e: PointerEvent) => {
185
+ if (!handle) return Infinity
186
+ const r = handle.getBoundingClientRect()
187
+ const dx = Math.max(r.left - e.clientX, 0, e.clientX - r.right)
188
+ const dy = Math.max(r.top - e.clientY, 0, e.clientY - r.bottom)
189
+ return Math.hypot(dx, dy)
190
+ }
191
+
192
+ const updateHover = (e: PointerEvent) => {
193
+ const toStart = distanceTo(startHandleElement, e)
194
+ const toEnd = distanceTo(endHandleElement, e)
195
+ hovered = Math.min(toStart, toEnd) > HOVER_TOLERANCE ? null : toStart <= toEnd ? 'start' : 'end'
196
+ }
197
+
158
198
  const onpointermove = (e: PointerEvent) => {
199
+ updateHover(e)
159
200
  if (!dragging) return
160
201
  if (!tracking) {
161
202
  const d = pointerOrigin
@@ -195,6 +236,7 @@
195
236
  tracking = false
196
237
  pointerOrigin = null
197
238
  element?.releasePointerCapture(e.pointerId)
239
+ updateHover(e)
198
240
  input?.dispatchEvent(new Event('change', { bubbles: true }))
199
241
  }
200
242
 
@@ -215,6 +257,7 @@
215
257
  dragging && 'np-dragging',
216
258
  tracking && 'np-tracking',
217
259
  pointerFocused && 'np-pointer-focused',
260
+ hasError && !disabled && 'np-error',
218
261
  attributes.class,
219
262
  ]}
220
263
  role="presentation"
@@ -222,8 +265,13 @@
222
265
  {onpointermove}
223
266
  onpointerup={endDrag}
224
267
  onpointercancel={endDrag}
268
+ onpointerleave={() => (hovered = null)}
225
269
  onkeydown={() => (pointerFocused = false)}
226
- onfocusout={() => (pointerFocused = false)}
270
+ onfocusin={(event) => (focused = event.target === endInputElement ? 'end' : 'start')}
271
+ onfocusout={() => {
272
+ focused = null
273
+ pointerFocused = false
274
+ }}
227
275
  >
228
276
  <div class="np-slider-track" bind:this={trackElement}>
229
277
  {#if hasStartStop}
@@ -247,21 +295,37 @@
247
295
  <span class={['np-slider-tick', isActive(t) && 'np-on-active']} style="--_t:{t}"></span>
248
296
  {/each}
249
297
 
250
- <div class="np-slider-handle np-slider-handle-start">
298
+ <div
299
+ bind:this={startHandleElement}
300
+ class="np-slider-handle np-slider-handle-start"
301
+ style="anchor-name: --np-slider-{uid}-start"
302
+ ></div>
303
+ {#if labeled}
304
+ <div
305
+ bind:this={startLabelElement}
306
+ popover="manual"
307
+ class="np-slider-label-anchor"
308
+ style="position-anchor: --np-slider-{uid}-start"
309
+ >
310
+ <div class="np-slider-label">{labelFor(range ? Math.min(lo, hi) : lo)}</div>
311
+ </div>
312
+ {/if}
313
+ {#if range}
314
+ <div
315
+ bind:this={endHandleElement}
316
+ class="np-slider-handle np-slider-handle-end"
317
+ style="anchor-name: --np-slider-{uid}-end"
318
+ ></div>
251
319
  {#if labeled}
252
- <div class="np-slider-label-anchor">
253
- <div class="np-slider-label">{labelFor(range ? Math.min(lo, hi) : lo)}</div>
320
+ <div
321
+ bind:this={endLabelElement}
322
+ popover="manual"
323
+ class="np-slider-label-anchor"
324
+ style="position-anchor: --np-slider-{uid}-end"
325
+ >
326
+ <div class="np-slider-label">{labelFor(Math.max(lo, hi))}</div>
254
327
  </div>
255
328
  {/if}
256
- </div>
257
- {#if range}
258
- <div class="np-slider-handle np-slider-handle-end">
259
- {#if labeled}
260
- <div class="np-slider-label-anchor">
261
- <div class="np-slider-label">{labelFor(Math.max(lo, hi))}</div>
262
- </div>
263
- {/if}
264
- </div>
265
329
  {/if}
266
330
  </div>
267
331
 
@@ -277,6 +341,7 @@
277
341
  {disabled}
278
342
  aria-orientation={orientation}
279
343
  aria-valuetext={format ? labelFor(lo) : attributes['aria-valuetext']}
344
+ aria-invalid={hasError ? 'true' : ariaInvalid}
280
345
  />
281
346
  {#if range}
282
347
  <input
@@ -377,6 +442,14 @@
377
442
  background: var(--np-slider-active-track-color, var(--np-color-primary));
378
443
  }
379
444
 
445
+ .np-error {
446
+ --np-slider-active-track-color: var(--np-color-error);
447
+ --np-slider-handle-color: var(--np-color-error);
448
+ --np-slider-active-stop-color: var(--np-color-on-error);
449
+ --np-slider-label-container-color: var(--np-color-error);
450
+ --np-slider-label-text-color: var(--np-color-on-error);
451
+ }
452
+
380
453
  .np-slider-inactive-start {
381
454
  inset-inline-start: 0;
382
455
  inset-inline-end: calc(100% - var(--_i1));
@@ -502,15 +575,42 @@
502
575
  block-size var(--np-motion-expressive-fast-effects);
503
576
  }
504
577
 
505
- .np-slider-label-anchor {
506
- position: absolute;
507
- inset-block-end: calc(100% + 0.375rem);
508
- inset-inline: 0;
509
- display: flex;
578
+ .np-slider-label-anchor[popover] {
579
+ position-area: block-start;
580
+ justify-self: anchor-center;
581
+ margin: 0 0 0.375rem 0;
582
+ border: none;
583
+ background: none;
584
+ padding: 0;
510
585
  justify-content: center;
511
586
  pointer-events: none;
512
587
  }
513
588
 
589
+ .np-slider-label-anchor:popover-open {
590
+ display: flex;
591
+ opacity: 1;
592
+ scale: 1;
593
+ }
594
+
595
+ @media (prefers-reduced-motion: no-preference) {
596
+ .np-slider-label-anchor:popover-open {
597
+ animation:
598
+ fadeIn var(--np-motion-expressive-fast-effects),
599
+ scaleIn var(--np-motion-expressive-fast-spatial);
600
+ }
601
+ }
602
+
603
+ @keyframes fadeIn {
604
+ from {
605
+ opacity: 0;
606
+ }
607
+ }
608
+ @keyframes scaleIn {
609
+ from {
610
+ scale: 0.8;
611
+ }
612
+ }
613
+
514
614
  .np-slider-label {
515
615
  flex: none;
516
616
  writing-mode: horizontal-tb;
@@ -526,18 +626,6 @@
526
626
  font-weight: 500;
527
627
  text-align: center;
528
628
  white-space: nowrap;
529
- opacity: 0;
530
- transition: opacity var(--np-motion-expressive-fast-effects);
531
- }
532
-
533
- .np-slider.np-labeled:hover .np-slider-label,
534
- .np-slider.np-labeled.np-dragging .np-slider-label,
535
- .np-slider.np-labeled:has(.np-slider-input:focus-visible) .np-slider-label {
536
- opacity: 1;
537
- }
538
-
539
- .np-slider.np-disabled .np-slider-label {
540
- opacity: 0;
541
629
  }
542
630
 
543
631
  .np-slider-input {
@@ -20,4 +20,7 @@ export interface SliderProps extends Omit<HTMLInputAttributes, 'type' | 'value'
20
20
  element?: HTMLDivElement;
21
21
  inputElement?: HTMLInputElement;
22
22
  endInputElement?: HTMLInputElement;
23
+ issues?: {
24
+ message: string;
25
+ }[];
23
26
  }