@dorsk/tsumikit 0.79.0 → 0.80.1
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.
- package/dist/components/atoms/Artwork.svelte +14 -10
- package/dist/components/atoms/Artwork.svelte.d.ts +1 -1
- package/dist/components/atoms/Badge.svelte +1 -4
- package/dist/components/atoms/Button.svelte +303 -9
- package/dist/components/atoms/Button.svelte.d.ts +4 -1
- package/dist/components/atoms/Card.svelte +142 -0
- package/dist/components/atoms/Checkbox.svelte +177 -1
- package/dist/components/atoms/Input.svelte +35 -0
- package/dist/components/atoms/Link.svelte +61 -0
- package/dist/components/atoms/Progress.svelte +74 -1
- package/dist/components/atoms/Progress.svelte.d.ts +1 -0
- package/dist/components/atoms/Select.svelte +30 -0
- package/dist/components/atoms/Skeleton.svelte +54 -3
- package/dist/components/atoms/Skeleton.svelte.d.ts +1 -0
- package/dist/components/atoms/Spinner.svelte +152 -3
- package/dist/components/atoms/Spinner.svelte.d.ts +1 -1
- package/dist/components/atoms/Switch.svelte +135 -0
- package/dist/components/atoms/Textarea.svelte +36 -0
- package/dist/components/atoms/Thinking.svelte +130 -0
- package/dist/components/atoms/Thinking.svelte.d.ts +15 -0
- package/dist/components/molecules/CopyButton.svelte +146 -5
- package/dist/components/molecules/Field.svelte +55 -1
- package/dist/components/molecules/FileButton.svelte +4 -2
- package/dist/components/molecules/InputGroup.svelte +1 -1
- package/dist/components/molecules/LookPicker.svelte +100 -0
- package/dist/components/molecules/LookPicker.svelte.d.ts +17 -0
- package/dist/components/molecules/MotionSettings.svelte +166 -0
- package/dist/components/molecules/MotionSettings.svelte.d.ts +18 -0
- package/dist/components/molecules/RadioGroup.svelte +61 -0
- package/dist/components/molecules/SectionHeader.svelte +2 -0
- package/dist/components/molecules/Tabs.svelte +157 -4
- package/dist/components/molecules/ThemePicker.svelte +8 -3
- package/dist/fx.d.ts +149 -0
- package/dist/fx.js +111 -0
- package/dist/fx.md +58 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/looks/README.md +101 -0
- package/dist/looks/depth.css +939 -0
- package/dist/looks/editorial.css +545 -0
- package/dist/looks/tactile.css +871 -0
- package/dist/markdown.js +47 -21
- package/dist/pointer.d.ts +3 -0
- package/dist/pointer.js +40 -0
- package/dist/stores/fx.svelte.d.ts +17 -0
- package/dist/stores/fx.svelte.js +64 -0
- package/dist/stores/theme.svelte.d.ts +8 -2
- package/dist/stores/theme.svelte.js +33 -7
- package/dist/styles/themes.css +249 -249
- package/dist/styles/tokens.css +4 -3
- package/dist/theme-mode.d.ts +31 -0
- package/dist/theme-mode.js +19 -0
- package/package.json +3 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// initials (or an icon) when there is no source or it fails to load. Width
|
|
5
5
|
// comes from `size`, height from `aspect`; `status` overlays a Dot/Badge in
|
|
6
6
|
// the bottom-right corner.
|
|
7
|
-
import type
|
|
7
|
+
import { untrack, type Snippet } from 'svelte';
|
|
8
8
|
import { artworkGradient, initials as initialsOf } from '../../artwork';
|
|
9
9
|
import Icon from './Icon.svelte';
|
|
10
10
|
import type { IconName } from './Icon.svelte';
|
|
@@ -51,23 +51,27 @@
|
|
|
51
51
|
...rest
|
|
52
52
|
}: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
$
|
|
56
|
-
src;
|
|
57
|
-
srcset;
|
|
58
|
-
failed = false;
|
|
59
|
-
});
|
|
54
|
+
const source = $derived(`${src ?? ''} ${srcset ?? ''}`);
|
|
55
|
+
let failedSource = $state<string | null>(null);
|
|
60
56
|
|
|
61
57
|
const key = $derived(seed ?? alt);
|
|
62
|
-
const showImage = $derived(Boolean(src) &&
|
|
58
|
+
const showImage = $derived(Boolean(src) && failedSource !== source);
|
|
63
59
|
const tile = $derived(showImage ? undefined : alt.trim() ? 'named' : 'decorative');
|
|
64
60
|
const gradient = $derived(showImage ? undefined : artworkGradient(key));
|
|
65
61
|
const text = $derived(fallback === 'initials' ? initialsOf(key) : '');
|
|
66
62
|
|
|
67
63
|
function fail() {
|
|
68
|
-
|
|
64
|
+
failedSource = source;
|
|
69
65
|
onerror?.();
|
|
70
66
|
}
|
|
67
|
+
|
|
68
|
+
function watchLoad(img: HTMLImageElement) {
|
|
69
|
+
img.addEventListener('error', fail);
|
|
70
|
+
untrack(() => {
|
|
71
|
+
if (img.complete && img.naturalWidth === 0) fail();
|
|
72
|
+
});
|
|
73
|
+
return () => img.removeEventListener('error', fail);
|
|
74
|
+
}
|
|
71
75
|
</script>
|
|
72
76
|
|
|
73
77
|
<div
|
|
@@ -83,7 +87,7 @@
|
|
|
83
87
|
{...rest}
|
|
84
88
|
>
|
|
85
89
|
{#if showImage}
|
|
86
|
-
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async"
|
|
90
|
+
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async" {@attach watchLoad} />
|
|
87
91
|
{:else if fallback === 'initials' && text}
|
|
88
92
|
<span class="initials" aria-hidden="true">{text}</span>
|
|
89
93
|
{:else if fallback === 'icon'}
|
|
@@ -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
|
-
|
|
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
|
|
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={
|
|
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
|
|
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}
|
|
@@ -253,15 +286,17 @@
|
|
|
253
286
|
font-size: var(--control-fs-md);
|
|
254
287
|
line-height: var(--lh-none);
|
|
255
288
|
--icon-size: var(--control-icon-md);
|
|
256
|
-
--btn-scale-dur: var(--dur-
|
|
289
|
+
--btn-scale-dur: var(--dur-release);
|
|
290
|
+
--btn-scale-ease: var(--ease-spring-bounce);
|
|
257
291
|
transition:
|
|
258
292
|
background var(--dur-fast) ease,
|
|
259
293
|
border-color var(--dur-fast) ease,
|
|
260
294
|
box-shadow var(--dur-fast) ease,
|
|
261
295
|
color var(--dur-fast) ease,
|
|
262
296
|
opacity var(--dur-fast) ease,
|
|
297
|
+
filter var(--dur-fast) ease,
|
|
263
298
|
translate var(--dur-fast) var(--ease-out),
|
|
264
|
-
scale var(--btn-scale-dur) var(--ease
|
|
299
|
+
scale var(--btn-scale-dur) var(--btn-scale-ease);
|
|
265
300
|
user-select: none;
|
|
266
301
|
white-space: normal;
|
|
267
302
|
overflow-wrap: break-word;
|
|
@@ -273,9 +308,10 @@
|
|
|
273
308
|
.btn:focus-visible:not(.btn-link, .btn-icon-inline, [aria-pressed='true']) {
|
|
274
309
|
box-shadow: var(--btn-shadow-rest), var(--focus-glow);
|
|
275
310
|
}
|
|
276
|
-
/* Press: scale in over --dur-press,
|
|
311
|
+
/* Press: scale in over --dur-press, spring back over --dur-release. */
|
|
277
312
|
.btn:active:not(:disabled, [aria-disabled='true'], .loading, .btn-link) {
|
|
278
313
|
--btn-scale-dur: var(--dur-press);
|
|
314
|
+
--btn-scale-ease: var(--ease-out);
|
|
279
315
|
scale: var(--press-scale);
|
|
280
316
|
}
|
|
281
317
|
.btn-icon:active:not(:disabled, [aria-disabled='true'], .loading),
|
|
@@ -809,4 +845,262 @@
|
|
|
809
845
|
color: var(--danger);
|
|
810
846
|
}
|
|
811
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
|
+
}
|
|
812
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';
|
|
@@ -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,6 +208,7 @@
|
|
|
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>
|
|
@@ -252,8 +293,109 @@
|
|
|
252
293
|
border-color: var(--surface-edge-strong);
|
|
253
294
|
translate: 0 calc(-1 * var(--hover-lift));
|
|
254
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;
|
|
312
|
+
}
|
|
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));
|
|
255
387
|
}
|
|
256
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
|
+
}
|
|
257
399
|
.card-max {
|
|
258
400
|
width: 100%;
|
|
259
401
|
}
|