@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
@@ -10,6 +10,7 @@
10
10
  // <button> + scoped styles rather than specializing the Button atom.
11
11
  import type { Snippet } from 'svelte';
12
12
  import type { HTMLButtonAttributes } from 'svelte/elements';
13
+ import { playSound } from '../../sound';
13
14
 
14
15
  let {
15
16
  pressed = false,
@@ -20,6 +21,8 @@
20
21
  shrink = true,
21
22
  type = 'button',
22
23
  disabled = false,
24
+ sound = true,
25
+ onclick,
23
26
  class: klass = '',
24
27
  children,
25
28
  ...rest
@@ -32,8 +35,17 @@
32
35
  grow?: boolean;
33
36
  // `shrink={false}` keeps the chip at its intrinsic width (flex: none).
34
37
  shrink?: boolean;
38
+ /** Play UI sounds when enabled globally; false silences this instance. */
39
+ sound?: boolean;
35
40
  children?: Snippet;
36
41
  } = $props();
42
+
43
+ function click(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
44
+ const next = !pressed;
45
+ onclick?.(e);
46
+ if (e.defaultPrevented) return;
47
+ playSound(next ? 'toggle-on' : 'toggle-off', sound);
48
+ }
37
49
  </script>
38
50
 
39
51
  <button
@@ -41,6 +53,7 @@
41
53
  {...rest}
42
54
  {type}
43
55
  {disabled}
56
+ onclick={click}
44
57
  class="toggle {klass}"
45
58
  class:pill
46
59
  class:struck
@@ -60,34 +73,38 @@
60
73
  display: inline-flex;
61
74
  align-items: center;
62
75
  justify-content: center;
63
- gap: 4px;
76
+ gap: var(--sp-1);
64
77
  box-sizing: border-box;
65
- height: var(--toggle-size, var(--box-xs));
66
- padding: 0 var(--sp-2);
78
+ height: var(--toggle-size, var(--control-sm));
79
+ padding: 0 var(--control-px-sm);
67
80
  border-radius: var(--r-sm);
68
- font-size: var(--fs-xs);
81
+ font-size: var(--control-fs-sm);
69
82
  font-weight: var(--fw-medium);
70
- line-height: 1;
83
+ line-height: var(--lh-none);
84
+ --icon-size: var(--control-icon-sm);
71
85
  background: var(--bg-elevated-2);
72
86
  color: var(--text-muted);
73
87
  border: 1px solid var(--border);
74
88
  white-space: nowrap;
75
89
  user-select: none;
76
90
  transition:
77
- background 0.12s var(--ease),
78
- border-color 0.12s var(--ease),
79
- color 0.12s var(--ease);
91
+ background var(--dur-fast) var(--ease),
92
+ border-color var(--dur-fast) var(--ease),
93
+ color var(--dur-fast) var(--ease),
94
+ scale var(--dur-fast) var(--ease-out);
80
95
  }
81
96
  .toggle:disabled {
82
- opacity: 0.45;
97
+ opacity: var(--disabled-opacity);
83
98
  cursor: not-allowed;
84
99
  }
85
100
  .toggle.pill {
86
101
  border-radius: var(--r-pill);
87
102
  }
88
103
  .toggle.md {
89
- height: var(--toggle-size, var(--control-height-compact));
90
- font-size: var(--fs-sm);
104
+ height: var(--toggle-size, var(--control-md));
105
+ padding: 0 var(--control-px-md);
106
+ font-size: var(--control-fs-md);
107
+ --icon-size: var(--control-icon-md);
91
108
  }
92
109
  .toggle.grow {
93
110
  flex: 1 1 0;
@@ -96,8 +113,14 @@
96
113
  .toggle.no-shrink {
97
114
  flex: none;
98
115
  }
99
- .toggle:hover:not(:disabled) {
100
- border-color: var(--border-strong);
116
+ @media (hover: hover) and (pointer: fine) {
117
+ .toggle:hover:not(:disabled) {
118
+ border-color: var(--border-strong);
119
+ }
120
+ }
121
+ .toggle:active:not(:disabled) {
122
+ scale: var(--press-scale);
123
+ transition-duration: var(--dur-press);
101
124
  }
102
125
  .toggle.on {
103
126
  --tc: var(--toggle-accent, var(--accent));
@@ -8,6 +8,8 @@ type $$ComponentProps = HTMLButtonAttributes & {
8
8
  size?: ControlSize;
9
9
  grow?: boolean;
10
10
  shrink?: boolean;
11
+ /** Play UI sounds when enabled globally; false silences this instance. */
12
+ sound?: boolean;
11
13
  children?: Snippet;
12
14
  };
13
15
  declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,3 +1,7 @@
1
+ <script lang="ts" module>
2
+ let lastClosed = -Infinity;
3
+ </script>
4
+
1
5
  <script lang="ts">
2
6
  // Lightweight tooltip. Shows on hover (after a short delay) and on keyboard
3
7
  // focus of the trigger; hides on blur / mouseleave / Escape (anywhere). The trigger is
@@ -15,7 +19,7 @@
15
19
  // by `closeDelay` and cancelled when the pointer (or focus) enters the panel,
16
20
  // so users can move in to select/copy text or click a button. Note the
17
21
  // role="tooltip" caveat: `aria-describedby` flattens the panel to its text.
18
- import type { Snippet } from 'svelte';
22
+ import { flushSync, type Snippet } from 'svelte';
19
23
  import { place } from '../../floating';
20
24
 
21
25
  let {
@@ -53,6 +57,8 @@
53
57
  style?: string;
54
58
  } = $props();
55
59
 
60
+ // A tooltip closed moments ago makes the next one open at once, unanimated.
61
+ const WARM_MS = 300;
56
62
  const id = `tip-${Math.random().toString(36).slice(2, 8)}`;
57
63
  let wrapEl = $state<HTMLElement | null>(null);
58
64
  let tipEl = $state<HTMLElement | null>(null);
@@ -68,23 +74,32 @@
68
74
  if (e.key === 'Escape') hideNow();
69
75
  }
70
76
 
77
+ let instant = $state(false);
71
78
  function show() {
72
79
  clearTimeout(hideTimer); // pointer came back within the grace period
73
80
  clearTimeout(showTimer);
74
- showTimer = setTimeout(() => {
81
+ const warm = performance.now() - lastClosed < WARM_MS;
82
+ const reveal = () => {
75
83
  if (!tipEl || tipEl.matches(':popover-open')) return; // re-entry guard
84
+ instant = warm;
85
+ flushSync();
76
86
  tipEl.showPopover(); // top layer — displayed before we measure it
77
87
  reposition();
78
88
  addEventListener('scroll', reposition, true);
79
89
  addEventListener('resize', reposition);
80
90
  document.addEventListener('keydown', onEscape);
81
- }, delay);
91
+ };
92
+ if (warm) reveal();
93
+ else showTimer = setTimeout(reveal, delay);
82
94
  }
83
95
  function hideNow() {
84
96
  clearTimeout(showTimer);
85
97
  clearTimeout(hideTimer);
86
98
  try {
87
- if (tipEl?.matches(':popover-open')) tipEl.hidePopover();
99
+ if (tipEl?.matches(':popover-open')) {
100
+ tipEl.hidePopover();
101
+ lastClosed = performance.now();
102
+ }
88
103
  } catch {}
89
104
  removeEventListener('scroll', reposition, true);
90
105
  removeEventListener('resize', reposition);
@@ -164,6 +179,7 @@
164
179
  popover="manual"
165
180
  class="tip"
166
181
  class:rich={!!content}
182
+ data-instant={instant ? '' : undefined}
167
183
  style:width
168
184
  style:max-width={maxWidth}
169
185
  use:panel
@@ -194,9 +210,16 @@
194
210
  border-radius: var(--r-sm);
195
211
  box-shadow: var(--shadow-md);
196
212
  font-size: var(--fs-xs);
197
- line-height: 1.4;
213
+ line-height: var(--lh-snug);
198
214
  white-space: normal;
199
215
  pointer-events: none; /* non-interactive: never steals hover/clicks */
216
+ transform-origin: var(--tsu-origin, bottom center);
217
+ opacity: 0;
218
+ scale: var(--tip-scale);
219
+ transition:
220
+ opacity var(--dur-exit-fast) var(--ease-out),
221
+ display var(--dur-exit-fast) allow-discrete,
222
+ overlay var(--dur-exit-fast) allow-discrete;
200
223
  }
201
224
  /* Rich panel: interactive + selectable so the user can move the pointer in
202
225
  to copy an id or click a button (the close grace period allows the trip). */
@@ -207,14 +230,22 @@
207
230
  padding: var(--sp-2);
208
231
  border-radius: var(--r-md);
209
232
  }
210
- /* Fade/scale in when shown (skipped under reduced-motion via the global rule). */
211
233
  .tip:popover-open {
212
- animation: tip-in 0.12s var(--ease);
234
+ opacity: 1;
235
+ scale: 1;
236
+ transition:
237
+ opacity var(--dur-fast) var(--ease-out),
238
+ scale var(--dur-fast) var(--ease-out),
239
+ display var(--dur-fast) allow-discrete,
240
+ overlay var(--dur-fast) allow-discrete;
213
241
  }
214
- @keyframes tip-in {
215
- from {
242
+ @starting-style {
243
+ .tip:popover-open {
216
244
  opacity: 0;
217
- transform: scale(0.96);
245
+ scale: var(--tip-scale);
218
246
  }
219
247
  }
248
+ .tip[data-instant] {
249
+ transition-duration: var(--dur-instant);
250
+ }
220
251
  </style>
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  /** Plain-text bubble. Ignored when `content` is provided. */
4
4
  text?: string;
@@ -8,6 +8,7 @@
8
8
  import type { HTMLAttributes } from 'svelte/elements';
9
9
  import type { IconName } from '../atoms/Icon.svelte';
10
10
  import IconButton from './IconButton.svelte';
11
+ import { playSound } from '../../sound';
11
12
 
12
13
  type Own = {
13
14
  /** Level 0–1. */
@@ -22,6 +23,8 @@
22
23
  label?: string;
23
24
  muteLabel?: string;
24
25
  unmuteLabel?: string;
26
+ /** Play UI sounds when enabled globally; false silences this instance. */
27
+ sound?: boolean;
25
28
  oninput?: (value: number) => void;
26
29
  onchange?: (value: number) => void;
27
30
  onmutechange?: (muted: boolean) => void;
@@ -40,6 +43,7 @@
40
43
  label = 'Volume',
41
44
  muteLabel = 'Mute',
42
45
  unmuteLabel = 'Unmute',
46
+ sound = true,
43
47
  oninput,
44
48
  onchange,
45
49
  onmutechange,
@@ -59,6 +63,7 @@
59
63
  muted ? 'volume-mute' : level === 0 ? 'volume' : level < 0.5 ? 'volume-low' : 'volume-high'
60
64
  );
61
65
  const valueText = $derived(muted ? 'Muted' : `${pct}%`);
66
+ const discrete = $derived(1 / step <= 20);
62
67
 
63
68
  function setMuted(m: boolean) {
64
69
  if (m === muted) return;
@@ -75,7 +80,8 @@
75
80
  }
76
81
 
77
82
  function commit(v: number) {
78
- onchange?.(apply(v));
83
+ const next = apply(v);
84
+ onchange?.(next);
79
85
  }
80
86
 
81
87
  function fracAt(el: HTMLElement, e: PointerEvent) {
@@ -117,6 +123,7 @@
117
123
 
118
124
  function onpointermove(e: PointerEvent) {
119
125
  if (pointerId !== e.pointerId) return;
126
+ if (discrete) playSound('tick', sound);
120
127
  apply(fracAt(e.currentTarget as HTMLElement, e));
121
128
  }
122
129
 
@@ -167,6 +174,7 @@
167
174
  }
168
175
  if (next === null) return;
169
176
  e.preventDefault();
177
+ playSound('tick', sound);
170
178
  commit(next);
171
179
  }
172
180
 
@@ -176,6 +184,7 @@
176
184
  const d = e.deltaY || e.deltaX;
177
185
  if (!d) return;
178
186
  e.preventDefault();
187
+ playSound('tick', sound);
179
188
  commit(level + (d < 0 ? step : -step));
180
189
  };
181
190
  el.addEventListener('wheel', onwheel, { passive: false });
@@ -199,6 +208,7 @@
199
208
  box="sm"
200
209
  size={size === 'sm' ? 16 : 18}
201
210
  {disabled}
211
+ {sound}
202
212
  onclick={() => setMuted(!muted)}
203
213
  data-part="mute"
204
214
  />
@@ -237,6 +247,7 @@
237
247
  --vol-thumb: 10px;
238
248
  --vol-length: 5rem;
239
249
  --vol-accent: var(--slider-accent, var(--accent));
250
+ --vol-halo: 0 0 0 4px color-mix(in srgb, var(--vol-accent) 20%, transparent);
240
251
  display: inline-flex;
241
252
  align-items: center;
242
253
  gap: var(--sp-1);
@@ -281,8 +292,8 @@
281
292
  }
282
293
  }
283
294
  .vol-slab[aria-disabled='true'] {
284
- cursor: default;
285
- opacity: 0.45;
295
+ cursor: not-allowed;
296
+ opacity: var(--disabled-opacity);
286
297
  }
287
298
  .vol-slab:focus-visible {
288
299
  outline: var(--focus-ring);
@@ -340,28 +351,37 @@
340
351
  top: 0;
341
352
  width: var(--vol-thumb);
342
353
  height: var(--vol-thumb);
343
- border-radius: 50%;
354
+ border-radius: var(--r-pill);
344
355
  background: var(--vol-accent);
345
356
  border: 2px solid var(--bg);
346
357
  box-shadow: var(--shadow-sm);
347
- transform: translate(-50%, -50%) scale(0.6);
358
+ transform: translate(-50%, -50%);
359
+ scale: 0.6;
348
360
  opacity: 0;
349
361
  transition:
350
- opacity 120ms var(--ease),
351
- transform 120ms var(--ease);
362
+ opacity var(--dur-fast) var(--ease),
363
+ scale var(--dur-press) var(--ease-out),
364
+ box-shadow var(--dur-fast) var(--ease);
352
365
  }
353
- .vol-slab:hover .vol-thumb,
354
366
  .vol-slab:focus-visible .vol-thumb,
355
367
  .vol--dragging .vol-thumb {
356
368
  opacity: 1;
357
- transform: translate(-50%, -50%) scale(1);
369
+ scale: 1;
358
370
  }
359
- .vol--muted .vol-fill {
360
- background: var(--text-faint);
371
+ .vol--dragging .vol-thumb {
372
+ scale: 1.15;
361
373
  }
362
- @media (prefers-reduced-motion: reduce) {
363
- .vol-thumb {
364
- transition: none;
374
+ @media (hover: hover) and (pointer: fine) {
375
+ .vol-slab:not([aria-disabled='true']):hover .vol-thumb {
376
+ opacity: 1;
377
+ scale: 1;
378
+ box-shadow: var(--shadow-sm), var(--vol-halo);
365
379
  }
380
+ .vol--dragging .vol-thumb {
381
+ scale: 1.15;
382
+ }
383
+ }
384
+ .vol--muted .vol-fill {
385
+ background: var(--text-faint);
366
386
  }
367
387
  </style>
@@ -18,6 +18,8 @@ type Own = {
18
18
  label?: string;
19
19
  muteLabel?: string;
20
20
  unmuteLabel?: string;
21
+ /** Play UI sounds when enabled globally; false silences this instance. */
22
+ sound?: boolean;
21
23
  oninput?: (value: number) => void;
22
24
  onchange?: (value: number) => void;
23
25
  onmutechange?: (muted: boolean) => void;
@@ -122,6 +122,7 @@
122
122
  min-width: 0;
123
123
  min-height: max-content;
124
124
  overflow: hidden;
125
+ --focus-ring-offset: var(--focus-ring-offset-inset);
125
126
  }
126
127
  .rail.full {
127
128
  flex: none;
@@ -143,6 +143,10 @@
143
143
  flex-direction: column;
144
144
  gap: var(--sp-3);
145
145
  padding: var(--sp-3);
146
+ scroll-padding-block: var(--sp-2);
147
+ }
148
+ .feed > * {
149
+ flex-shrink: 0;
146
150
  }
147
151
  .feed-items {
148
152
  display: flex;
@@ -254,8 +254,13 @@
254
254
  {:else if col.sortable}
255
255
  <button type="button" class="dt-sort" onclick={() => toggleSort(col)}>
256
256
  <span>{col.label}</span>
257
- <span class="dt-arrow" aria-hidden="true" class:active={sortKey === col.key}>
258
- {sortKey === col.key ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
257
+ <span
258
+ class="dt-arrow"
259
+ aria-hidden="true"
260
+ class:active={sortKey === col.key}
261
+ class:desc={sortKey === col.key && sortDir === 'desc'}
262
+ >
263
+ {sortKey === col.key ? '↑' : '↕'}
259
264
  </span>
260
265
  </button>
261
266
  {:else}
@@ -425,31 +430,47 @@
425
430
  color: inherit;
426
431
  cursor: pointer;
427
432
  }
428
- .dt-sort:hover {
429
- color: var(--text);
430
- }
431
433
  .dt-arrow {
434
+ display: inline-block;
432
435
  opacity: 0.4;
433
436
  font-size: 0.9em;
437
+ transition:
438
+ rotate var(--dur-base) var(--ease-out),
439
+ opacity var(--dur-fast) var(--ease);
434
440
  }
435
441
  .dt-arrow.active {
436
442
  opacity: 1;
437
443
  color: var(--accent);
438
444
  }
445
+ .dt-arrow.desc {
446
+ rotate: 180deg;
447
+ }
439
448
  tbody tr:last-child td {
440
449
  border-bottom: none;
441
450
  }
442
451
  tr.clickable {
443
452
  cursor: pointer;
444
- transition: background 0.12s var(--ease);
453
+ transition: background var(--dur-fast) var(--ease);
445
454
  }
446
- tr.clickable:hover,
447
455
  tr.clickable:focus-visible {
448
456
  background: var(--bg-elevated-2);
449
- }
450
- tr.clickable:focus-visible {
451
457
  outline: var(--focus-ring);
452
- outline-offset: -2px;
458
+ outline-offset: var(--focus-ring-offset-inset);
459
+ }
460
+ @media (hover: hover) and (pointer: fine) {
461
+ .dt-sort:hover {
462
+ color: var(--text);
463
+ }
464
+ tr.clickable:hover {
465
+ background: var(--bg-elevated-2);
466
+ transition-duration: var(--dur-instant);
467
+ }
468
+ tr:hover .dt-actions {
469
+ opacity: 1;
470
+ }
471
+ .dt-more-btn:hover {
472
+ background: var(--bg-elevated-2);
473
+ }
453
474
  }
454
475
  td.truncate {
455
476
  white-space: nowrap;
@@ -487,7 +508,7 @@
487
508
  }
488
509
  .dt-actions {
489
510
  opacity: 0;
490
- transition: opacity 0.12s var(--ease);
511
+ transition: opacity var(--dur-fast) var(--ease);
491
512
  }
492
513
  .dt-actions-inner {
493
514
  display: inline-flex;
@@ -495,7 +516,6 @@
495
516
  gap: var(--sp-1);
496
517
  white-space: nowrap;
497
518
  }
498
- tr:hover .dt-actions,
499
519
  tr:focus-within .dt-actions {
500
520
  opacity: 1;
501
521
  }
@@ -503,6 +523,10 @@
503
523
  .dt-actions {
504
524
  opacity: 1;
505
525
  }
526
+ /* The last row's checkbox slab would otherwise add a scroll range. */
527
+ tbody tr:last-child td :global(.checkbox::before) {
528
+ inset-block-end: 0;
529
+ }
506
530
  /* Sort slab: inline-end stays at 0 so it never reaches the next header
507
531
  cell, and the inline-start reach stays inside the cell's own padding. */
508
532
  .dt-sort {
@@ -664,9 +688,8 @@
664
688
  font: inherit;
665
689
  color: var(--accent);
666
690
  cursor: pointer;
667
- border-radius: var(--r-md, 6px);
691
+ border-radius: var(--r-md);
668
692
  }
669
- .dt-more-btn:hover,
670
693
  .dt-more-btn:focus-visible {
671
694
  background: var(--bg-elevated-2);
672
695
  }
package/dist/floating.js CHANGED
@@ -56,7 +56,33 @@ export function placeAt(t, floating, placement = 'bottom-start', gap = 6) {
56
56
  left = clamp(left, gap, vw - f.width - gap);
57
57
  floating.style.top = `${Math.round(top)}px`;
58
58
  floating.style.left = `${Math.round(left)}px`;
59
+ const resolved = side === 'top' || side === 'bottom'
60
+ ? top + f.height / 2 < t.top + t.height / 2
61
+ ? 'top'
62
+ : 'bottom'
63
+ : left + f.width / 2 < t.left + t.width / 2
64
+ ? 'left'
65
+ : 'right';
66
+ const cx = Math.round(clamp(t.left + t.width / 2 - left, 0, f.width));
67
+ const cy = Math.round(clamp(t.top + t.height / 2 - top, 0, f.height));
68
+ floating.dataset.side = resolved;
69
+ floating.style.setProperty('--tsu-origin', ORIGIN[resolved](cx, cy));
70
+ floating.style.setProperty('--tsu-shift', SHIFT[resolved]);
59
71
  }
72
+ // Enter motion grows out of the trigger: the origin sits on the trigger-facing
73
+ // edge, level with the trigger centre, and the panel starts shifted towards it.
74
+ const ORIGIN = {
75
+ top: (cx) => `${cx}px 100%`,
76
+ bottom: (cx) => `${cx}px 0`,
77
+ left: (_cx, cy) => `100% ${cy}px`,
78
+ right: (_cx, cy) => `0 ${cy}px`,
79
+ };
80
+ const SHIFT = {
81
+ top: '0 var(--shift-sm)',
82
+ bottom: '0 calc(-1 * var(--shift-sm))',
83
+ left: 'var(--shift-sm) 0',
84
+ right: 'calc(-1 * var(--shift-sm)) 0',
85
+ };
60
86
  /** Angles (degrees) of `count` petals spread evenly from `from` to `to`. */
61
87
  export function fanAngles(count, from, to) {
62
88
  if (count <= 1)
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export type { SelectOption } from './components/atoms/Select.svelte';
25
25
  export { default as Select } from './components/atoms/Select.svelte';
26
26
  export { default as Skeleton } from './components/atoms/Skeleton.svelte';
27
27
  export { default as Slider } from './components/atoms/Slider.svelte';
28
+ export type { SpinnerVariant } from './components/atoms/Spinner.svelte';
28
29
  export { default as Spinner } from './components/atoms/Spinner.svelte';
29
30
  export { default as Swatch } from './components/atoms/Swatch.svelte';
30
31
  export { default as Switch } from './components/atoms/Switch.svelte';
@@ -89,6 +90,7 @@ export { default as SeekSlider } from './components/molecules/SeekSlider.svelte'
89
90
  export { default as SegmentedControl, type SegmentOption, } from './components/molecules/SegmentedControl.svelte';
90
91
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
91
92
  export { default as SortableList, SORTABLE_LIST_MESSAGES, type SortableListMessages, type SortableListMove, type SortableListRowContext, } from './components/molecules/SortableList.svelte';
93
+ export { default as SoundSettings } from './components/molecules/SoundSettings.svelte';
92
94
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
93
95
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
94
96
  export { default as SwipeRow, type SwipeAction } from './components/molecules/SwipeRow.svelte';
@@ -114,7 +116,9 @@ export * as filterQuery from './query';
114
116
  export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
115
117
  export { type OptionSection, sectionOptions } from './select-options';
116
118
  export type { ControlSize } from './size';
119
+ export { playSound, type SoundMuted, type SoundName } from './sound';
117
120
  export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
121
+ export { sound } from './stores/sound.svelte';
118
122
  export { type Mode, THEMES, theme } from './stores/theme.svelte';
119
123
  export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type ToastToneInput, toasts, } from './stores/toast.svelte';
120
124
  export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, } from './theme-mode';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Styles are NOT auto-imported (so consumers control bundling). Import once at
4
4
  // your app root:
5
- // import '@dorsk/tsumikit/styles/app.css';
5
+ // import './styles/app.css';
6
6
  // then use the components below.
7
7
  export { artworkGradient, artworkHue, initials } from './artwork';
8
8
  export { autoresize } from './autoresize';
@@ -97,6 +97,7 @@ export { default as SeekSlider } from './components/molecules/SeekSlider.svelte'
97
97
  export { default as SegmentedControl, } from './components/molecules/SegmentedControl.svelte';
98
98
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
99
99
  export { default as SortableList, SORTABLE_LIST_MESSAGES, } from './components/molecules/SortableList.svelte';
100
+ export { default as SoundSettings } from './components/molecules/SoundSettings.svelte';
100
101
  export { default as SplitButton } from './components/molecules/SplitButton.svelte';
101
102
  export { default as StatusBar } from './components/molecules/StatusBar.svelte';
102
103
  export { default as SwipeRow } from './components/molecules/SwipeRow.svelte';
@@ -123,8 +124,10 @@ export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time
123
124
  export * as filterQuery from './query';
124
125
  export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
125
126
  export { sectionOptions } from './select-options';
127
+ export { playSound } from './sound';
126
128
  export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
127
129
  // ---- stores / actions ----
130
+ export { sound } from './stores/sound.svelte';
128
131
  export { THEMES, theme } from './stores/theme.svelte';
129
132
  export { toasts, } from './stores/toast.svelte';
130
133
  export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, } from './theme-mode';
@@ -0,0 +1,3 @@
1
+ import type { SoundName } from './stores/sound.svelte';
2
+ export declare function unlock(): void;
3
+ export declare function play(name: SoundName, volume: number): void;