@dorsk/tsumikit 0.78.0 → 0.80.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 (90) hide show
  1. package/dist/components/atoms/Artwork.svelte +5 -3
  2. package/dist/components/atoms/Badge.svelte +2 -5
  3. package/dist/components/atoms/Button.svelte +327 -17
  4. package/dist/components/atoms/Button.svelte.d.ts +4 -1
  5. package/dist/components/atoms/Card.svelte +160 -12
  6. package/dist/components/atoms/Checkbox.svelte +181 -3
  7. package/dist/components/atoms/Gauge.svelte +2 -2
  8. package/dist/components/atoms/Input.svelte +41 -3
  9. package/dist/components/atoms/Kbd.svelte +1 -1
  10. package/dist/components/atoms/Link.svelte +61 -0
  11. package/dist/components/atoms/Progress.svelte +75 -2
  12. package/dist/components/atoms/Progress.svelte.d.ts +1 -0
  13. package/dist/components/atoms/SegmentedProgress.svelte +2 -2
  14. package/dist/components/atoms/Select.svelte +39 -3
  15. package/dist/components/atoms/Skeleton.svelte +54 -3
  16. package/dist/components/atoms/Skeleton.svelte.d.ts +1 -0
  17. package/dist/components/atoms/Slider.svelte +8 -6
  18. package/dist/components/atoms/Spinner.svelte +152 -3
  19. package/dist/components/atoms/Spinner.svelte.d.ts +1 -1
  20. package/dist/components/atoms/Swatch.svelte +1 -1
  21. package/dist/components/atoms/Switch.svelte +138 -2
  22. package/dist/components/atoms/Textarea.svelte +42 -3
  23. package/dist/components/atoms/Thinking.svelte +130 -0
  24. package/dist/components/atoms/Thinking.svelte.d.ts +15 -0
  25. package/dist/components/layouts/AppShell.svelte +6 -6
  26. package/dist/components/layouts/MasterDetail.svelte +3 -3
  27. package/dist/components/layouts/NavBar.svelte +4 -4
  28. package/dist/components/layouts/NavSection.svelte +1 -1
  29. package/dist/components/layouts/ResizablePanel.svelte +7 -7
  30. package/dist/components/layouts/TabBar.svelte +2 -2
  31. package/dist/components/molecules/Accordion.svelte +2 -2
  32. package/dist/components/molecules/AttachmentList.svelte +2 -2
  33. package/dist/components/molecules/CapBar.svelte +2 -2
  34. package/dist/components/molecules/CodeBlock.svelte +5 -5
  35. package/dist/components/molecules/Combobox.svelte +3 -3
  36. package/dist/components/molecules/CopyButton.svelte +146 -5
  37. package/dist/components/molecules/Drawer.svelte +8 -8
  38. package/dist/components/molecules/EmojiPicker.svelte +1 -1
  39. package/dist/components/molecules/Field.svelte +55 -1
  40. package/dist/components/molecules/FileButton.svelte +20 -6
  41. package/dist/components/molecules/FilterInput.svelte +4 -4
  42. package/dist/components/molecules/GitRef.svelte +2 -2
  43. package/dist/components/molecules/GlyphStack.svelte +21 -3
  44. package/dist/components/molecules/InputGroup.svelte +3 -3
  45. package/dist/components/molecules/LookPicker.svelte +87 -0
  46. package/dist/components/molecules/LookPicker.svelte.d.ts +17 -0
  47. package/dist/components/molecules/Modal.svelte +5 -5
  48. package/dist/components/molecules/MotionSettings.svelte +166 -0
  49. package/dist/components/molecules/MotionSettings.svelte.d.ts +18 -0
  50. package/dist/components/molecules/OptionButton.svelte +8 -3
  51. package/dist/components/molecules/Pagination.svelte +3 -1
  52. package/dist/components/molecules/Popover.svelte +18 -11
  53. package/dist/components/molecules/Prose.svelte +6 -6
  54. package/dist/components/molecules/RadialMenu.svelte +3 -3
  55. package/dist/components/molecules/RadioGroup.svelte +67 -4
  56. package/dist/components/molecules/RichSelect.svelte +6 -4
  57. package/dist/components/molecules/SectionHeader.svelte +1 -1
  58. package/dist/components/molecules/SeekSlider.svelte +6 -6
  59. package/dist/components/molecules/SegmentedControl.svelte +7 -7
  60. package/dist/components/molecules/SortableList.svelte +2 -2
  61. package/dist/components/molecules/SplitButton.svelte +1 -1
  62. package/dist/components/molecules/Tabs.svelte +160 -7
  63. package/dist/components/molecules/ThemePicker.svelte +1 -1
  64. package/dist/components/molecules/Timestamp.svelte +1 -1
  65. package/dist/components/molecules/Toggle.svelte +5 -3
  66. package/dist/components/molecules/Tooltip.svelte +2 -2
  67. package/dist/components/molecules/VolumeSlider.svelte +3 -3
  68. package/dist/components/organisms/ConversationFeed.svelte +1 -1
  69. package/dist/components/organisms/ConversationPanel.svelte +3 -3
  70. package/dist/components/organisms/DataTable.svelte +18 -16
  71. package/dist/fx.d.ts +149 -0
  72. package/dist/fx.js +111 -0
  73. package/dist/fx.md +58 -0
  74. package/dist/index.d.ts +8 -2
  75. package/dist/index.js +8 -2
  76. package/dist/looks/README.md +101 -0
  77. package/dist/looks/depth.css +939 -0
  78. package/dist/looks/editorial.css +545 -0
  79. package/dist/looks/tactile.css +871 -0
  80. package/dist/pointer.d.ts +3 -0
  81. package/dist/pointer.js +40 -0
  82. package/dist/stores/fx.svelte.d.ts +17 -0
  83. package/dist/stores/fx.svelte.js +64 -0
  84. package/dist/stores/theme.svelte.d.ts +8 -2
  85. package/dist/stores/theme.svelte.js +33 -7
  86. package/dist/styles/themes.css +249 -249
  87. package/dist/styles/tokens.css +37 -2
  88. package/dist/theme-mode.d.ts +31 -0
  89. package/dist/theme-mode.js +19 -0
  90. package/package.json +3 -2
@@ -281,11 +281,11 @@
281
281
  max-height: var(--cb-max-height, min(16rem, 40vh));
282
282
  overflow-y: auto;
283
283
  padding: var(--sp-1);
284
- background: var(--cb-bg, var(--bg-elevated));
284
+ background: var(--cb-bg, var(--raised-fill));
285
285
  color: var(--text);
286
- border: 1px solid var(--cb-border, var(--border-strong));
286
+ border: var(--edge-width) solid var(--cb-border, var(--raised-edge));
287
287
  border-radius: var(--cb-radius, var(--r-md));
288
- box-shadow: var(--shadow-md);
288
+ box-shadow: var(--raised-shadow);
289
289
  transform-origin: var(--tsu-origin, top left);
290
290
  opacity: 1;
291
291
  scale: 1;
@@ -39,6 +39,7 @@
39
39
 
40
40
  const labelShown = $derived(showLabel ?? box === undefined);
41
41
  let copied = $state(false);
42
+ let run = $state(0);
42
43
  let status = $state('');
43
44
  let timer: ReturnType<typeof setTimeout> | undefined;
44
45
 
@@ -46,6 +47,7 @@
46
47
  const ok = await copyToClipboard(text);
47
48
  playSound(ok ? 'success' : 'error', sound);
48
49
  copied = ok;
50
+ if (ok) run++;
49
51
  status = ok ? copiedLabel : 'Copy failed';
50
52
  clearTimeout(timer);
51
53
  timer = setTimeout(() => {
@@ -63,26 +65,33 @@
63
65
  class="copy-btn {labelShown && box !== undefined ? 'copy-labelled' : ''} {klass}"
64
66
  sound={false}
65
67
  onclick={copy}
68
+ data-copied={copied || undefined}
66
69
  aria-label={copied ? copiedLabel : label}
67
70
  title={label}
68
71
  >
69
- <span class="glyphs" class:copied aria-hidden="true">
72
+ <span class="glyphs" class:copied class:tinted={variant === 'ghost' || variant === 'default'} aria-hidden="true">
70
73
  <span class="glyph copy"><Icon name="copy" /></span>
71
74
  <span class="glyph check"><Icon name="check" /></span>
75
+ <svg class="glyph tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round">
76
+ <path d="M4.5 12.5l5 5L19.5 7" pathLength="1" />
77
+ </svg>
78
+ {#key run}<span class="sparks"></span>{/key}
72
79
  </span>
73
- {#if labelShown}<span>{copied ? copiedLabel : label}</span>{/if}
80
+ {#if labelShown}
81
+ <span class="words" class:copied aria-hidden="true"><span class="word idle">{label}</span><span class="word done">{copiedLabel}</span></span>
82
+ {/if}
74
83
  </Button>
75
84
  <span class="sr-only" role="status" aria-live="polite">{status}</span>
76
85
 
77
86
  <style>
78
- /* Both glyphs share one grid cell so the swap never changes the width. The
79
- check springs in (the kit's one bounce: a rare confirmation); the copy
80
- glyph fades out fast and back in over the base duration on revert. */
87
+ /* Both glyphs share one grid cell and both words another, so the swap never
88
+ changes the width. */
81
89
  :global(.copy-btn.btn-box.copy-labelled) {
82
90
  width: auto;
83
91
  padding-inline: var(--sp-2);
84
92
  }
85
93
  .glyphs {
94
+ position: relative;
86
95
  display: inline-grid;
87
96
  place-items: center;
88
97
  }
@@ -111,4 +120,136 @@
111
120
  opacity var(--dur-spring-bounce) ease,
112
121
  scale var(--dur-spring-bounce) var(--ease-spring-bounce);
113
122
  }
123
+ .tick,
124
+ .sparks {
125
+ display: none;
126
+ }
127
+ .words {
128
+ display: inline-grid;
129
+ overflow: hidden;
130
+ text-align: start;
131
+ }
132
+ .word {
133
+ grid-area: 1 / 1;
134
+ }
135
+ .done,
136
+ .copied .idle {
137
+ opacity: 0;
138
+ }
139
+ .copied .done {
140
+ opacity: 1;
141
+ }
142
+
143
+ /* Morph burst: the check springs in on a turn inside a ring of eight sparks. */
144
+ .glyphs:not(:global([data-fx-copy]:not([data-fx-copy='burst'])) *),
145
+ :global([data-fx-copy='burst']) .glyphs:not(:global([data-fx-copy='burst'] [data-fx-copy]:not([data-fx-copy='burst'])) *) {
146
+ & .check {
147
+ scale: 0.4;
148
+ rotate: 60deg;
149
+ transition-property: opacity, scale, rotate;
150
+ transition-duration: var(--dur-exit-fast);
151
+ transition-timing-function: ease;
152
+ }
153
+ &.copied .check {
154
+ scale: 1;
155
+ rotate: 0deg;
156
+ transition-duration: var(--dur-fast), var(--dur-spring), var(--dur-spring);
157
+ transition-delay: calc(var(--dur-exit-fast) / 2);
158
+ transition-timing-function: ease, var(--ease-spring-bounce), var(--ease-out);
159
+ }
160
+ & .sparks {
161
+ --spark-a: color-mix(in srgb, var(--ok) 85%, transparent);
162
+ --spark-b: color-mix(in srgb, var(--accent) 85%, transparent);
163
+ position: absolute;
164
+ inset: -9px;
165
+ display: block;
166
+ pointer-events: none;
167
+ background:
168
+ radial-gradient(circle at 50% 4%, var(--spark-a) 1.6px, transparent 2.2px),
169
+ radial-gradient(circle at 83% 17%, var(--spark-b) 1.3px, transparent 1.9px),
170
+ radial-gradient(circle at 96% 50%, var(--spark-a) 1.6px, transparent 2.2px),
171
+ radial-gradient(circle at 83% 83%, var(--spark-b) 1.3px, transparent 1.9px),
172
+ radial-gradient(circle at 50% 96%, var(--spark-a) 1.6px, transparent 2.2px),
173
+ radial-gradient(circle at 17% 83%, var(--spark-b) 1.3px, transparent 1.9px),
174
+ radial-gradient(circle at 4% 50%, var(--spark-a) 1.6px, transparent 2.2px),
175
+ radial-gradient(circle at 17% 17%, var(--spark-b) 1.3px, transparent 1.9px);
176
+ opacity: 0;
177
+ }
178
+ &.copied .sparks {
179
+ animation: copy-burst calc(var(--dur-slow) + var(--dur-exit-fast)) var(--ease-out);
180
+ }
181
+ }
182
+ @keyframes copy-burst {
183
+ 0% {
184
+ opacity: 0;
185
+ scale: 0.35;
186
+ rotate: -20deg;
187
+ }
188
+ 30% {
189
+ opacity: 1;
190
+ }
191
+ 100% {
192
+ opacity: 0;
193
+ scale: 1.15;
194
+ rotate: 0deg;
195
+ }
196
+ }
197
+
198
+ /* Tick and slide: the tick draws itself while the label rolls up into its
199
+ copied form. */
200
+ :global([data-fx-copy='tick']) .glyphs:not(:global([data-fx-copy='tick'] [data-fx-copy]:not([data-fx-copy='tick'])) *) {
201
+ & .check {
202
+ display: none;
203
+ }
204
+ & .tick {
205
+ display: block;
206
+ width: 1em;
207
+ height: 1em;
208
+ opacity: 0;
209
+ transition: opacity var(--dur-exit-fast) ease var(--dur-instant);
210
+ }
211
+ &.copied .tick {
212
+ opacity: 1;
213
+ transition-duration: var(--dur-fast);
214
+ }
215
+ & .tick path {
216
+ stroke-dasharray: 1;
217
+ stroke-dashoffset: 1;
218
+ transition: stroke-dashoffset var(--dur-instant) ease var(--dur-exit-fast);
219
+ }
220
+ &.copied .tick path {
221
+ stroke-dashoffset: 0;
222
+ transition: stroke-dashoffset var(--dur-slow) var(--ease-out) calc(var(--dur-fast) / 2);
223
+ }
224
+ & .copy {
225
+ transition-property: opacity, scale;
226
+ transition-duration: var(--dur-base), var(--dur-spring);
227
+ transition-timing-function: ease, var(--ease-spring);
228
+ }
229
+ &.copied .copy {
230
+ scale: 0.6;
231
+ transition-duration: var(--dur-exit-fast), var(--dur-exit);
232
+ }
233
+ }
234
+ :global([data-fx-copy='tick']) .words:not(:global([data-fx-copy='tick'] [data-fx-copy]:not([data-fx-copy='tick'])) *) {
235
+ & .word {
236
+ transition-property: opacity, translate;
237
+ transition-duration: var(--dur-base), var(--dur-spring);
238
+ transition-timing-function: ease, var(--ease-spring);
239
+ }
240
+ & .done {
241
+ translate: 0 100%;
242
+ }
243
+ &.copied .idle {
244
+ translate: 0 -100%;
245
+ transition-duration: var(--dur-exit-fast), var(--dur-exit);
246
+ }
247
+ &.copied .done {
248
+ translate: 0 0;
249
+ transition-delay: calc(var(--dur-exit-fast) / 2);
250
+ }
251
+ }
252
+ .tinted.copied :is(.check, .tick) {
253
+ color: var(--ok);
254
+ }
114
255
  </style>
@@ -215,7 +215,7 @@
215
215
  'foot';
216
216
  width: 100%;
217
217
  height: 100%;
218
- background: var(--bg-elevated);
218
+ background: var(--overlay-fill);
219
219
  will-change: translate;
220
220
  padding-top: var(--safe-top);
221
221
  padding-bottom: var(--safe-bottom);
@@ -242,7 +242,7 @@
242
242
  align-items: center;
243
243
  gap: var(--sp-2);
244
244
  padding: var(--sp-4);
245
- border-bottom: 1px solid var(--border);
245
+ border-bottom: var(--edge-width) solid var(--surface-edge);
246
246
  }
247
247
  .panel-icon {
248
248
  display: inline-flex;
@@ -269,7 +269,7 @@
269
269
  display: flex;
270
270
  gap: var(--sp-1);
271
271
  padding: var(--sp-2);
272
- border-bottom: 1px solid var(--border);
272
+ border-bottom: var(--edge-width) solid var(--surface-edge);
273
273
  overflow-x: auto;
274
274
  scrollbar-width: thin;
275
275
  }
@@ -290,7 +290,7 @@
290
290
  justify-content: flex-end;
291
291
  gap: var(--sp-2);
292
292
  padding: var(--sp-4);
293
- border-top: 1px solid var(--border);
293
+ border-top: var(--edge-width) solid var(--surface-edge);
294
294
  }
295
295
 
296
296
  @media (min-width: 48rem) {
@@ -298,8 +298,8 @@
298
298
  width: min(var(--drawer-w), 100%);
299
299
  padding-top: 0;
300
300
  padding-bottom: 0;
301
- border-left: 1px solid var(--border-strong);
302
- box-shadow: var(--shadow-lg);
301
+ border-left: var(--edge-width) solid var(--border-strong);
302
+ box-shadow: var(--overlay-shadow);
303
303
  grid-template-columns: minmax(0, 1fr);
304
304
  grid-template-rows: auto minmax(0, 1fr) auto;
305
305
  grid-template-areas:
@@ -309,7 +309,7 @@
309
309
  }
310
310
  .drawer[data-side='left'] .panel {
311
311
  border-left: 0;
312
- border-right: 1px solid var(--border-strong);
312
+ border-right: var(--edge-width) solid var(--border-strong);
313
313
  }
314
314
  .panel.has-nav {
315
315
  grid-template-columns: var(--drawer-nav-w) minmax(0, 1fr);
@@ -325,7 +325,7 @@
325
325
  overflow-x: hidden;
326
326
  overflow-y: auto;
327
327
  border-bottom: 0;
328
- border-right: 1px solid var(--border);
328
+ border-right: var(--edge-width) solid var(--surface-edge);
329
329
  }
330
330
  .panel-nav-mobile {
331
331
  display: none;
@@ -223,7 +223,7 @@
223
223
  align-items: center;
224
224
  justify-content: center;
225
225
  padding: var(--sp-1);
226
- border: 1px solid transparent;
226
+ border: var(--edge-width) solid transparent;
227
227
  border-radius: var(--r-sm);
228
228
  background: none;
229
229
  color: inherit;
@@ -59,7 +59,7 @@
59
59
 
60
60
  <div class="field {klass}" class:field-grow={grow} class:field-no-shrink={!shrink} class:field-inline={layout === 'inline'} data-tsu="Field">
61
61
  {#if label}
62
- <label class="label" for={forId} style:width={labelWidth}>{label}</label>
62
+ <label class="label" for={forId} style:width={labelWidth}><span class="label-text">{label}</span></label>
63
63
  {/if}
64
64
  {@render children?.()}
65
65
  {#if hint}
@@ -103,4 +103,58 @@
103
103
  font-size: var(--fs-xs);
104
104
  color: var(--danger);
105
105
  }
106
+
107
+ /* Spring label: over a md text field the label rests inside the control and
108
+ springs into its top edge on focus or once filled. */
109
+ :global([data-fx-focus='label']) .field:not(.field-inline):not(:global([data-fx-focus='label'] [data-fx-focus]:not([data-fx-focus='label'])) *):has(> .label + :global(input.input:not(.input-sm, .input-lg, .grouped)), > .label + :global(.textarea-wrap:not(.grouped) > .textarea:not(.textarea-sm, .textarea-lg))) {
110
+ --float-h: 3.25rem;
111
+ position: relative;
112
+ & > .label {
113
+ position: absolute;
114
+ inset-block-start: 0;
115
+ inset-inline-start: 0;
116
+ z-index: 1;
117
+ display: flex;
118
+ align-items: center;
119
+ max-width: 100%;
120
+ height: var(--float-h);
121
+ padding-inline: calc(var(--control-px-md) + var(--edge-width));
122
+ font-size: var(--control-fs-md);
123
+ pointer-events: none;
124
+ }
125
+ & .label-text {
126
+ overflow: hidden;
127
+ text-overflow: ellipsis;
128
+ white-space: nowrap;
129
+ line-height: var(--lh-none);
130
+ transform-origin: left center;
131
+ transition-property: translate, scale, color;
132
+ transition-duration: var(--dur-spring), var(--dur-spring), var(--dur-fast);
133
+ transition-timing-function: var(--ease-spring-bounce), var(--ease-spring-bounce), ease;
134
+ }
135
+ & :global(:is(.input, .textarea)) {
136
+ min-height: var(--float-h);
137
+ padding-top: 1.35rem;
138
+ padding-bottom: 0.4rem;
139
+ }
140
+ & :global(:is(.input, .textarea):not(:focus)::placeholder) {
141
+ color: transparent;
142
+ }
143
+ & :global(:is(.input, .textarea):focus) {
144
+ animation: none;
145
+ box-shadow:
146
+ var(--control-shadow),
147
+ inset 0 -1px 0 var(--focus-tint);
148
+ }
149
+ &:has(:global(:is(.input, .textarea):is(:focus, :not(:placeholder-shown), :autofill))) .label-text {
150
+ translate: 0 -0.7rem;
151
+ scale: 0.78;
152
+ }
153
+ &:has(:global(:is(.input, .textarea):focus)) > .label {
154
+ color: var(--accent);
155
+ }
156
+ &:has(:global([aria-invalid='true'])) > .label {
157
+ color: var(--danger);
158
+ }
159
+ }
106
160
  </style>
@@ -107,9 +107,12 @@
107
107
  gap: var(--sp-2);
108
108
  padding: var(--sp-2) var(--btn-px-md);
109
109
  min-height: var(--control-height-default);
110
- border: 1px solid var(--border-strong);
110
+ border: var(--edge-width) solid var(--btn-edge);
111
111
  border-radius: var(--r-md);
112
- background: var(--surface);
112
+ background: var(--btn-fill);
113
+ --file-shadow-rest: var(--btn-shadow);
114
+ --file-shadow-lift: var(--btn-shadow-hover);
115
+ box-shadow: var(--file-shadow-rest);
113
116
  color: var(--text);
114
117
  font-weight: var(--fw-medium);
115
118
  font-size: var(--control-fs-md);
@@ -119,16 +122,20 @@
119
122
  cursor: pointer;
120
123
  user-select: none;
121
124
  --icon-size: var(--control-icon-md);
122
- --file-scale-dur: var(--dur-fast);
125
+ --file-scale-dur: var(--dur-release);
126
+ --file-scale-ease: var(--ease-spring-bounce);
123
127
  transition:
124
128
  background var(--dur-fast) ease,
125
129
  border-color var(--dur-fast) ease,
130
+ box-shadow var(--dur-fast) ease,
126
131
  color var(--dur-fast) ease,
127
132
  opacity var(--dur-fast) ease,
128
- scale var(--file-scale-dur) var(--ease-out);
133
+ translate var(--dur-fast) var(--ease-out),
134
+ scale var(--file-scale-dur) var(--file-scale-ease);
129
135
  }
130
136
  .file-btn:active:not(.disabled) {
131
137
  --file-scale-dur: var(--dur-press);
138
+ --file-scale-ease: var(--ease-out);
132
139
  scale: var(--press-scale);
133
140
  }
134
141
  .file-btn.icon-only:active:not(.disabled),
@@ -197,20 +204,25 @@
197
204
  font-size: var(--icon-xl);
198
205
  }
199
206
  .file-btn.primary {
200
- background: var(--accent);
201
- border-color: var(--accent);
207
+ background: var(--btn-primary-fill);
208
+ border-color: var(--btn-primary-edge);
209
+ --file-shadow-rest: var(--btn-primary-shadow);
210
+ --file-shadow-lift: var(--btn-primary-shadow-hover);
202
211
  color: var(--text-on-accent);
203
212
  font-weight: var(--fw-semibold);
204
213
  }
205
214
  .file-btn.ghost {
206
215
  background: transparent;
207
216
  border-color: transparent;
217
+ --file-shadow-rest: none;
218
+ --file-shadow-lift: none;
208
219
  }
209
220
  /* The hidden input keeps focusability (sr-only, not display:none), so mirror
210
221
  its focus onto the label for a visible ring. */
211
222
  .file-btn:focus-within {
212
223
  outline: var(--focus-ring);
213
224
  outline-offset: var(--focus-ring-offset);
225
+ box-shadow: var(--file-shadow-rest), var(--focus-glow);
214
226
  }
215
227
  .file-btn.disabled {
216
228
  opacity: var(--disabled-opacity);
@@ -219,6 +231,8 @@
219
231
  @media (hover: hover) and (pointer: fine) {
220
232
  .file-btn:hover:not(.disabled) {
221
233
  border-color: var(--accent);
234
+ translate: 0 calc(-1 * var(--hover-lift));
235
+ --file-shadow-rest: var(--file-shadow-lift);
222
236
  }
223
237
  .file-btn.primary:hover:not(.disabled) {
224
238
  filter: brightness(1.08);
@@ -473,7 +473,7 @@
473
473
  padding: 0 var(--control-px-md);
474
474
  min-height: var(--control-height-default);
475
475
  background: var(--bg-elevated);
476
- border: 1px solid var(--border-strong);
476
+ border: var(--edge-width) solid var(--control-edge);
477
477
  border-radius: var(--r-md);
478
478
  transition:
479
479
  border-color var(--dur-fast) ease,
@@ -502,7 +502,7 @@
502
502
  font-size: var(--fs-xs);
503
503
  line-height: var(--lh-none);
504
504
  color: var(--text-faint);
505
- border: 1px solid var(--border);
505
+ border: var(--edge-width) solid var(--border);
506
506
  border-radius: var(--r-sm);
507
507
  padding: var(--sp-0-5) var(--sp-1);
508
508
  }
@@ -600,8 +600,8 @@
600
600
  left: 0;
601
601
  right: 0;
602
602
  z-index: var(--z-drawer);
603
- background: var(--bg-elevated);
604
- border: 1px solid var(--border-strong);
603
+ background: var(--raised-fill);
604
+ border: var(--edge-width) solid var(--raised-edge);
605
605
  border-radius: var(--r-md);
606
606
  box-shadow: var(--shadow-lg);
607
607
  padding: var(--sp-1);
@@ -125,9 +125,9 @@
125
125
  min-width: 0;
126
126
  padding: var(--sp-0-5) var(--sp-2);
127
127
  border-radius: var(--r-pill);
128
- background: var(--bg-elevated-2);
128
+ background: var(--surface-fill-2);
129
129
  color: var(--text-muted);
130
- border: 1px solid var(--border);
130
+ border: var(--edge-width) solid var(--surface-edge);
131
131
  text-decoration: none;
132
132
  }
133
133
  .branch {
@@ -314,6 +314,8 @@
314
314
  height: 1rem;
315
315
  max-width: 1rem;
316
316
  max-height: 1rem;
317
+ min-width: 0;
318
+ min-height: 0;
317
319
  display: inline-flex;
318
320
  align-items: center;
319
321
  justify-content: center;
@@ -321,6 +323,22 @@
321
323
  font-size: var(--fs-xs);
322
324
  line-height: var(--lh-none);
323
325
  }
326
+ /* Controls with a minimum box (e.g. a `box="xs"` IconButton) shrink to the cell instead of clipping. */
327
+ .stacked > .cell > :global(*) {
328
+ min-width: 0;
329
+ min-height: 0;
330
+ max-width: 1rem;
331
+ max-height: 1rem;
332
+ }
333
+ /* A labelled Dot folds to its disc; the label stays in the accessibility tree. */
334
+ .stacked :global(.dot-row > :not(.dot)) {
335
+ position: absolute;
336
+ width: 1px;
337
+ height: 1px;
338
+ overflow: hidden;
339
+ clip-path: inset(50%);
340
+ white-space: nowrap;
341
+ }
324
342
  .stacked > :global(*) :global(:focus-visible),
325
343
  .stacked > :global(:focus-visible) {
326
344
  outline-offset: var(--focus-ring-offset-inset);
@@ -343,11 +361,11 @@
343
361
  margin: 0;
344
362
  padding: var(--sp-1);
345
363
  max-width: calc(100vw - 2 * var(--sp-2));
346
- background: var(--bg-elevated);
364
+ background: var(--raised-fill);
347
365
  color: var(--text);
348
- border: 1px solid var(--border-strong);
366
+ border: var(--edge-width) solid var(--raised-edge);
349
367
  border-radius: var(--r-md);
350
- box-shadow: var(--shadow-md);
368
+ box-shadow: var(--raised-shadow);
351
369
  opacity: 0;
352
370
  scale: var(--enter-scale);
353
371
  transition:
@@ -221,8 +221,8 @@
221
221
  position: absolute;
222
222
  inset: auto 0 0 0;
223
223
  height: var(--ig-h);
224
- background: var(--textarea-bg, var(--bg));
225
- border: 1px solid var(--textarea-border, var(--border-strong));
224
+ background: var(--textarea-bg, var(--control-fill));
225
+ border: var(--edge-width) solid var(--textarea-border, var(--control-edge));
226
226
  border-top: 0;
227
227
  border-radius: 0 0 var(--ig-radius) var(--ig-radius);
228
228
  transition: border-color var(--dur-fast) ease;
@@ -234,7 +234,7 @@
234
234
  .ig-bar:has(.ig-field :global([aria-invalid='true']))::before {
235
235
  border-color: var(--danger);
236
236
  }
237
- .ig:has(:focus-visible)::after {
237
+ .ig:has(:global(:focus-visible))::after {
238
238
  content: '';
239
239
  position: absolute;
240
240
  inset: 0;
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from 'svelte';
3
+ import SegmentedControl, { type SegmentOption } from './SegmentedControl.svelte';
4
+ import { theme } from '../../stores/theme.svelte';
5
+ import { LOOKS, type LookId } from '../../theme-mode';
6
+
7
+ let {
8
+ looks,
9
+ experimental = true,
10
+ experimentalLabel = 'beta',
11
+ label = 'Look',
12
+ size = 'md',
13
+ control,
14
+ block,
15
+ scroll,
16
+ sound,
17
+ onchange,
18
+ class: klass = '',
19
+ }: Pick<ComponentProps<typeof SegmentedControl>, 'size' | 'control' | 'block' | 'scroll' | 'sound'> & {
20
+ /** Restrict and order the offered looks; defaults to every registered look. */
21
+ looks?: readonly LookId[];
22
+ /** Offer looks whose status is `experimental`. */
23
+ experimental?: boolean;
24
+ /** Badge on experimental looks; empty hides it. */
25
+ experimentalLabel?: string;
26
+ label?: string;
27
+ onchange?: (look: LookId) => void;
28
+ class?: string;
29
+ } = $props();
30
+
31
+ const options = $derived(
32
+ (looks ?? LOOKS.map((l) => l.id)).flatMap((id) => {
33
+ const l = LOOKS.find((x) => x.id === id);
34
+ const beta = l?.status === 'experimental';
35
+ if (!l || (beta && !experimental)) return [];
36
+ return [{ value: l.id, label: l.label }];
37
+ })
38
+ );
39
+ const tagged = new Set<string>(LOOKS.filter((l) => l.status === 'experimental').map((l) => l.id));
40
+ </script>
41
+
42
+ {#snippet option(o: SegmentOption)}
43
+ <span class="look">
44
+ <span class="name">{o.label}</span>
45
+ {#if experimentalLabel && tagged.has(o.value)}<span class="tag">{experimentalLabel}</span>{/if}
46
+ </span>
47
+ {/snippet}
48
+
49
+ <SegmentedControl
50
+ {options}
51
+ value={theme.look}
52
+ onchange={(v) => {
53
+ theme.setLook(v);
54
+ onchange?.(theme.look);
55
+ }}
56
+ {label}
57
+ {size}
58
+ {control}
59
+ {block}
60
+ {scroll}
61
+ {sound}
62
+ class="look-picker {klass}"
63
+ {option}
64
+ />
65
+
66
+ <style>
67
+ .look {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: var(--sp-1);
71
+ min-width: 0;
72
+ }
73
+ :global(.seg-block) .name {
74
+ min-width: 0;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+ .tag {
79
+ flex: none;
80
+ padding: 0 var(--sp-1);
81
+ border: 1px solid currentColor;
82
+ border-radius: var(--r-pill);
83
+ font-size: var(--fs-xs);
84
+ font-weight: var(--fw-semibold);
85
+ line-height: var(--lh-tight);
86
+ }
87
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps } from 'svelte';
2
+ import SegmentedControl from './SegmentedControl.svelte';
3
+ import { type LookId } from '../../theme-mode';
4
+ type $$ComponentProps = Pick<ComponentProps<typeof SegmentedControl>, 'size' | 'control' | 'block' | 'scroll' | 'sound'> & {
5
+ /** Restrict and order the offered looks; defaults to every registered look. */
6
+ looks?: readonly LookId[];
7
+ /** Offer looks whose status is `experimental`. */
8
+ experimental?: boolean;
9
+ /** Badge on experimental looks; empty hides it. */
10
+ experimentalLabel?: string;
11
+ label?: string;
12
+ onchange?: (look: LookId) => void;
13
+ class?: string;
14
+ };
15
+ declare const LookPicker: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type LookPicker = ReturnType<typeof LookPicker>;
17
+ export default LookPicker;
@@ -286,10 +286,10 @@
286
286
  max-height: calc(100dvh - var(--safe-top) - var(--sp-6));
287
287
  display: flex;
288
288
  flex-direction: column;
289
- background: var(--bg-elevated);
290
- border: 1px solid var(--border);
289
+ background: var(--overlay-fill);
290
+ border: var(--edge-width) solid var(--overlay-edge);
291
291
  border-radius: var(--r-lg) var(--r-lg) 0 0;
292
- box-shadow: var(--shadow-lg);
292
+ box-shadow: var(--overlay-shadow);
293
293
  padding-bottom: var(--safe-bottom);
294
294
  /* Bottom sheet: slides up on enter, down on exit. */
295
295
  opacity: 0;
@@ -357,7 +357,7 @@
357
357
  align-items: center;
358
358
  gap: var(--sp-2);
359
359
  padding: var(--sp-4);
360
- border-bottom: 1px solid var(--border);
360
+ border-bottom: var(--edge-width) solid var(--surface-edge);
361
361
  }
362
362
  .sheet-toned {
363
363
  border-top: 3px solid var(--modal-tone);
@@ -382,7 +382,7 @@
382
382
  justify-content: flex-end;
383
383
  gap: var(--sp-2);
384
384
  padding: var(--sp-4);
385
- border-top: 1px solid var(--border);
385
+ border-top: var(--edge-width) solid var(--surface-edge);
386
386
  }
387
387
  /* Opt-in: a flex item is shrink-to-fit, so a layout wrapper handed to the
388
388
  footer would size to its content and hug the right edge instead of