@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
@@ -102,13 +102,14 @@
102
102
  justify-content: center;
103
103
  overflow: hidden;
104
104
  flex: none;
105
- background: var(--bg-elevated-2);
106
- border: 1px solid var(--border);
105
+ background: var(--surface-fill-2);
106
+ border: var(--edge-width) solid var(--surface-edge);
107
107
  color: var(--text-muted);
108
108
  container-type: inline-size;
109
109
  transition:
110
110
  border-color var(--dur-fast) var(--ease),
111
111
  box-shadow var(--dur-fast) var(--ease),
112
+ translate var(--dur-fast) var(--ease-out),
112
113
  scale var(--dur-fast) var(--ease-out);
113
114
  }
114
115
  .r-sm {
@@ -125,8 +126,9 @@
125
126
  }
126
127
  @media (hover: hover) and (pointer: fine) {
127
128
  .hover:hover {
128
- border-color: var(--border-strong);
129
+ border-color: var(--surface-edge-strong);
129
130
  box-shadow: var(--shadow-md);
131
+ translate: 0 calc(-1 * var(--hover-lift));
130
132
  }
131
133
  }
132
134
  .hover:active {
@@ -195,7 +195,7 @@
195
195
  --icon-size: var(--icon-xs);
196
196
  background: var(--badge-bg, var(--bg-elevated-2));
197
197
  color: var(--badge-fg, var(--text-muted));
198
- border: 1px solid var(--badge-border, var(--border));
198
+ border: var(--edge-width) solid var(--badge-border, var(--border));
199
199
  white-space: nowrap;
200
200
  max-width: var(--badge-max-width, 100%);
201
201
  }
@@ -213,7 +213,7 @@
213
213
  line-height: var(--lh-none);
214
214
  }
215
215
  .toned {
216
- color: var(--badge-fg, var(--badge-tone));
216
+ color: var(--badge-fg, color-mix(in oklab, var(--badge-tone) 75%, var(--text)));
217
217
  border-color: var(--badge-border, color-mix(in srgb, var(--badge-tone) 40%, transparent));
218
218
  background: var(--badge-bg, color-mix(in srgb, var(--badge-tone) 12%, transparent));
219
219
  }
@@ -310,15 +310,12 @@
310
310
  font: inherit;
311
311
  line-height: var(--lh-none);
312
312
  cursor: pointer;
313
- opacity: 0.6;
314
313
  transition:
315
- opacity var(--dur-fast) var(--ease),
316
314
  background var(--dur-fast) var(--ease),
317
315
  scale var(--dur-fast) var(--ease-out);
318
316
  }
319
317
  @media (hover: hover) and (pointer: fine) {
320
318
  .action:hover {
321
- opacity: 1;
322
319
  background: color-mix(in srgb, currentColor 12%, transparent);
323
320
  transition-duration: var(--dur-instant);
324
321
  }
@@ -57,8 +57,20 @@
57
57
  // moving layout; `compact` opts out in dense tables.
58
58
  hitArea?: 'auto' | 'compact';
59
59
  // Async/busy state: shows a spinner, blocks clicks, sets aria-busy. Stays
60
- // disabled-equivalent while true (so a double-submit can't fire).
61
- loading?: boolean;
60
+ // disabled-equivalent while true (so a double-submit can't fire). `'done'`
61
+ // is the settled state after a busy run: clickable again, a check beside
62
+ // the label (with `loadingStyle="morph"` the label springs back in).
63
+ loading?: boolean | 'done';
64
+ // How a busy button shows it: an inline spinner before the label, the label
65
+ // morphing into a centred spinner at constant width, or a progress fill
66
+ // pouring in from the left (driven by `progress`).
67
+ loadingStyle?: 'spinner' | 'morph' | 'fill';
68
+ // 0–1 for `loadingStyle="fill"`; omit for an indeterminate idle fill.
69
+ progress?: number;
70
+ // Text beside the check in the `'done'` state; defaults to the children.
71
+ // With `morph`, keep the children constant and set this instead so both
72
+ // stacked labels size the button and its width never changes.
73
+ doneLabel?: string;
62
74
  // Corner count indicator (unread, selected, pending). Hidden at 0/undefined;
63
75
  // shows `${countMax}+` past the cap while the accessible name carries the
64
76
  // exact number. Tint via `--btn-count-bg` / `--btn-count-fg`.
@@ -100,6 +112,9 @@
100
112
  collapseLabel = 'never',
101
113
  hitArea = 'auto',
102
114
  loading = false,
115
+ loadingStyle = 'spinner',
116
+ progress,
117
+ doneLabel,
103
118
  count,
104
119
  countMax = 99,
105
120
  layout = 'inline',
@@ -120,7 +135,10 @@
120
135
  // A link-flavoured Button is a real <a>; native `disabled`/`type` don't apply,
121
136
  // so an inactive link drops `href` (no focus, no keyboard activation).
122
137
  const tag = $derived(href !== undefined || as === 'a' ? 'a' : 'button');
123
- const inactive = $derived(disabled || loading);
138
+ const busy = $derived(loading === true);
139
+ const done = $derived(loading === 'done');
140
+ const layered = $derived(loadingStyle !== 'spinner');
141
+ const inactive = $derived(disabled || busy);
124
142
  const elementAttrs = $derived(
125
143
  tag === 'a'
126
144
  ? inactive
@@ -174,7 +192,7 @@
174
192
  {...rest}
175
193
  {...elementAttrs}
176
194
  aria-label={ariaLabel}
177
- aria-busy={loading || undefined}
195
+ aria-busy={busy || undefined}
178
196
  aria-pressed={pressed ?? rest['aria-pressed']}
179
197
  aria-keyshortcuts={rest['aria-keyshortcuts'] ?? keyHint}
180
198
  {title}
@@ -198,6 +216,7 @@
198
216
  class:btn-collapse-container={collapseLabel === 'container'}
199
217
  class:hit-compact={hitArea === 'compact'}
200
218
  style:--btn-box={box ? `var(--box-${box})` : undefined}
219
+ style:--btn-progress={loadingStyle === 'fill' && progress !== undefined ? Math.min(1, Math.max(0, progress)) : undefined}
201
220
  class:btn-tone-accent={t === 'accent'}
202
221
  class:btn-tone-success={t === 'ok'}
203
222
  class:btn-tone-info={t === 'info'}
@@ -205,13 +224,17 @@
205
224
  class:btn-tone-danger={t === 'danger'}
206
225
  class:btn-icon-inline={iconInline}
207
226
  class:hover-danger={hoverDanger}
208
- class:loading
227
+ class:loading={busy}
228
+ class:btn-done={done}
229
+ class:btn-morph={loadingStyle === 'morph'}
230
+ class:btn-fill={loadingStyle === 'fill'}
209
231
  class:btn-counted={counted}
210
232
  class:btn-stacked={stacked}
211
233
  class:btn-hinted={keyHint !== undefined}
212
234
  onclick={handleClick}
213
235
  >
214
- {#if loading}<span class="btn-spinner" aria-hidden="true"></span>{/if}
236
+ {#if loadingStyle === 'fill'}<span class="btn-fill-bar" aria-hidden="true"></span>{/if}
237
+ {#if busy && !layered}<span class="btn-spinner" aria-hidden="true"></span>{:else if done && !layered}<span class="btn-check" aria-hidden="true"><Icon name="check" /></span>{/if}
215
238
  {#if glyph !== undefined}
216
239
  <span class="btn-glyph" aria-hidden="true">
217
240
  {#if typeof glyph !== 'string'}{@render glyph()}{:else if isIconName(glyph)}<Icon name={glyph} />{:else}{glyph}{/if}
@@ -219,6 +242,16 @@
219
242
  {/if}
220
243
  {#if stacked}
221
244
  <span class="btn-stack-label">{@render children?.()}</span>
245
+ {:else if layered}
246
+ <span class="btn-layers">
247
+ <span class="btn-layer btn-layer-label">{@render children?.()}</span>
248
+ {#if loadingStyle === 'morph'}
249
+ <span class="btn-layer btn-layer-spin" aria-hidden="true"></span>
250
+ <span class="btn-layer btn-layer-done" aria-hidden="true"><Icon name="check" />{#if doneLabel !== undefined}{doneLabel}{:else}{@render children?.()}{/if}</span>
251
+ {:else}
252
+ <span class="btn-layer btn-layer-ink" aria-hidden="true">{@render children?.()}</span>
253
+ {/if}
254
+ </span>
222
255
  {:else}
223
256
  {@render children?.()}
224
257
  {/if}
@@ -242,21 +275,28 @@
242
275
  padding: var(--sp-2) var(--btn-px-md);
243
276
  min-height: var(--btn-size, var(--control-height-default));
244
277
  max-width: 100%;
245
- border: 1px solid var(--btn-border, var(--border-strong));
278
+ border: var(--edge-width) solid var(--btn-border, var(--btn-edge));
246
279
  border-radius: var(--btn-radius, var(--r-md));
247
- background: var(--btn-bg, var(--surface));
280
+ background: var(--btn-bg, var(--btn-fill));
281
+ --btn-shadow-rest: var(--btn-shadow);
282
+ --btn-shadow-lift: var(--btn-shadow-hover);
283
+ box-shadow: var(--btn-shadow-rest);
248
284
  color: var(--btn-fg, var(--text));
249
285
  font-weight: var(--fw-medium);
250
286
  font-size: var(--control-fs-md);
251
287
  line-height: var(--lh-none);
252
288
  --icon-size: var(--control-icon-md);
253
- --btn-scale-dur: var(--dur-fast);
289
+ --btn-scale-dur: var(--dur-release);
290
+ --btn-scale-ease: var(--ease-spring-bounce);
254
291
  transition:
255
292
  background var(--dur-fast) ease,
256
293
  border-color var(--dur-fast) ease,
294
+ box-shadow var(--dur-fast) ease,
257
295
  color var(--dur-fast) ease,
258
296
  opacity var(--dur-fast) ease,
259
- scale var(--btn-scale-dur) var(--ease-out);
297
+ filter var(--dur-fast) ease,
298
+ translate var(--dur-fast) var(--ease-out),
299
+ scale var(--btn-scale-dur) var(--btn-scale-ease);
260
300
  user-select: none;
261
301
  white-space: normal;
262
302
  overflow-wrap: break-word;
@@ -265,9 +305,13 @@
265
305
  opacity: var(--disabled-opacity);
266
306
  cursor: not-allowed;
267
307
  }
268
- /* Press: scale in over --dur-press, release over --dur-fast. */
308
+ .btn:focus-visible:not(.btn-link, .btn-icon-inline, [aria-pressed='true']) {
309
+ box-shadow: var(--btn-shadow-rest), var(--focus-glow);
310
+ }
311
+ /* Press: scale in over --dur-press, spring back over --dur-release. */
269
312
  .btn:active:not(:disabled, [aria-disabled='true'], .loading, .btn-link) {
270
313
  --btn-scale-dur: var(--dur-press);
314
+ --btn-scale-ease: var(--ease-out);
271
315
  scale: var(--press-scale);
272
316
  }
273
317
  .btn-icon:active:not(:disabled, [aria-disabled='true'], .loading),
@@ -277,8 +321,10 @@
277
321
  scale: var(--press-scale-sm);
278
322
  }
279
323
  .btn-primary {
280
- background: var(--btn-bg, var(--accent));
281
- border-color: var(--btn-border, var(--accent));
324
+ background: var(--btn-bg, var(--btn-primary-fill));
325
+ border-color: var(--btn-border, var(--btn-primary-edge));
326
+ --btn-shadow-rest: var(--btn-primary-shadow);
327
+ --btn-shadow-lift: var(--btn-primary-shadow-hover);
282
328
  color: var(--btn-fg, var(--text-on-accent));
283
329
  font-weight: var(--fw-semibold);
284
330
  }
@@ -289,6 +335,8 @@
289
335
  .btn-ghost {
290
336
  background: var(--btn-bg, transparent);
291
337
  border-color: var(--btn-border, transparent);
338
+ --btn-shadow-rest: none;
339
+ --btn-shadow-lift: none;
292
340
  }
293
341
  .btn-sm {
294
342
  min-height: var(--btn-size, var(--control-height-compact));
@@ -372,9 +420,9 @@
372
420
  height: var(--btn-size, var(--control-height));
373
421
  min-height: var(--btn-size, var(--control-height));
374
422
  padding: 0 var(--control-px-md);
375
- border: 1px solid var(--btn-border, var(--border-strong));
423
+ border: var(--edge-width) solid var(--btn-border, var(--btn-edge));
376
424
  border-radius: var(--btn-radius, var(--r-md));
377
- background: var(--btn-bg, var(--surface));
425
+ background: var(--btn-bg, var(--btn-fill));
378
426
  color: var(--btn-fg, var(--text));
379
427
  font-weight: var(--fw-medium);
380
428
  font-size: var(--control-fs-md);
@@ -390,8 +438,8 @@
390
438
  the primary action with the neutral --surface; restore the accent fill when
391
439
  both are present. */
392
440
  .btn-control.btn-primary {
393
- background: var(--btn-bg, var(--accent));
394
- border-color: var(--btn-border, var(--accent));
441
+ background: var(--btn-bg, var(--btn-primary-fill));
442
+ border-color: var(--btn-border, var(--btn-primary-edge));
395
443
  color: var(--btn-fg, var(--text-on-accent));
396
444
  }
397
445
  .btn-control.btn-tone-success {
@@ -730,6 +778,8 @@
730
778
  @media (hover: hover) and (pointer: fine) {
731
779
  .btn:hover:not(:disabled) {
732
780
  border-color: var(--accent);
781
+ translate: 0 calc(-1 * var(--hover-lift));
782
+ --btn-shadow-rest: var(--btn-shadow-lift);
733
783
  }
734
784
  .btn-primary:hover:not(:disabled) {
735
785
  filter: brightness(1.08);
@@ -782,15 +832,275 @@
782
832
  .btn-link:hover:not(:disabled) {
783
833
  background: none;
784
834
  border: none;
835
+ translate: none;
785
836
  text-decoration: underline;
786
837
  }
787
838
  .btn-icon-inline:hover:not(:disabled) {
788
839
  border: none;
789
840
  background: none;
841
+ translate: none;
790
842
  color: var(--text);
791
843
  }
792
844
  .btn-icon-inline.hover-danger:hover:not(:disabled) {
793
845
  color: var(--danger);
794
846
  }
795
847
  }
848
+ /* Loading styles. `morph`: label, spinner and done check stack in one grid
849
+ cell so the width never changes. `fill`: a progress bar under the label,
850
+ the label recoloured where the bar has passed. */
851
+ .btn-check {
852
+ display: inline-flex;
853
+ flex: none;
854
+ --icon-size: 0.9em;
855
+ }
856
+ .btn-layers {
857
+ position: relative;
858
+ display: grid;
859
+ place-items: center;
860
+ min-width: 0;
861
+ }
862
+ .btn-layer {
863
+ grid-area: 1 / 1;
864
+ display: inline-flex;
865
+ align-items: center;
866
+ gap: var(--sp-2);
867
+ white-space: nowrap;
868
+ font-variant-numeric: tabular-nums;
869
+ transition:
870
+ opacity var(--dur-base) var(--ease-out),
871
+ scale var(--dur-base) var(--ease-out),
872
+ translate var(--dur-base) var(--ease-out);
873
+ }
874
+ .btn-layer-spin {
875
+ width: 1em;
876
+ height: 1em;
877
+ border-radius: var(--r-pill);
878
+ mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
879
+ background: conic-gradient(from 0deg, transparent 0 30%, currentColor);
880
+ opacity: 0;
881
+ scale: 0.4;
882
+ }
883
+ .btn-layer-done {
884
+ opacity: 0;
885
+ translate: 0 0.5em;
886
+ --icon-size: 0.9em;
887
+ }
888
+ .btn-morph.loading .btn-layer-label,
889
+ .btn-morph.btn-done .btn-layer-label {
890
+ opacity: 0;
891
+ scale: 0.85;
892
+ }
893
+ .btn-morph.loading .btn-layer-spin {
894
+ opacity: 1;
895
+ scale: 1;
896
+ animation: btn-spin 0.9s linear infinite;
897
+ }
898
+ .btn-morph.btn-done .btn-layer-done {
899
+ opacity: 1;
900
+ translate: none;
901
+ transition-timing-function: var(--ease-spring-bounce);
902
+ transition-duration: var(--dur-spring);
903
+ }
904
+ .btn-fill {
905
+ position: relative;
906
+ isolation: isolate;
907
+ overflow: hidden;
908
+ --btn-progress-fill: var(--accent);
909
+ --btn-progress-fg: var(--text-on-accent);
910
+ }
911
+ .btn-fill.btn-primary {
912
+ --btn-progress-fill: color-mix(in srgb, var(--text-on-accent) 22%, transparent);
913
+ --btn-progress-fg: currentColor;
914
+ }
915
+ .btn-fill-bar {
916
+ position: absolute;
917
+ inset: 0;
918
+ z-index: 0;
919
+ background: var(--btn-progress-fill);
920
+ scale: var(--btn-progress, 0) 1;
921
+ transform-origin: left;
922
+ transition: scale var(--dur-slow) var(--ease-out);
923
+ pointer-events: none;
924
+ }
925
+ .btn-fill .btn-layers {
926
+ z-index: 1;
927
+ }
928
+ .btn-layer-ink {
929
+ position: absolute;
930
+ inset: 0;
931
+ justify-content: center;
932
+ color: var(--btn-progress-fg);
933
+ clip-path: inset(0 calc((1 - var(--btn-progress, 0)) * 100%) 0 0);
934
+ transition: clip-path var(--dur-slow) var(--ease-out);
935
+ }
936
+ .btn-fill:not(.loading, .btn-done) .btn-fill-bar,
937
+ .btn-fill:not(.loading, .btn-done) .btn-layer-ink {
938
+ transition-duration: var(--dur-fast);
939
+ }
940
+
941
+ /* Ambient hover: an effect chosen by `data-fx-hover` on the nearest ancestor
942
+ that sets it (`smoke` when none does, `none` turns it off). Two layers under
943
+ the content run their loop only while hovered; keyboard focus holds a still
944
+ frame. Boxless buttons and a corner count (clipped by overflow) opt out. */
945
+ .btn {
946
+ --fx-ink: color-mix(in srgb, var(--btn-tone, var(--accent)) 26%, transparent);
947
+ --fx-ember: color-mix(in srgb, var(--warn) 55%, var(--accent));
948
+ --fx-a1: color-mix(in srgb, var(--accent) 34%, transparent);
949
+ --fx-a2: color-mix(in srgb, var(--info) 30%, transparent);
950
+ --fx-a3: color-mix(in srgb, var(--ok) 30%, transparent);
951
+ }
952
+ .btn-danger {
953
+ --fx-ink: color-mix(in srgb, var(--danger) 26%, transparent);
954
+ }
955
+ .btn-primary {
956
+ --fx-ink: color-mix(in srgb, var(--text-on-accent) 16%, transparent);
957
+ --fx-ember: color-mix(in srgb, var(--warn) 70%, var(--text-on-accent));
958
+ --fx-a1: color-mix(in srgb, var(--text-on-accent) 18%, transparent);
959
+ --fx-a2: color-mix(in srgb, var(--info) 45%, transparent);
960
+ --fx-a3: color-mix(in srgb, var(--ok) 40%, transparent);
961
+ }
962
+ @media (hover: hover) and (pointer: fine) {
963
+ .btn:where(:not(.btn-link, .btn-icon-inline, .btn-counted)):where(:not(:global([data-fx-hover]) *), :global([data-fx-hover]:not([data-fx-hover='none'])) *) {
964
+ position: relative;
965
+ isolation: isolate;
966
+ overflow: hidden;
967
+ }
968
+ .btn:where(:not(.btn-link, .btn-icon-inline, .btn-counted)):where(:not(:global([data-fx-hover]) *), :global([data-fx-hover]:not([data-fx-hover='none'])) *)::before,
969
+ .btn:where(:not(.btn-link, .btn-icon-inline, .btn-counted)):where(:not(:global([data-fx-hover]) *), :global([data-fx-hover]:not([data-fx-hover='none'])) *)::after {
970
+ content: '';
971
+ position: absolute;
972
+ inset: 0;
973
+ z-index: -1;
974
+ pointer-events: none;
975
+ opacity: 0;
976
+ transition: opacity var(--dur-fast) ease;
977
+ }
978
+ .btn:hover:not(:disabled)::before,
979
+ .btn:hover:not(:disabled)::after,
980
+ .btn:focus-visible::before,
981
+ .btn:focus-visible::after {
982
+ opacity: 1;
983
+ }
984
+
985
+ /* smoke: two soft puffs drift up and across the fill */
986
+ .btn:where(:not(:global([data-fx-hover]) *), :global([data-fx-hover='smoke']) *):where(:not(:global([data-fx-hover='smoke'] [data-fx-hover]:not([data-fx-hover='smoke'])) *))::before,
987
+ .btn:where(:not(:global([data-fx-hover]) *), :global([data-fx-hover='smoke']) *):where(:not(:global([data-fx-hover='smoke'] [data-fx-hover]:not([data-fx-hover='smoke'])) *))::after {
988
+ inset: -60% -30%;
989
+ background:
990
+ radial-gradient(40% 55% at 30% 60%, var(--fx-ink), transparent 70%),
991
+ radial-gradient(35% 45% at 70% 40%, var(--fx-ink), transparent 70%);
992
+ translate: 0 15%;
993
+ animation: fx-smoke 7s var(--ease-in-out) infinite alternate;
994
+ }
995
+ .btn:where(:not(:global([data-fx-hover]) *), :global([data-fx-hover='smoke']) *):where(:not(:global([data-fx-hover='smoke'] [data-fx-hover]:not([data-fx-hover='smoke'])) *))::after {
996
+ background:
997
+ radial-gradient(30% 40% at 55% 70%, var(--fx-ink), transparent 70%),
998
+ radial-gradient(25% 35% at 20% 30%, var(--fx-ink), transparent 70%);
999
+ translate: 0 -10%;
1000
+ animation: fx-smoke-b 9s var(--ease-in-out) infinite alternate;
1001
+ }
1002
+
1003
+ /* ember: a warm glow breathes at the bottom edge, sparks rise; press sinks 1px */
1004
+ .btn:where(:global([data-fx-hover='ember']) *):where(:not(:global([data-fx-hover='ember'] [data-fx-hover]:not([data-fx-hover='ember'])) *)) {
1005
+ --press-scale: var(--press-scale-lg);
1006
+ }
1007
+ .btn:where(:global([data-fx-hover='ember']) *):where(:not(:global([data-fx-hover='ember'] [data-fx-hover]:not([data-fx-hover='ember'])) *)):active:not(:disabled, [aria-disabled='true'], .loading, .btn-link) {
1008
+ translate: 0 calc(var(--shift-xs) / 2 - var(--hover-lift));
1009
+ --btn-shadow-rest: none;
1010
+ }
1011
+ .btn:where(:global([data-fx-hover='ember']) *):where(:not(:global([data-fx-hover='ember'] [data-fx-hover]:not([data-fx-hover='ember'])) *))::before {
1012
+ background: radial-gradient(70% 90% at 50% 115%, color-mix(in srgb, var(--fx-ember) 55%, transparent), transparent 70%);
1013
+ transform-origin: 50% 100%;
1014
+ animation: fx-ember 2.4s var(--ease-in-out) infinite;
1015
+ }
1016
+ .btn:where(:global([data-fx-hover='ember']) *):where(:not(:global([data-fx-hover='ember'] [data-fx-hover]:not([data-fx-hover='ember'])) *))::after {
1017
+ inset: 0 0 -100% 0;
1018
+ background:
1019
+ radial-gradient(circle at 18% 62%, var(--fx-ember) 0 1px, transparent 1.6px),
1020
+ radial-gradient(circle at 36% 88%, var(--fx-ember) 0 1.3px, transparent 1.9px),
1021
+ radial-gradient(circle at 52% 70%, var(--fx-ember) 0 0.9px, transparent 1.5px),
1022
+ radial-gradient(circle at 70% 94%, var(--fx-ember) 0 1.2px, transparent 1.8px),
1023
+ radial-gradient(circle at 84% 58%, var(--fx-ember) 0 1px, transparent 1.6px),
1024
+ radial-gradient(circle at 92% 80%, var(--fx-ember) 0 0.8px, transparent 1.4px);
1025
+ animation: fx-sparks 3.2s linear infinite;
1026
+ }
1027
+
1028
+ /* aurora: accent, info and success flow through the fill in two crossing layers */
1029
+ .btn:where(:global([data-fx-hover='aurora']) *):where(:not(:global([data-fx-hover='aurora'] [data-fx-hover]:not([data-fx-hover='aurora'])) *))::before,
1030
+ .btn:where(:global([data-fx-hover='aurora']) *):where(:not(:global([data-fx-hover='aurora'] [data-fx-hover]:not([data-fx-hover='aurora'])) *))::after {
1031
+ inset: 0 auto 0 0;
1032
+ width: 300%;
1033
+ background: linear-gradient(90deg, var(--fx-a1), var(--fx-a2), var(--fx-a3), var(--fx-a1));
1034
+ background-size: 33.3334% 100%;
1035
+ mask-image: radial-gradient(120% 110% at 50% -10%, #000 40%, transparent 100%);
1036
+ translate: -33.3334% 0;
1037
+ animation: fx-aurora 7s linear infinite;
1038
+ }
1039
+ .btn:where(:global([data-fx-hover='aurora']) *):where(:not(:global([data-fx-hover='aurora'] [data-fx-hover]:not([data-fx-hover='aurora'])) *))::after {
1040
+ background: linear-gradient(90deg, var(--fx-a3), var(--fx-a1), var(--fx-a2), var(--fx-a3));
1041
+ background-size: 33.3334% 100%;
1042
+ mask-image: radial-gradient(120% 110% at 50% 110%, #000 30%, transparent 100%);
1043
+ translate: -66.6667% 0;
1044
+ animation: fx-aurora-back 11s linear infinite;
1045
+ }
1046
+ .btn:not(:hover)::before,
1047
+ .btn:not(:hover)::after,
1048
+ .btn:disabled::before,
1049
+ .btn:disabled::after {
1050
+ animation-play-state: paused;
1051
+ }
1052
+ }
1053
+ @keyframes fx-smoke {
1054
+ from {
1055
+ translate: -14% 22%;
1056
+ rotate: -4deg;
1057
+ }
1058
+ to {
1059
+ translate: 12% -20%;
1060
+ rotate: 5deg;
1061
+ }
1062
+ }
1063
+ @keyframes fx-smoke-b {
1064
+ from {
1065
+ translate: 10% 18%;
1066
+ rotate: 3deg;
1067
+ }
1068
+ to {
1069
+ translate: -12% -22%;
1070
+ rotate: -5deg;
1071
+ }
1072
+ }
1073
+ @keyframes fx-ember {
1074
+ 0%,
1075
+ 100% {
1076
+ scale: 1 1;
1077
+ }
1078
+ 50% {
1079
+ scale: 1.12 1.25;
1080
+ }
1081
+ }
1082
+ @keyframes fx-sparks {
1083
+ from {
1084
+ translate: 0 0;
1085
+ }
1086
+ to {
1087
+ translate: 0 -100%;
1088
+ }
1089
+ }
1090
+ @keyframes fx-aurora {
1091
+ from {
1092
+ translate: -33.3334% 0;
1093
+ }
1094
+ to {
1095
+ translate: 0 0;
1096
+ }
1097
+ }
1098
+ @keyframes fx-aurora-back {
1099
+ from {
1100
+ translate: -33.3334% 0;
1101
+ }
1102
+ to {
1103
+ translate: -66.6667% 0;
1104
+ }
1105
+ }
796
1106
  </style>
@@ -23,7 +23,10 @@ type ButtonProps = HTMLButtonAttributes & {
23
23
  hoverDanger?: boolean;
24
24
  collapseLabel?: 'never' | 'mobile' | 'container';
25
25
  hitArea?: 'auto' | 'compact';
26
- loading?: boolean;
26
+ loading?: boolean | 'done';
27
+ loadingStyle?: 'spinner' | 'morph' | 'fill';
28
+ progress?: number;
29
+ doneLabel?: string;
27
30
  count?: number;
28
31
  countMax?: number;
29
32
  layout?: 'inline' | 'stacked';