@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
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Icon, { isIconName } from './Icon.svelte';
3
3
  import { formatCount, hasCount } from '../../count';
4
+ import { playSound } from '../../sound';
4
5
  import type { ControlSize } from '../../size';
5
6
  import { canonicalTone, type Tone } from '../../tone';
6
7
  import type { Snippet } from 'svelte';
@@ -32,7 +33,7 @@
32
33
  href?: string;
33
34
  target?: string;
34
35
  rel?: string;
35
- // Square icon-only tap target (IconButton), `--box-md` (2.25rem) unless
36
+ // Square icon-only tap target (IconButton), `--box-md` unless
36
37
  // `box` says otherwise. `iconInline` is the borderless, compact variant
37
38
  // (chip-remove ✕, inline edit ✎); pair with `hoverDanger` to tint it red on
38
39
  // hover (delete affordances).
@@ -73,6 +74,8 @@
73
74
  shortcut?: string;
74
75
  // Two-state toggle: sets `aria-pressed`.
75
76
  pressed?: boolean;
77
+ /** Play UI sounds when enabled globally; false silences this instance. */
78
+ sound?: boolean;
76
79
  class?: string;
77
80
  children?: Snippet;
78
81
  };
@@ -103,6 +106,7 @@
103
106
  glyph,
104
107
  shortcut,
105
108
  pressed,
109
+ sound = true,
106
110
  type = 'button',
107
111
  disabled = false,
108
112
  title,
@@ -129,6 +133,7 @@
129
133
  e.preventDefault();
130
134
  return;
131
135
  }
136
+ playSound('click', sound);
132
137
  onclick?.(e);
133
138
  };
134
139
 
@@ -234,64 +239,68 @@
234
239
  align-items: center;
235
240
  justify-content: center;
236
241
  gap: var(--sp-2);
237
- padding: var(--sp-2) var(--sp-4);
242
+ padding: var(--sp-2) var(--btn-px-md);
238
243
  min-height: var(--btn-size, var(--control-height-default));
244
+ max-width: 100%;
239
245
  border: 1px solid var(--btn-border, var(--border-strong));
240
246
  border-radius: var(--btn-radius, var(--r-md));
241
247
  background: var(--btn-bg, var(--surface));
242
248
  color: var(--btn-fg, var(--text));
243
249
  font-weight: var(--fw-medium);
244
- font-size: var(--fs-sm);
245
- line-height: 1;
250
+ font-size: var(--control-fs-md);
251
+ line-height: var(--lh-none);
252
+ --icon-size: var(--control-icon-md);
253
+ --btn-scale-dur: var(--dur-fast);
246
254
  transition:
247
- background 0.12s var(--ease),
248
- border-color 0.12s var(--ease),
249
- opacity 0.12s var(--ease);
255
+ background var(--dur-fast) ease,
256
+ border-color var(--dur-fast) ease,
257
+ color var(--dur-fast) ease,
258
+ opacity var(--dur-fast) ease,
259
+ scale var(--btn-scale-dur) var(--ease-out);
250
260
  user-select: none;
251
- white-space: nowrap;
252
- }
253
- .btn:hover:not(:disabled) {
254
- border-color: var(--accent);
261
+ white-space: normal;
262
+ overflow-wrap: break-word;
255
263
  }
256
264
  .btn:disabled {
257
- opacity: 0.45;
265
+ opacity: var(--disabled-opacity);
258
266
  cursor: not-allowed;
259
267
  }
268
+ /* Press: scale in over --dur-press, release over --dur-fast. */
269
+ .btn:active:not(:disabled, [aria-disabled='true'], .loading, .btn-link) {
270
+ --btn-scale-dur: var(--dur-press);
271
+ scale: var(--press-scale);
272
+ }
273
+ .btn-icon:active:not(:disabled, [aria-disabled='true'], .loading),
274
+ .btn-box:active:not(:disabled, [aria-disabled='true'], .loading),
275
+ .btn-square:active:not(:disabled, [aria-disabled='true'], .loading),
276
+ .btn-icon-inline:active:not(:disabled, [aria-disabled='true'], .loading) {
277
+ scale: var(--press-scale-sm);
278
+ }
260
279
  .btn-primary {
261
280
  background: var(--btn-bg, var(--accent));
262
281
  border-color: var(--btn-border, var(--accent));
263
282
  color: var(--btn-fg, var(--text-on-accent));
264
283
  font-weight: var(--fw-semibold);
265
284
  }
266
- .btn-primary:hover:not(:disabled) {
267
- filter: brightness(1.08);
268
- }
269
285
  .btn-danger {
270
286
  color: var(--danger);
271
287
  border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
272
288
  }
273
- .btn-danger:hover:not(:disabled) {
274
- background: color-mix(in srgb, var(--danger) 14%, transparent);
275
- border-color: var(--danger);
276
- }
277
289
  .btn-ghost {
278
290
  background: var(--btn-bg, transparent);
279
291
  border-color: var(--btn-border, transparent);
280
292
  }
281
- .btn-ghost:hover:not(:disabled) {
282
- background: var(--bg-elevated-2);
283
- border-color: transparent;
284
- }
285
293
  .btn-sm {
286
- height: var(--btn-size, var(--control-height-compact));
287
294
  min-height: var(--btn-size, var(--control-height-compact));
288
- padding: var(--sp-1) var(--sp-3);
289
- font-size: var(--fs-xs);
295
+ padding: var(--sp-1) var(--btn-px-sm);
296
+ font-size: var(--control-fs-sm);
297
+ --icon-size: var(--control-icon-sm);
290
298
  }
291
299
  .btn-lg {
292
300
  min-height: var(--btn-size, var(--control-height-large));
293
- padding: var(--sp-3) var(--sp-5);
294
- font-size: var(--fs-base);
301
+ padding: var(--sp-3) var(--btn-px-lg);
302
+ font-size: var(--control-fs-lg);
303
+ --icon-size: var(--control-icon-lg);
295
304
  }
296
305
  .btn-block {
297
306
  width: 100%;
@@ -305,7 +314,7 @@
305
314
  text-decoration: none;
306
315
  }
307
316
  a.btn[aria-disabled='true'] {
308
- opacity: 0.45;
317
+ opacity: var(--disabled-opacity);
309
318
  pointer-events: none;
310
319
  cursor: not-allowed;
311
320
  }
@@ -335,14 +344,6 @@
335
344
  color: var(--btn-tone);
336
345
  border-color: color-mix(in srgb, var(--btn-tone) 50%, var(--border));
337
346
  }
338
- .btn-tone-accent:hover:not(:disabled),
339
- .btn-tone-success:hover:not(:disabled),
340
- .btn-tone-info:hover:not(:disabled),
341
- .btn-tone-warn:hover:not(:disabled),
342
- .btn-tone-danger:hover:not(:disabled) {
343
- background: color-mix(in srgb, var(--btn-tone) 14%, transparent);
344
- border-color: var(--btn-tone);
345
- }
346
347
 
347
348
  /* Icon-chip: larger square outlined tap target for header/toolbar actions.
348
349
  Combines with a tone for tinted severity chips. */
@@ -370,26 +371,19 @@
370
371
  gap: var(--sp-2);
371
372
  height: var(--btn-size, var(--control-height));
372
373
  min-height: var(--btn-size, var(--control-height));
373
- padding: 0 var(--sp-3);
374
+ padding: 0 var(--control-px-md);
374
375
  border: 1px solid var(--btn-border, var(--border-strong));
375
376
  border-radius: var(--btn-radius, var(--r-md));
376
377
  background: var(--btn-bg, var(--surface));
377
378
  color: var(--btn-fg, var(--text));
378
379
  font-weight: var(--fw-medium);
379
- font-size: var(--fs-sm);
380
- line-height: 1;
381
- white-space: nowrap;
382
- transition:
383
- background 0.12s var(--ease),
384
- border-color 0.12s var(--ease),
385
- opacity 0.12s var(--ease);
380
+ font-size: var(--control-fs-md);
381
+ line-height: var(--lh-none);
382
+ white-space: normal;
386
383
  user-select: none;
387
384
  }
388
- .btn-control:hover:not(:disabled) {
389
- border-color: var(--accent);
390
- }
391
385
  .btn-control:disabled {
392
- opacity: 0.45;
386
+ opacity: var(--disabled-opacity);
393
387
  cursor: not-allowed;
394
388
  }
395
389
  /* .btn-control follows .btn-primary in source order and would otherwise paint
@@ -400,28 +394,15 @@
400
394
  border-color: var(--btn-border, var(--accent));
401
395
  color: var(--btn-fg, var(--text-on-accent));
402
396
  }
403
- .btn-control.btn-primary:hover:not(:disabled) {
404
- border-color: var(--accent);
405
- filter: brightness(1.08);
406
- }
407
397
  .btn-control.btn-tone-success {
408
398
  color: var(--ok);
409
399
  border-color: color-mix(in srgb, var(--ok) 50%, var(--border));
410
400
  }
411
- .btn-control.btn-tone-success:hover:not(:disabled) {
412
- background: color-mix(in srgb, var(--ok) 14%, transparent);
413
- border-color: var(--ok);
414
- }
415
401
  .btn-control.btn-danger,
416
402
  .btn-control.btn-tone-danger {
417
403
  color: var(--danger);
418
404
  border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
419
405
  }
420
- .btn-control.btn-danger:hover:not(:disabled),
421
- .btn-control.btn-tone-danger:hover:not(:disabled) {
422
- background: color-mix(in srgb, var(--danger) 14%, transparent);
423
- border-color: var(--danger);
424
- }
425
406
  /* A positive primary action uses success as its fill, not merely as the
426
407
  neutral tint. This follows the control override so both heights match. */
427
408
  .btn-primary.btn-tone-success {
@@ -429,11 +410,6 @@
429
410
  border-color: var(--ok);
430
411
  color: var(--text-on-success);
431
412
  }
432
- .btn-primary.btn-tone-success:hover:not(:disabled) {
433
- background: var(--ok);
434
- border-color: var(--ok);
435
- filter: brightness(1.08);
436
- }
437
413
  /* Other tones on a primary action tint the accent fill rather than repaint the
438
414
  text, so the on-accent foreground stays readable (bare tone-as-text would be
439
415
  accent-on-accent). success keeps its own filled guard above. */
@@ -445,14 +421,6 @@
445
421
  border-color: color-mix(in srgb, var(--btn-tone) 35%, var(--accent));
446
422
  color: var(--text-on-accent);
447
423
  }
448
- .btn-primary.btn-tone-accent:hover:not(:disabled),
449
- .btn-primary.btn-tone-info:hover:not(:disabled),
450
- .btn-primary.btn-tone-warn:hover:not(:disabled),
451
- .btn-primary.btn-tone-danger:hover:not(:disabled) {
452
- background: color-mix(in srgb, var(--btn-tone) 35%, var(--accent));
453
- border-color: color-mix(in srgb, var(--btn-tone) 35%, var(--accent));
454
- filter: brightness(1.08);
455
- }
456
424
 
457
425
  .no-shrink {
458
426
  flex: none;
@@ -472,10 +440,9 @@
472
440
  box-shadow: none;
473
441
  border-radius: 0;
474
442
  font-size: inherit;
475
- line-height: 1;
443
+ line-height: var(--lh-none);
476
444
  color: var(--btn-tone, var(--link));
477
445
  }
478
- .btn-link:hover:not(:disabled),
479
446
  .btn-link:focus-visible {
480
447
  background: none;
481
448
  border: none;
@@ -524,14 +491,6 @@
524
491
  background: none;
525
492
  color: var(--text-muted);
526
493
  }
527
- .btn-icon-inline:hover:not(:disabled) {
528
- border: none;
529
- background: none;
530
- color: var(--text);
531
- }
532
- .btn-icon-inline.hover-danger:hover:not(:disabled) {
533
- color: var(--danger);
534
- }
535
494
 
536
495
  /* Label collapse: `[data-label]` children disappear below the threshold and
537
496
  the button falls back to icon-only padding. */
@@ -629,14 +588,14 @@
629
588
  }
630
589
  .btn-stacked.btn-sm {
631
590
  min-height: var(--btn-size, var(--control-height-default));
632
- min-width: var(--btn-size, calc(var(--control-height-default) + var(--sp-1) + 0.125rem));
633
- gap: 0.1875rem;
591
+ min-width: var(--btn-size, calc(var(--control-height-default) + var(--sp-1) + var(--sp-0-5)));
592
+ gap: var(--sp-0-5);
634
593
  padding: var(--sp-1);
635
594
  font-size: calc(var(--fs-xs) * 0.85);
636
595
  }
637
596
  .btn-stacked.btn-lg {
638
- min-height: var(--btn-size, calc(var(--control-height-large) + var(--sp-2) + 0.125rem));
639
- min-width: var(--btn-size, calc(var(--control-height-large) + var(--sp-2) + 0.125rem));
597
+ min-height: var(--btn-size, calc(var(--control-height-large) + var(--sp-2) + var(--sp-0-5)));
598
+ min-width: var(--btn-size, calc(var(--control-height-large) + var(--sp-2) + var(--sp-0-5)));
640
599
  padding: var(--sp-2) var(--sp-3);
641
600
  font-size: var(--fs-sm);
642
601
  }
@@ -645,7 +604,8 @@
645
604
  align-items: center;
646
605
  justify-content: center;
647
606
  flex: none;
648
- line-height: 1;
607
+ line-height: var(--lh-none);
608
+ font-size: var(--icon-size);
649
609
  }
650
610
  .btn-stacked .btn-glyph {
651
611
  font-size: var(--fs-lg);
@@ -677,13 +637,13 @@
677
637
  }
678
638
  .btn-shortcut {
679
639
  position: absolute;
680
- top: 0.125rem;
681
- left: 0.25rem;
640
+ top: var(--sp-0-5);
641
+ left: var(--sp-1);
682
642
  font-family: var(--font-mono);
683
643
  font-size: calc(var(--fs-xs) * 0.75);
684
644
  font-weight: var(--fw-medium);
685
645
  color: var(--text-faint);
686
- line-height: 1;
646
+ line-height: var(--lh-none);
687
647
  pointer-events: none;
688
648
  }
689
649
 
@@ -701,8 +661,14 @@
701
661
  flex: none;
702
662
  border: 2px solid currentColor;
703
663
  border-top-color: transparent;
704
- border-radius: 50%;
664
+ border-radius: var(--r-pill);
705
665
  animation: btn-spin 0.6s linear infinite;
666
+ transition: opacity var(--dur-fast) ease;
667
+ }
668
+ @starting-style {
669
+ .btn-spinner {
670
+ opacity: 0;
671
+ }
706
672
  }
707
673
  @keyframes btn-spin {
708
674
  to {
@@ -733,7 +699,7 @@
733
699
  font-size: var(--fs-xs);
734
700
  font-weight: var(--fw-semibold);
735
701
  font-variant-numeric: tabular-nums;
736
- line-height: 1;
702
+ line-height: var(--lh-none);
737
703
  white-space: nowrap;
738
704
  pointer-events: none;
739
705
  }
@@ -741,7 +707,7 @@
741
707
  .btn-link .btn-count,
742
708
  .btn-icon-inline .btn-count {
743
709
  --btn-count-size: 1rem;
744
- padding: 0 0.2rem;
710
+ padding: 0 var(--sp-0-5);
745
711
  font-size: var(--fs-xs);
746
712
  }
747
713
  /* The accent fill would swallow an accent pill: invert on primary. */
@@ -759,4 +725,72 @@
759
725
  clip-path: inset(50%);
760
726
  white-space: nowrap;
761
727
  }
728
+
729
+ /* Hover only on devices that have it: taps leave sticky hovers on touch. */
730
+ @media (hover: hover) and (pointer: fine) {
731
+ .btn:hover:not(:disabled) {
732
+ border-color: var(--accent);
733
+ }
734
+ .btn-primary:hover:not(:disabled) {
735
+ filter: brightness(1.08);
736
+ }
737
+ .btn-danger:hover:not(:disabled) {
738
+ background: color-mix(in srgb, var(--danger) 14%, transparent);
739
+ border-color: var(--danger);
740
+ }
741
+ .btn-ghost:hover:not(:disabled) {
742
+ background: var(--bg-elevated-2);
743
+ border-color: transparent;
744
+ }
745
+ .btn-tone-accent:hover:not(:disabled),
746
+ .btn-tone-success:hover:not(:disabled),
747
+ .btn-tone-info:hover:not(:disabled),
748
+ .btn-tone-warn:hover:not(:disabled),
749
+ .btn-tone-danger:hover:not(:disabled) {
750
+ background: color-mix(in srgb, var(--btn-tone) 14%, transparent);
751
+ border-color: var(--btn-tone);
752
+ }
753
+ .btn-control:hover:not(:disabled) {
754
+ border-color: var(--accent);
755
+ }
756
+ .btn-control.btn-primary:hover:not(:disabled) {
757
+ border-color: var(--accent);
758
+ filter: brightness(1.08);
759
+ }
760
+ .btn-control.btn-tone-success:hover:not(:disabled) {
761
+ background: color-mix(in srgb, var(--ok) 14%, transparent);
762
+ border-color: var(--ok);
763
+ }
764
+ .btn-control.btn-danger:hover:not(:disabled),
765
+ .btn-control.btn-tone-danger:hover:not(:disabled) {
766
+ background: color-mix(in srgb, var(--danger) 14%, transparent);
767
+ border-color: var(--danger);
768
+ }
769
+ .btn-primary.btn-tone-success:hover:not(:disabled) {
770
+ background: var(--ok);
771
+ border-color: var(--ok);
772
+ filter: brightness(1.08);
773
+ }
774
+ .btn-primary.btn-tone-accent:hover:not(:disabled),
775
+ .btn-primary.btn-tone-info:hover:not(:disabled),
776
+ .btn-primary.btn-tone-warn:hover:not(:disabled),
777
+ .btn-primary.btn-tone-danger:hover:not(:disabled) {
778
+ background: color-mix(in srgb, var(--btn-tone) 35%, var(--accent));
779
+ border-color: color-mix(in srgb, var(--btn-tone) 35%, var(--accent));
780
+ filter: brightness(1.08);
781
+ }
782
+ .btn-link:hover:not(:disabled) {
783
+ background: none;
784
+ border: none;
785
+ text-decoration: underline;
786
+ }
787
+ .btn-icon-inline:hover:not(:disabled) {
788
+ border: none;
789
+ background: none;
790
+ color: var(--text);
791
+ }
792
+ .btn-icon-inline.hover-danger:hover:not(:disabled) {
793
+ color: var(--danger);
794
+ }
795
+ }
762
796
  </style>
@@ -30,6 +30,8 @@ type ButtonProps = HTMLButtonAttributes & {
30
30
  glyph?: Snippet | string;
31
31
  shortcut?: string;
32
32
  pressed?: boolean;
33
+ /** Play UI sounds when enabled globally; false silences this instance. */
34
+ sound?: boolean;
33
35
  class?: string;
34
36
  children?: Snippet;
35
37
  };
@@ -234,14 +234,20 @@
234
234
  .card-tap {
235
235
  cursor: pointer;
236
236
  transition:
237
- border-color 0.12s var(--ease),
238
- background 0.12s var(--ease);
237
+ border-color var(--dur-fast) var(--ease),
238
+ background var(--dur-fast) var(--ease),
239
+ box-shadow var(--dur-fast) var(--ease),
240
+ scale var(--dur-fast) var(--ease-out);
239
241
  }
240
242
  .card-tap:active {
241
243
  background: var(--bg-elevated-2);
244
+ scale: var(--press-scale-lg);
245
+ transition-duration: var(--dur-press);
242
246
  }
243
- .card-tap:hover {
244
- border-color: var(--border-strong);
247
+ @media (hover: hover) and (pointer: fine) {
248
+ .card-tap:hover {
249
+ border-color: var(--border-strong);
250
+ }
245
251
  }
246
252
  .card-max {
247
253
  width: 100%;
@@ -6,6 +6,7 @@
6
6
  import type { HTMLInputAttributes } from 'svelte/elements';
7
7
  import { getFieldContext } from '../../field-context';
8
8
  import type { ControlSize } from '../../size';
9
+ import { playSound } from '../../sound';
9
10
 
10
11
  let {
11
12
  checked = $bindable(false),
@@ -14,11 +15,13 @@
14
15
  label,
15
16
  labelHidden = false,
16
17
  size = 'md',
18
+ sound = true,
17
19
  id,
18
20
  'aria-describedby': ariaDescribedby,
19
21
  'aria-invalid': ariaInvalid,
20
22
  class: klass = '',
21
23
  el = $bindable(null),
24
+ onchange,
22
25
  ...rest
23
26
  }: Omit<HTMLInputAttributes, 'size'> & {
24
27
  checked?: boolean;
@@ -31,6 +34,8 @@
31
34
  /** Box + label scale, as on `Switch`. Override the box alone with
32
35
  * `style="--cb-box: 1rem"`. */
33
36
  size?: ControlSize;
37
+ /** Play UI sounds when enabled globally; false silences this instance. */
38
+ sound?: boolean;
34
39
  id?: string;
35
40
  'aria-describedby'?: string | null;
36
41
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
@@ -40,6 +45,11 @@
40
45
  const field = getFieldContext();
41
46
  const isInvalid = $derived(invalid || !!field?.invalid);
42
47
 
48
+ function change(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
49
+ onchange?.(e);
50
+ playSound(e.currentTarget.checked ? 'toggle-on' : 'toggle-off', sound);
51
+ }
52
+
43
53
  // `indeterminate` is a property, not an attribute — sync it imperatively.
44
54
  $effect(() => {
45
55
  if (el) el.indeterminate = indeterminate;
@@ -57,6 +67,7 @@
57
67
  type="checkbox"
58
68
  bind:checked
59
69
  {...rest}
70
+ onchange={change}
60
71
  id={id ?? field?.id}
61
72
  aria-describedby={ariaDescribedby ?? field?.describedBy}
62
73
  aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
@@ -83,24 +94,29 @@
83
94
  margin: 0;
84
95
  }
85
96
  .checkbox-sm {
86
- --cb-box: 0.95rem;
97
+ --cb-box: var(--chip-xs);
87
98
  font-size: var(--fs-xs);
88
99
  }
89
100
  .checkbox-lg {
90
- --cb-box: 1.35rem;
101
+ --cb-box: var(--chip-md);
91
102
  font-size: var(--fs-base);
92
103
  }
93
104
  .box {
94
105
  position: relative;
95
106
  flex: none;
96
- width: var(--cb-box, 1.15rem);
97
- height: var(--cb-box, 1.15rem);
107
+ width: var(--cb-box, var(--chip-sm));
108
+ height: var(--cb-box, var(--chip-sm));
98
109
  border: 1px solid var(--border-strong);
99
110
  border-radius: var(--r-sm);
100
111
  background: var(--bg);
101
112
  transition:
102
- background 0.12s var(--ease),
103
- border-color 0.12s var(--ease);
113
+ background var(--dur-fast) var(--ease),
114
+ border-color var(--dur-fast) var(--ease),
115
+ scale var(--dur-fast) var(--ease-out);
116
+ }
117
+ .checkbox:active:not(:has(input:disabled)) .box {
118
+ scale: var(--press-scale-sm);
119
+ transition-duration: var(--dur-press);
104
120
  }
105
121
  /* check mark */
106
122
  .box::after {
@@ -110,7 +126,10 @@
110
126
  background: var(--text-on-accent);
111
127
  clip-path: polygon(41% 67%, 79% 26%, 87% 35%, 41% 84%, 15% 56%, 24% 47%);
112
128
  opacity: 0;
113
- transition: opacity 0.1s var(--ease);
129
+ scale: 0.6;
130
+ transition:
131
+ opacity var(--dur-exit-fast) var(--ease-out),
132
+ scale var(--dur-exit-fast) var(--ease-out);
114
133
  }
115
134
  input:checked + .box {
116
135
  background: var(--accent);
@@ -118,6 +137,10 @@
118
137
  }
119
138
  input:checked + .box::after {
120
139
  opacity: 1;
140
+ scale: 1;
141
+ transition:
142
+ opacity var(--dur-spring) var(--ease-spring),
143
+ scale var(--dur-spring) var(--ease-spring);
121
144
  }
122
145
  /* indeterminate dash */
123
146
  input:indeterminate + .box {
@@ -126,17 +149,21 @@
126
149
  }
127
150
  input:indeterminate + .box::after {
128
151
  opacity: 1;
152
+ scale: 1;
129
153
  clip-path: polygon(22% 42%, 78% 42%, 78% 58%, 22% 58%);
154
+ transition:
155
+ opacity var(--dur-spring) var(--ease-spring),
156
+ scale var(--dur-spring) var(--ease-spring);
130
157
  }
131
158
  input:focus-visible + .box {
132
159
  outline: var(--focus-ring);
133
- outline-offset: 2px;
160
+ outline-offset: var(--focus-ring-offset);
134
161
  }
135
162
  input[aria-invalid='true']:not(:checked):not(:indeterminate) + .box {
136
163
  border-color: var(--danger);
137
164
  }
138
165
  input:disabled ~ * {
139
- opacity: 0.45;
166
+ opacity: var(--disabled-opacity);
140
167
  }
141
168
  .checkbox:has(input:disabled) {
142
169
  cursor: not-allowed;
@@ -11,6 +11,8 @@ type $$ComponentProps = Omit<HTMLInputAttributes, 'size'> & {
11
11
  /** Box + label scale, as on `Switch`. Override the box alone with
12
12
  * `style="--cb-box: 1rem"`. */
13
13
  size?: ControlSize;
14
+ /** Play UI sounds when enabled globally; false silences this instance. */
15
+ sound?: boolean;
14
16
  id?: string;
15
17
  'aria-describedby'?: string | null;
16
18
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
@@ -91,7 +91,7 @@
91
91
  {/each}
92
92
  </span>
93
93
  {:else}
94
- <span class="fill" style:height="{pct}%"></span>
94
+ <span class="fill" style:--p={pct / 100}></span>
95
95
  {/if}
96
96
  {#if corner}
97
97
  <span class="corner">{@render corner()}</span>
@@ -120,12 +120,14 @@
120
120
  .interactive {
121
121
  cursor: pointer;
122
122
  }
123
- .interactive:hover {
124
- border-color: var(--gauge-fill);
123
+ @media (hover: hover) and (pointer: fine) {
124
+ .interactive:hover {
125
+ border-color: var(--gauge-fill);
126
+ }
125
127
  }
126
128
  .interactive:focus-visible {
127
- outline: 2px solid var(--accent);
128
- outline-offset: 2px;
129
+ outline: var(--focus-ring);
130
+ outline-offset: var(--focus-ring-offset);
129
131
  }
130
132
  .tone-ok {
131
133
  --gauge-fill: var(--ok);
@@ -138,23 +140,27 @@
138
140
  }
139
141
  .fill {
140
142
  position: absolute;
141
- inset: auto 0 0 0;
142
- border-radius: calc(var(--r-sm) - 1px);
143
+ inset: 0;
144
+ border-radius: calc(var(--r-sm) - var(--sp-px));
143
145
  background: var(--gauge-fill);
144
- transition: height 0.2s var(--ease), background 0.2s var(--ease);
146
+ transform-origin: bottom;
147
+ scale: 1 var(--p, 0);
148
+ transition:
149
+ scale var(--dur-base) var(--ease-out),
150
+ background var(--dur-fast) var(--ease);
145
151
  }
146
152
  .segments {
147
153
  position: absolute;
148
- inset: 2px;
154
+ inset: var(--sp-0-5);
149
155
  display: flex;
150
156
  flex-direction: column-reverse;
151
- gap: 2px;
157
+ gap: var(--sp-0-5);
152
158
  }
153
159
  .segment {
154
160
  flex: 1 1 0%;
155
- border-radius: 2px;
161
+ border-radius: var(--r-xs);
156
162
  background: var(--bg-elevated);
157
- transition: background 0.2s var(--ease);
163
+ transition: background var(--dur-fast) var(--ease);
158
164
  }
159
165
  .segment.lit {
160
166
  background: var(--gauge-fill);
@@ -164,8 +170,8 @@
164
170
  right: -0.35em;
165
171
  bottom: -0.35em;
166
172
  display: inline-flex;
167
- font-size: 0.625rem;
168
- line-height: 1;
173
+ font-size: var(--fs-xs);
174
+ line-height: var(--lh-none);
169
175
  pointer-events: none;
170
176
  }
171
177
  </style>
@@ -146,7 +146,7 @@
146
146
  line-height: var(--lh-normal);
147
147
  }
148
148
  .lh-none {
149
- line-height: 1;
149
+ line-height: var(--lh-none);
150
150
  }
151
151
  .grow {
152
152
  flex: 1 1 0;
@@ -201,6 +201,7 @@
201
201
  <style>
202
202
  .icon {
203
203
  flex: none;
204
+ font-size: var(--icon-size, 1em);
204
205
  width: 1em;
205
206
  height: 1em;
206
207
  }