@dorsk/tsumikit 0.74.0 → 0.76.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 (132) hide show
  1. package/README.md +38 -1
  2. package/dist/autoresize.d.ts +2 -0
  3. package/dist/autoresize.js +8 -1
  4. package/dist/components/atoms/Artwork.svelte +12 -5
  5. package/dist/components/atoms/Avatar.svelte +1 -1
  6. package/dist/components/atoms/Badge.svelte +59 -23
  7. package/dist/components/atoms/Button.svelte +130 -96
  8. package/dist/components/atoms/Button.svelte.d.ts +2 -0
  9. package/dist/components/atoms/Card.svelte +10 -4
  10. package/dist/components/atoms/Checkbox.svelte +36 -9
  11. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  12. package/dist/components/atoms/Gauge.svelte +20 -14
  13. package/dist/components/atoms/Heading.svelte +1 -1
  14. package/dist/components/atoms/Icon.svelte +1 -0
  15. package/dist/components/atoms/Input.svelte +67 -13
  16. package/dist/components/atoms/Kbd.svelte +3 -3
  17. package/dist/components/atoms/Link.svelte +7 -2
  18. package/dist/components/atoms/Progress.svelte +4 -2
  19. package/dist/components/atoms/Scrim.svelte +3 -8
  20. package/dist/components/atoms/SegmentedProgress.svelte +5 -4
  21. package/dist/components/atoms/Select.svelte +58 -27
  22. package/dist/components/atoms/Slider.svelte +79 -22
  23. package/dist/components/atoms/Slider.svelte.d.ts +2 -0
  24. package/dist/components/atoms/Swatch.svelte +19 -8
  25. package/dist/components/atoms/Switch.svelte +34 -21
  26. package/dist/components/atoms/Switch.svelte.d.ts +2 -0
  27. package/dist/components/atoms/Text.svelte +1 -1
  28. package/dist/components/atoms/Textarea.svelte +34 -12
  29. package/dist/components/atoms/TimeDisplay.svelte +4 -2
  30. package/dist/components/layouts/AppShell.svelte +26 -15
  31. package/dist/components/layouts/MasterDetail.svelte +23 -21
  32. package/dist/components/layouts/NavBar.svelte +11 -8
  33. package/dist/components/layouts/NavItem.svelte +15 -7
  34. package/dist/components/layouts/NavSection.svelte +1 -1
  35. package/dist/components/layouts/ResizablePanel.svelte +37 -16
  36. package/dist/components/layouts/TabBar.svelte +24 -14
  37. package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
  38. package/dist/components/molecules/Accordion.svelte +4 -0
  39. package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
  40. package/dist/components/molecules/AttachmentList.svelte +14 -11
  41. package/dist/components/molecules/Breadcrumb.svelte +13 -9
  42. package/dist/components/molecules/Callout.svelte +3 -3
  43. package/dist/components/molecules/CapBar.svelte +23 -22
  44. package/dist/components/molecules/Carousel.svelte +27 -17
  45. package/dist/components/molecules/ChatBubble.svelte +2 -2
  46. package/dist/components/molecules/CodeBlock.svelte +2 -2
  47. package/dist/components/molecules/Combobox.svelte +30 -1
  48. package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
  49. package/dist/components/molecules/Composer.svelte +2 -2
  50. package/dist/components/molecules/ConfirmModal.svelte +4 -0
  51. package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
  52. package/dist/components/molecules/CopyButton.svelte +46 -2
  53. package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
  54. package/dist/components/molecules/Disclosure.svelte +23 -12
  55. package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
  56. package/dist/components/molecules/Drawer.svelte +43 -23
  57. package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
  58. package/dist/components/molecules/Dropzone.svelte +24 -8
  59. package/dist/components/molecules/EmojiPicker.svelte +27 -8
  60. package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
  61. package/dist/components/molecules/EmptyState.svelte +5 -5
  62. package/dist/components/molecules/Fieldset.svelte +8 -13
  63. package/dist/components/molecules/FileButton.svelte +57 -31
  64. package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
  65. package/dist/components/molecules/FilterInput.svelte +51 -11
  66. package/dist/components/molecules/FontScalePicker.svelte +20 -9
  67. package/dist/components/molecules/GitRef.svelte +9 -7
  68. package/dist/components/molecules/GlyphStack.svelte +46 -14
  69. package/dist/components/molecules/IconButton.svelte +6 -3
  70. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  71. package/dist/components/molecules/InputGroup.svelte +13 -12
  72. package/dist/components/molecules/LoadMore.svelte +6 -1
  73. package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
  74. package/dist/components/molecules/Menu.svelte +33 -7
  75. package/dist/components/molecules/Menu.svelte.d.ts +2 -0
  76. package/dist/components/molecules/Metric.svelte +11 -9
  77. package/dist/components/molecules/Modal.svelte +76 -26
  78. package/dist/components/molecules/Modal.svelte.d.ts +2 -0
  79. package/dist/components/molecules/OptionButton.svelte +28 -10
  80. package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
  81. package/dist/components/molecules/Pagination.svelte +26 -9
  82. package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
  83. package/dist/components/molecules/Popover.svelte +107 -51
  84. package/dist/components/molecules/Popover.svelte.d.ts +2 -0
  85. package/dist/components/molecules/Prose.svelte +1 -1
  86. package/dist/components/molecules/PullToRefresh.svelte +14 -10
  87. package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
  88. package/dist/components/molecules/RadialMenu.svelte +52 -19
  89. package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
  90. package/dist/components/molecules/RadioGroup.svelte +38 -23
  91. package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
  92. package/dist/components/molecules/RichSelect.svelte +583 -0
  93. package/dist/components/molecules/RichSelect.svelte.d.ts +68 -0
  94. package/dist/components/molecules/SectionHeader.svelte +13 -8
  95. package/dist/components/molecules/SeekSlider.svelte +38 -27
  96. package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
  97. package/dist/components/molecules/SegmentedControl.svelte +139 -86
  98. package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
  99. package/dist/components/molecules/SelectButton.svelte +21 -6
  100. package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
  101. package/dist/components/molecules/SortableList.svelte +10 -11
  102. package/dist/components/molecules/SoundSettings.svelte +161 -0
  103. package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
  104. package/dist/components/molecules/SplitButton.svelte +30 -3
  105. package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
  106. package/dist/components/molecules/SwipeRow.svelte +18 -15
  107. package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
  108. package/dist/components/molecules/Tabs.svelte +92 -10
  109. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  110. package/dist/components/molecules/ThemePicker.svelte +25 -10
  111. package/dist/components/molecules/Timestamp.svelte +12 -7
  112. package/dist/components/molecules/Toaster.svelte +65 -14
  113. package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
  114. package/dist/components/molecules/Toggle.svelte +36 -13
  115. package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
  116. package/dist/components/molecules/Tooltip.svelte +41 -10
  117. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  118. package/dist/components/molecules/VolumeSlider.svelte +34 -14
  119. package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
  120. package/dist/components/molecules/WorkingDir.svelte +1 -0
  121. package/dist/components/molecules/rich-select-keyboard.d.ts +82 -0
  122. package/dist/components/molecules/rich-select-keyboard.js +114 -0
  123. package/dist/components/organisms/ConversationFeed.svelte +24 -1
  124. package/dist/components/organisms/DataTable.svelte +37 -14
  125. package/dist/floating.js +26 -0
  126. package/dist/index.d.ts +2 -0
  127. package/dist/index.js +2 -0
  128. package/dist/sound-synth.js +17 -12
  129. package/dist/stores/toast.svelte.d.ts +9 -0
  130. package/dist/stores/toast.svelte.js +45 -9
  131. package/dist/styles/reset.css +3 -3
  132. package/package.json +1 -1
@@ -0,0 +1,583 @@
1
+ <script lang="ts" module>
2
+ export type RichSelectOptionContext<T> = {
3
+ option: T;
4
+ index: number;
5
+ selected: boolean;
6
+ active: boolean;
7
+ /** True for the copy rendered inside the closed trigger. */
8
+ inTrigger: boolean;
9
+ };
10
+ </script>
11
+
12
+ <script lang="ts" generics="T">
13
+ import { tick, type Snippet } from 'svelte';
14
+ import Icon from '../atoms/Icon.svelte';
15
+ import { playSound } from '../../sound';
16
+ import Popover from './Popover.svelte';
17
+ import { createTypeahead, richSelectAction, typeaheadIndex } from './rich-select-keyboard.js';
18
+ import { slotForTop } from './sortable-list-order.js';
19
+
20
+ type Key = string | number;
21
+
22
+ type Props = {
23
+ options: readonly T[];
24
+ /** Key of the selected option (`getKey`). */
25
+ value?: Key;
26
+ onchange?: (value: Key, option: T) => void;
27
+ getKey?: (option: T) => Key;
28
+ /** Text used by typeahead and announcements. */
29
+ getLabel?: (option: T) => string;
30
+ /** The card, rendered in the closed trigger and for every listbox row.
31
+ * It sits inside the trigger button, so it must not contain controls. */
32
+ option: Snippet<[RichSelectOptionContext<T>]>;
33
+ /** Per-row actions beside the card. They never select the option. */
34
+ trailing?: Snippet<[{ option: T; index: number; close: () => void }]>;
35
+ footer?: Snippet<[{ close: () => void }]>;
36
+ /** Enables the drag handles and Alt+ArrowUp/Down; reorder `options` in it. */
37
+ onreorder?: (from: number, to: number) => void;
38
+ placeholder?: Snippet | string;
39
+ /** Accessible name of the select. */
40
+ label: string;
41
+ disabled?: boolean;
42
+ box?: 'sm' | 'md' | 'lg';
43
+ placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
44
+ open?: boolean;
45
+ /** Play UI sounds when enabled globally; false silences this instance. */
46
+ sound?: boolean;
47
+ /** Spoken after a keyboard reorder. */
48
+ movedMessage?: (label: string, position: number, total: number) => string;
49
+ class?: string;
50
+ style?: string;
51
+ };
52
+
53
+ const fallbackKey = (o: T): Key => {
54
+ const r = o as { id?: Key; value?: Key; key?: Key };
55
+ return r?.key ?? r?.id ?? r?.value ?? String(o);
56
+ };
57
+ const fallbackLabel = (o: T): string => {
58
+ const r = o as { label?: string; name?: string };
59
+ return r?.label ?? r?.name ?? String(o);
60
+ };
61
+
62
+ let {
63
+ options,
64
+ value = $bindable(),
65
+ onchange,
66
+ getKey = fallbackKey,
67
+ getLabel = fallbackLabel,
68
+ option,
69
+ trailing,
70
+ footer,
71
+ onreorder,
72
+ placeholder = 'Select…',
73
+ label,
74
+ disabled = false,
75
+ box = 'md',
76
+ placement = 'bottom-start',
77
+ open = $bindable(false),
78
+ sound = true,
79
+ movedMessage = (name, position, total) => `${name} moved to position ${position} of ${total}`,
80
+ class: klass = '',
81
+ style = '',
82
+ }: Props = $props();
83
+
84
+ const uid = Math.random().toString(36).slice(2, 8);
85
+ const listId = `rs-list-${uid}`;
86
+ const optionId = (i: number) => `rs-opt-${uid}-${i}`;
87
+
88
+ let root = $state<HTMLDivElement | null>(null);
89
+ let active = $state(-1);
90
+ let announcement = $state('');
91
+ let triggerWidth = $state(0);
92
+ let dragFrom = $state(-1);
93
+ let dragTo = $state(-1);
94
+ let dragDy = $state(0);
95
+ let dragHeight = 0;
96
+
97
+ const selectedIndex = $derived(options.findIndex((o) => getKey(o) === value));
98
+ const selectedOption = $derived(selectedIndex >= 0 ? options[selectedIndex] : undefined);
99
+ const typeahead = createTypeahead();
100
+
101
+ const triggerEl = () => root?.querySelector<HTMLElement>('.rs-trigger') ?? null;
102
+ const rowEls = () => [...(root?.querySelectorAll<HTMLElement>('.rs-row') ?? [])];
103
+
104
+ $effect(() => {
105
+ const el = triggerEl();
106
+ if (!el || typeof ResizeObserver === 'undefined') return;
107
+ const ro = new ResizeObserver(() => (triggerWidth = el.offsetWidth));
108
+ ro.observe(el);
109
+ return () => ro.disconnect();
110
+ });
111
+
112
+ $effect(() => {
113
+ triggerEl()?.setAttribute('role', 'combobox');
114
+ });
115
+
116
+ $effect(() => {
117
+ if (!open) return;
118
+ if (active < 0 || active >= options.length) active = selectedIndex >= 0 ? selectedIndex : 0;
119
+ });
120
+
121
+ $effect(() => {
122
+ if (!open || active < 0) return;
123
+ const el = root?.ownerDocument.getElementById(optionId(active));
124
+ el?.scrollIntoView?.({ block: 'nearest' });
125
+ });
126
+
127
+ function close() {
128
+ open = false;
129
+ }
130
+
131
+ function focusTrigger() {
132
+ triggerEl()?.focus();
133
+ }
134
+
135
+ function choose(index: number, closeAfter: boolean) {
136
+ const next = options[index];
137
+ if (next === undefined) return;
138
+ const key = getKey(next);
139
+ active = index;
140
+ if (key !== value) {
141
+ value = key;
142
+ onchange?.(key, next);
143
+ playSound(closeAfter ? 'click' : 'tick', sound);
144
+ }
145
+ if (closeAfter) {
146
+ close();
147
+ focusTrigger();
148
+ }
149
+ }
150
+
151
+ function reorder(from: number, to: number) {
152
+ if (!onreorder || from === to) return;
153
+ const moved = options[from];
154
+ onreorder(from, to);
155
+ active = to;
156
+ if (moved !== undefined) announcement = movedMessage(getLabel(moved), to + 1, options.length);
157
+ playSound('tick', sound);
158
+ }
159
+
160
+ function trailingFocusables(index: number) {
161
+ const row = rowEls()[index];
162
+ return [
163
+ ...(row?.querySelectorAll<HTMLElement>(
164
+ '.rs-trailing :is(button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])):not(:disabled)'
165
+ ) ?? []),
166
+ ];
167
+ }
168
+
169
+ function onTriggerKey(e: KeyboardEvent) {
170
+ const action = richSelectAction(e, {
171
+ open,
172
+ count: options.length,
173
+ index: open ? active : selectedIndex,
174
+ reorder: onreorder !== undefined,
175
+ trailing: open && active >= 0 && trailingFocusables(active).length > 0,
176
+ });
177
+ if (!action) return;
178
+ if (action.type === 'close' && e.key === 'Tab') {
179
+ close();
180
+ return;
181
+ }
182
+ e.preventDefault();
183
+ e.stopPropagation();
184
+ switch (action.type) {
185
+ case 'open':
186
+ active = selectedIndex >= 0 ? selectedIndex : 0;
187
+ open = true;
188
+ break;
189
+ case 'close':
190
+ close();
191
+ focusTrigger();
192
+ break;
193
+ case 'select':
194
+ choose(active, true);
195
+ break;
196
+ case 'move':
197
+ active = action.index;
198
+ break;
199
+ case 'change':
200
+ choose(action.index, false);
201
+ break;
202
+ case 'reorder':
203
+ reorder(action.from, action.to);
204
+ break;
205
+ case 'trailing':
206
+ trailingFocusables(active)[0]?.focus();
207
+ break;
208
+ case 'type': {
209
+ const from = open ? active : selectedIndex;
210
+ const hit = typeaheadIndex(options.map(getLabel), typeahead.push(action.char), from);
211
+ if (hit === undefined) break;
212
+ if (open) active = hit;
213
+ else choose(hit, false);
214
+ break;
215
+ }
216
+ }
217
+ }
218
+
219
+ function onPanelKey(e: KeyboardEvent, target: HTMLElement) {
220
+ if (!target.closest('.rs-trailing')) {
221
+ if (e.key === 'Escape') {
222
+ e.preventDefault();
223
+ e.stopPropagation();
224
+ close();
225
+ focusTrigger();
226
+ }
227
+ return;
228
+ }
229
+ const row = target.closest<HTMLElement>('.rs-row');
230
+ const index = row ? rowEls().indexOf(row) : -1;
231
+ const items = index >= 0 ? trailingFocusables(index) : [];
232
+ const at = items.indexOf(target);
233
+ const back = e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey && at <= 0);
234
+ if (e.key === 'Escape' || back || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
235
+ e.preventDefault();
236
+ e.stopPropagation();
237
+ if (e.key === 'ArrowLeft' && at > 0) {
238
+ items[at - 1].focus();
239
+ return;
240
+ }
241
+ if (e.key === 'Escape') close();
242
+ else if (e.key === 'ArrowUp') active = Math.max(index - 1, 0);
243
+ else if (e.key === 'ArrowDown') active = Math.min(index + 1, options.length - 1);
244
+ focusTrigger();
245
+ return;
246
+ }
247
+ if (e.key === 'ArrowRight' && at >= 0 && at < items.length - 1) {
248
+ e.preventDefault();
249
+ e.stopPropagation();
250
+ items[at + 1].focus();
251
+ }
252
+ }
253
+
254
+ function onKeydownCapture(e: KeyboardEvent) {
255
+ if (disabled) return;
256
+ const target = e.target as HTMLElement;
257
+ if (target === triggerEl()) onTriggerKey(e);
258
+ else if (target.closest('.rs-panel')) onPanelKey(e, target);
259
+ }
260
+
261
+ function onGripDown(e: PointerEvent, index: number) {
262
+ if (!onreorder || e.button !== 0) return;
263
+ e.preventDefault();
264
+ const grip = e.currentTarget as HTMLElement;
265
+ grip.setPointerCapture?.(e.pointerId);
266
+ const rows = rowEls();
267
+ const startY = e.clientY;
268
+ const top = (rows[index]?.offsetTop ?? 0) - (rows[0]?.offsetTop ?? 0);
269
+ const others = rows.filter((_, i) => i !== index).map((r) => r.offsetHeight);
270
+ dragHeight = rows[index]?.offsetHeight ?? 0;
271
+ dragFrom = index;
272
+ dragTo = index;
273
+ dragDy = 0;
274
+ active = index;
275
+ const move = (ev: PointerEvent) => {
276
+ dragDy = ev.clientY - startY;
277
+ dragTo = slotForTop(top + dragDy, others);
278
+ };
279
+ const end = (ev: PointerEvent) => {
280
+ grip.removeEventListener('pointermove', move);
281
+ grip.removeEventListener('pointerup', end);
282
+ grip.removeEventListener('pointercancel', end);
283
+ const from = dragFrom;
284
+ const to = dragTo;
285
+ dragFrom = -1;
286
+ dragTo = -1;
287
+ dragDy = 0;
288
+ if (ev.type === 'pointerup') reorder(from, to);
289
+ };
290
+ grip.addEventListener('pointermove', move);
291
+ grip.addEventListener('pointerup', end);
292
+ grip.addEventListener('pointercancel', end);
293
+ }
294
+
295
+ function shiftFor(i: number) {
296
+ if (dragFrom < 0) return undefined;
297
+ if (i === dragFrom) return `0 ${dragDy}px`;
298
+ if (dragFrom < i && i <= dragTo) return `0 ${-dragHeight}px`;
299
+ if (dragTo <= i && i < dragFrom) return `0 ${dragHeight}px`;
300
+ return undefined;
301
+ }
302
+
303
+ async function onOpen() {
304
+ typeahead.reset();
305
+ await tick();
306
+ const el = active >= 0 ? root?.ownerDocument.getElementById(optionId(active)) : null;
307
+ el?.scrollIntoView?.({ block: 'nearest' });
308
+ }
309
+
310
+ function onClose() {
311
+ typeahead.reset();
312
+ dragFrom = -1;
313
+ }
314
+
315
+ const panelStyle = $derived(
316
+ triggerWidth > 0
317
+ ? `min-width: max(10rem, ${triggerWidth}px); max-width: min(max(22rem, ${triggerWidth}px), calc(100vw - 2 * var(--sp-3)))`
318
+ : ''
319
+ );
320
+ </script>
321
+
322
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
323
+ <div
324
+ bind:this={root}
325
+ data-tsu="RichSelect"
326
+ class="rich-select box-{box} {klass}"
327
+ class:is-disabled={disabled}
328
+ class:is-dragging={dragFrom >= 0}
329
+ class:is-open={open}
330
+ {style}
331
+ onkeydowncapture={onKeydownCapture}
332
+ >
333
+ <Popover
334
+ bind:open
335
+ bare
336
+ {label}
337
+ {placement}
338
+ {disabled}
339
+ {sound}
340
+ role="group"
341
+ haspopup="listbox"
342
+ triggerClass="rs-trigger"
343
+ panelClass="rs-panel"
344
+ {panelStyle}
345
+ onopen={onOpen}
346
+ onclose={onClose}
347
+ aria-controls={listId}
348
+ aria-activedescendant={open && active >= 0 ? optionId(active) : undefined}
349
+ >
350
+ {#snippet trigger()}
351
+ <span class="rs-value">
352
+ {#if selectedOption !== undefined}
353
+ {@render option({ option: selectedOption, index: selectedIndex, selected: true, active: false, inTrigger: true })}
354
+ {:else if typeof placeholder === 'string'}
355
+ <span class="rs-placeholder">{placeholder}</span>
356
+ {:else}
357
+ {@render placeholder()}
358
+ {/if}
359
+ </span>
360
+ <span class="rs-chevron" aria-hidden="true"><Icon name="chevron-down" /></span>
361
+ {/snippet}
362
+ <div class="rs-list" role="listbox" id={listId} aria-label={label}>
363
+ {#each options as opt, i (getKey(opt))}
364
+ {@const selected = i === selectedIndex}
365
+ <div
366
+ class="rs-row"
367
+ role="presentation"
368
+ data-active={i === active ? '' : undefined}
369
+ data-selected={selected ? '' : undefined}
370
+ data-dragging={i === dragFrom ? '' : undefined}
371
+ style:translate={shiftFor(i)}
372
+ >
373
+ {#if onreorder}
374
+ <span class="rs-grip" aria-hidden="true" onpointerdown={(e) => onGripDown(e, i)}></span>
375
+ {/if}
376
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
377
+ <div
378
+ class="rs-option"
379
+ role="option"
380
+ id={optionId(i)}
381
+ aria-selected={selected}
382
+ tabindex="-1"
383
+ onclick={() => choose(i, true)}
384
+ onmousedown={(e) => e.preventDefault()}
385
+ onpointermove={() => {
386
+ if (dragFrom < 0) active = i;
387
+ }}
388
+ >
389
+ {@render option({ option: opt, index: i, selected, active: i === active, inTrigger: false })}
390
+ </div>
391
+ {#if trailing}
392
+ <div class="rs-trailing">{@render trailing({ option: opt, index: i, close })}</div>
393
+ {/if}
394
+ </div>
395
+ {/each}
396
+ </div>
397
+ {#if footer}
398
+ <div class="rs-footer">{@render footer({ close })}</div>
399
+ {/if}
400
+ </Popover>
401
+ <span class="sr-only" aria-live="polite">{announcement}</span>
402
+ </div>
403
+
404
+ <style>
405
+ .rich-select {
406
+ display: block;
407
+ min-width: 0;
408
+ --rs-height: var(--control-md);
409
+ }
410
+ .rich-select.box-sm {
411
+ --rs-height: var(--control-sm);
412
+ }
413
+ .rich-select.box-lg {
414
+ --rs-height: var(--control-lg);
415
+ }
416
+ .rich-select :global(.pop-trigger.rs-trigger) {
417
+ display: flex;
418
+ align-items: center;
419
+ gap: var(--sp-2);
420
+ box-sizing: border-box;
421
+ width: 100%;
422
+ min-width: 0;
423
+ min-height: var(--rs-height);
424
+ padding: var(--sp-1) var(--sp-2) var(--sp-1) var(--sp-1);
425
+ border: 1px solid var(--border-strong);
426
+ border-radius: var(--r-md);
427
+ background: var(--surface);
428
+ color: var(--text);
429
+ text-align: start;
430
+ cursor: pointer;
431
+ transition:
432
+ border-color var(--dur-fast) var(--ease),
433
+ scale var(--dur-fast) var(--ease-out);
434
+ }
435
+ .rich-select :global(.pop-trigger.rs-trigger:focus-visible) {
436
+ outline: var(--focus-ring);
437
+ outline-offset: var(--focus-ring-offset);
438
+ }
439
+ .rich-select :global(.pop-trigger.rs-trigger:active:not(:disabled)) {
440
+ scale: var(--press-scale-lg);
441
+ transition-duration: var(--dur-press);
442
+ }
443
+ .rich-select :global(.pop-trigger.rs-trigger[aria-expanded='true']) {
444
+ border-color: var(--accent);
445
+ }
446
+ .rich-select :global(.pop-trigger.rs-trigger:disabled) {
447
+ opacity: var(--disabled-opacity);
448
+ cursor: not-allowed;
449
+ }
450
+ @media (hover: hover) and (pointer: fine) {
451
+ .rich-select :global(.pop-trigger.rs-trigger:hover:not(:disabled)) {
452
+ border-color: var(--accent);
453
+ }
454
+ }
455
+ .rs-value {
456
+ flex: 1 1 auto;
457
+ min-width: 0;
458
+ overflow: hidden;
459
+ }
460
+ .rs-placeholder {
461
+ display: block;
462
+ padding-inline: var(--sp-1);
463
+ color: var(--text-muted);
464
+ font-size: var(--control-fs-md);
465
+ line-height: var(--lh-tight);
466
+ overflow: hidden;
467
+ text-overflow: ellipsis;
468
+ white-space: nowrap;
469
+ }
470
+ .rs-chevron {
471
+ display: inline-flex;
472
+ flex: none;
473
+ color: var(--text-muted);
474
+ --icon-size: var(--control-icon-md);
475
+ transition: rotate var(--dur-fast) var(--ease-out);
476
+ }
477
+ .rich-select.is-open .rs-chevron {
478
+ rotate: 180deg;
479
+ }
480
+ .rich-select :global(.pop-panel.rs-panel) {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: var(--sp-1);
484
+ box-sizing: border-box;
485
+ max-height: min(70vh, 28rem);
486
+ padding: var(--sp-1);
487
+ }
488
+ .rs-list {
489
+ display: flex;
490
+ flex-direction: column;
491
+ gap: var(--sp-0-5);
492
+ min-height: 0;
493
+ overflow-y: auto;
494
+ overscroll-behavior: contain;
495
+ }
496
+ .rs-row {
497
+ position: relative;
498
+ display: flex;
499
+ align-items: center;
500
+ gap: var(--sp-1);
501
+ min-height: var(--control-sm);
502
+ border-radius: var(--r-sm);
503
+ transition:
504
+ background-color var(--dur-fast) var(--ease),
505
+ translate var(--dur-fast) var(--ease-out);
506
+ }
507
+ .rs-row[data-active] {
508
+ background: var(--bg-elevated-2);
509
+ transition-duration: var(--dur-instant), var(--dur-fast);
510
+ }
511
+ .rs-row[data-selected] {
512
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
513
+ }
514
+ .rs-row[data-selected][data-active] {
515
+ background: color-mix(in srgb, var(--accent) 18%, var(--bg-elevated-2));
516
+ }
517
+ .rs-row[data-selected]::before {
518
+ content: '';
519
+ position: absolute;
520
+ inset-block: var(--sp-1);
521
+ inset-inline-start: 0;
522
+ width: 3px;
523
+ border-radius: var(--r-pill);
524
+ background: var(--accent);
525
+ }
526
+ .rs-row[data-dragging] {
527
+ z-index: 1;
528
+ box-shadow: var(--shadow-md);
529
+ background: var(--bg-elevated);
530
+ transition: none;
531
+ }
532
+ .rs-option {
533
+ flex: 1 1 auto;
534
+ min-width: 0;
535
+ padding: var(--sp-1);
536
+ border-radius: var(--r-sm);
537
+ cursor: pointer;
538
+ outline: none;
539
+ }
540
+ .rs-trailing {
541
+ display: flex;
542
+ flex: none;
543
+ align-items: center;
544
+ gap: var(--sp-1);
545
+ padding-inline-end: var(--sp-1);
546
+ }
547
+ .rs-grip {
548
+ flex: none;
549
+ align-self: stretch;
550
+ width: 24px;
551
+ min-height: 24px;
552
+ margin-inline-start: var(--sp-0-5);
553
+ border-radius: var(--r-sm);
554
+ display: grid;
555
+ place-items: center;
556
+ color: var(--text-muted);
557
+ cursor: grab;
558
+ touch-action: none;
559
+ opacity: 0.6;
560
+ transition: opacity var(--dur-fast) var(--ease);
561
+ }
562
+ .rs-grip::before {
563
+ content: '';
564
+ width: 10px;
565
+ height: 16px;
566
+ background: radial-gradient(circle, currentColor 1.5px, transparent 1.75px) 0 0 / 5px 5.333px;
567
+ }
568
+ @media (hover: hover) and (pointer: fine) {
569
+ .rs-grip:hover {
570
+ opacity: 1;
571
+ }
572
+ }
573
+ .is-dragging .rs-grip {
574
+ cursor: grabbing;
575
+ }
576
+ .rs-footer {
577
+ flex: none;
578
+ display: flex;
579
+ flex-direction: column;
580
+ padding-top: var(--sp-1);
581
+ border-top: 1px solid var(--border);
582
+ }
583
+ </style>
@@ -0,0 +1,68 @@
1
+ export type RichSelectOptionContext<T> = {
2
+ option: T;
3
+ index: number;
4
+ selected: boolean;
5
+ active: boolean;
6
+ /** True for the copy rendered inside the closed trigger. */
7
+ inTrigger: boolean;
8
+ };
9
+ import { type Snippet } from 'svelte';
10
+ declare function $$render<T>(): {
11
+ props: {
12
+ options: readonly T[];
13
+ /** Key of the selected option (`getKey`). */
14
+ value?: string | number;
15
+ onchange?: (value: string | number, option: T) => void;
16
+ getKey?: (option: T) => string | number;
17
+ /** Text used by typeahead and announcements. */
18
+ getLabel?: (option: T) => string;
19
+ /** The card, rendered in the closed trigger and for every listbox row.
20
+ * It sits inside the trigger button, so it must not contain controls. */
21
+ option: Snippet<[RichSelectOptionContext<T>]>;
22
+ /** Per-row actions beside the card. They never select the option. */
23
+ trailing?: Snippet<[{
24
+ option: T;
25
+ index: number;
26
+ close: () => void;
27
+ }]>;
28
+ footer?: Snippet<[{
29
+ close: () => void;
30
+ }]>;
31
+ /** Enables the drag handles and Alt+ArrowUp/Down; reorder `options` in it. */
32
+ onreorder?: (from: number, to: number) => void;
33
+ placeholder?: Snippet | string;
34
+ /** Accessible name of the select. */
35
+ label: string;
36
+ disabled?: boolean;
37
+ box?: "sm" | "md" | "lg";
38
+ placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
39
+ open?: boolean;
40
+ /** Play UI sounds when enabled globally; false silences this instance. */
41
+ sound?: boolean;
42
+ /** Spoken after a keyboard reorder. */
43
+ movedMessage?: (label: string, position: number, total: number) => string;
44
+ class?: string;
45
+ style?: string;
46
+ };
47
+ exports: {};
48
+ bindings: "open" | "value";
49
+ slots: {};
50
+ events: {};
51
+ };
52
+ declare class __sveltets_Render<T> {
53
+ props(): ReturnType<typeof $$render<T>>['props'];
54
+ events(): ReturnType<typeof $$render<T>>['events'];
55
+ slots(): ReturnType<typeof $$render<T>>['slots'];
56
+ bindings(): "open" | "value";
57
+ exports(): {};
58
+ }
59
+ interface $$IsomorphicComponent {
60
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
61
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
62
+ } & ReturnType<__sveltets_Render<T>['exports']>;
63
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
64
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
65
+ }
66
+ declare const RichSelect: $$IsomorphicComponent;
67
+ type RichSelect<T> = InstanceType<typeof RichSelect<T>>;
68
+ export default RichSelect;
@@ -86,6 +86,7 @@
86
86
  class:sh-sticky={sticky}
87
87
  class:sh-uppercase={uppercase}
88
88
  class:sh-open={open}
89
+ class:sh-collapsible={collapsible}
89
90
  {...rest}
90
91
  >
91
92
  <div class="sh-row">
@@ -221,7 +222,7 @@
221
222
  display: inline-flex;
222
223
  flex: none;
223
224
  color: var(--sh-tone);
224
- line-height: 1;
225
+ line-height: var(--lh-none);
225
226
  }
226
227
  .sh-swatch {
227
228
  flex: none;
@@ -263,23 +264,27 @@
263
264
  border-radius: var(--r-sm);
264
265
  }
265
266
  .sh-toggle:focus-visible {
266
- outline: 2px solid var(--accent);
267
- outline-offset: 2px;
267
+ outline: var(--focus-ring);
268
+ outline-offset: var(--focus-ring-offset);
268
269
  }
269
270
  .section-header :global(.sh-chevron) {
270
271
  flex: none;
271
272
  color: var(--text-muted);
272
- transition: transform 0.15s var(--ease);
273
+ transition: rotate var(--dur-base) var(--ease-out);
273
274
  }
274
275
  .sh-open :global(.sh-chevron) {
275
- transform: rotate(90deg);
276
+ rotate: 90deg;
276
277
  }
277
278
  .sh-panel {
278
279
  margin-top: var(--sp-3);
280
+ transition:
281
+ opacity var(--dur-base) var(--ease-out),
282
+ translate var(--dur-base) var(--ease-out);
279
283
  }
280
- @media (prefers-reduced-motion: reduce) {
281
- .section-header :global(.sh-chevron) {
282
- transition: none;
284
+ @starting-style {
285
+ .sh-collapsible.sh-open .sh-panel {
286
+ opacity: 0;
287
+ translate: 0 calc(-1 * var(--shift-xs));
283
288
  }
284
289
  }
285
290
  </style>