@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
@@ -113,9 +113,9 @@
113
113
  full width and the scrollbar ends above the controls. */
114
114
  .ig {
115
115
  --ig-h: var(--control-height-default);
116
- --ig-pad-x: var(--sp-3);
116
+ --ig-pad-x: var(--control-px-md);
117
117
  --ig-gap: var(--sp-2);
118
- --ig-inset: 2px;
118
+ --ig-inset: var(--sp-0-5);
119
119
  --ig-radius: var(--r-md);
120
120
  position: relative;
121
121
  display: flex;
@@ -123,12 +123,12 @@
123
123
  }
124
124
  .ig-sm {
125
125
  --ig-h: var(--control-height-compact);
126
- --ig-pad-x: var(--sp-2);
126
+ --ig-pad-x: var(--control-px-sm);
127
127
  --ig-gap: var(--sp-1);
128
128
  }
129
129
  .ig-lg {
130
130
  --ig-h: var(--control-height-large);
131
- --ig-pad-x: var(--sp-4);
131
+ --ig-pad-x: var(--control-px-lg);
132
132
  }
133
133
  .ig-field {
134
134
  width: 100%;
@@ -138,10 +138,10 @@
138
138
  following the field's; inner corners go square. In the bar they hug the
139
139
  bottom and side borders, so only the bottom-outer corner is rounded. */
140
140
  .ig-adorn {
141
- --ig-fuse-r: calc(var(--ig-radius) - 1px);
141
+ --ig-fuse-r: calc(var(--ig-radius) - var(--sp-px));
142
142
  position: absolute;
143
- top: 1px;
144
- bottom: 1px;
143
+ top: var(--sp-px);
144
+ bottom: var(--sp-px);
145
145
  z-index: 1;
146
146
  display: flex;
147
147
  align-items: center;
@@ -149,10 +149,10 @@
149
149
  pointer-events: none;
150
150
  }
151
151
  .ig-leading {
152
- inset-inline-start: 1px;
152
+ inset-inline-start: var(--sp-px);
153
153
  }
154
154
  .ig-trailing {
155
- inset-inline-end: 1px;
155
+ inset-inline-end: var(--sp-px);
156
156
  }
157
157
  .ig-adorn > :global(*) {
158
158
  pointer-events: auto;
@@ -197,7 +197,7 @@
197
197
  }
198
198
  .ig-bar .ig-adorn {
199
199
  top: auto;
200
- height: calc(var(--ig-h) - 1px);
200
+ height: calc(var(--ig-h) - var(--sp-px));
201
201
  }
202
202
  .ig-bar .ig-leading > :global(:first-child) {
203
203
  --btn-radius: 0 0 0 var(--ig-fuse-r);
@@ -225,7 +225,7 @@
225
225
  border: 1px solid var(--textarea-border, var(--border-strong));
226
226
  border-top: 0;
227
227
  border-radius: 0 0 var(--ig-radius) var(--ig-radius);
228
- transition: border-color 0.12s var(--ease);
228
+ transition: border-color var(--dur-fast) ease;
229
229
  pointer-events: none;
230
230
  }
231
231
  .ig-bar:has(.ig-field :global(:focus))::before {
@@ -244,7 +244,8 @@
244
244
  pointer-events: none;
245
245
  }
246
246
  .ig.disabled {
247
- opacity: 0.45;
247
+ opacity: var(--disabled-opacity);
248
+ cursor: not-allowed;
248
249
  }
249
250
  .ig.disabled .ig-adorn > :global(:disabled) {
250
251
  opacity: 1;
@@ -15,6 +15,7 @@
15
15
  retryLabel = 'Retry',
16
16
  doneLabel = 'No more items',
17
17
  pill = false,
18
+ sound = true,
18
19
  class: klass = '',
19
20
  ...rest
20
21
  }: {
@@ -26,6 +27,8 @@
26
27
  retryLabel?: string;
27
28
  doneLabel?: string;
28
29
  pill?: boolean;
30
+ /** Play UI sounds when enabled globally; false silences this instance. */
31
+ sound?: boolean;
29
32
  class?: string;
30
33
  [key: string]: unknown;
31
34
  } = $props();
@@ -44,12 +47,13 @@
44
47
  <Text variant="caption" class="load-more-done">{doneLabel}</Text>
45
48
  {:else if state === 'error'}
46
49
  <Text tone="danger" size="sm" class="load-more-error">{errorLabel}</Text>
47
- <Button size="sm" onclick={() => onload?.()}>{retryLabel}</Button>
50
+ <Button size="sm" {sound} onclick={() => onload?.()}>{retryLabel}</Button>
48
51
  {:else}
49
52
  <Button
50
53
  size="sm"
51
54
  variant={pill ? 'default' : 'ghost'}
52
55
  loading={state === 'loading'}
56
+ {sound}
53
57
  class={pill ? 'load-more-chip' : ''}
54
58
  onclick={() => onload?.()}
55
59
  >
@@ -67,6 +71,7 @@
67
71
  gap: var(--sp-2);
68
72
  padding: var(--sp-2);
69
73
  min-height: var(--sp-8);
74
+ flex-shrink: 0;
70
75
  }
71
76
  .load-more-pill {
72
77
  padding: var(--sp-1);
@@ -7,6 +7,8 @@ type $$ComponentProps = {
7
7
  retryLabel?: string;
8
8
  doneLabel?: string;
9
9
  pill?: boolean;
10
+ /** Play UI sounds when enabled globally; false silences this instance. */
11
+ sound?: boolean;
10
12
  class?: string;
11
13
  [key: string]: unknown;
12
14
  };
@@ -51,6 +51,7 @@
51
51
  import Icon from '../atoms/Icon.svelte';
52
52
  import Badge from '../atoms/Badge.svelte';
53
53
  import { menuItemCloses, menuItemGlyphs } from './menu-item';
54
+ import { playSound } from '../../sound';
54
55
 
55
56
  type PopoverProps = ComponentProps<typeof Popover>;
56
57
  type TriggerChrome = Pick<
@@ -103,6 +104,7 @@
103
104
  style: styleProp = '',
104
105
  panelClass = '',
105
106
  panelStyle = '',
107
+ sound = true,
106
108
  ...rest
107
109
  }: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
108
110
 
@@ -123,9 +125,13 @@
123
125
  * size or cap it (`min-width`, `max-height`, `padding`). */
124
126
  panelClass?: string;
125
127
  panelStyle?: string;
128
+ /** Play UI sounds when enabled globally; false silences this instance. */
129
+ sound?: boolean;
126
130
  } & TriggerChrome;
127
131
 
128
132
  let listEl = $state<HTMLDivElement | null>(null);
133
+ // The activated row keeps its accent flash while the menu runs its exit.
134
+ let flashed = $state<string | null>(null);
129
135
 
130
136
  const ROW_TARGET = ':is(select, input, textarea, button):not(:disabled), a[href], [tabindex]:not([tabindex="-1"])';
131
137
 
@@ -162,9 +168,13 @@
162
168
  focusAt(b.length - 1);
163
169
  }
164
170
  }
165
- function select(item: MenuItem, close: () => void) {
171
+ function select(item: MenuItem, key: string, close: () => void) {
166
172
  if (item.disabled) return;
167
- if (menuItemCloses(item, closeOnSelect)) close();
173
+ playSound('click', sound);
174
+ if (menuItemCloses(item, closeOnSelect)) {
175
+ flashed = key;
176
+ close();
177
+ }
168
178
  item.onselect?.();
169
179
  }
170
180
  </script>
@@ -193,11 +203,13 @@
193
203
  {gap}
194
204
  {count}
195
205
  {countMax}
206
+ {sound}
196
207
  role="menu"
197
208
  haspopup="menu"
198
209
  bind:open
199
210
  {onclose}
200
211
  onopen={() => {
212
+ flashed = null;
201
213
  queueMicrotask(() => focusAt(0));
202
214
  onopen?.();
203
215
  }}
@@ -205,6 +217,7 @@
205
217
  {#snippet children({ close }: { close: () => void })}
206
218
  <div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
207
219
  {#each items as item, i (item.id ?? `${i}:${item.label}`)}
220
+ {@const key = item.id ?? `${i}:${item.label}`}
208
221
  {#if item.control}
209
222
  <div {...item.attrs as HTMLAttributes<HTMLDivElement>} role="none" class="menu-item menu-row" class:danger={item.danger} inert={item.disabled}>
210
223
  {#if item.icon}<Icon name={item.icon} />{/if}
@@ -219,9 +232,10 @@
219
232
  class="menu-item"
220
233
  class:danger={item.danger}
221
234
  class:on={item.pressed}
235
+ data-flash={flashed === key || undefined}
222
236
  disabled={item.disabled}
223
237
  tabindex="-1"
224
- onclick={() => select(item, close)}
238
+ onclick={() => select(item, key, close)}
225
239
  >
226
240
  {#if item.content}
227
241
  {@render item.content(item)}
@@ -251,7 +265,7 @@
251
265
  .menu {
252
266
  display: flex;
253
267
  flex-direction: column;
254
- gap: 1px;
268
+ gap: var(--sp-px);
255
269
  }
256
270
  .menu-item {
257
271
  display: flex;
@@ -266,15 +280,27 @@
266
280
  border-radius: var(--r-sm);
267
281
  font-size: var(--fs-sm);
268
282
  white-space: nowrap;
283
+ transition: background-color var(--dur-fast) var(--ease);
269
284
  }
270
285
  .menu-row {
271
286
  position: relative;
272
287
  }
273
- .menu-item:hover:not(:disabled),
288
+ /* Highlight lands instantly and fades out: rows change at pointer speed. */
274
289
  .menu-item:focus-visible,
275
- .menu-row:focus-within {
290
+ .menu-row:focus-within,
291
+ .menu-item[data-flash] {
276
292
  background: var(--bg-elevated-2);
277
293
  outline: none;
294
+ transition-duration: var(--dur-instant);
295
+ }
296
+ .menu-item[data-flash] {
297
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
298
+ }
299
+ @media (hover: hover) and (pointer: fine) {
300
+ .menu-item:hover:not(:disabled) {
301
+ background: var(--bg-elevated-2);
302
+ transition-duration: var(--dur-instant);
303
+ }
278
304
  }
279
305
  .menu-item.danger {
280
306
  color: var(--danger);
@@ -305,7 +331,7 @@
305
331
  }
306
332
  .menu-item:disabled,
307
333
  .menu-row[inert] {
308
- opacity: 0.45;
334
+ opacity: var(--disabled-opacity);
309
335
  cursor: not-allowed;
310
336
  }
311
337
  </style>
@@ -58,6 +58,8 @@ type Own = {
58
58
  * size or cap it (`min-width`, `max-height`, `padding`). */
59
59
  panelClass?: string;
60
60
  panelStyle?: string;
61
+ /** Play UI sounds when enabled globally; false silences this instance. */
62
+ sound?: boolean;
61
63
  } & TriggerChrome;
62
64
  type $$ComponentProps = Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own;
63
65
  declare const Menu: import("svelte").Component<$$ComponentProps, {}, "open">;
@@ -188,7 +188,7 @@
188
188
  letter-spacing: 0.06em;
189
189
  color: var(--text-muted);
190
190
  font-family: var(--font-mono);
191
- line-height: 1.4;
191
+ line-height: var(--lh-snug);
192
192
  }
193
193
  .metric-chip {
194
194
  display: inline-flex;
@@ -198,7 +198,7 @@
198
198
  width: 1.75rem;
199
199
  height: 1.75rem;
200
200
  border-radius: var(--r-md);
201
- font-size: 1rem;
201
+ font-size: var(--fs-md);
202
202
  color: var(--metric-tone);
203
203
  background: color-mix(in srgb, var(--metric-tone) 14%, transparent);
204
204
  border: 1px solid color-mix(in srgb, var(--metric-tone) 30%, transparent);
@@ -208,7 +208,7 @@
208
208
  align-items: baseline;
209
209
  gap: var(--sp-1);
210
210
  font-family: var(--font-mono);
211
- line-height: 1.1;
211
+ line-height: var(--lh-none);
212
212
  }
213
213
  .metric-num {
214
214
  min-width: 0;
@@ -229,7 +229,7 @@
229
229
  .metric-sub {
230
230
  font-size: var(--fs-xs);
231
231
  color: var(--text-faint);
232
- line-height: 1.4;
232
+ line-height: var(--lh-snug);
233
233
  }
234
234
  :global(.metric-sm) .metric-num {
235
235
  font-size: var(--fs-lg);
@@ -237,7 +237,7 @@
237
237
  :global(.metric-sm) .metric-chip {
238
238
  width: 1.5rem;
239
239
  height: 1.5rem;
240
- font-size: 0.875rem;
240
+ font-size: var(--fs-sm);
241
241
  }
242
242
  .metric-inline {
243
243
  flex-direction: row;
@@ -260,12 +260,14 @@
260
260
  cursor: pointer;
261
261
  border-radius: var(--r-sm);
262
262
  }
263
- .metric-inline.metric-tap:hover .metric-label {
264
- color: var(--text);
263
+ @media (hover: hover) and (pointer: fine) {
264
+ .metric-inline.metric-tap:hover .metric-label {
265
+ color: var(--text);
266
+ }
265
267
  }
266
268
  .metric-inline.metric-tap:focus-visible {
267
- outline: 2px solid var(--accent);
268
- outline-offset: 2px;
269
+ outline: var(--focus-ring);
270
+ outline-offset: var(--focus-ring-offset);
269
271
  }
270
272
  .metric-segments {
271
273
  display: flex;
@@ -10,6 +10,7 @@
10
10
  import type { Snippet } from 'svelte';
11
11
  import { browser } from '../../env';
12
12
  import { readStorage, writeStorage } from '../../stores/storage';
13
+ import { playSound } from '../../sound';
13
14
  import Icon, { type IconName } from '../atoms/Icon.svelte';
14
15
  import Spinner from '../atoms/Spinner.svelte';
15
16
  import IconButton from './IconButton.svelte';
@@ -37,6 +38,7 @@
37
38
  style: styleProp = '',
38
39
  bodyClass = '',
39
40
  maxHeight,
41
+ sound = true,
40
42
  }: {
41
43
  title: string;
42
44
  /** Controlled visibility. When provided the `<dialog>` stays mounted and
@@ -67,6 +69,8 @@
67
69
  bodyClass?: string;
68
70
  /** Cap the sheet height; the body scrolls inside it. */
69
71
  maxHeight?: string;
72
+ /** Play UI sounds when enabled globally; false silences this instance. */
73
+ sound?: boolean;
70
74
  } = $props();
71
75
  const t = $derived(canonicalTone(tone));
72
76
 
@@ -87,19 +91,26 @@
87
91
 
88
92
  // Open as a modal (top layer + trap + inert background + focus mgmt). The
89
93
  // native element restores focus to the trigger automatically on close.
94
+ // Mounting (or mounting with `open` already true) is a sync, not a
95
+ // transition, so it stays silent.
96
+ let synced = false;
90
97
  $effect(() => {
91
98
  if (!dialogEl) return;
92
99
  if (!controlled) {
93
100
  dialogEl.showModal();
94
101
  return;
95
102
  }
96
- if (open && !dialogEl.open) dialogEl.showModal();
97
- else if (!open && dialogEl.open) dialogEl.close();
103
+ if (open && !dialogEl.open) {
104
+ dialogEl.showModal();
105
+ if (synced) playSound('open', sound);
106
+ } else if (!open && dialogEl.open) dialogEl.close();
107
+ synced = true;
98
108
  });
99
109
 
100
110
  function requestClose() {
101
111
  if (busy) return;
102
112
  if (controlled) open = false;
113
+ else playSound('close', sound);
103
114
  onclose?.();
104
115
  }
105
116
 
@@ -174,6 +185,7 @@
174
185
  }}
175
186
  onclose={() => {
176
187
  if (controlled) open = false;
188
+ playSound('close', sound);
177
189
  }}
178
190
  onpointerdown={onDialogPointerDown}
179
191
  onclick={onDialogClick}
@@ -189,7 +201,7 @@
189
201
  >
190
202
  <div class="sheet-head">
191
203
  {#if t !== 'neutral'}
192
- <span class="sheet-tone-icon"><Icon name={TONE_ICON[t]} size={18} /></span>
204
+ <span class="sheet-tone-icon"><Icon name={TONE_ICON[t]} /></span>
193
205
  {/if}
194
206
  <span id={titleId} class="sheet-title truncate">{title}</span>
195
207
  {#if busy}<Spinner label="Working" />{/if}
@@ -234,17 +246,28 @@
234
246
  display: flex;
235
247
  align-items: flex-end;
236
248
  justify-content: center;
249
+ /* Exits run natively on close(): the box leaves the top layer once the sheet is gone. */
250
+ transition:
251
+ display var(--dur-exit-slow) allow-discrete,
252
+ overlay var(--dur-exit-slow) allow-discrete;
237
253
  }
238
254
  .modal:not([open]) {
239
255
  display: none;
240
256
  }
241
257
  .modal::backdrop {
242
258
  background: rgba(0, 0, 0, 0.55);
243
- /* animate the backdrop in (progressive enhancement) */
244
- animation: backdrop-in 0.18s var(--ease);
259
+ opacity: 0;
260
+ transition:
261
+ opacity var(--dur-exit) var(--ease-out),
262
+ display var(--dur-exit) allow-discrete,
263
+ overlay var(--dur-exit) allow-discrete;
264
+ }
265
+ .modal[open]::backdrop {
266
+ opacity: 1;
267
+ transition-duration: var(--dur-base);
245
268
  }
246
- @keyframes backdrop-in {
247
- from {
269
+ @starting-style {
270
+ .modal[open]::backdrop {
248
271
  opacity: 0;
249
272
  }
250
273
  }
@@ -267,8 +290,24 @@
267
290
  border: 1px solid var(--border);
268
291
  border-radius: var(--r-lg) var(--r-lg) 0 0;
269
292
  box-shadow: var(--shadow-lg);
270
- animation: sheet-up 0.18s var(--ease);
271
293
  padding-bottom: var(--safe-bottom);
294
+ /* Bottom sheet: slides up on enter, down on exit. */
295
+ opacity: 0;
296
+ translate: 0 100%;
297
+ transition:
298
+ opacity var(--dur-exit-slow) var(--ease-out),
299
+ translate var(--dur-exit-slow) var(--ease-drawer);
300
+ }
301
+ .modal[open] .sheet {
302
+ opacity: 1;
303
+ translate: 0;
304
+ transition-duration: var(--dur-sheet);
305
+ }
306
+ @starting-style {
307
+ .modal[open] .sheet {
308
+ opacity: 0;
309
+ translate: 0 100%;
310
+ }
272
311
  }
273
312
  @media (min-width: 640px) {
274
313
  .sheet {
@@ -276,6 +315,22 @@
276
315
  padding-bottom: 0;
277
316
  width: var(--sheet-w, var(--sw));
278
317
  max-width: min(var(--sheet-w, var(--sw)), calc(100vw - 2rem));
318
+ /* Centred dialog: scales from the middle instead of sliding. */
319
+ translate: 0;
320
+ scale: var(--enter-scale);
321
+ transition:
322
+ opacity var(--dur-exit) var(--ease-out),
323
+ scale var(--dur-exit) var(--ease-out);
324
+ }
325
+ .modal[open] .sheet {
326
+ scale: 1;
327
+ transition-duration: var(--dur-slow);
328
+ }
329
+ @starting-style {
330
+ .modal[open] .sheet {
331
+ translate: 0;
332
+ scale: var(--enter-scale);
333
+ }
279
334
  }
280
335
  .sheet-sm {
281
336
  --sw: 24rem;
@@ -287,18 +342,6 @@
287
342
  --sw: 72rem;
288
343
  }
289
344
  }
290
- @keyframes sheet-up {
291
- from {
292
- transform: translateY(8%);
293
- opacity: 0.4;
294
- }
295
- }
296
- @media (prefers-reduced-motion: reduce) {
297
- .modal::backdrop,
298
- .sheet {
299
- animation: none;
300
- }
301
- }
302
345
  .modal.resizing .sheet {
303
346
  user-select: none;
304
347
  /* Make per-frame width changes cheap to paint: hint the animated property,
@@ -322,6 +365,7 @@
322
365
  .sheet-tone-icon {
323
366
  display: inline-flex;
324
367
  flex: none;
368
+ font-size: var(--icon-lg);
325
369
  color: var(--modal-tone);
326
370
  }
327
371
  .sheet-title {
@@ -372,9 +416,9 @@
372
416
  bottom: 0;
373
417
  right: 6px;
374
418
  width: 2px;
375
- border-radius: 999px;
419
+ border-radius: var(--r-pill);
376
420
  background: transparent;
377
- transition: background 0.12s var(--ease);
421
+ transition: background-color var(--dur-fast) var(--ease);
378
422
  }
379
423
  .sheet-resize::after {
380
424
  content: '';
@@ -384,17 +428,23 @@
384
428
  transform: translateY(-50%);
385
429
  width: 4px;
386
430
  height: 44px;
387
- border-radius: 999px;
431
+ border-radius: var(--r-pill);
388
432
  background: var(--border-strong);
389
- transition: background 0.12s var(--ease);
433
+ transition: background-color var(--dur-fast) var(--ease);
390
434
  }
391
- .sheet-resize:hover::before,
392
435
  .modal.resizing .sheet-resize::before {
393
436
  background: color-mix(in srgb, var(--accent) 45%, transparent);
394
437
  }
395
- .sheet-resize:hover::after,
396
438
  .modal.resizing .sheet-resize::after {
397
439
  background: var(--accent);
398
440
  }
399
441
  }
442
+ @media (min-width: 640px) and (hover: hover) and (pointer: fine) {
443
+ .sheet-resize:hover::before {
444
+ background: color-mix(in srgb, var(--accent) 45%, transparent);
445
+ }
446
+ .sheet-resize:hover::after {
447
+ background: var(--accent);
448
+ }
449
+ }
400
450
  </style>
@@ -30,6 +30,8 @@ type $$ComponentProps = {
30
30
  bodyClass?: string;
31
31
  /** Cap the sheet height; the body scrolls inside it. */
32
32
  maxHeight?: string;
33
+ /** Play UI sounds when enabled globally; false silences this instance. */
34
+ sound?: boolean;
33
35
  };
34
36
  declare const Modal: import("svelte").Component<$$ComponentProps, {}, "open">;
35
37
  type Modal = ReturnType<typeof Modal>;
@@ -9,6 +9,7 @@
9
9
  // own <button> + scoped styles rather than specializing the Button atom.
10
10
  import type { Snippet } from 'svelte';
11
11
  import type { HTMLButtonAttributes } from 'svelte/elements';
12
+ import { playSound } from '../../sound';
12
13
 
13
14
  let {
14
15
  selected = false,
@@ -22,6 +23,8 @@
22
23
  onfocuschange,
23
24
  onfocus,
24
25
  onpointerenter,
26
+ onclick,
27
+ sound = true,
25
28
  'aria-describedby': describedBy,
26
29
  class: klass = '',
27
30
  children,
@@ -41,6 +44,8 @@
41
44
  align?: 'center' | 'start';
42
45
  /** Stretch to fill the container width (full-width list/menu row). */
43
46
  block?: boolean;
47
+ /** Play UI sounds when enabled globally; false silences this instance. */
48
+ sound?: boolean;
44
49
  children?: Snippet;
45
50
  } = $props();
46
51
 
@@ -68,6 +73,11 @@
68
73
  onpointerenter?.(e);
69
74
  activate();
70
75
  }}
76
+ onclick={(e) => {
77
+ if (disabled) return;
78
+ playSound('click', sound);
79
+ onclick?.(e);
80
+ }}
71
81
  class="opt {klass}"
72
82
  class:row
73
83
  class:align-start={align === 'start'}
@@ -83,7 +93,7 @@
83
93
  .opt {
84
94
  display: flex;
85
95
  flex-direction: column;
86
- gap: 2px;
96
+ gap: var(--sp-0-5);
87
97
  padding: var(--sp-2);
88
98
  background: var(--bg);
89
99
  border: 1px solid var(--border-strong);
@@ -92,17 +102,20 @@
92
102
  text-align: left;
93
103
  white-space: normal;
94
104
  user-select: none;
105
+ --opt-scale-dur: var(--dur-fast);
95
106
  transition:
96
- background 0.12s var(--ease),
97
- border-color 0.12s var(--ease),
98
- color 0.12s var(--ease);
107
+ background var(--dur-fast) ease,
108
+ border-color var(--dur-fast) ease,
109
+ color var(--dur-fast) ease,
110
+ scale var(--opt-scale-dur) var(--ease-out);
99
111
  }
100
112
  .opt:disabled {
101
- opacity: 0.45;
113
+ opacity: var(--disabled-opacity);
102
114
  cursor: not-allowed;
103
115
  }
104
- .opt:hover:not(:disabled):not(.selected) {
105
- border-color: var(--border-strong);
116
+ .opt:active:not(:disabled) {
117
+ --opt-scale-dur: var(--dur-press);
118
+ scale: var(--press-scale);
106
119
  }
107
120
  .opt.row {
108
121
  flex-direction: row;
@@ -140,8 +153,13 @@
140
153
  background: color-mix(in srgb, var(--oc) 14%, var(--bg));
141
154
  color: var(--oc);
142
155
  }
143
- .opt.selected:hover:not(:disabled) {
144
- border-color: var(--oc);
145
- background: color-mix(in srgb, var(--oc) 20%, var(--bg));
156
+ @media (hover: hover) and (pointer: fine) {
157
+ .opt:hover:not(:disabled):not(.selected) {
158
+ border-color: var(--border-strong);
159
+ }
160
+ .opt.selected:hover:not(:disabled) {
161
+ border-color: var(--oc);
162
+ background: color-mix(in srgb, var(--oc) 20%, var(--bg));
163
+ }
146
164
  }
147
165
  </style>
@@ -15,6 +15,8 @@ type $$ComponentProps = Omit<HTMLButtonAttributes, 'value'> & {
15
15
  align?: 'center' | 'start';
16
16
  /** Stretch to fill the container width (full-width list/menu row). */
17
17
  block?: boolean;
18
+ /** Play UI sounds when enabled globally; false silences this instance. */
19
+ sound?: boolean;
18
20
  children?: Snippet;
19
21
  };
20
22
  declare const OptionButton: import("svelte").Component<$$ComponentProps, {}, "">;