@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
|
@@ -123,7 +123,9 @@
|
|
|
123
123
|
indLeft = left;
|
|
124
124
|
indWidth = width;
|
|
125
125
|
ind.style.width = `${width}px`;
|
|
126
|
-
|
|
126
|
+
const fx = getComputedStyle(ind).getPropertyValue('--tabs-fx').trim();
|
|
127
|
+
const still = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
128
|
+
if (!animate || !placed || width === 0 || still) {
|
|
127
129
|
ind.style.transition = 'none';
|
|
128
130
|
ind.style.translate = `${left}px 0`;
|
|
129
131
|
ind.style.scale = '1 1';
|
|
@@ -132,6 +134,22 @@
|
|
|
132
134
|
placed = true;
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
137
|
+
if (fx === 'stretch' && typeof ind.animate === 'function') {
|
|
138
|
+
const lo = Math.min(prevLeft, left);
|
|
139
|
+
const hi = Math.max(prevLeft + prevWidth, left + width);
|
|
140
|
+
const ease = (name: string) => getComputedStyle(ind).getPropertyValue(name).trim() || 'ease-out';
|
|
141
|
+
ind.style.translate = `${left}px 0`;
|
|
142
|
+
ind.style.scale = '1 1';
|
|
143
|
+
ind.animate(
|
|
144
|
+
[
|
|
145
|
+
{ translate: `${prevLeft}px 0`, scale: `${prevWidth / width} 1`, easing: ease('--ease-out') },
|
|
146
|
+
{ translate: `${lo}px 0`, scale: `${(hi - lo) / width} 1`, offset: 0.4, easing: ease('--ease-spring') },
|
|
147
|
+
{ translate: `${left}px 0`, scale: '1 1' }
|
|
148
|
+
],
|
|
149
|
+
{ duration: 280 }
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
135
153
|
ind.style.transition = 'none';
|
|
136
154
|
ind.style.translate = `${prevLeft}px 0`;
|
|
137
155
|
ind.style.scale = `${prevWidth / width} 1`;
|
|
@@ -152,6 +170,14 @@
|
|
|
152
170
|
return () => ro.disconnect();
|
|
153
171
|
});
|
|
154
172
|
|
|
173
|
+
function inkFrom(e: MouseEvent) {
|
|
174
|
+
const el = e.currentTarget as HTMLElement;
|
|
175
|
+
const r = el.getBoundingClientRect();
|
|
176
|
+
const pointer = e.detail > 0;
|
|
177
|
+
el.style.setProperty('--ink-x', pointer ? `${e.clientX - r.left}px` : '50%');
|
|
178
|
+
el.style.setProperty('--ink-y', pointer ? `${e.clientY - r.top}px` : '50%');
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
function select(id: string, focus = false) {
|
|
156
182
|
if (tabs.find((t) => t.id === id)?.disabled) return;
|
|
157
183
|
if (value !== id) playSound('click', sound);
|
|
@@ -161,7 +187,11 @@
|
|
|
161
187
|
// The list scrolls when it is too narrow, so keyboard selection has to
|
|
162
188
|
// bring the tab back into view or it moves somewhere unseen.
|
|
163
189
|
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
164
|
-
if (focus)
|
|
190
|
+
if (focus) {
|
|
191
|
+
el?.style.removeProperty('--ink-x');
|
|
192
|
+
el?.style.removeProperty('--ink-y');
|
|
193
|
+
el?.focus();
|
|
194
|
+
}
|
|
165
195
|
});
|
|
166
196
|
}
|
|
167
197
|
// Step to the next non-disabled tab in a direction, wrapping around.
|
|
@@ -236,7 +266,10 @@
|
|
|
236
266
|
class="tab closable"
|
|
237
267
|
class:selected={value === t.id}
|
|
238
268
|
class:disabled={t.disabled}
|
|
239
|
-
onclick={() =>
|
|
269
|
+
onclick={(e) => {
|
|
270
|
+
inkFrom(e);
|
|
271
|
+
select(t.id);
|
|
272
|
+
}}
|
|
240
273
|
onauxclick={(e) => onauxclick(e, t.id)}
|
|
241
274
|
>
|
|
242
275
|
{@render body(t)}
|
|
@@ -268,7 +301,10 @@
|
|
|
268
301
|
tabindex={stop?.id === t.id ? 0 : -1}
|
|
269
302
|
class="tab"
|
|
270
303
|
class:selected={value === t.id}
|
|
271
|
-
onclick={() =>
|
|
304
|
+
onclick={(e) => {
|
|
305
|
+
inkFrom(e);
|
|
306
|
+
select(t.id);
|
|
307
|
+
}}
|
|
272
308
|
>
|
|
273
309
|
{@render body(t)}
|
|
274
310
|
</button>
|
|
@@ -389,4 +425,121 @@
|
|
|
389
425
|
outline-offset: var(--focus-ring-offset);
|
|
390
426
|
border-radius: var(--r-sm);
|
|
391
427
|
}
|
|
428
|
+
|
|
429
|
+
/* Sliding pill: a raised pill glides behind the selected tab with a slight overshoot. */
|
|
430
|
+
.tabs:not(:global([data-fx-tabs]:not([data-fx-tabs='pill'])) *),
|
|
431
|
+
:global([data-fx-tabs='pill']) .tabs:not(:global([data-fx-tabs='pill'] [data-fx-tabs]:not([data-fx-tabs='pill'])) *) {
|
|
432
|
+
--tabs-fx: pill;
|
|
433
|
+
& .tablist {
|
|
434
|
+
isolation: isolate;
|
|
435
|
+
flex: 0 1 auto;
|
|
436
|
+
padding: 3px;
|
|
437
|
+
border-bottom: 0;
|
|
438
|
+
border-radius: var(--r-pill);
|
|
439
|
+
background: var(--surface-fill-2);
|
|
440
|
+
box-shadow: inset 0 0 0 var(--edge-width) var(--surface-edge);
|
|
441
|
+
}
|
|
442
|
+
& .actions {
|
|
443
|
+
border-bottom: 0;
|
|
444
|
+
}
|
|
445
|
+
& .tab {
|
|
446
|
+
min-height: calc(var(--control-md) - 6px);
|
|
447
|
+
margin-bottom: 0;
|
|
448
|
+
border-bottom: 0;
|
|
449
|
+
border-radius: var(--r-pill);
|
|
450
|
+
}
|
|
451
|
+
& .tab.selected {
|
|
452
|
+
color: var(--text);
|
|
453
|
+
}
|
|
454
|
+
& .tablist:not(.placed) .tab.selected {
|
|
455
|
+
background: var(--surface);
|
|
456
|
+
}
|
|
457
|
+
& .tab.selected:focus-visible {
|
|
458
|
+
outline: none;
|
|
459
|
+
}
|
|
460
|
+
& .indicator {
|
|
461
|
+
top: 3px;
|
|
462
|
+
bottom: 3px;
|
|
463
|
+
z-index: -1;
|
|
464
|
+
height: auto;
|
|
465
|
+
border-radius: var(--r-pill);
|
|
466
|
+
background: var(--surface);
|
|
467
|
+
box-shadow:
|
|
468
|
+
0 0 0 var(--edge-width) color-mix(in srgb, var(--accent) 35%, var(--surface-edge)),
|
|
469
|
+
0 1px 3px color-mix(in srgb, var(--text) 14%, transparent);
|
|
470
|
+
transition-timing-function: var(--ease-spring-bounce);
|
|
471
|
+
}
|
|
472
|
+
& .tablist:has(.tab.selected:focus-visible) .indicator {
|
|
473
|
+
box-shadow:
|
|
474
|
+
0 0 0 2px var(--accent),
|
|
475
|
+
0 1px 3px color-mix(in srgb, var(--text) 14%, transparent);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/* Stretch line: the script stretches the line across both tabs, then lets the tail catch up. */
|
|
479
|
+
:global([data-fx-tabs='stretch']) .tabs:not(:global([data-fx-tabs='stretch'] [data-fx-tabs]:not([data-fx-tabs='stretch'])) *) {
|
|
480
|
+
--tabs-fx: stretch;
|
|
481
|
+
}
|
|
482
|
+
/* Ink blot: a tint spreads from the click point and a short mark springs in under the label. */
|
|
483
|
+
:global([data-fx-tabs='ink']) .tabs:not(:global([data-fx-tabs='ink'] [data-fx-tabs]:not([data-fx-tabs='ink'])) *) {
|
|
484
|
+
--tabs-fx: ink;
|
|
485
|
+
& .indicator {
|
|
486
|
+
display: none;
|
|
487
|
+
}
|
|
488
|
+
& .tab {
|
|
489
|
+
position: relative;
|
|
490
|
+
isolation: isolate;
|
|
491
|
+
overflow: hidden;
|
|
492
|
+
border-radius: var(--r-md) var(--r-md) 0 0;
|
|
493
|
+
}
|
|
494
|
+
& .tab::before,
|
|
495
|
+
& .tab::after {
|
|
496
|
+
content: '';
|
|
497
|
+
position: absolute;
|
|
498
|
+
pointer-events: none;
|
|
499
|
+
}
|
|
500
|
+
& .tab::before {
|
|
501
|
+
left: var(--ink-x, 50%);
|
|
502
|
+
top: var(--ink-y, 50%);
|
|
503
|
+
z-index: -1;
|
|
504
|
+
width: 260%;
|
|
505
|
+
aspect-ratio: 1;
|
|
506
|
+
translate: -50% -50%;
|
|
507
|
+
border-radius: var(--r-pill);
|
|
508
|
+
background: radial-gradient(
|
|
509
|
+
circle closest-side,
|
|
510
|
+
color-mix(in srgb, var(--accent) 22%, transparent) 0 55%,
|
|
511
|
+
color-mix(in srgb, var(--accent) 14%, transparent) 80%,
|
|
512
|
+
transparent
|
|
513
|
+
);
|
|
514
|
+
scale: 0;
|
|
515
|
+
opacity: 0;
|
|
516
|
+
transition-property: opacity, scale;
|
|
517
|
+
transition-duration: var(--dur-exit), var(--dur-instant);
|
|
518
|
+
transition-delay: var(--dur-instant), var(--dur-exit);
|
|
519
|
+
transition-timing-function: ease;
|
|
520
|
+
}
|
|
521
|
+
& .tab.selected::before {
|
|
522
|
+
scale: 1;
|
|
523
|
+
opacity: 1;
|
|
524
|
+
transition-duration: var(--dur-fast), var(--dur-slow);
|
|
525
|
+
transition-delay: var(--dur-instant);
|
|
526
|
+
transition-timing-function: ease, var(--ease-out);
|
|
527
|
+
}
|
|
528
|
+
& .tab::after {
|
|
529
|
+
inset: auto 30% 4px;
|
|
530
|
+
height: 2px;
|
|
531
|
+
border-radius: var(--r-xs);
|
|
532
|
+
background: var(--accent);
|
|
533
|
+
opacity: 0;
|
|
534
|
+
scale: 0.2 1;
|
|
535
|
+
transition-property: opacity, scale;
|
|
536
|
+
transition-duration: var(--dur-exit-fast);
|
|
537
|
+
}
|
|
538
|
+
& .tab.selected::after {
|
|
539
|
+
opacity: 1;
|
|
540
|
+
scale: 1 1;
|
|
541
|
+
transition-duration: var(--dur-fast), var(--dur-spring);
|
|
542
|
+
transition-timing-function: ease, var(--ease-spring-bounce);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
392
545
|
</style>
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
// (--c-bg, --c-surface, --c-text, --c-accent; the derived --bg etc. are
|
|
6
6
|
// resolved once at :root and would not re-scope). The root default theme
|
|
7
7
|
// has no [data-theme] block, so its swatch carries the :root values.
|
|
8
|
+
// Prerendered, the trigger swatch is unscoped: it shows the theme prepainted
|
|
9
|
+
// on <html> until hydration.
|
|
8
10
|
import type { ComponentProps } from 'svelte';
|
|
9
11
|
import type { AnchorAttributes } from '../../anchor';
|
|
10
12
|
import Popover from './Popover.svelte';
|
|
13
|
+
import { browser } from '../../env';
|
|
11
14
|
import { type ThemeDef, theme } from '../../stores/theme.svelte';
|
|
12
15
|
import { AUTO_THEME } from '../../theme-mode';
|
|
13
16
|
|
|
@@ -63,7 +66,9 @@
|
|
|
63
66
|
);
|
|
64
67
|
const named = (id: string) => theme.all.find((t) => t.id === id)?.label ?? id;
|
|
65
68
|
const autoCaption = $derived(`${autoLabel} · ${named(theme.pref.light)} / ${named(theme.pref.dark)}`);
|
|
66
|
-
const title = $derived(
|
|
69
|
+
const title = $derived(
|
|
70
|
+
!browser ? 'Theme' : isAuto ? `${autoLabel} · ${theme.label}` : `Theme: ${theme.label}`
|
|
71
|
+
);
|
|
67
72
|
const caption = $derived(
|
|
68
73
|
hoveredAuto || (isAuto && !hovered)
|
|
69
74
|
? autoCaption
|
|
@@ -71,7 +76,7 @@
|
|
|
71
76
|
);
|
|
72
77
|
</script>
|
|
73
78
|
|
|
74
|
-
{#snippet swatch(id: string)}
|
|
79
|
+
{#snippet swatch(id: string | undefined)}
|
|
75
80
|
<span class="swatch" data-theme={id} aria-hidden="true">
|
|
76
81
|
<i class="q bg"></i><i class="q surface"></i><i class="q text"></i><i class="q accent"></i>
|
|
77
82
|
</span>
|
|
@@ -94,7 +99,7 @@
|
|
|
94
99
|
triggerClass={klass}
|
|
95
100
|
style={styleProp}
|
|
96
101
|
>
|
|
97
|
-
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(theme.current)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
|
|
102
|
+
{#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(browser ? theme.current : undefined)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
|
|
98
103
|
<div class="panel">
|
|
99
104
|
{#each groups as g (g.mode)}
|
|
100
105
|
<div class="group-label">{g.label}</div>
|
package/dist/fx.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export declare const FX: {
|
|
2
|
+
readonly hover: {
|
|
3
|
+
readonly label: "Button hover";
|
|
4
|
+
readonly variants: readonly [{
|
|
5
|
+
readonly id: "smoke";
|
|
6
|
+
readonly label: "Smoke";
|
|
7
|
+
}, {
|
|
8
|
+
readonly id: "ember";
|
|
9
|
+
readonly label: "Ember";
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: "aurora";
|
|
12
|
+
readonly label: "Aurora";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "none";
|
|
15
|
+
readonly label: "Off";
|
|
16
|
+
}];
|
|
17
|
+
};
|
|
18
|
+
readonly check: {
|
|
19
|
+
readonly label: "Checkbox";
|
|
20
|
+
readonly variants: readonly [{
|
|
21
|
+
readonly id: "draw";
|
|
22
|
+
readonly label: "Draw";
|
|
23
|
+
}, {
|
|
24
|
+
readonly id: "pop";
|
|
25
|
+
readonly label: "Pop";
|
|
26
|
+
}, {
|
|
27
|
+
readonly id: "confetti";
|
|
28
|
+
readonly label: "Confetti";
|
|
29
|
+
}, {
|
|
30
|
+
readonly id: "morph";
|
|
31
|
+
readonly label: "Morph";
|
|
32
|
+
}, {
|
|
33
|
+
readonly id: "none";
|
|
34
|
+
readonly label: "Off";
|
|
35
|
+
}];
|
|
36
|
+
};
|
|
37
|
+
readonly radio: {
|
|
38
|
+
readonly label: "Radio";
|
|
39
|
+
readonly variants: readonly [{
|
|
40
|
+
readonly id: "spring";
|
|
41
|
+
readonly label: "Spring";
|
|
42
|
+
}, {
|
|
43
|
+
readonly id: "liquid";
|
|
44
|
+
readonly label: "Liquid";
|
|
45
|
+
}, {
|
|
46
|
+
readonly id: "none";
|
|
47
|
+
readonly label: "Off";
|
|
48
|
+
}];
|
|
49
|
+
};
|
|
50
|
+
readonly switch: {
|
|
51
|
+
readonly label: "Switch";
|
|
52
|
+
readonly variants: readonly [{
|
|
53
|
+
readonly id: "squash";
|
|
54
|
+
readonly label: "Squash";
|
|
55
|
+
}, {
|
|
56
|
+
readonly id: "glow";
|
|
57
|
+
readonly label: "Glow";
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "calm";
|
|
60
|
+
readonly label: "Calm";
|
|
61
|
+
}, {
|
|
62
|
+
readonly id: "none";
|
|
63
|
+
readonly label: "Off";
|
|
64
|
+
}];
|
|
65
|
+
};
|
|
66
|
+
readonly focus: {
|
|
67
|
+
readonly label: "Input focus";
|
|
68
|
+
readonly variants: readonly [{
|
|
69
|
+
readonly id: "bloom";
|
|
70
|
+
readonly label: "Bloom";
|
|
71
|
+
}, {
|
|
72
|
+
readonly id: "label";
|
|
73
|
+
readonly label: "Spring label";
|
|
74
|
+
}, {
|
|
75
|
+
readonly id: "none";
|
|
76
|
+
readonly label: "Off";
|
|
77
|
+
}];
|
|
78
|
+
};
|
|
79
|
+
readonly tabs: {
|
|
80
|
+
readonly label: "Tabs";
|
|
81
|
+
readonly variants: readonly [{
|
|
82
|
+
readonly id: "pill";
|
|
83
|
+
readonly label: "Pill";
|
|
84
|
+
}, {
|
|
85
|
+
readonly id: "stretch";
|
|
86
|
+
readonly label: "Stretch";
|
|
87
|
+
}, {
|
|
88
|
+
readonly id: "ink";
|
|
89
|
+
readonly label: "Ink";
|
|
90
|
+
}, {
|
|
91
|
+
readonly id: "none";
|
|
92
|
+
readonly label: "Off";
|
|
93
|
+
}];
|
|
94
|
+
};
|
|
95
|
+
readonly copy: {
|
|
96
|
+
readonly label: "Copy";
|
|
97
|
+
readonly variants: readonly [{
|
|
98
|
+
readonly id: "burst";
|
|
99
|
+
readonly label: "Burst";
|
|
100
|
+
}, {
|
|
101
|
+
readonly id: "tick";
|
|
102
|
+
readonly label: "Tick";
|
|
103
|
+
}, {
|
|
104
|
+
readonly id: "none";
|
|
105
|
+
readonly label: "Off";
|
|
106
|
+
}];
|
|
107
|
+
};
|
|
108
|
+
readonly link: {
|
|
109
|
+
readonly label: "Link hover";
|
|
110
|
+
readonly variants: readonly [{
|
|
111
|
+
readonly id: "sweep";
|
|
112
|
+
readonly label: "Sweep";
|
|
113
|
+
}, {
|
|
114
|
+
readonly id: "highlight";
|
|
115
|
+
readonly label: "Highlight";
|
|
116
|
+
}, {
|
|
117
|
+
readonly id: "none";
|
|
118
|
+
readonly label: "Off";
|
|
119
|
+
}];
|
|
120
|
+
};
|
|
121
|
+
readonly card: {
|
|
122
|
+
readonly label: "Card hover";
|
|
123
|
+
readonly variants: readonly [{
|
|
124
|
+
readonly id: "spotlight";
|
|
125
|
+
readonly label: "Spotlight";
|
|
126
|
+
}, {
|
|
127
|
+
readonly id: "orbit";
|
|
128
|
+
readonly label: "Orbit";
|
|
129
|
+
}, {
|
|
130
|
+
readonly id: "none";
|
|
131
|
+
readonly label: "Off";
|
|
132
|
+
}];
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
export type FxFamily = keyof typeof FX;
|
|
136
|
+
export type FxVariant<F extends FxFamily = FxFamily> = (typeof FX)[F]['variants'][number]['id'];
|
|
137
|
+
export type FxSettings = {
|
|
138
|
+
[F in FxFamily]: FxVariant<F>;
|
|
139
|
+
};
|
|
140
|
+
export declare const FX_FAMILIES: FxFamily[];
|
|
141
|
+
export declare const DEFAULT_FX: FxSettings;
|
|
142
|
+
export declare function fxAttr(family: FxFamily): `data-fx-${FxFamily}`;
|
|
143
|
+
export declare function isFxVariant<F extends FxFamily>(family: F, v: unknown): v is FxVariant<F>;
|
|
144
|
+
/** Settings from an untrusted blob: unknown families are dropped, unknown
|
|
145
|
+
* variants fall back to `defaults`. */
|
|
146
|
+
export declare function parseFx(raw: unknown, defaults?: FxSettings): FxSettings;
|
|
147
|
+
export declare function fxDiff(settings: FxSettings): Partial<FxSettings>;
|
|
148
|
+
/** Inline <head> script painting the persisted `data-fx-*` before first paint. */
|
|
149
|
+
export declare function fxPrepaint(key: string): string;
|
package/dist/fx.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// `data-fx-<family>="<variant>"` on <html> or any ancestor picks a family's
|
|
2
|
+
// effect; without the attribute the first variant applies.
|
|
3
|
+
export const FX = {
|
|
4
|
+
hover: {
|
|
5
|
+
label: 'Button hover',
|
|
6
|
+
variants: [
|
|
7
|
+
{ id: 'smoke', label: 'Smoke' },
|
|
8
|
+
{ id: 'ember', label: 'Ember' },
|
|
9
|
+
{ id: 'aurora', label: 'Aurora' },
|
|
10
|
+
{ id: 'none', label: 'Off' },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
check: {
|
|
14
|
+
label: 'Checkbox',
|
|
15
|
+
variants: [
|
|
16
|
+
{ id: 'draw', label: 'Draw' },
|
|
17
|
+
{ id: 'pop', label: 'Pop' },
|
|
18
|
+
{ id: 'confetti', label: 'Confetti' },
|
|
19
|
+
{ id: 'morph', label: 'Morph' },
|
|
20
|
+
{ id: 'none', label: 'Off' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
radio: {
|
|
24
|
+
label: 'Radio',
|
|
25
|
+
variants: [
|
|
26
|
+
{ id: 'spring', label: 'Spring' },
|
|
27
|
+
{ id: 'liquid', label: 'Liquid' },
|
|
28
|
+
{ id: 'none', label: 'Off' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
switch: {
|
|
32
|
+
label: 'Switch',
|
|
33
|
+
variants: [
|
|
34
|
+
{ id: 'squash', label: 'Squash' },
|
|
35
|
+
{ id: 'glow', label: 'Glow' },
|
|
36
|
+
{ id: 'calm', label: 'Calm' },
|
|
37
|
+
{ id: 'none', label: 'Off' },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
focus: {
|
|
41
|
+
label: 'Input focus',
|
|
42
|
+
variants: [
|
|
43
|
+
{ id: 'bloom', label: 'Bloom' },
|
|
44
|
+
{ id: 'label', label: 'Spring label' },
|
|
45
|
+
{ id: 'none', label: 'Off' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
tabs: {
|
|
49
|
+
label: 'Tabs',
|
|
50
|
+
variants: [
|
|
51
|
+
{ id: 'pill', label: 'Pill' },
|
|
52
|
+
{ id: 'stretch', label: 'Stretch' },
|
|
53
|
+
{ id: 'ink', label: 'Ink' },
|
|
54
|
+
{ id: 'none', label: 'Off' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
copy: {
|
|
58
|
+
label: 'Copy',
|
|
59
|
+
variants: [
|
|
60
|
+
{ id: 'burst', label: 'Burst' },
|
|
61
|
+
{ id: 'tick', label: 'Tick' },
|
|
62
|
+
{ id: 'none', label: 'Off' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
link: {
|
|
66
|
+
label: 'Link hover',
|
|
67
|
+
variants: [
|
|
68
|
+
{ id: 'sweep', label: 'Sweep' },
|
|
69
|
+
{ id: 'highlight', label: 'Highlight' },
|
|
70
|
+
{ id: 'none', label: 'Off' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
card: {
|
|
74
|
+
label: 'Card hover',
|
|
75
|
+
variants: [
|
|
76
|
+
{ id: 'spotlight', label: 'Spotlight' },
|
|
77
|
+
{ id: 'orbit', label: 'Orbit' },
|
|
78
|
+
{ id: 'none', label: 'Off' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export const FX_FAMILIES = Object.keys(FX);
|
|
83
|
+
export const DEFAULT_FX = Object.fromEntries(FX_FAMILIES.map((f) => [f, FX[f].variants[0].id]));
|
|
84
|
+
export function fxAttr(family) {
|
|
85
|
+
return `data-fx-${family}`;
|
|
86
|
+
}
|
|
87
|
+
export function isFxVariant(family, v) {
|
|
88
|
+
return FX[family].variants.some((x) => x.id === v);
|
|
89
|
+
}
|
|
90
|
+
/** Settings from an untrusted blob: unknown families are dropped, unknown
|
|
91
|
+
* variants fall back to `defaults`. */
|
|
92
|
+
export function parseFx(raw, defaults = DEFAULT_FX) {
|
|
93
|
+
const blob = raw && typeof raw === 'object' ? raw : {};
|
|
94
|
+
const out = { ...defaults };
|
|
95
|
+
for (const f of FX_FAMILIES)
|
|
96
|
+
if (isFxVariant(f, blob[f]))
|
|
97
|
+
out[f] = blob[f];
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
export function fxDiff(settings) {
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const f of FX_FAMILIES)
|
|
103
|
+
if (settings[f] !== DEFAULT_FX[f])
|
|
104
|
+
out[f] = settings[f];
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Inline <head> script painting the persisted `data-fx-*` before first paint. */
|
|
108
|
+
export function fxPrepaint(key) {
|
|
109
|
+
const valid = Object.fromEntries(FX_FAMILIES.map((f) => [f, FX[f].variants.slice(1).map((v) => v.id)]));
|
|
110
|
+
return `(function(){try{var s=localStorage.getItem(${JSON.stringify(key)});if(!s)return;var b=JSON.parse(s),v=${JSON.stringify(valid)},d=document.documentElement;if(!b||typeof b!=='object')return;for(var f in v)if(v[f].indexOf(b[f])>=0)d.setAttribute('data-fx-'+f,b[f])}catch(e){}})();`;
|
|
111
|
+
}
|
package/dist/fx.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Micro-interactions
|
|
2
|
+
|
|
3
|
+
Each component family has a set of effects. One attribute on `<html>`, or on any ancestor, picks the
|
|
4
|
+
effect for that family:
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<html data-fx-hover="ember" data-fx-check="pop"></html>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
With no attribute the first variant applies. `none` turns the family's effect off.
|
|
11
|
+
|
|
12
|
+
| Family | Attribute | Variants (first = default) |
|
|
13
|
+
| -------- | ---------------- | --------------------------------------- |
|
|
14
|
+
| `hover` | `data-fx-hover` | `smoke` `ember` `aurora` `none` |
|
|
15
|
+
| `check` | `data-fx-check` | `draw` `pop` `confetti` `morph` `none` |
|
|
16
|
+
| `radio` | `data-fx-radio` | `spring` `liquid` `none` |
|
|
17
|
+
| `switch` | `data-fx-switch` | `squash` `glow` `calm` `none` |
|
|
18
|
+
| `focus` | `data-fx-focus` | `bloom` `label` `none` |
|
|
19
|
+
| `tabs` | `data-fx-tabs` | `pill` `stretch` `ink` `none` |
|
|
20
|
+
| `copy` | `data-fx-copy` | `burst` `tick` `none` |
|
|
21
|
+
| `link` | `data-fx-link` | `sweep` `highlight` `none` |
|
|
22
|
+
| `card` | `data-fx-card` | `spotlight` `orbit` `none` |
|
|
23
|
+
|
|
24
|
+
Loaders aren't a page setting. They're component props: Spinner, Progress, Skeleton and Button
|
|
25
|
+
`loadingStyle`.
|
|
26
|
+
|
|
27
|
+
All effects take their colours from the theme, fall back to a static state change under
|
|
28
|
+
`prefers-reduced-motion: reduce`, only animate hover on fine pointers, and never shift layout.
|
|
29
|
+
|
|
30
|
+
## Persisted settings
|
|
31
|
+
|
|
32
|
+
```svelte
|
|
33
|
+
<script>
|
|
34
|
+
import { FX_PREPAINT, MotionSettings, fx } from '@dorsk/tsumikit';
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<svelte:head>{@html `<script>${FX_PREPAINT}</` + `script>`}</svelte:head>
|
|
38
|
+
|
|
39
|
+
<MotionSettings size="sm" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `fx` holds the settings (`fx.settings`, `fx.get(family)`, `fx.set(family, variant)`, `fx.reset()`), stores
|
|
43
|
+
the non-default families in localStorage under `tsumikit-fx` and writes `data-fx-*` on `<html>`. Defaults
|
|
44
|
+
leave the attribute absent.
|
|
45
|
+
- `FX_PREPAINT` paints the saved attributes before first paint. Inline it in `<head>` next to
|
|
46
|
+
`THEME_PREPAINT`.
|
|
47
|
+
- `fx.onchange` and `fx.hydrate(settings)` let an app keep the settings in its own (server-side) store.
|
|
48
|
+
- `FX`, `FX_FAMILIES`, `DEFAULT_FX` and `parseFx` expose the registry for custom controls.
|
|
49
|
+
|
|
50
|
+
## Writing an effect
|
|
51
|
+
|
|
52
|
+
Scope every variant with `:where()` so the selector keeps zero specificity. Give the first variant the
|
|
53
|
+
`:not([…])` default:
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
:where(:root:not([data-fx-hover]), [data-fx-hover='smoke']) .btn-primary::before { … }
|
|
57
|
+
:where([data-fx-hover='ember']) .btn-primary::before { … }
|
|
58
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { default as Swatch } from './components/atoms/Swatch.svelte';
|
|
|
31
31
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
|
32
32
|
export { default as Text } from './components/atoms/Text.svelte';
|
|
33
33
|
export { default as Textarea } from './components/atoms/Textarea.svelte';
|
|
34
|
+
export { default as Thinking } from './components/atoms/Thinking.svelte';
|
|
34
35
|
export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
|
|
35
36
|
export { default as AppShell } from './components/layouts/AppShell.svelte';
|
|
36
37
|
export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
@@ -74,10 +75,12 @@ export { default as IconButton } from './components/molecules/IconButton.svelte'
|
|
|
74
75
|
export { default as InputGroup } from './components/molecules/InputGroup.svelte';
|
|
75
76
|
export { default as KeyValue, type KeyValueRow, type KeyValueTone, } from './components/molecules/KeyValue.svelte';
|
|
76
77
|
export { default as LoadMore } from './components/molecules/LoadMore.svelte';
|
|
78
|
+
export { default as LookPicker } from './components/molecules/LookPicker.svelte';
|
|
77
79
|
export { default as Menu, type MenuItem } from './components/molecules/Menu.svelte';
|
|
78
80
|
export { default as Meter } from './components/molecules/Meter.svelte';
|
|
79
81
|
export { default as Metric, default as StatTile, type MetricSegment, } from './components/molecules/Metric.svelte';
|
|
80
82
|
export { default as Modal } from './components/molecules/Modal.svelte';
|
|
83
|
+
export { default as MotionSettings } from './components/molecules/MotionSettings.svelte';
|
|
81
84
|
export { default as OptionButton } from './components/molecules/OptionButton.svelte';
|
|
82
85
|
export { default as Pagination } from './components/molecules/Pagination.svelte';
|
|
83
86
|
export { default as Popover } from './components/molecules/Popover.svelte';
|
|
@@ -111,18 +114,21 @@ export { default as FilterSearchBar } from './components/organisms/FilterSearchB
|
|
|
111
114
|
export { formatCount, hasCount } from './count';
|
|
112
115
|
export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
|
|
113
116
|
export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
117
|
+
export { DEFAULT_FX, FX, FX_FAMILIES, type FxFamily, type FxSettings, type FxVariant, fxAttr, fxDiff, fxPrepaint, isFxVariant, parseFx, } from './fx';
|
|
114
118
|
export { escapeHtml, type Highlight, type RenderMarkdownOptions, renderMarkdown, stripAnsi, } from './markdown';
|
|
115
119
|
export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
|
|
120
|
+
export { trackPointer } from './pointer';
|
|
116
121
|
export * as filterQuery from './query';
|
|
117
122
|
export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
|
|
118
123
|
export { type OptionSection, sectionOptions } from './select-options';
|
|
119
124
|
export type { ControlSize } from './size';
|
|
120
125
|
export { playSound, type SoundMuted, type SoundName } from './sound';
|
|
121
126
|
export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
|
|
127
|
+
export { FX_PREPAINT, fx } from './stores/fx.svelte';
|
|
122
128
|
export { sound } from './stores/sound.svelte';
|
|
123
|
-
export { type Mode, THEMES, theme } from './stores/theme.svelte';
|
|
129
|
+
export { type Mode, THEME_PREPAINT, THEMES, theme } from './stores/theme.svelte';
|
|
124
130
|
export { type Toast, type ToastAction, type ToastOptions, type ToastTone, type ToastToneInput, toasts, } from './stores/toast.svelte';
|
|
125
|
-
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, } from './theme-mode';
|
|
131
|
+
export { AUTO_THEME, chooseTheme, DEFAULT_LOOK, DEFAULT_THEME_PREFERENCE, LOOKS, type LookId, type LookStatus, parseLook, pickerValue, preferenceFrom, resolveTheme, type SlotOf, type ThemeChoice, type ThemePreference, type ThemeSlot, themePrepaint, } from './theme-mode';
|
|
126
132
|
export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
|
|
127
133
|
export { canonicalTone, type Tone } from './tone';
|
|
128
134
|
export { pathCandidates, type TruncateMode, type TruncateOptions, truncate } from './truncate';
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ export { default as Switch } from './components/atoms/Switch.svelte';
|
|
|
34
34
|
// ---- atoms ----
|
|
35
35
|
export { default as Text } from './components/atoms/Text.svelte';
|
|
36
36
|
export { default as Textarea } from './components/atoms/Textarea.svelte';
|
|
37
|
+
export { default as Thinking } from './components/atoms/Thinking.svelte';
|
|
37
38
|
export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
|
|
38
39
|
// ---- layouts ----
|
|
39
40
|
export { default as AppShell } from './components/layouts/AppShell.svelte';
|
|
@@ -79,12 +80,14 @@ export { default as IconButton } from './components/molecules/IconButton.svelte'
|
|
|
79
80
|
export { default as InputGroup } from './components/molecules/InputGroup.svelte';
|
|
80
81
|
export { default as KeyValue, } from './components/molecules/KeyValue.svelte';
|
|
81
82
|
export { default as LoadMore } from './components/molecules/LoadMore.svelte';
|
|
83
|
+
export { default as LookPicker } from './components/molecules/LookPicker.svelte';
|
|
82
84
|
export { default as Menu } from './components/molecules/Menu.svelte';
|
|
83
85
|
export { default as Meter } from './components/molecules/Meter.svelte';
|
|
84
86
|
export { default as Metric,
|
|
85
87
|
// StatTile is an alias for Metric — same component, dashboard-friendly name.
|
|
86
88
|
default as StatTile, } from './components/molecules/Metric.svelte';
|
|
87
89
|
export { default as Modal } from './components/molecules/Modal.svelte';
|
|
90
|
+
export { default as MotionSettings } from './components/molecules/MotionSettings.svelte';
|
|
88
91
|
export { default as OptionButton } from './components/molecules/OptionButton.svelte';
|
|
89
92
|
export { default as Pagination } from './components/molecules/Pagination.svelte';
|
|
90
93
|
export { default as Popover } from './components/molecules/Popover.svelte';
|
|
@@ -119,19 +122,22 @@ export { default as FilterSearchBar } from './components/organisms/FilterSearchB
|
|
|
119
122
|
export { formatCount, hasCount } from './count';
|
|
120
123
|
export { EMOJI_GROUPS, searchEmoji } from './emoji';
|
|
121
124
|
export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
125
|
+
export { DEFAULT_FX, FX, FX_FAMILIES, fxAttr, fxDiff, fxPrepaint, isFxVariant, parseFx, } from './fx';
|
|
122
126
|
export { escapeHtml, renderMarkdown, stripAnsi, } from './markdown';
|
|
123
127
|
export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
|
|
128
|
+
export { trackPointer } from './pointer';
|
|
124
129
|
// ---- query core (headless: schema / parser / AST / suggest / compilers) ----
|
|
125
130
|
export * as filterQuery from './query';
|
|
126
131
|
export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
|
|
127
132
|
export { sectionOptions } from './select-options';
|
|
128
133
|
export { playSound } from './sound';
|
|
129
134
|
export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
|
|
135
|
+
export { FX_PREPAINT, fx } from './stores/fx.svelte';
|
|
130
136
|
// ---- stores / actions ----
|
|
131
137
|
export { sound } from './stores/sound.svelte';
|
|
132
|
-
export { THEMES, theme } from './stores/theme.svelte';
|
|
138
|
+
export { THEME_PREPAINT, THEMES, theme } from './stores/theme.svelte';
|
|
133
139
|
export { toasts, } from './stores/toast.svelte';
|
|
134
|
-
export { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, pickerValue, preferenceFrom, resolveTheme, } from './theme-mode';
|
|
140
|
+
export { AUTO_THEME, chooseTheme, DEFAULT_LOOK, DEFAULT_THEME_PREFERENCE, LOOKS, parseLook, pickerValue, preferenceFrom, resolveTheme, themePrepaint, } from './theme-mode';
|
|
135
141
|
export { formatTimestamp, localTimeZone, relativeTime, } from './timestamp';
|
|
136
142
|
export { canonicalTone } from './tone';
|
|
137
143
|
export { pathCandidates, truncate } from './truncate';
|