@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
@@ -136,15 +136,17 @@
136
136
 
137
137
  <style>
138
138
  .input {
139
+ --input-icon: var(--control-icon-md);
139
140
  width: 100%;
140
141
  min-height: var(--input-size, var(--control-height-default));
141
- padding: var(--sp-2) var(--sp-3);
142
+ padding: var(--sp-2) var(--control-px-md);
143
+ font-size: var(--control-fs-md);
142
144
  line-height: var(--lh-tight);
143
145
  background: var(--input-bg, var(--bg));
144
146
  border: 1px solid var(--input-border, var(--border-strong));
145
147
  border-radius: var(--input-radius, var(--r-md));
146
148
  color: var(--input-fg, var(--text));
147
- transition: border-color 0.12s var(--ease);
149
+ transition: border-color var(--dur-fast) ease;
148
150
  }
149
151
  .input:focus {
150
152
  outline: none;
@@ -154,21 +156,33 @@
154
156
  outline: var(--focus-ring);
155
157
  outline-offset: var(--focus-ring-offset);
156
158
  }
159
+ .input:disabled {
160
+ opacity: var(--disabled-opacity);
161
+ cursor: not-allowed;
162
+ }
157
163
  .input-sm {
158
164
  min-height: var(--input-size, var(--control-height-compact));
159
- padding: var(--sp-1) var(--sp-2);
160
- font-size: var(--fs-sm);
165
+ padding: var(--sp-1) var(--control-px-sm);
166
+ font-size: var(--control-fs-sm);
167
+ --input-icon: var(--control-icon-sm);
161
168
  }
162
169
  .input-lg {
163
170
  min-height: var(--input-size, var(--control-height-large));
164
- padding: var(--sp-3) var(--sp-4);
165
- font-size: max(16px, var(--fs-md));
171
+ padding: var(--sp-3) var(--control-px-lg);
172
+ font-size: var(--control-fs-lg);
173
+ --input-icon: var(--control-icon-lg);
166
174
  }
167
175
  /* iOS auto-zooms any field under 16px on focus; on touch the size modifiers
168
176
  carry their difference through padding and height alone. */
169
177
  @media (pointer: coarse) {
178
+ .input {
179
+ font-size: max(16px, var(--control-fs-md));
180
+ }
170
181
  .input-sm {
171
- font-size: max(16px, var(--fs-sm));
182
+ font-size: max(16px, var(--control-fs-sm));
183
+ }
184
+ .input-lg {
185
+ font-size: max(16px, var(--control-fs-lg));
172
186
  }
173
187
  .input-sm:not(.grouped) {
174
188
  min-height: max(var(--touch-target-compact), var(--input-size, var(--control-height-compact)));
@@ -192,8 +206,8 @@
192
206
  .input[aria-invalid='true']:focus {
193
207
  border-color: var(--danger);
194
208
  }
195
- /* Inside an InputGroup the group draws the focus ring and the overlaid
196
- adornments report their widths; the text stays clear of them. */
209
+ /* Inside an InputGroup the group draws the focus ring, dims the whole group
210
+ and the overlaid adornments report their widths; the text stays clear of them. */
197
211
  .input.grouped {
198
212
  width: 100%;
199
213
  border-radius: var(--ig-radius);
@@ -203,6 +217,9 @@
203
217
  .input.grouped:focus-visible {
204
218
  outline: none;
205
219
  }
220
+ .input.grouped:disabled {
221
+ opacity: 1;
222
+ }
206
223
  .mono {
207
224
  font-family: var(--font-mono);
208
225
  }
@@ -216,11 +233,18 @@
216
233
  .input-wrap .input {
217
234
  width: 100%;
218
235
  }
236
+ /* The wrap dims as one so the icon and the clear button fade with the field. */
237
+ .input-wrap:has(.input:disabled) {
238
+ opacity: var(--disabled-opacity);
239
+ }
240
+ .input-wrap .input:disabled {
241
+ opacity: 1;
242
+ }
219
243
  .has-icon {
220
- padding-inline-start: calc(var(--sp-3) + 1em + var(--sp-2));
244
+ padding-inline-start: calc(var(--control-px-md) + var(--input-icon) + var(--sp-2));
221
245
  }
222
246
  .has-clear {
223
- padding-inline-end: calc(var(--sp-3) + 1em + var(--sp-2));
247
+ padding-inline-end: calc(var(--control-px-md) + var(--input-icon) + var(--sp-2));
224
248
  }
225
249
  .input-icon,
226
250
  .input-clear {
@@ -229,13 +253,21 @@
229
253
  bottom: 0;
230
254
  display: flex;
231
255
  align-items: center;
256
+ font-size: var(--input-icon, var(--control-icon-md));
232
257
  color: var(--text-faint);
233
258
  pointer-events: none;
234
259
  }
260
+ .input-wrap:has(.input-sm) {
261
+ --input-icon: var(--control-icon-sm);
262
+ }
263
+ .input-wrap:has(.input-lg) {
264
+ --input-icon: var(--control-icon-lg);
265
+ }
235
266
  .input-icon {
236
- left: var(--sp-3);
267
+ left: var(--control-px-md);
237
268
  }
238
269
  .input-clear {
270
+ --clear-scale-dur: var(--dur-fast);
239
271
  right: var(--sp-2);
240
272
  pointer-events: auto;
241
273
  border: 0;
@@ -243,9 +275,31 @@
243
275
  padding: 0 var(--sp-1);
244
276
  cursor: pointer;
245
277
  border-radius: var(--r-sm);
278
+ transition:
279
+ color var(--dur-fast) ease,
280
+ opacity var(--dur-fast) ease,
281
+ scale var(--clear-scale-dur) var(--ease-out);
282
+ }
283
+ @starting-style {
284
+ .input-clear {
285
+ opacity: 0;
286
+ }
246
287
  }
247
- .input-clear:hover,
248
288
  .input-clear:focus-visible {
249
289
  color: var(--text);
290
+ outline: var(--focus-ring);
291
+ outline-offset: var(--focus-ring-offset-inset);
292
+ }
293
+ .input-clear:active {
294
+ --clear-scale-dur: var(--dur-press);
295
+ scale: var(--press-scale-sm);
296
+ }
297
+ @media (hover: hover) and (pointer: fine) {
298
+ .input:hover:not(:disabled, :focus, [aria-invalid='true']) {
299
+ border-color: var(--text-faint);
300
+ }
301
+ .input-clear:hover {
302
+ color: var(--text);
303
+ }
250
304
  }
251
305
  </style>
@@ -95,7 +95,7 @@
95
95
  .kbd-group {
96
96
  display: inline-flex;
97
97
  align-items: center;
98
- gap: 2px;
98
+ gap: var(--sp-0-5);
99
99
  font-size: var(--fs-xs);
100
100
  color: var(--text-muted);
101
101
  vertical-align: baseline;
@@ -106,7 +106,7 @@
106
106
  .kbd {
107
107
  font-family: var(--font-mono);
108
108
  font-size: inherit;
109
- line-height: 1.4;
109
+ line-height: var(--lh-snug);
110
110
  padding: 0 var(--sp-1);
111
111
  border: 1px solid var(--border-strong);
112
112
  border-bottom-width: 2px;
@@ -124,7 +124,7 @@
124
124
  white-space: nowrap;
125
125
  }
126
126
  .kbd-sep {
127
- font-size: 0.85em;
127
+ font-size: var(--fs-2xs);
128
128
  color: var(--text-faint);
129
129
  }
130
130
  </style>
@@ -56,6 +56,9 @@
56
56
  text-decoration: underline;
57
57
  font: inherit;
58
58
  text-align: start;
59
+ transition:
60
+ color var(--dur-fast) ease,
61
+ text-decoration-color var(--dur-fast) ease;
59
62
  }
60
63
  .tone-info {
61
64
  color: var(--info);
@@ -70,8 +73,10 @@
70
73
  .underline-none {
71
74
  text-decoration: none;
72
75
  }
73
- .underline-hover:hover {
74
- text-decoration: underline;
76
+ @media (hover: hover) and (pointer: fine) {
77
+ .underline-hover:hover {
78
+ text-decoration: underline;
79
+ }
75
80
  }
76
81
  .align-center {
77
82
  text-align: center;
@@ -95,7 +95,7 @@
95
95
  aria-valuemax={indeterminate ? undefined : max}
96
96
  aria-valuenow={indeterminate ? undefined : value}
97
97
  >
98
- <div class="bar" style={indeterminate ? undefined : `width: ${pct}%`}></div>
98
+ <div class="bar" style={indeterminate ? undefined : `--p: ${pct / 100}`}></div>
99
99
  {#each markers as m, i (`${i}:${m.at}`)}
100
100
  <span
101
101
  class="marker tone-{m.tone ? canonicalTone(m.tone) : 'text'}"
@@ -154,10 +154,12 @@
154
154
  height: 0.3125rem;
155
155
  }
156
156
  .bar {
157
+ width: 100%;
157
158
  height: 100%;
158
159
  background: var(--fill, var(--accent));
159
160
  border-radius: inherit;
160
- transition: width 0.2s var(--ease);
161
+ translate: calc((var(--p, 1) - 1) * 100%) 0;
162
+ transition: translate var(--dur-base) var(--ease-out);
161
163
  }
162
164
  .marker {
163
165
  position: absolute;
@@ -88,16 +88,11 @@
88
88
  border: 0;
89
89
  background: color-mix(in srgb, var(--bg) 45%, transparent);
90
90
  cursor: default;
91
- animation: scrim-fade 0.18s var(--ease);
91
+ transition: opacity var(--dur-base) var(--ease-out);
92
92
  }
93
- @keyframes scrim-fade {
94
- from {
95
- opacity: 0;
96
- }
97
- }
98
- @media (prefers-reduced-motion: reduce) {
93
+ @starting-style {
99
94
  .scrim {
100
- animation: none;
95
+ opacity: 0;
101
96
  }
102
97
  }
103
98
  </style>
@@ -103,7 +103,7 @@
103
103
  >
104
104
  {#each segments as seg, i (i)}
105
105
  <div class="segment tone-{seg.tone ?? 'accent'}" style="flex-grow: {grow(seg)}" title={seg.label}>
106
- <div class="bar" style="width: {pct(seg)}%"></div>
106
+ <div class="bar" style="--p: {pct(seg) / 100}"></div>
107
107
  </div>
108
108
  {/each}
109
109
  {#if remainder > 0}
@@ -124,7 +124,7 @@
124
124
  >
125
125
  {#each segments as seg, i (i)}
126
126
  <div class="segment tone-{seg.tone ?? 'accent'}" style="flex-grow: {grow(seg)}" title={seg.label}>
127
- <div class="bar" style="width: {pct(seg)}%"></div>
127
+ <div class="bar" style="--p: {pct(seg) / 100}"></div>
128
128
  </div>
129
129
  {/each}
130
130
  </div>
@@ -180,9 +180,11 @@
180
180
  border-bottom-right-radius: var(--r-pill);
181
181
  }
182
182
  .bar {
183
+ width: 100%;
183
184
  height: 100%;
184
185
  background: var(--fill, var(--accent));
185
- transition: width 0.2s var(--ease);
186
+ translate: calc((var(--p, 1) - 1) * 100%) 0;
187
+ transition: translate var(--dur-base) var(--ease-out);
186
188
  }
187
189
  .tone-success,
188
190
  .tone-ok {
@@ -210,7 +212,6 @@
210
212
  min-width: 0;
211
213
  border-radius: 0;
212
214
  background: transparent;
213
- transition: flex-grow 0.2s var(--ease);
214
215
  }
215
216
 
216
217
  .segmented-progress-wrap {
@@ -171,7 +171,7 @@
171
171
  {#if hasFace}
172
172
  <span class="select-face" class:compact={small} aria-hidden="true">
173
173
  {#if selected?.icon}
174
- <Icon name={selected.icon} size={small ? 14 : 16} />
174
+ <Icon name={selected.icon} />
175
175
  {:else if selected?.emoji}
176
176
  <span class="select-emoji">{selected.emoji}</span>
177
177
  {/if}
@@ -183,7 +183,7 @@
183
183
  {/if}
184
184
  {#if showChevron}
185
185
  <span class="select-chevron" aria-hidden="true">
186
- <Icon name="chevron-down" size={16} />
186
+ <Icon name="chevron-down" />
187
187
  </span>
188
188
  {/if}
189
189
  </div>
@@ -191,6 +191,7 @@
191
191
 
192
192
  <style>
193
193
  .select-wrap {
194
+ --select-icon: var(--control-icon-md);
194
195
  position: relative;
195
196
  display: block;
196
197
  width: 100%;
@@ -203,39 +204,46 @@
203
204
  flex: 1 1 0;
204
205
  min-width: 0;
205
206
  }
207
+ .select-wrap:has(.select.compact) {
208
+ --select-icon: var(--control-icon-sm);
209
+ }
210
+ .select-wrap:has(.select.select-lg) {
211
+ --select-icon: var(--control-icon-lg);
212
+ }
206
213
  .select {
207
214
  width: 100%;
208
215
  min-height: var(--select-size, var(--control-height-default));
209
- padding: var(--sp-2) var(--sp-3);
216
+ padding: var(--sp-2) var(--control-px-md);
217
+ font-size: var(--control-fs-md);
210
218
  line-height: var(--lh-tight);
211
219
  background: var(--select-bg, var(--bg));
212
220
  border: 1px solid var(--select-border, var(--border-strong));
213
221
  border-radius: var(--select-radius, var(--r-md));
214
222
  color: var(--select-fg, var(--text));
215
- transition: border-color 0.12s var(--ease);
223
+ transition: border-color var(--dur-fast) ease;
216
224
  }
217
225
  /* Default variant: drop the OS chevron, reserve room for our own. */
218
226
  .select-wrap .select {
219
227
  appearance: none;
220
228
  -webkit-appearance: none;
221
- padding-right: calc(var(--sp-3) + 1.25rem);
229
+ padding-right: calc(var(--control-px-md) + var(--select-icon) + var(--sp-1));
222
230
  }
223
231
  /* No custom chevron: reclaim the reserved right padding. */
224
232
  .select-wrap.no-chevron .select {
225
- padding-right: var(--sp-3);
233
+ padding-right: var(--control-px-md);
226
234
  }
227
- /* Compact inline form for dense headers/toolbars. */
228
235
  .select.select-lg {
229
236
  min-height: var(--select-size, var(--control-height-large));
230
- padding: var(--sp-3) var(--sp-4);
231
- font-size: max(16px, var(--fs-md));
237
+ padding: var(--sp-3) var(--control-px-lg);
238
+ font-size: var(--control-fs-lg);
232
239
  }
240
+ /* Compact inline form for dense headers/toolbars. */
233
241
  .select.compact {
234
- padding: var(--sp-1) var(--sp-2);
235
- font-size: var(--fs-xs);
242
+ padding: var(--sp-1) var(--control-px-sm);
243
+ font-size: var(--control-fs-sm);
236
244
  }
237
245
  .select-wrap:not(.no-chevron) .select.compact {
238
- padding-right: calc(var(--sp-2) + 1rem);
246
+ padding-right: calc(var(--control-px-sm) + var(--select-icon) + var(--sp-1));
239
247
  }
240
248
  /* Toolbar contract: size="sm" shares the compact control height so it lines up
241
249
  with Button size="sm", Popover size="sm" and SegmentedControl size="sm". */
@@ -263,6 +271,13 @@
263
271
  .select[aria-invalid='true']:focus {
264
272
  border-color: var(--danger);
265
273
  }
274
+ .select:disabled {
275
+ cursor: not-allowed;
276
+ }
277
+ /* The wrap dims as one so the face and the chevron fade with the control. */
278
+ .select-wrap:has(.select:disabled) {
279
+ opacity: var(--disabled-opacity);
280
+ }
266
281
  /* The face draws the trigger text; the native text underneath stays laid out
267
282
  (so `width="auto"` still sizes to it) but invisible. Popup options keep
268
283
  their own colour so the transparency does not inherit into the list. */
@@ -280,38 +295,45 @@
280
295
  display: flex;
281
296
  align-items: center;
282
297
  gap: var(--sp-2);
283
- padding: 0 var(--sp-3);
284
- padding-right: calc(var(--sp-3) + 1.25rem);
298
+ padding: 0 var(--control-px-md);
299
+ padding-right: calc(var(--control-px-md) + var(--select-icon) + var(--sp-1));
300
+ font-size: var(--control-fs-md);
285
301
  color: var(--select-fg, var(--text));
286
302
  pointer-events: none;
287
303
  overflow: hidden;
288
304
  }
305
+ .select-face > :global(svg) {
306
+ flex: none;
307
+ font-size: var(--select-icon);
308
+ }
289
309
  .select-wrap.no-chevron .select-face {
290
- padding-right: var(--sp-3);
310
+ padding-right: var(--control-px-md);
291
311
  }
292
312
  .select-face.compact {
293
- padding: 0 var(--sp-2);
294
- padding-right: calc(var(--sp-2) + 1rem);
295
- font-size: var(--fs-xs);
313
+ padding: 0 var(--control-px-sm);
314
+ padding-right: calc(var(--control-px-sm) + var(--select-icon) + var(--sp-1));
315
+ font-size: var(--control-fs-sm);
296
316
  }
297
317
  .select-wrap.no-chevron .select-face.compact {
298
- padding-right: var(--sp-2);
318
+ padding-right: var(--control-px-sm);
299
319
  }
300
320
  /* iOS auto-zooms any field under 16px on focus; on touch the size modifiers
301
321
  carry their difference through padding and height alone. */
302
322
  @media (pointer: coarse) {
303
- .select.compact {
304
- font-size: max(16px, var(--fs-xs));
323
+ .select,
324
+ .select-face {
325
+ font-size: max(16px, var(--control-fs-md));
305
326
  }
327
+ .select.select-lg {
328
+ font-size: max(16px, var(--control-fs-lg));
329
+ }
330
+ .select.compact,
306
331
  .select-face.compact {
307
- font-size: max(16px, var(--fs-xs));
332
+ font-size: max(16px, var(--control-fs-sm));
308
333
  }
309
334
  }
310
- .select:disabled ~ .select-face {
311
- color: var(--text-muted);
312
- }
313
335
  .select-emoji {
314
- line-height: 1;
336
+ line-height: var(--lh-none);
315
337
  }
316
338
  .select-label {
317
339
  flex: 1 1 auto;
@@ -330,12 +352,21 @@
330
352
  .select-chevron {
331
353
  position: absolute;
332
354
  top: 50%;
333
- right: var(--sp-3);
355
+ right: var(--control-px-md);
334
356
  transform: translateY(-50%);
335
357
  display: inline-flex;
358
+ font-size: var(--select-icon);
336
359
  color: var(--text-muted);
337
360
  pointer-events: none; /* clicks fall through to the select */
338
361
  }
362
+ .select-wrap:has(.select.compact) .select-chevron {
363
+ right: var(--control-px-sm);
364
+ }
365
+ @media (hover: hover) and (pointer: fine) {
366
+ .select:hover:not(:disabled, :focus, [aria-invalid='true'], .ghost, .embedded) {
367
+ border-color: var(--text-faint);
368
+ }
369
+ }
339
370
  /* Transparent overlay: fills the positioned parent, the trigger shows through. */
340
371
  .select.ghost {
341
372
  position: absolute;
@@ -7,6 +7,7 @@
7
7
  // Recolor per-instance via `--slider-accent` (defaults to the theme accent).
8
8
  import type { HTMLInputAttributes } from 'svelte/elements';
9
9
  import { getFieldContext, warnUnlabelled } from '../../field-context';
10
+ import { playSound } from '../../sound';
10
11
 
11
12
  let {
12
13
  value = $bindable(0),
@@ -23,6 +24,11 @@
23
24
  ticks = false,
24
25
  marks = [],
25
26
  width,
27
+ sound = true,
28
+ oninput,
29
+ onpointerdown,
30
+ onpointerup,
31
+ onpointercancel,
26
32
  class: klass = '',
27
33
  el = $bindable(null),
28
34
  ...rest
@@ -43,6 +49,8 @@
43
49
  marks?: { value: number; label: string }[];
44
50
  /** Fixed width (`flex: none`). */
45
51
  width?: string;
52
+ /** Play UI sounds when enabled globally; false silences this instance. */
53
+ sound?: boolean;
46
54
  id?: string;
47
55
  'aria-describedby'?: string | null;
48
56
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
@@ -60,6 +68,32 @@
60
68
 
61
69
  $effect(() => warnUnlabelled(el, 'Slider'));
62
70
 
71
+ let dragging = false;
72
+ const discrete = $derived((+max - +min) / (+step || 1) <= 20);
73
+ function tick() {
74
+ if (!dragging || discrete) playSound('tick', sound);
75
+ }
76
+ function handleInput(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
77
+ tick();
78
+ oninput?.(e);
79
+ }
80
+ function handlePointerDown(e: PointerEvent & { currentTarget: EventTarget & HTMLInputElement }) {
81
+ dragging = true;
82
+ onpointerdown?.(e);
83
+ }
84
+ function handlePointerUp(e: PointerEvent & { currentTarget: EventTarget & HTMLInputElement }) {
85
+ dragging = false;
86
+ onpointerup?.(e);
87
+ }
88
+ function handlePointerCancel(e: PointerEvent & { currentTarget: EventTarget & HTMLInputElement }) {
89
+ dragging = false;
90
+ onpointercancel?.(e);
91
+ }
92
+ function pick(v: number) {
93
+ value = v;
94
+ playSound('tick', sound);
95
+ }
96
+
63
97
  const frac = (v: number) => Math.max(0, Math.min(1, (v - +min) / (+max - +min || 1)));
64
98
  const pct = $derived(frac(Number(value)) * 100);
65
99
  const tickValues = $derived.by(() => {
@@ -84,6 +118,10 @@
84
118
  aria-label={label}
85
119
  aria-valuetext={valueText}
86
120
  {...rest}
121
+ oninput={handleInput}
122
+ onpointerdown={handlePointerDown}
123
+ onpointerup={handlePointerUp}
124
+ onpointercancel={handlePointerCancel}
87
125
  aria-describedby={ariaDescribedby ?? field?.describedBy}
88
126
  aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
89
127
  />
@@ -101,7 +139,7 @@
101
139
  {#if marks.length > 0}
102
140
  <span class="marks" aria-hidden="true">
103
141
  {#each marks as m, i (`${i}:${m.value}`)}
104
- <button type="button" class="mark" class:on={Number(value) === m.value} style="--f: {frac(m.value)}" tabindex="-1" onclick={() => (value = m.value)}>{m.label}</button>
142
+ <button type="button" class="mark" class:on={Number(value) === m.value} style="--f: {frac(m.value)}" tabindex="-1" onclick={() => pick(m.value)}>{m.label}</button>
105
143
  {/each}
106
144
  </span>
107
145
  {/if}
@@ -138,6 +176,9 @@
138
176
  color: var(--text);
139
177
  }
140
178
  .slider {
179
+ --thumb: 1.2rem;
180
+ --track: 0.35rem;
181
+ --halo: 0 0 0 4px color-mix(in srgb, var(--slider-accent, var(--accent)) 20%, transparent);
141
182
  display: flex;
142
183
  align-items: center;
143
184
  gap: var(--sp-3);
@@ -164,7 +205,7 @@
164
205
  }
165
206
  /* Track — WebKit/Blink. Filled up to --pct with the accent, then the rail. */
166
207
  input[type='range']::-webkit-slider-runnable-track {
167
- height: 0.35rem;
208
+ height: var(--track);
168
209
  border-radius: var(--r-pill);
169
210
  background: linear-gradient(
170
211
  to right,
@@ -173,12 +214,12 @@
173
214
  );
174
215
  }
175
216
  input[type='range']::-moz-range-track {
176
- height: 0.35rem;
217
+ height: var(--track);
177
218
  border-radius: var(--r-pill);
178
219
  background: var(--bg-elevated-2);
179
220
  }
180
221
  input[type='range']::-moz-range-progress {
181
- height: 0.35rem;
222
+ height: var(--track);
182
223
  border-radius: var(--r-pill);
183
224
  background: var(--slider-accent, var(--accent));
184
225
  }
@@ -186,36 +227,52 @@
186
227
  input[type='range']::-webkit-slider-thumb {
187
228
  appearance: none;
188
229
  -webkit-appearance: none;
189
- margin-top: -0.425rem; /* center on the 0.35rem track */
190
- width: 1.2rem;
191
- height: 1.2rem;
192
- border-radius: 50%;
230
+ margin-top: calc((var(--track) - var(--thumb)) / 2);
231
+ width: var(--thumb);
232
+ height: var(--thumb);
233
+ border-radius: var(--r-pill);
193
234
  background: var(--slider-accent, var(--accent));
194
235
  border: 2px solid var(--bg);
195
236
  box-shadow: var(--shadow-sm);
196
- transition: transform 0.1s var(--ease);
237
+ transition:
238
+ scale var(--dur-press) var(--ease-out),
239
+ box-shadow var(--dur-fast) var(--ease);
197
240
  }
198
241
  input[type='range']::-moz-range-thumb {
199
- width: 1.2rem;
200
- height: 1.2rem;
201
- border-radius: 50%;
242
+ width: var(--thumb);
243
+ height: var(--thumb);
244
+ border-radius: var(--r-pill);
202
245
  background: var(--slider-accent, var(--accent));
203
246
  border: 2px solid var(--bg);
204
247
  box-shadow: var(--shadow-sm);
248
+ transition:
249
+ scale var(--dur-press) var(--ease-out),
250
+ box-shadow var(--dur-fast) var(--ease);
205
251
  }
206
- input[type='range']:active::-webkit-slider-thumb {
207
- transform: scale(1.12);
252
+ @media (hover: hover) and (pointer: fine) {
253
+ input[type='range']:not(:disabled):hover::-webkit-slider-thumb {
254
+ box-shadow: var(--shadow-sm), var(--halo);
255
+ }
256
+ input[type='range']:not(:disabled):hover::-moz-range-thumb {
257
+ box-shadow: var(--shadow-sm), var(--halo);
258
+ }
259
+ }
260
+ input[type='range']:not(:disabled):active::-webkit-slider-thumb {
261
+ scale: 1.15;
262
+ }
263
+ input[type='range']:not(:disabled):active::-moz-range-thumb {
264
+ scale: 1.15;
208
265
  }
209
266
  input[type='range']:focus-visible {
210
267
  outline: none;
211
268
  }
212
269
  input[type='range']:focus-visible::-webkit-slider-thumb {
213
270
  outline: var(--focus-ring);
214
- outline-offset: 2px;
271
+ outline-offset: var(--focus-ring-offset);
215
272
  }
216
273
  input[type='range']:focus-visible::-moz-range-thumb {
217
274
  outline: var(--focus-ring);
218
- outline-offset: 2px;
275
+ outline-offset: var(--focus-ring-offset);
219
276
  }
220
277
  @media (pointer: coarse) {
221
278
  input[type='range'] {
@@ -223,7 +280,7 @@
223
280
  }
224
281
  }
225
282
  input[type='range']:disabled {
226
- opacity: 0.45;
283
+ opacity: var(--disabled-opacity);
227
284
  cursor: not-allowed;
228
285
  }
229
286
  .ticks {
@@ -235,11 +292,11 @@
235
292
  .tick {
236
293
  position: absolute;
237
294
  top: 50%;
238
- left: calc(0.6rem + (100% - 1.2rem) * var(--f));
239
- width: 0.35rem;
240
- height: 0.35rem;
241
- margin: -0.175rem 0 0 -0.175rem;
242
- border-radius: 50%;
295
+ left: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--f));
296
+ width: var(--track);
297
+ height: var(--track);
298
+ margin: calc(var(--track) / -2) 0 0 calc(var(--track) / -2);
299
+ border-radius: var(--r-pill);
243
300
  background: var(--border-strong);
244
301
  }
245
302
  .tick.reached {
@@ -19,6 +19,8 @@ type $$ComponentProps = HTMLInputAttributes & {
19
19
  }[];
20
20
  /** Fixed width (`flex: none`). */
21
21
  width?: string;
22
+ /** Play UI sounds when enabled globally; false silences this instance. */
23
+ sound?: boolean;
22
24
  id?: string;
23
25
  'aria-describedby'?: string | null;
24
26
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];