@dorsk/tsumikit 0.73.2 → 0.75.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 (137) 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 +12 -6
  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/Skeleton.svelte +14 -8
  23. package/dist/components/atoms/Slider.svelte +79 -22
  24. package/dist/components/atoms/Slider.svelte.d.ts +2 -0
  25. package/dist/components/atoms/Spinner.svelte +464 -5
  26. package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
  27. package/dist/components/atoms/Swatch.svelte +19 -8
  28. package/dist/components/atoms/Switch.svelte +34 -21
  29. package/dist/components/atoms/Switch.svelte.d.ts +2 -0
  30. package/dist/components/atoms/Text.svelte +1 -1
  31. package/dist/components/atoms/Textarea.svelte +34 -12
  32. package/dist/components/atoms/TimeDisplay.svelte +4 -2
  33. package/dist/components/layouts/AppShell.svelte +26 -15
  34. package/dist/components/layouts/MasterDetail.svelte +23 -21
  35. package/dist/components/layouts/NavBar.svelte +11 -8
  36. package/dist/components/layouts/NavItem.svelte +15 -7
  37. package/dist/components/layouts/NavSection.svelte +1 -1
  38. package/dist/components/layouts/ResizablePanel.svelte +37 -16
  39. package/dist/components/layouts/TabBar.svelte +24 -14
  40. package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
  41. package/dist/components/molecules/Accordion.svelte +4 -0
  42. package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
  43. package/dist/components/molecules/AttachmentList.svelte +14 -11
  44. package/dist/components/molecules/Breadcrumb.svelte +13 -9
  45. package/dist/components/molecules/Callout.svelte +3 -3
  46. package/dist/components/molecules/CapBar.svelte +23 -22
  47. package/dist/components/molecules/Carousel.svelte +27 -17
  48. package/dist/components/molecules/ChatBubble.svelte +2 -2
  49. package/dist/components/molecules/CodeBlock.svelte +2 -2
  50. package/dist/components/molecules/Combobox.svelte +30 -1
  51. package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
  52. package/dist/components/molecules/Composer.svelte +2 -2
  53. package/dist/components/molecules/ConfirmModal.svelte +4 -0
  54. package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
  55. package/dist/components/molecules/CopyButton.svelte +46 -2
  56. package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
  57. package/dist/components/molecules/Disclosure.svelte +23 -12
  58. package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
  59. package/dist/components/molecules/Drawer.svelte +43 -23
  60. package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
  61. package/dist/components/molecules/Dropzone.svelte +24 -8
  62. package/dist/components/molecules/EmojiPicker.svelte +27 -8
  63. package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
  64. package/dist/components/molecules/EmptyState.svelte +5 -5
  65. package/dist/components/molecules/Fieldset.svelte +8 -13
  66. package/dist/components/molecules/FileButton.svelte +57 -31
  67. package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
  68. package/dist/components/molecules/FilterInput.svelte +51 -11
  69. package/dist/components/molecules/FontScalePicker.svelte +20 -9
  70. package/dist/components/molecules/GitRef.svelte +9 -7
  71. package/dist/components/molecules/GlyphStack.svelte +38 -14
  72. package/dist/components/molecules/IconButton.svelte +6 -3
  73. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  74. package/dist/components/molecules/InputGroup.svelte +13 -12
  75. package/dist/components/molecules/LoadMore.svelte +6 -1
  76. package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
  77. package/dist/components/molecules/Menu.svelte +33 -7
  78. package/dist/components/molecules/Menu.svelte.d.ts +2 -0
  79. package/dist/components/molecules/Metric.svelte +11 -9
  80. package/dist/components/molecules/Modal.svelte +76 -26
  81. package/dist/components/molecules/Modal.svelte.d.ts +2 -0
  82. package/dist/components/molecules/OptionButton.svelte +28 -10
  83. package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
  84. package/dist/components/molecules/Pagination.svelte +26 -9
  85. package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
  86. package/dist/components/molecules/Popover.svelte +107 -51
  87. package/dist/components/molecules/Popover.svelte.d.ts +2 -0
  88. package/dist/components/molecules/Prose.svelte +1 -1
  89. package/dist/components/molecules/PullToRefresh.svelte +14 -10
  90. package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
  91. package/dist/components/molecules/RadialMenu.svelte +52 -19
  92. package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
  93. package/dist/components/molecules/RadioGroup.svelte +38 -23
  94. package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
  95. package/dist/components/molecules/SectionHeader.svelte +13 -8
  96. package/dist/components/molecules/SeekSlider.svelte +38 -27
  97. package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
  98. package/dist/components/molecules/SegmentedControl.svelte +139 -86
  99. package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
  100. package/dist/components/molecules/SelectButton.svelte +21 -6
  101. package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
  102. package/dist/components/molecules/SortableList.svelte +10 -11
  103. package/dist/components/molecules/SoundSettings.svelte +161 -0
  104. package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
  105. package/dist/components/molecules/SplitButton.svelte +30 -3
  106. package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
  107. package/dist/components/molecules/SwipeRow.svelte +18 -15
  108. package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
  109. package/dist/components/molecules/Tabs.svelte +92 -10
  110. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  111. package/dist/components/molecules/ThemePicker.svelte +25 -10
  112. package/dist/components/molecules/Timestamp.svelte +12 -7
  113. package/dist/components/molecules/Toaster.svelte +65 -14
  114. package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
  115. package/dist/components/molecules/Toggle.svelte +36 -13
  116. package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
  117. package/dist/components/molecules/Tooltip.svelte +41 -10
  118. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  119. package/dist/components/molecules/VolumeSlider.svelte +34 -14
  120. package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
  121. package/dist/components/molecules/WorkingDir.svelte +1 -0
  122. package/dist/components/organisms/ConversationFeed.svelte +4 -0
  123. package/dist/components/organisms/DataTable.svelte +37 -14
  124. package/dist/floating.js +26 -0
  125. package/dist/index.d.ts +4 -0
  126. package/dist/index.js +4 -1
  127. package/dist/sound-synth.d.ts +3 -0
  128. package/dist/sound-synth.js +91 -0
  129. package/dist/sound.d.ts +1 -0
  130. package/dist/sound.js +3 -0
  131. package/dist/stores/sound.svelte.d.ts +20 -0
  132. package/dist/stores/sound.svelte.js +150 -0
  133. package/dist/stores/toast.svelte.d.ts +9 -0
  134. package/dist/stores/toast.svelte.js +45 -9
  135. package/dist/styles/reset.css +19 -2
  136. package/dist/styles/tokens.css +89 -6
  137. package/package.json +1 -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, {}, "">;
@@ -5,6 +5,7 @@
5
5
  // `offset` written back). Numbered buttons collapse to prev / "3 / 12" /
6
6
  // next when the container is narrower than 24rem.
7
7
  import Text from '../atoms/Text.svelte';
8
+ import { playSound } from '../../sound';
8
9
  import IconButton from './IconButton.svelte';
9
10
 
10
11
  let {
@@ -22,6 +23,7 @@
22
23
  shrink = true,
23
24
  block = false,
24
25
  label = 'Pagination',
26
+ sound = true,
25
27
  class: klass = ''
26
28
  }: {
27
29
  /** 1-based current page. */
@@ -46,6 +48,8 @@
46
48
  /** Full-width block. */
47
49
  block?: boolean;
48
50
  label?: string;
51
+ /** Play UI sounds when enabled globally; false silences this instance. */
52
+ sound?: boolean;
49
53
  class?: string;
50
54
  } = $props();
51
55
 
@@ -96,6 +100,7 @@
96
100
  function go(p: number) {
97
101
  const next = Math.min(count, Math.max(1, p));
98
102
  if (next === current) return;
103
+ playSound('click', sound);
99
104
  page = next;
100
105
  if (offsetMode) offset = (next - 1) * (limit as number);
101
106
  onchange?.(next);
@@ -116,6 +121,7 @@
116
121
  label="Previous page"
117
122
  box={size === 'sm' ? 'sm' : 'md'}
118
123
  disabled={current <= 1}
124
+ sound={false}
119
125
  onclick={() => go(current - 1)}
120
126
  />
121
127
  <ol class="pages">
@@ -144,6 +150,7 @@
144
150
  label="Next page"
145
151
  box={size === 'sm' ? 'sm' : 'md'}
146
152
  disabled={current >= count}
153
+ sound={false}
147
154
  onclick={() => go(current + 1)}
148
155
  />
149
156
  {#if showRange}
@@ -181,21 +188,30 @@
181
188
  .page {
182
189
  min-width: var(--box-md);
183
190
  height: var(--box-md);
184
- padding: 0 var(--sp-2);
191
+ padding: 0 var(--control-px-md);
185
192
  border: 1px solid transparent;
186
- border-radius: 999px;
193
+ border-radius: var(--r-md);
187
194
  background: transparent;
188
195
  color: var(--text-muted);
189
- font-size: var(--fs-sm);
196
+ font-size: var(--control-fs-md);
190
197
  font-variant-numeric: tabular-nums;
191
198
  cursor: pointer;
199
+ --page-scale-dur: var(--dur-fast);
192
200
  transition:
193
- background 0.12s var(--ease),
194
- color 0.12s var(--ease);
201
+ background var(--dur-fast) ease,
202
+ border-color var(--dur-fast) ease,
203
+ color var(--dur-fast) ease,
204
+ scale var(--page-scale-dur) var(--ease-out);
205
+ }
206
+ .page:active {
207
+ --page-scale-dur: var(--dur-press);
208
+ scale: var(--press-scale);
195
209
  }
196
- .page:hover {
197
- background: var(--bg-elevated-2);
198
- color: var(--text);
210
+ @media (hover: hover) and (pointer: fine) {
211
+ .page:hover {
212
+ background: var(--bg-elevated-2);
213
+ color: var(--text);
214
+ }
199
215
  }
200
216
  .page.on {
201
217
  background: color-mix(in srgb, var(--accent) 16%, transparent);
@@ -206,7 +222,8 @@
206
222
  .pagination-sm .page {
207
223
  min-width: var(--box-sm);
208
224
  height: var(--box-sm);
209
- font-size: var(--fs-xs);
225
+ padding: 0 var(--control-px-sm);
226
+ font-size: var(--control-fs-sm);
210
227
  }
211
228
  .ellipsis {
212
229
  display: inline-flex;
@@ -22,6 +22,8 @@ type $$ComponentProps = {
22
22
  /** Full-width block. */
23
23
  block?: boolean;
24
24
  label?: string;
25
+ /** Play UI sounds when enabled globally; false silences this instance. */
26
+ sound?: boolean;
25
27
  class?: string;
26
28
  };
27
29
  declare const Pagination: import("svelte").Component<$$ComponentProps, {}, "offset" | "page">;