@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
@@ -96,6 +96,45 @@
96
96
  const framed = $derived(!!(header || footer || title));
97
97
  const native = $derived(as === 'button' || as === 'a');
98
98
 
99
+ let el = $state<HTMLElement>();
100
+ const lit = $derived(tap || interactive);
101
+
102
+ // The spotlight follows the pointer itself unless the app already runs trackPointer().
103
+ $effect(() => {
104
+ const node = el;
105
+ if (!node || !lit) return;
106
+ let frame = 0;
107
+ let x = 0;
108
+ let y = 0;
109
+ const flush = () => {
110
+ frame = 0;
111
+ const r = node.getBoundingClientRect();
112
+ node.style.setProperty('--pointer-x', `${x - r.left}px`);
113
+ node.style.setProperty('--pointer-y', `${y - r.top}px`);
114
+ };
115
+ const move = (e: PointerEvent) => {
116
+ if (e.pointerType !== 'mouse') return;
117
+ x = e.clientX;
118
+ y = e.clientY;
119
+ if (!frame) frame = requestAnimationFrame(flush);
120
+ };
121
+ const enter = (e: PointerEvent) => {
122
+ if (e.pointerType !== 'mouse' || globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return;
123
+ if (getComputedStyle(node).getPropertyValue('--card-fx').trim() !== 'spotlight') return;
124
+ move(e);
125
+ node.addEventListener('pointermove', move, { passive: true });
126
+ };
127
+ const leave = () => node.removeEventListener('pointermove', move);
128
+ node.addEventListener('pointerenter', enter, { passive: true });
129
+ node.addEventListener('pointerleave', leave, { passive: true });
130
+ return () => {
131
+ leave();
132
+ node.removeEventListener('pointerenter', enter);
133
+ node.removeEventListener('pointerleave', leave);
134
+ if (frame) cancelAnimationFrame(frame);
135
+ };
136
+ });
137
+
99
138
  const NESTED = 'button,a,input,select,textarea,[role=button],[contenteditable],[contenteditable=true]';
100
139
  function fromNested(e: Event): boolean {
101
140
  const target = e.target as Element | null;
@@ -118,6 +157,7 @@
118
157
 
119
158
  <svelte:element
120
159
  this={as}
160
+ bind:this={el}
121
161
  data-tsu="Card"
122
162
  class="card {klass}"
123
163
  class:pad-none={padding === 'none'}
@@ -168,16 +208,19 @@
168
208
  {:else}
169
209
  {@render children?.()}
170
210
  {/if}
211
+ {#if lit}<span class="card-fx" aria-hidden="true"></span>{/if}
171
212
  </svelte:element>
172
213
 
173
214
  <style>
174
215
  .card {
175
216
  --card-pad: var(--sp-4);
176
217
  min-width: 0;
177
- background: var(--bg-elevated);
218
+ background: var(--surface-fill);
178
219
  --card-border-style: solid;
179
- border: 1px var(--card-border-style) var(--border);
220
+ border: var(--edge-width) var(--card-border-style) var(--surface-edge);
180
221
  border-radius: var(--r-lg);
222
+ --card-shadow-rest: var(--surface-shadow);
223
+ box-shadow: var(--card-shadow-rest);
181
224
  padding: var(--card-pad);
182
225
  }
183
226
  .card-gap {
@@ -194,10 +237,10 @@
194
237
  padding: var(--card-pad);
195
238
  }
196
239
  .card-head {
197
- border-bottom: 1px solid var(--border);
240
+ border-bottom: var(--edge-width) solid var(--surface-edge);
198
241
  }
199
242
  .card-foot {
200
- border-top: 1px solid var(--border);
243
+ border-top: var(--edge-width) solid var(--surface-edge);
201
244
  }
202
245
  /* Theme-aware surface shade, selected by prop so it adapts across themes
203
246
  instead of being overridden with app-level :global hacks. `base` is the
@@ -206,7 +249,7 @@
206
249
  background: var(--surface);
207
250
  }
208
251
  .surface-sunken {
209
- background: var(--bg-elevated-2);
252
+ background: var(--surface-fill-2);
210
253
  }
211
254
  .border-dashed {
212
255
  --card-border-style: dashed;
@@ -237,18 +280,122 @@
237
280
  border-color var(--dur-fast) var(--ease),
238
281
  background var(--dur-fast) var(--ease),
239
282
  box-shadow var(--dur-fast) var(--ease),
283
+ translate var(--dur-fast) var(--ease-out),
240
284
  scale var(--dur-fast) var(--ease-out);
241
285
  }
242
286
  .card-tap:active {
243
- background: var(--bg-elevated-2);
287
+ background: var(--surface-fill-2);
244
288
  scale: var(--press-scale-lg);
245
289
  transition-duration: var(--dur-press);
246
290
  }
247
291
  @media (hover: hover) and (pointer: fine) {
248
292
  .card-tap:hover {
249
- border-color: var(--border-strong);
293
+ border-color: var(--surface-edge-strong);
294
+ translate: 0 calc(-1 * var(--hover-lift));
295
+ --card-shadow-rest: var(--surface-shadow-hover);
296
+ --card-glow: 100%;
297
+ --card-lit: 1;
298
+ --card-fade: var(--dur-fast);
299
+ --card-spin: var(--card-orbit-spin);
300
+ }
301
+ }
302
+ .card-tap:focus-visible {
303
+ --card-glow: 100%;
304
+ --card-lit: 1;
305
+ --card-fade: var(--dur-fast);
306
+ --card-spin: var(--card-orbit-spin);
307
+ }
308
+
309
+ @media (hover: hover) and (pointer: fine) {
310
+ .card-tap:hover > .card-fx {
311
+ --card-orbit: 360deg;
250
312
  }
251
313
  }
314
+ .card-tap:focus-visible > .card-fx {
315
+ --card-orbit: 360deg;
316
+ }
317
+
318
+ /* Hover light for tappable cards: a wash on the surface plus a ring on the edge. */
319
+ .card-tap {
320
+ position: relative;
321
+ }
322
+ .card-fx {
323
+ position: absolute;
324
+ inset: calc(-1 * var(--edge-width));
325
+ padding: 1.5px;
326
+ border-radius: inherit;
327
+ pointer-events: none;
328
+ opacity: var(--card-lit, 0);
329
+ mask:
330
+ linear-gradient(var(--text), var(--text)) content-box exclude,
331
+ linear-gradient(var(--text), var(--text));
332
+ transition:
333
+ opacity var(--card-fade, var(--dur-exit)) ease,
334
+ --card-orbit var(--card-spin, var(--dur-instant)) var(--ease-out);
335
+ }
336
+ .card-tap:not(:global([data-fx-card='none']) *),
337
+ :global(:is([data-fx-card='spotlight'], [data-fx-card='orbit'])) .card-tap:not(:global(:is([data-fx-card='spotlight'], [data-fx-card='orbit']) [data-fx-card='none']) *) {
338
+ transition:
339
+ border-color var(--dur-fast) var(--ease),
340
+ background var(--dur-fast) var(--ease),
341
+ box-shadow var(--dur-fast) var(--ease),
342
+ translate var(--dur-fast) var(--ease-out),
343
+ scale var(--dur-fast) var(--ease-out),
344
+ --card-glow var(--card-fade, var(--dur-exit)) ease;
345
+ }
346
+
347
+ /* Spotlight: a soft light follows the pointer and lights the nearest edge. */
348
+ .card-tap:not(:global([data-fx-card]:not([data-fx-card='spotlight'])) *),
349
+ :global([data-fx-card='spotlight']) .card-tap:not(:global([data-fx-card='spotlight'] [data-fx-card]:not([data-fx-card='spotlight'])) *) {
350
+ --card-fx: spotlight;
351
+ background-image: radial-gradient(
352
+ circle 11rem at var(--card-spot-x) var(--card-spot-y),
353
+ color-mix(in srgb, var(--accent) calc(var(--card-glow) * 0.2), transparent),
354
+ transparent 70%
355
+ );
356
+ --card-spot-x: var(--pointer-x, 50%);
357
+ --card-spot-y: var(--pointer-y, 33%);
358
+ & .card-fx {
359
+ background: radial-gradient(
360
+ circle 7rem at calc(var(--card-spot-x) + var(--edge-width)) calc(var(--card-spot-y) + var(--edge-width)),
361
+ var(--accent) 15%,
362
+ transparent
363
+ );
364
+ }
365
+ }
366
+
367
+ /* Border orbit: on hover-enter a comet makes exactly one revolution of the edge, then rests. */
368
+ :global([data-fx-card='orbit']) .card-tap:not(:global([data-fx-card='orbit'] [data-fx-card]:not([data-fx-card='orbit'])) *) {
369
+ --card-fx: orbit;
370
+ background-image: linear-gradient(
371
+ 160deg,
372
+ color-mix(in srgb, var(--accent) calc(var(--card-glow) * 0.09), transparent),
373
+ transparent 55%
374
+ );
375
+ --card-orbit-spin: calc(var(--dur-slow) * 4);
376
+ & .card-fx {
377
+ background:
378
+ conic-gradient(
379
+ from var(--card-orbit),
380
+ transparent 0 62%,
381
+ color-mix(in srgb, var(--accent) 40%, transparent) 80%,
382
+ var(--accent) 92%,
383
+ color-mix(in srgb, var(--text) 85%, var(--accent)) 96%,
384
+ transparent 100%
385
+ ),
386
+ linear-gradient(color-mix(in srgb, var(--accent) 22%, transparent), color-mix(in srgb, var(--accent) 22%, transparent));
387
+ }
388
+ }
389
+ @property --card-glow {
390
+ syntax: '<percentage>';
391
+ inherits: false;
392
+ initial-value: 0%;
393
+ }
394
+ @property --card-orbit {
395
+ syntax: '<angle>';
396
+ inherits: false;
397
+ initial-value: 0deg;
398
+ }
252
399
  .card-max {
253
400
  width: 100%;
254
401
  }
@@ -275,9 +422,10 @@
275
422
  .card-warn,
276
423
  .card-danger,
277
424
  .card-info,
425
+ .card-accent,
278
426
  .card-attention {
279
- border-color: color-mix(in srgb, var(--card-tone) 55%, var(--border));
280
- background: color-mix(in srgb, var(--card-tone) 8%, var(--bg-elevated));
427
+ border-color: color-mix(in srgb, var(--card-tone) 55%, var(--surface-edge));
428
+ background: color-mix(in srgb, var(--card-tone) 8%, var(--surface-fill));
281
429
  }
282
430
  .card-attention {
283
431
  background: var(--attention-bg);
@@ -291,8 +439,8 @@
291
439
  /* Defaults; overridden inline by the stackY/stackX props. */
292
440
  --stack-y: 8px;
293
441
  --stack-x: 2px;
294
- --stack-bg: var(--bg-elevated-2);
295
- --stack-border: var(--border);
442
+ --stack-bg: var(--surface-fill-2);
443
+ --stack-border: var(--surface-edge);
296
444
  /* Reserve room for the two peeking layers so they aren't clipped. */
297
445
  margin-right: calc(var(--stack-x) * 2);
298
446
  margin-bottom: calc(var(--stack-y) * 2);
@@ -306,7 +454,7 @@
306
454
  /* Opaque fill so each layer fully hides the one behind it — only the
307
455
  bottom-right peek (and its single border line) stays visible. */
308
456
  background: var(--stack-bg);
309
- border: 1px var(--card-border-style) var(--stack-border);
457
+ border: var(--edge-width) var(--card-border-style) var(--stack-border);
310
458
  }
311
459
  /* Nearest back layer — sits just under the front surface. */
312
460
  .card-stacked::before {
@@ -44,8 +44,10 @@
44
44
 
45
45
  const field = getFieldContext();
46
46
  const isInvalid = $derived(invalid || !!field?.invalid);
47
+ let armed = $state(false);
47
48
 
48
49
  function change(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
50
+ armed = true;
49
51
  onchange?.(e);
50
52
  playSound(e.currentTarget.checked ? 'toggle-on' : 'toggle-off', sound);
51
53
  }
@@ -60,6 +62,7 @@
60
62
  class="checkbox {klass}"
61
63
  class:checkbox-sm={size === 'sm'}
62
64
  class:checkbox-lg={size === 'lg'}
65
+ class:armed
63
66
  data-tsu="Checkbox"
64
67
  >
65
68
  <input
@@ -72,7 +75,10 @@
72
75
  aria-describedby={ariaDescribedby ?? field?.describedBy}
73
76
  aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
74
77
  />
75
- <span class="box" aria-hidden="true"></span>
78
+ <span class="box" aria-hidden="true">
79
+ <svg viewBox="0 0 20 20"><path class="tick" pathLength="1" d="M5.5 10.5l3 3 6-7" /></svg>
80
+ <span class="fx"></span>
81
+ </span>
76
82
  <span class="label-text" class:sr-only={labelHidden}>{label}</span>
77
83
  </label>
78
84
 
@@ -106,9 +112,11 @@
106
112
  flex: none;
107
113
  width: var(--cb-box, var(--chip-sm));
108
114
  height: var(--cb-box, var(--chip-sm));
109
- border: 1px solid var(--border-strong);
115
+ border: var(--edge-width) solid var(--control-edge);
110
116
  border-radius: var(--r-sm);
111
- background: var(--bg);
117
+ background: var(--control-fill);
118
+ box-shadow: var(--control-shadow);
119
+ isolation: isolate;
112
120
  transition:
113
121
  background var(--dur-fast) var(--ease),
114
122
  border-color var(--dur-fast) var(--ease),
@@ -155,9 +163,179 @@
155
163
  opacity var(--dur-spring) var(--ease-spring),
156
164
  scale var(--dur-spring) var(--ease-spring);
157
165
  }
166
+ svg {
167
+ position: absolute;
168
+ inset: -1px;
169
+ width: calc(100% + 2px);
170
+ height: calc(100% + 2px);
171
+ overflow: visible;
172
+ fill: none;
173
+ stroke-linecap: round;
174
+ stroke-linejoin: round;
175
+ }
176
+ .tick {
177
+ stroke: var(--text-on-accent);
178
+ stroke-width: 2.25;
179
+ stroke-dasharray: 1 2;
180
+ stroke-dashoffset: 1.05;
181
+ transition: stroke-dashoffset var(--dur-exit-fast) var(--ease-out);
182
+ }
183
+ .fx {
184
+ position: absolute;
185
+ inset: 0;
186
+ z-index: -1;
187
+ font-size: var(--cb-box, var(--chip-sm));
188
+ pointer-events: none;
189
+ }
190
+ .fx::before,
191
+ .fx::after {
192
+ content: '';
193
+ position: absolute;
194
+ opacity: 0;
195
+ }
196
+
197
+ /* Effects follow the closest data-fx-check ancestor; draw is the default. */
198
+ .checkbox:not(:global([data-fx-check]:not([data-fx-check='draw'])) *),
199
+ :global([data-fx-check='draw']) .checkbox:not(:global([data-fx-check='draw'] [data-fx-check]:not([data-fx-check='draw'])) *),
200
+ :global([data-fx-check='pop']) .checkbox:not(:global([data-fx-check='pop'] [data-fx-check]:not([data-fx-check='pop'])) *),
201
+ :global([data-fx-check='confetti']) .checkbox:not(:global([data-fx-check='confetti'] [data-fx-check]:not([data-fx-check='confetti'])) *) {
202
+ & input:checked:not(:indeterminate) + .box::after {
203
+ opacity: 0;
204
+ transition: none;
205
+ }
206
+ & input:checked:not(:indeterminate) + .box .tick {
207
+ stroke-dashoffset: 0;
208
+ transition: stroke-dashoffset var(--dur-exit-slow) var(--ease-out) calc(var(--dur-fast) / 3);
209
+ }
210
+ }
211
+
212
+ :global([data-fx-check='pop']) .checkbox:not(:global([data-fx-check='pop'] [data-fx-check]:not([data-fx-check='pop'])) *) {
213
+ &.armed input:checked + .box {
214
+ animation: cb-pop var(--dur-spring) var(--ease-out);
215
+ }
216
+ }
217
+ @keyframes cb-pop {
218
+ 30% {
219
+ scale: 0.8;
220
+ }
221
+ 65% {
222
+ scale: 1.14;
223
+ }
224
+ }
225
+
226
+ :global([data-fx-check='confetti']) .checkbox:not(:global([data-fx-check='confetti'] [data-fx-check]:not([data-fx-check='confetti'])) *) {
227
+ & .fx::before,
228
+ & .fx::after {
229
+ inset: -0.85em;
230
+ border-radius: var(--r-pill);
231
+ }
232
+ & .fx::before {
233
+ background:
234
+ radial-gradient(circle at 50% 6%, var(--accent) 0 0.06em, transparent 0.08em),
235
+ radial-gradient(circle at 94% 50%, var(--info) 0 0.06em, transparent 0.08em),
236
+ radial-gradient(circle at 50% 94%, var(--accent) 0 0.06em, transparent 0.08em),
237
+ radial-gradient(circle at 6% 50%, var(--ok) 0 0.06em, transparent 0.08em);
238
+ }
239
+ & .fx::after {
240
+ background:
241
+ radial-gradient(circle at 81% 19%, var(--warn) 0 0.045em, transparent 0.065em),
242
+ radial-gradient(circle at 81% 81%, var(--accent) 0 0.045em, transparent 0.065em),
243
+ radial-gradient(circle at 19% 81%, var(--info) 0 0.045em, transparent 0.065em),
244
+ radial-gradient(circle at 19% 19%, var(--accent) 0 0.045em, transparent 0.065em);
245
+ }
246
+ &.armed input:checked:not(:indeterminate) + .box .fx::before {
247
+ animation: cb-burst var(--dur-spring) var(--ease);
248
+ }
249
+ &.armed input:checked:not(:indeterminate) + .box .fx::after {
250
+ animation: cb-burst-late var(--dur-spring) var(--ease);
251
+ }
252
+ }
253
+ @keyframes cb-burst {
254
+ 0% {
255
+ opacity: 0;
256
+ scale: 0.35;
257
+ }
258
+ 20%,
259
+ 60% {
260
+ opacity: 1;
261
+ }
262
+ 100% {
263
+ opacity: 0;
264
+ scale: 1.05;
265
+ rotate: 18deg;
266
+ }
267
+ }
268
+ @keyframes cb-burst-late {
269
+ 0% {
270
+ opacity: 0;
271
+ scale: 0.3;
272
+ }
273
+ 30%,
274
+ 70% {
275
+ opacity: 1;
276
+ }
277
+ 100% {
278
+ opacity: 0;
279
+ scale: 0.9;
280
+ rotate: -14deg;
281
+ }
282
+ }
283
+
284
+ /* Two bars, the short and long legs of the tick, fold flat into the dash. */
285
+ :global([data-fx-check='morph']) .checkbox:not(:global([data-fx-check='morph'] [data-fx-check]:not([data-fx-check='morph'])) *) {
286
+ & .box::after {
287
+ display: none;
288
+ }
289
+ & .fx {
290
+ z-index: 0;
291
+ }
292
+ & .fx::before,
293
+ & .fx::after {
294
+ left: 0;
295
+ top: 0;
296
+ height: 0.11em;
297
+ border-radius: var(--r-pill);
298
+ background: var(--text-on-accent);
299
+ scale: 0;
300
+ transition:
301
+ opacity var(--dur-fast) var(--ease),
302
+ translate var(--dur-spring) var(--ease-spring),
303
+ rotate var(--dur-spring) var(--ease-spring),
304
+ scale var(--dur-spring) var(--ease-spring);
305
+ }
306
+ & .fx::before {
307
+ width: 0.26em;
308
+ translate: 0.22em 0.545em;
309
+ rotate: 45deg;
310
+ }
311
+ & .fx::after {
312
+ width: 0.5em;
313
+ translate: 0.325em 0.445em;
314
+ rotate: -49deg;
315
+ }
316
+ & input:checked + .box .fx::before,
317
+ & input:checked + .box .fx::after {
318
+ opacity: 1;
319
+ scale: 1;
320
+ }
321
+ & input:indeterminate + .box .fx::before {
322
+ opacity: 1;
323
+ translate: 0.37em 0.445em;
324
+ rotate: 0deg;
325
+ scale: 1;
326
+ }
327
+ & input:indeterminate + .box .fx::after {
328
+ opacity: 1;
329
+ translate: 0.25em 0.445em;
330
+ rotate: 0deg;
331
+ scale: 1;
332
+ }
333
+ }
334
+
158
335
  input:focus-visible + .box {
159
336
  outline: var(--focus-ring);
160
337
  outline-offset: var(--focus-ring-offset);
338
+ box-shadow: var(--control-shadow), var(--focus-glow);
161
339
  }
162
340
  input[aria-invalid='true']:not(:checked):not(:indeterminate) + .box {
163
341
  border-color: var(--danger);
@@ -108,9 +108,9 @@
108
108
  height: var(--gauge-h, 1.25rem);
109
109
  margin: 0;
110
110
  padding: 0;
111
- border: 1px solid var(--border);
111
+ border: var(--edge-width) solid var(--track-edge);
112
112
  border-radius: var(--r-sm);
113
- background: var(--bg-elevated-2);
113
+ background: var(--track-fill);
114
114
  overflow: visible;
115
115
  color: inherit;
116
116
  font: inherit;
@@ -106,6 +106,7 @@
106
106
  style:width={wrapped ? undefined : width}
107
107
  bind:value
108
108
  {...rest}
109
+ placeholder={rest.placeholder ?? (field ? ' ' : undefined)}
109
110
  disabled={isDisabled}
110
111
  onkeydown={onenter || onsubmit || onkeydown ? handleKeydown : undefined}
111
112
  id={id ?? field?.id}
@@ -142,9 +143,10 @@
142
143
  padding: var(--sp-2) var(--control-px-md);
143
144
  font-size: var(--control-fs-md);
144
145
  line-height: var(--lh-tight);
145
- background: var(--input-bg, var(--bg));
146
- border: 1px solid var(--input-border, var(--border-strong));
146
+ background: var(--input-bg, var(--control-fill));
147
+ border: var(--edge-width) solid var(--input-border, var(--control-edge));
147
148
  border-radius: var(--input-radius, var(--r-md));
149
+ box-shadow: var(--control-shadow);
148
150
  color: var(--input-fg, var(--text));
149
151
  transition: border-color var(--dur-fast) ease;
150
152
  }
@@ -155,6 +157,11 @@
155
157
  .input:focus-visible {
156
158
  outline: var(--focus-ring);
157
159
  outline-offset: var(--focus-ring-offset);
160
+ box-shadow: var(--control-shadow), var(--focus-glow);
161
+ }
162
+ .input::placeholder {
163
+ color: var(--text-faint);
164
+ opacity: 1;
158
165
  }
159
166
  .input:disabled {
160
167
  opacity: var(--disabled-opacity);
@@ -206,6 +213,36 @@
206
213
  .input[aria-invalid='true']:focus {
207
214
  border-color: var(--danger);
208
215
  }
216
+ .input {
217
+ --focus-tint: var(--accent);
218
+ }
219
+ .input[aria-invalid='true'] {
220
+ --focus-tint: var(--danger);
221
+ }
222
+ /* Focus bloom: a solid ring whose soft halo swells, then settles to a quieter glow. */
223
+ .input:not(.grouped):focus:not(:global([data-fx-focus='none']) *),
224
+ :global(:is([data-fx-focus='bloom'], [data-fx-focus='label'])) .input:not(.grouped):focus:not(:global(:is([data-fx-focus='bloom'], [data-fx-focus='label']) [data-fx-focus='none']) *) {
225
+ outline: none;
226
+ box-shadow:
227
+ var(--control-shadow),
228
+ 0 0 0 var(--focus-ring-width) var(--focus-tint),
229
+ 0 0 16px 2px color-mix(in srgb, var(--focus-tint) 24%, transparent);
230
+ animation: focus-bloom var(--dur-fast) var(--ease-out);
231
+ }
232
+ @keyframes focus-bloom {
233
+ 0% {
234
+ box-shadow:
235
+ var(--control-shadow),
236
+ 0 0 0 0 transparent,
237
+ 0 0 0 0 transparent;
238
+ }
239
+ 45% {
240
+ box-shadow:
241
+ var(--control-shadow),
242
+ 0 0 0 calc(var(--focus-ring-width) + 1px) var(--focus-tint),
243
+ 0 0 20px 4px color-mix(in srgb, var(--focus-tint) 32%, transparent);
244
+ }
245
+ }
209
246
  /* Inside an InputGroup the group draws the focus ring, dims the whole group
210
247
  and the overlaid adornments report their widths; the text stays clear of them. */
211
248
  .input.grouped {
@@ -216,6 +253,7 @@
216
253
  }
217
254
  .input.grouped:focus-visible {
218
255
  outline: none;
256
+ box-shadow: var(--control-shadow);
219
257
  }
220
258
  .input.grouped:disabled {
221
259
  opacity: 1;
@@ -296,7 +334,7 @@
296
334
  }
297
335
  @media (hover: hover) and (pointer: fine) {
298
336
  .input:hover:not(:disabled, :focus, [aria-invalid='true']) {
299
- border-color: var(--text-faint);
337
+ border-color: var(--control-edge-hover);
300
338
  }
301
339
  .input-clear:hover {
302
340
  color: var(--text);
@@ -108,7 +108,7 @@
108
108
  font-size: inherit;
109
109
  line-height: var(--lh-snug);
110
110
  padding: 0 var(--sp-1);
111
- border: 1px solid var(--border-strong);
111
+ border: var(--edge-width) solid var(--border-strong);
112
112
  border-bottom-width: 2px;
113
113
  border-radius: var(--r-sm);
114
114
  background: var(--bg-elevated-2);
@@ -77,10 +77,71 @@
77
77
  .underline-hover:hover {
78
78
  text-decoration: underline;
79
79
  }
80
+ .link:hover {
81
+ --link-on: 0%;
82
+ --link-draw: 100%;
83
+ --link-mark: color-mix(in srgb, currentColor 30%, transparent);
84
+ --link-deco: transparent;
85
+ --link-in: var(--dur-press);
86
+ --link-grow: var(--dur-fast);
87
+ --link-hold: var(--dur-instant);
88
+ }
80
89
  }
81
90
  .align-center {
82
91
  text-align: center;
83
92
  }
93
+
94
+ /* Hover, focus and press only raise these; each variant decides what they paint. */
95
+ .link:is(:focus-visible, :active) {
96
+ --link-on: 0%;
97
+ --link-draw: 100%;
98
+ --link-mark: color-mix(in srgb, currentColor 30%, transparent);
99
+ --link-deco: transparent;
100
+ --link-in: var(--dur-press);
101
+ --link-grow: var(--dur-fast);
102
+ --link-hold: var(--dur-instant);
103
+ }
104
+
105
+ /* Sweep line: the underline fills in from the left and leaves to the right. */
106
+ .link:not(.underline-none):not(:global([data-fx-link]:not([data-fx-link='sweep'])) *),
107
+ :global([data-fx-link='sweep']) .link:not(.underline-none):not(:global([data-fx-link='sweep'] [data-fx-link]:not([data-fx-link='sweep'])) *) {
108
+ --link-base: 0%;
109
+ text-decoration: none;
110
+ background-image:
111
+ linear-gradient(currentColor, currentColor),
112
+ linear-gradient(color-mix(in srgb, currentColor 30%, transparent), color-mix(in srgb, currentColor 30%, transparent));
113
+ background-repeat: no-repeat;
114
+ background-position: var(--link-on, 100%) 100%, 0 100%;
115
+ background-size: var(--link-draw, 0%) 1.5px, var(--link-base) 1px;
116
+ transition:
117
+ color var(--dur-fast) ease,
118
+ background-size var(--dur-fast) var(--ease-out);
119
+ &.underline-always {
120
+ --link-base: 100%;
121
+ }
122
+ }
123
+
124
+ /* Highlighter: a marker swipes in behind the words and fades on leave. */
125
+ :global([data-fx-link='highlight']) .link:not(.underline-none):not(:global([data-fx-link='highlight'] [data-fx-link]:not([data-fx-link='highlight'])) *) {
126
+ margin-inline: -0.18em;
127
+ padding-inline: 0.18em;
128
+ border-radius: 0.2em 0.35em 0.25em 0.4em;
129
+ background-image: linear-gradient(100deg, var(--link-mark), color-mix(in srgb, var(--link-mark) 70%, transparent));
130
+ background-repeat: no-repeat;
131
+ background-size: var(--link-draw, 0%) 100%;
132
+ text-decoration-color: var(--link-deco, color-mix(in srgb, currentColor 45%, transparent));
133
+ text-underline-offset: 0.2em;
134
+ transition:
135
+ color var(--dur-fast) ease,
136
+ text-decoration-color var(--dur-fast) ease,
137
+ --link-mark var(--link-in, var(--dur-exit)) ease,
138
+ background-size var(--link-grow, var(--dur-instant)) var(--ease-out) var(--link-hold, var(--dur-exit));
139
+ }
140
+ @property --link-mark {
141
+ syntax: '<color>';
142
+ inherits: false;
143
+ initial-value: transparent;
144
+ }
84
145
  @media (pointer: coarse) {
85
146
  .link {
86
147
  position: relative;