@dorsk/tsumikit 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +38 -1
  2. package/dist/autoresize.d.ts +2 -0
  3. package/dist/autoresize.js +8 -1
  4. package/dist/components/atoms/Artwork.svelte +12 -5
  5. package/dist/components/atoms/Avatar.svelte +1 -1
  6. package/dist/components/atoms/Badge.svelte +59 -23
  7. package/dist/components/atoms/Button.svelte +130 -96
  8. package/dist/components/atoms/Button.svelte.d.ts +2 -0
  9. package/dist/components/atoms/Card.svelte +10 -4
  10. package/dist/components/atoms/Checkbox.svelte +36 -9
  11. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  12. package/dist/components/atoms/Gauge.svelte +20 -14
  13. package/dist/components/atoms/Heading.svelte +1 -1
  14. package/dist/components/atoms/Icon.svelte +1 -0
  15. package/dist/components/atoms/Input.svelte +67 -13
  16. package/dist/components/atoms/Kbd.svelte +3 -3
  17. package/dist/components/atoms/Link.svelte +7 -2
  18. package/dist/components/atoms/Progress.svelte +4 -2
  19. package/dist/components/atoms/Scrim.svelte +3 -8
  20. package/dist/components/atoms/SegmentedProgress.svelte +5 -4
  21. package/dist/components/atoms/Select.svelte +58 -27
  22. package/dist/components/atoms/Slider.svelte +79 -22
  23. package/dist/components/atoms/Slider.svelte.d.ts +2 -0
  24. package/dist/components/atoms/Swatch.svelte +19 -8
  25. package/dist/components/atoms/Switch.svelte +34 -21
  26. package/dist/components/atoms/Switch.svelte.d.ts +2 -0
  27. package/dist/components/atoms/Text.svelte +1 -1
  28. package/dist/components/atoms/Textarea.svelte +34 -12
  29. package/dist/components/atoms/TimeDisplay.svelte +4 -2
  30. package/dist/components/layouts/AppShell.svelte +26 -15
  31. package/dist/components/layouts/MasterDetail.svelte +23 -21
  32. package/dist/components/layouts/NavBar.svelte +11 -8
  33. package/dist/components/layouts/NavItem.svelte +15 -7
  34. package/dist/components/layouts/NavSection.svelte +1 -1
  35. package/dist/components/layouts/ResizablePanel.svelte +37 -16
  36. package/dist/components/layouts/TabBar.svelte +24 -14
  37. package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
  38. package/dist/components/molecules/Accordion.svelte +4 -0
  39. package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
  40. package/dist/components/molecules/AttachmentList.svelte +14 -11
  41. package/dist/components/molecules/Breadcrumb.svelte +13 -9
  42. package/dist/components/molecules/Callout.svelte +3 -3
  43. package/dist/components/molecules/CapBar.svelte +23 -22
  44. package/dist/components/molecules/Carousel.svelte +27 -17
  45. package/dist/components/molecules/ChatBubble.svelte +2 -2
  46. package/dist/components/molecules/CodeBlock.svelte +2 -2
  47. package/dist/components/molecules/Combobox.svelte +30 -1
  48. package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
  49. package/dist/components/molecules/Composer.svelte +2 -2
  50. package/dist/components/molecules/ConfirmModal.svelte +4 -0
  51. package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
  52. package/dist/components/molecules/CopyButton.svelte +46 -2
  53. package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
  54. package/dist/components/molecules/Disclosure.svelte +23 -12
  55. package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
  56. package/dist/components/molecules/Drawer.svelte +43 -23
  57. package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
  58. package/dist/components/molecules/Dropzone.svelte +24 -8
  59. package/dist/components/molecules/EmojiPicker.svelte +27 -8
  60. package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
  61. package/dist/components/molecules/EmptyState.svelte +5 -5
  62. package/dist/components/molecules/Fieldset.svelte +8 -13
  63. package/dist/components/molecules/FileButton.svelte +57 -31
  64. package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
  65. package/dist/components/molecules/FilterInput.svelte +51 -11
  66. package/dist/components/molecules/FontScalePicker.svelte +20 -9
  67. package/dist/components/molecules/GitRef.svelte +9 -7
  68. package/dist/components/molecules/GlyphStack.svelte +46 -14
  69. package/dist/components/molecules/IconButton.svelte +6 -3
  70. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  71. package/dist/components/molecules/InputGroup.svelte +13 -12
  72. package/dist/components/molecules/LoadMore.svelte +6 -1
  73. package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
  74. package/dist/components/molecules/Menu.svelte +33 -7
  75. package/dist/components/molecules/Menu.svelte.d.ts +2 -0
  76. package/dist/components/molecules/Metric.svelte +11 -9
  77. package/dist/components/molecules/Modal.svelte +76 -26
  78. package/dist/components/molecules/Modal.svelte.d.ts +2 -0
  79. package/dist/components/molecules/OptionButton.svelte +28 -10
  80. package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
  81. package/dist/components/molecules/Pagination.svelte +26 -9
  82. package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
  83. package/dist/components/molecules/Popover.svelte +107 -51
  84. package/dist/components/molecules/Popover.svelte.d.ts +2 -0
  85. package/dist/components/molecules/Prose.svelte +1 -1
  86. package/dist/components/molecules/PullToRefresh.svelte +14 -10
  87. package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
  88. package/dist/components/molecules/RadialMenu.svelte +52 -19
  89. package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
  90. package/dist/components/molecules/RadioGroup.svelte +38 -23
  91. package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
  92. package/dist/components/molecules/RichSelect.svelte +583 -0
  93. package/dist/components/molecules/RichSelect.svelte.d.ts +68 -0
  94. package/dist/components/molecules/SectionHeader.svelte +13 -8
  95. package/dist/components/molecules/SeekSlider.svelte +38 -27
  96. package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
  97. package/dist/components/molecules/SegmentedControl.svelte +139 -86
  98. package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
  99. package/dist/components/molecules/SelectButton.svelte +21 -6
  100. package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
  101. package/dist/components/molecules/SortableList.svelte +10 -11
  102. package/dist/components/molecules/SoundSettings.svelte +161 -0
  103. package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
  104. package/dist/components/molecules/SplitButton.svelte +30 -3
  105. package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
  106. package/dist/components/molecules/SwipeRow.svelte +18 -15
  107. package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
  108. package/dist/components/molecules/Tabs.svelte +92 -10
  109. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  110. package/dist/components/molecules/ThemePicker.svelte +25 -10
  111. package/dist/components/molecules/Timestamp.svelte +12 -7
  112. package/dist/components/molecules/Toaster.svelte +65 -14
  113. package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
  114. package/dist/components/molecules/Toggle.svelte +36 -13
  115. package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
  116. package/dist/components/molecules/Tooltip.svelte +41 -10
  117. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  118. package/dist/components/molecules/VolumeSlider.svelte +34 -14
  119. package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
  120. package/dist/components/molecules/WorkingDir.svelte +1 -0
  121. package/dist/components/molecules/rich-select-keyboard.d.ts +82 -0
  122. package/dist/components/molecules/rich-select-keyboard.js +114 -0
  123. package/dist/components/organisms/ConversationFeed.svelte +24 -1
  124. package/dist/components/organisms/DataTable.svelte +37 -14
  125. package/dist/floating.js +26 -0
  126. package/dist/index.d.ts +2 -0
  127. package/dist/index.js +2 -0
  128. package/dist/sound-synth.js +17 -12
  129. package/dist/stores/toast.svelte.d.ts +9 -0
  130. package/dist/stores/toast.svelte.js +45 -9
  131. package/dist/styles/reset.css +3 -3
  132. package/package.json +1 -1
@@ -5,6 +5,7 @@
5
5
  // JS to open the dialog. Emits the chosen files via `onfiles`. Dependency-free.
6
6
  import Icon from '../atoms/Icon.svelte';
7
7
  import type { IconName } from '../atoms/Icon.svelte';
8
+ import { playSound } from '../../sound';
8
9
 
9
10
  let {
10
11
  onfiles,
@@ -20,6 +21,7 @@
20
21
  control = false,
21
22
  box,
22
23
  hitArea = 'auto',
24
+ sound = true,
23
25
  class: klass = ''
24
26
  }: {
25
27
  onfiles: (files: File[]) => void;
@@ -47,6 +49,8 @@
47
49
  box?: 'xs' | 'sm' | 'md' | 'lg';
48
50
  /** Icon-only buttons grow a touch hit slab on coarse pointers; `compact` opts out. */
49
51
  hitArea?: 'auto' | 'compact';
52
+ /** Play UI sounds when enabled globally; false silences this instance. */
53
+ sound?: boolean;
50
54
  class?: string;
51
55
  } = $props();
52
56
 
@@ -81,6 +85,7 @@
81
85
  {accept}
82
86
  {multiple}
83
87
  {disabled}
88
+ onclick={() => playSound('click', sound)}
84
89
  onchange={onchange}
85
90
  />
86
91
  {#if icon}
@@ -100,41 +105,52 @@
100
105
  align-items: center;
101
106
  justify-content: center;
102
107
  gap: var(--sp-2);
103
- padding: var(--sp-2) var(--sp-4);
104
- min-height: 2.5rem;
108
+ padding: var(--sp-2) var(--btn-px-md);
109
+ min-height: var(--control-height-default);
105
110
  border: 1px solid var(--border-strong);
106
111
  border-radius: var(--r-md);
107
112
  background: var(--surface);
108
113
  color: var(--text);
109
114
  font-weight: var(--fw-medium);
110
- font-size: var(--fs-sm);
111
- line-height: 1;
115
+ font-size: var(--control-fs-md);
116
+ line-height: var(--lh-none);
112
117
  white-space: nowrap;
113
118
  cursor: pointer;
114
119
  user-select: none;
120
+ --icon-size: var(--control-icon-md);
121
+ --file-scale-dur: var(--dur-fast);
115
122
  transition:
116
- background 0.12s var(--ease),
117
- border-color 0.12s var(--ease),
118
- opacity 0.12s var(--ease);
123
+ background var(--dur-fast) ease,
124
+ border-color var(--dur-fast) ease,
125
+ color var(--dur-fast) ease,
126
+ opacity var(--dur-fast) ease,
127
+ scale var(--file-scale-dur) var(--ease-out);
119
128
  }
120
- .file-btn:hover:not(.disabled) {
121
- border-color: var(--accent);
129
+ .file-btn:active:not(.disabled) {
130
+ --file-scale-dur: var(--dur-press);
131
+ scale: var(--press-scale);
132
+ }
133
+ .file-btn.icon-only:active:not(.disabled),
134
+ .file-btn.box:active:not(.disabled) {
135
+ scale: var(--press-scale-sm);
122
136
  }
123
- /* Size variants mirror the Button atom (sm 2rem / md 2.5rem / lg 3rem). */
137
+ /* Size variants mirror the Button atom. */
124
138
  .file-btn.sm {
125
- min-height: 2rem;
126
- padding: var(--sp-1) var(--sp-3);
127
- font-size: var(--fs-xs);
139
+ min-height: var(--control-height-compact);
140
+ padding: var(--sp-1) var(--btn-px-sm);
141
+ font-size: var(--control-fs-sm);
142
+ --icon-size: var(--control-icon-sm);
128
143
  }
129
144
  .file-btn.lg {
130
- min-height: 3rem;
131
- padding: var(--sp-3) var(--sp-5);
132
- font-size: var(--fs-base);
145
+ min-height: var(--control-height-large);
146
+ padding: var(--sp-3) var(--btn-px-lg);
147
+ font-size: var(--control-fs-lg);
148
+ --icon-size: var(--control-icon-lg);
133
149
  }
134
150
  .file-btn.control {
135
151
  height: var(--control-height);
136
152
  min-height: var(--control-height);
137
- padding: 0 var(--sp-3);
153
+ padding: 0 var(--control-px-md);
138
154
  }
139
155
  /* icon-only: square it up and drop the label gap. */
140
156
  .file-btn.icon-only {
@@ -162,12 +178,17 @@
162
178
  inset-inline-end: 0;
163
179
  }
164
180
  }
165
- /* Emoji glyph is bumped above the text size — at 1em a paperclip is hard to
166
- read, so render it a touch larger for legibility. */
181
+ /* An emoji reads small next to an SVG glyph of the same size: one icon tier up. */
167
182
  .emoji {
168
183
  display: inline-flex;
169
- font-size: 1.35em;
170
- line-height: 1;
184
+ font-size: var(--icon-lg);
185
+ line-height: var(--lh-none);
186
+ }
187
+ .file-btn.sm .emoji {
188
+ font-size: var(--icon-md);
189
+ }
190
+ .file-btn.lg .emoji {
191
+ font-size: var(--icon-xl);
171
192
  }
172
193
  .file-btn.primary {
173
194
  background: var(--accent);
@@ -175,25 +196,30 @@
175
196
  color: var(--text-on-accent);
176
197
  font-weight: var(--fw-semibold);
177
198
  }
178
- .file-btn.primary:hover:not(.disabled) {
179
- filter: brightness(1.08);
180
- }
181
199
  .file-btn.ghost {
182
200
  background: transparent;
183
201
  border-color: transparent;
184
202
  }
185
- .file-btn.ghost:hover:not(.disabled) {
186
- background: var(--bg-elevated-2);
187
- border-color: transparent;
188
- }
189
203
  /* The hidden input keeps focusability (sr-only, not display:none), so mirror
190
204
  its focus onto the label for a visible ring. */
191
205
  .file-btn:focus-within {
192
- outline: 2px solid var(--accent);
193
- outline-offset: 2px;
206
+ outline: var(--focus-ring);
207
+ outline-offset: var(--focus-ring-offset);
194
208
  }
195
209
  .file-btn.disabled {
196
- opacity: 0.45;
210
+ opacity: var(--disabled-opacity);
197
211
  cursor: not-allowed;
198
212
  }
213
+ @media (hover: hover) and (pointer: fine) {
214
+ .file-btn:hover:not(.disabled) {
215
+ border-color: var(--accent);
216
+ }
217
+ .file-btn.primary:hover:not(.disabled) {
218
+ filter: brightness(1.08);
219
+ }
220
+ .file-btn.ghost:hover:not(.disabled) {
221
+ background: var(--bg-elevated-2);
222
+ border-color: transparent;
223
+ }
224
+ }
199
225
  </style>
@@ -26,6 +26,8 @@ type $$ComponentProps = {
26
26
  box?: 'xs' | 'sm' | 'md' | 'lg';
27
27
  /** Icon-only buttons grow a touch hit slab on coarse pointers; `compact` opts out. */
28
28
  hitArea?: 'auto' | 'compact';
29
+ /** Play UI sounds when enabled globally; false silences this instance. */
30
+ sound?: boolean;
29
31
  class?: string;
30
32
  };
31
33
  declare const FileButton: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -465,20 +465,26 @@
465
465
  min-width: 0;
466
466
  }
467
467
  .fi__bar {
468
+ --fi-icon: var(--control-icon-md);
468
469
  display: flex;
469
470
  align-items: center;
470
471
  gap: var(--sp-2);
471
- padding: 0 var(--sp-3);
472
+ padding: 0 var(--control-px-md);
472
473
  min-height: var(--control-height-default);
473
474
  background: var(--bg-elevated);
474
475
  border: 1px solid var(--border-strong);
475
476
  border-radius: var(--r-md);
477
+ transition:
478
+ border-color var(--dur-fast) ease,
479
+ box-shadow var(--dur-fast) ease;
476
480
  }
477
481
  .fi--sm .fi__bar {
478
482
  min-height: var(--control-height-compact);
483
+ padding: 0 var(--control-px-sm);
484
+ --fi-icon: var(--control-icon-sm);
479
485
  }
480
486
  .fi--sm .fi__input {
481
- font-size: var(--fs-sm);
487
+ font-size: var(--control-fs-sm);
482
488
  }
483
489
  .fi--pill .fi__bar {
484
490
  border-radius: var(--r-pill);
@@ -493,11 +499,11 @@
493
499
  .fi__kbd {
494
500
  font-family: var(--font-mono);
495
501
  font-size: var(--fs-xs);
496
- line-height: 1;
502
+ line-height: var(--lh-none);
497
503
  color: var(--text-faint);
498
504
  border: 1px solid var(--border);
499
505
  border-radius: var(--r-sm);
500
- padding: 2px var(--sp-1);
506
+ padding: var(--sp-0-5) var(--sp-1);
501
507
  }
502
508
  .fi__bar:focus-within {
503
509
  border-color: var(--accent);
@@ -509,6 +515,7 @@
509
515
  }
510
516
  .fi__icon {
511
517
  display: flex;
518
+ font-size: var(--fi-icon);
512
519
  color: var(--text-faint);
513
520
  }
514
521
  .fi__field {
@@ -533,16 +540,16 @@
533
540
  background: transparent;
534
541
  color: var(--text);
535
542
  font-family: var(--font-mono);
536
- font-size: var(--fs-md);
543
+ font-size: var(--control-fs-md);
537
544
  outline: none;
538
545
  min-width: 0;
539
546
  }
540
547
  @media (pointer: coarse) {
541
548
  .fi__input {
542
- font-size: max(16px, var(--fs-md));
549
+ font-size: max(16px, var(--control-fs-md));
543
550
  }
544
551
  .fi--sm .fi__input {
545
- font-size: max(16px, var(--fs-sm));
552
+ font-size: max(16px, var(--control-fs-sm));
546
553
  }
547
554
  }
548
555
  .fi--sans .fi__input {
@@ -555,22 +562,40 @@
555
562
  color: transparent;
556
563
  }
557
564
  .fi__clear {
565
+ --clear-scale-dur: var(--dur-fast);
558
566
  display: flex;
559
567
  border: 0;
560
568
  background: transparent;
569
+ font-size: var(--fi-icon);
561
570
  color: var(--text-faint);
562
571
  cursor: pointer;
563
572
  padding: var(--sp-1);
564
573
  border-radius: var(--r-sm);
574
+ transition:
575
+ color var(--dur-fast) ease,
576
+ background-color var(--dur-fast) ease,
577
+ opacity var(--dur-fast) ease,
578
+ scale var(--clear-scale-dur) var(--ease-out);
579
+ }
580
+ @starting-style {
581
+ .fi__clear {
582
+ opacity: 0;
583
+ }
565
584
  }
566
- .fi__clear:hover {
567
- color: var(--text);
568
- background: var(--bg-elevated-2);
585
+ .fi__clear:active {
586
+ --clear-scale-dur: var(--dur-press);
587
+ scale: var(--press-scale-sm);
588
+ }
589
+ @media (hover: hover) and (pointer: fine) {
590
+ .fi__clear:hover {
591
+ color: var(--text);
592
+ background: var(--bg-elevated-2);
593
+ }
569
594
  }
570
595
 
571
596
  .fi__menu {
572
597
  position: absolute;
573
- top: calc(100% + 4px);
598
+ top: calc(100% + var(--sp-1));
574
599
  left: 0;
575
600
  right: 0;
576
601
  z-index: var(--z-drawer);
@@ -581,6 +606,18 @@
581
606
  padding: var(--sp-1);
582
607
  max-height: 320px;
583
608
  overflow-y: auto;
609
+ transform-origin: top left;
610
+ transition:
611
+ opacity var(--dur-fast) var(--ease-out),
612
+ scale var(--dur-fast) var(--ease-out),
613
+ translate var(--dur-fast) var(--ease-out);
614
+ }
615
+ @starting-style {
616
+ .fi__menu {
617
+ opacity: 0;
618
+ scale: var(--enter-scale);
619
+ translate: 0 calc(-1 * var(--shift-sm));
620
+ }
584
621
  }
585
622
  .fi__menu-head {
586
623
  font-size: var(--fs-xs);
@@ -602,9 +639,12 @@
602
639
  border-radius: var(--r-sm);
603
640
  cursor: pointer;
604
641
  font-size: var(--fs-sm);
642
+ transition: background-color var(--dur-fast) ease;
605
643
  }
644
+ /* Highlight lands instantly and fades out: list navigation is high frequency. */
606
645
  .fi__opt.is-active {
607
646
  background: var(--accent-dim);
647
+ transition-duration: var(--dur-instant);
608
648
  }
609
649
  .fi__opt-label {
610
650
  font-weight: var(--fw-medium);
@@ -75,14 +75,14 @@
75
75
  .glyph {
76
76
  font-weight: var(--fw-bold);
77
77
  font-size: 1rem;
78
- line-height: 1;
78
+ line-height: var(--lh-none);
79
79
  }
80
80
  .panel {
81
81
  width: min(15rem, calc(100vw - var(--sp-6)));
82
82
  box-sizing: border-box;
83
83
  overflow: hidden;
84
84
  font-size: 0.875rem;
85
- line-height: 1.4;
85
+ line-height: var(--lh-snug);
86
86
  padding: var(--sp-2) var(--sp-3);
87
87
  }
88
88
  .row {
@@ -106,16 +106,27 @@
106
106
  color: var(--text-muted);
107
107
  font-family: inherit;
108
108
  font-weight: var(--fw-bold);
109
- line-height: 1;
109
+ line-height: var(--lh-none);
110
110
  cursor: pointer;
111
- }
112
- .end:hover {
113
- background: var(--bg-elevated-2);
114
- color: var(--text);
111
+ --end-scale-dur: var(--dur-fast);
112
+ transition:
113
+ background-color var(--dur-fast) ease,
114
+ color var(--dur-fast) ease,
115
+ scale var(--end-scale-dur) var(--ease-out);
115
116
  }
116
117
  .end:focus-visible {
117
- outline: 2px solid var(--accent);
118
- outline-offset: 1px;
118
+ outline: var(--focus-ring);
119
+ outline-offset: var(--focus-ring-offset-inset);
120
+ }
121
+ .end:active {
122
+ --end-scale-dur: var(--dur-press);
123
+ scale: var(--press-scale-sm);
124
+ }
125
+ @media (hover: hover) and (pointer: fine) {
126
+ .end:hover {
127
+ background: var(--bg-elevated-2);
128
+ color: var(--text);
129
+ }
119
130
  }
120
131
  .small {
121
132
  font-size: 0.75rem;
@@ -115,7 +115,7 @@
115
115
  min-width: 0;
116
116
  font-family: var(--font-mono);
117
117
  font-size: var(--fs-xs);
118
- line-height: 1.4;
118
+ line-height: var(--lh-snug);
119
119
  white-space: nowrap;
120
120
  }
121
121
  .chip {
@@ -123,7 +123,7 @@
123
123
  align-items: center;
124
124
  gap: 0.25em;
125
125
  min-width: 0;
126
- padding: 0.15rem var(--sp-2);
126
+ padding: var(--sp-0-5) var(--sp-2);
127
127
  border-radius: var(--r-pill);
128
128
  background: var(--bg-elevated-2);
129
129
  color: var(--text-muted);
@@ -137,14 +137,16 @@
137
137
  color: var(--git-ref-tone);
138
138
  border-color: color-mix(in srgb, var(--git-ref-tone) 40%, transparent);
139
139
  background: color-mix(in srgb, var(--git-ref-tone) 12%, transparent);
140
- transition: border-color 0.12s var(--ease);
140
+ transition: border-color var(--dur-fast) var(--ease);
141
141
  }
142
- .pr:hover {
143
- border-color: var(--git-ref-tone);
142
+ @media (hover: hover) and (pointer: fine) {
143
+ .pr:hover {
144
+ border-color: var(--git-ref-tone);
145
+ }
144
146
  }
145
147
  .pr:focus-visible {
146
- outline: 2px solid var(--git-ref-tone);
147
- outline-offset: 2px;
148
+ outline: var(--focus-ring-width) solid var(--git-ref-tone);
149
+ outline-offset: var(--focus-ring-offset);
148
150
  }
149
151
  .text {
150
152
  min-width: 0;
@@ -55,6 +55,10 @@
55
55
  }: Omit<HTMLAttributes<HTMLSpanElement>, keyof Own> & Own = $props();
56
56
 
57
57
  let root = $state<HTMLElement>();
58
+ // Coarse pointers cannot hit 1rem cells, so a folded stack with items opens
59
+ // its tiles on tap unless the caller chose an expand mode.
60
+ let coarse = $state(false);
61
+ const mode = $derived(expand !== 'none' ? expand : coarse ? 'tap' : 'none');
58
62
  let width = $state(Number.POSITIVE_INFINITY);
59
63
  let limit = $state(0);
60
64
  const stacked = $derived(stack === 'always' || (stack === 'auto' && width < limit));
@@ -82,7 +86,7 @@
82
86
  const CONTROL = 'button, a[href], input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])';
83
87
 
84
88
  const tiles = $derived(items?.slice(0, 4) ?? []);
85
- const expandable = $derived(expand !== 'none' && stacked && tiles.length > 0);
89
+ const expandable = $derived(mode !== 'none' && stacked && tiles.length > 0);
86
90
  let panel = $state<HTMLDivElement>();
87
91
  let open = $state(false);
88
92
  let armed = $state(-1);
@@ -151,7 +155,7 @@
151
155
  }
152
156
 
153
157
  function onPointerDown(e: PointerEvent) {
154
- if (!expandable || expand !== 'slide' || e.button !== 0) return;
158
+ if (!expandable || mode !== 'slide' || e.button !== 0) return;
155
159
  e.preventDefault();
156
160
  e.stopPropagation();
157
161
  slide.down(e, root);
@@ -177,12 +181,21 @@
177
181
  if (!expandable || e.detail === 0) return;
178
182
  e.preventDefault();
179
183
  e.stopPropagation();
180
- if (expand === 'tap') {
184
+ if (mode === 'tap') {
181
185
  if (open) hide();
182
186
  else show();
183
187
  }
184
188
  }
185
189
 
190
+ $effect(() => {
191
+ if (!browser || typeof matchMedia !== 'function') return;
192
+ const mq = matchMedia('(pointer: coarse)');
193
+ const sync = () => (coarse = mq.matches);
194
+ sync();
195
+ mq.addEventListener?.('change', sync);
196
+ return () => mq.removeEventListener?.('change', sync);
197
+ });
198
+
186
199
  $effect(() => {
187
200
  if (!expandable && open) hide();
188
201
  });
@@ -229,7 +242,7 @@
229
242
  class:stacked
230
243
  data-stack={stack}
231
244
  data-stacked={stacked || undefined}
232
- data-expand={expandable ? expand : undefined}
245
+ data-expand={expandable ? mode : undefined}
233
246
  {...rest}
234
247
  onpointerdown={onPointerDown}
235
248
  onpointermove={onPointerMove}
@@ -249,13 +262,13 @@
249
262
  {/if}
250
263
  </span>
251
264
 
252
- {#if expand !== 'none' && items}
265
+ {#if mode !== 'none' && items}
253
266
  <!-- `manual`: an `auto` popover would light-dismiss on the captured slide release. -->
254
267
  <div
255
268
  bind:this={panel}
256
269
  popover="manual"
257
270
  class="expand-panel"
258
- data-expand-panel={expand}
271
+ data-expand-panel={mode}
259
272
  role="group"
260
273
  aria-label={expandLabel}
261
274
  ontoggle={onToggle}
@@ -291,10 +304,12 @@
291
304
  display: inline-grid;
292
305
  grid-template-columns: repeat(2, 1rem);
293
306
  grid-auto-rows: 1rem;
294
- gap: 1px;
307
+ gap: var(--sp-px);
295
308
  place-items: center;
296
309
  }
310
+ /* Cells clip, so any focus ring inside draws inset. */
297
311
  .stacked > :global(*) {
312
+ --focus-ring-offset: var(--focus-ring-offset-inset);
298
313
  width: 1rem;
299
314
  height: 1rem;
300
315
  max-width: 1rem;
@@ -304,11 +319,19 @@
304
319
  justify-content: center;
305
320
  overflow: hidden;
306
321
  font-size: var(--fs-xs);
307
- line-height: 1;
322
+ line-height: var(--lh-none);
323
+ }
324
+ .stacked > :global(*) :global(:focus-visible),
325
+ .stacked > :global(:focus-visible) {
326
+ outline-offset: var(--focus-ring-offset-inset);
308
327
  }
309
328
  .glyph-stack[data-expand] {
310
329
  cursor: pointer;
311
330
  }
331
+ /* An expandable stack is one target: presses land on the stack, not on 1rem cells. */
332
+ .glyph-stack[data-expand] > .cell {
333
+ pointer-events: none;
334
+ }
312
335
  .glyph-stack[data-expand='slide'] {
313
336
  touch-action: none;
314
337
  user-select: none;
@@ -325,18 +348,24 @@
325
348
  border: 1px solid var(--border-strong);
326
349
  border-radius: var(--r-md);
327
350
  box-shadow: var(--shadow-md);
351
+ opacity: 0;
352
+ scale: var(--enter-scale);
353
+ transition:
354
+ opacity var(--dur-base) var(--ease-out),
355
+ scale var(--dur-base) var(--ease-out);
328
356
  }
329
357
  .expand-panel:popover-open {
330
358
  display: grid;
331
359
  grid-template-columns: repeat(2, minmax(var(--glyph-stack-tile, 3rem), 1fr));
332
360
  grid-auto-rows: minmax(var(--glyph-stack-tile, 3rem), 1fr);
333
361
  gap: var(--sp-1);
334
- animation: glyph-expand 0.12s var(--ease);
362
+ opacity: 1;
363
+ scale: 1;
335
364
  }
336
- @keyframes glyph-expand {
337
- from {
365
+ @starting-style {
366
+ .expand-panel:popover-open {
338
367
  opacity: 0;
339
- transform: scale(0.6);
368
+ scale: var(--enter-scale);
340
369
  }
341
370
  }
342
371
  .tile {
@@ -344,13 +373,16 @@
344
373
  align-items: center;
345
374
  justify-content: center;
346
375
  padding: var(--sp-1);
347
- border-radius: var(--r-sm);
376
+ border-radius: calc(var(--r-md) - var(--sp-1) - 1px);
348
377
  background: var(--surface);
349
- transition: background 0.08s var(--ease);
378
+ transition:
379
+ background var(--dur-fast) var(--ease),
380
+ box-shadow var(--dur-fast) var(--ease);
350
381
  }
351
382
  .tile.armed {
352
383
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
353
384
  box-shadow: inset 0 0 0 2px var(--accent);
385
+ transition-duration: var(--dur-instant);
354
386
  }
355
387
  .tile-body {
356
388
  display: inline-flex;
@@ -26,8 +26,7 @@
26
26
  tone?: Tone | 'none';
27
27
  // Outlined `--box-lg` square icon-chip (header/toolbar actions).
28
28
  chip?: boolean;
29
- // Shared square box scale (`--box-xs/sm/md/lg`); default `md` is the classic
30
- // 2.25rem box. Forwarded to Button.
29
+ // Shared square box scale (`--box-xs/sm/md/lg`). Forwarded to Button.
31
30
  box?: 'xs' | 'sm' | 'md' | 'lg';
32
31
  // SVG glyph px. Emoji text glyphs render at ×1.35 of it.
33
32
  size?: number;
@@ -63,6 +62,8 @@
63
62
  * in the accessible name. */
64
63
  count?: number;
65
64
  countMax?: number;
65
+ /** Play UI sounds when enabled globally; false silences this instance. */
66
+ sound?: boolean;
66
67
  class?: string;
67
68
  };
68
69
 
@@ -90,6 +91,7 @@
90
91
  loading = false,
91
92
  count,
92
93
  countMax,
94
+ sound = true,
93
95
  disabled = false,
94
96
  onclick,
95
97
  class: klass = '',
@@ -118,6 +120,7 @@
118
120
  {loading}
119
121
  {count}
120
122
  {countMax}
123
+ {sound}
121
124
  {title}
122
125
  {onclick}
123
126
  icon={!inline && !chip && !showLabel}
@@ -166,6 +169,6 @@
166
169
  display: inline-flex;
167
170
  align-items: center;
168
171
  justify-content: center;
169
- line-height: 1;
172
+ line-height: var(--lh-none);
170
173
  }
171
174
  </style>
@@ -40,6 +40,8 @@ type IconButtonProps = HTMLButtonAttributes & {
40
40
  * in the accessible name. */
41
41
  count?: number;
42
42
  countMax?: number;
43
+ /** Play UI sounds when enabled globally; false silences this instance. */
44
+ sound?: boolean;
43
45
  class?: string;
44
46
  };
45
47
  declare const IconButton: import("svelte").Component<IconButtonProps, {}, "">;