@dorsk/tsumikit 0.73.2 → 0.75.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.
- package/README.md +38 -1
- package/dist/autoresize.d.ts +2 -0
- package/dist/autoresize.js +8 -1
- package/dist/components/atoms/Artwork.svelte +12 -5
- package/dist/components/atoms/Avatar.svelte +1 -1
- package/dist/components/atoms/Badge.svelte +59 -23
- package/dist/components/atoms/Button.svelte +130 -96
- package/dist/components/atoms/Button.svelte.d.ts +2 -0
- package/dist/components/atoms/Card.svelte +10 -4
- package/dist/components/atoms/Checkbox.svelte +36 -9
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Gauge.svelte +20 -14
- package/dist/components/atoms/Heading.svelte +1 -1
- package/dist/components/atoms/Icon.svelte +1 -0
- package/dist/components/atoms/Input.svelte +67 -13
- package/dist/components/atoms/Kbd.svelte +3 -3
- package/dist/components/atoms/Link.svelte +7 -2
- package/dist/components/atoms/Progress.svelte +12 -6
- package/dist/components/atoms/Scrim.svelte +3 -8
- package/dist/components/atoms/SegmentedProgress.svelte +5 -4
- package/dist/components/atoms/Select.svelte +58 -27
- package/dist/components/atoms/Skeleton.svelte +14 -8
- package/dist/components/atoms/Slider.svelte +79 -22
- package/dist/components/atoms/Slider.svelte.d.ts +2 -0
- package/dist/components/atoms/Spinner.svelte +464 -5
- package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
- package/dist/components/atoms/Swatch.svelte +19 -8
- package/dist/components/atoms/Switch.svelte +34 -21
- package/dist/components/atoms/Switch.svelte.d.ts +2 -0
- package/dist/components/atoms/Text.svelte +1 -1
- package/dist/components/atoms/Textarea.svelte +34 -12
- package/dist/components/atoms/TimeDisplay.svelte +4 -2
- package/dist/components/layouts/AppShell.svelte +26 -15
- package/dist/components/layouts/MasterDetail.svelte +23 -21
- package/dist/components/layouts/NavBar.svelte +11 -8
- package/dist/components/layouts/NavItem.svelte +15 -7
- package/dist/components/layouts/NavSection.svelte +1 -1
- package/dist/components/layouts/ResizablePanel.svelte +37 -16
- package/dist/components/layouts/TabBar.svelte +24 -14
- package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
- package/dist/components/molecules/Accordion.svelte +4 -0
- package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
- package/dist/components/molecules/AttachmentList.svelte +14 -11
- package/dist/components/molecules/Breadcrumb.svelte +13 -9
- package/dist/components/molecules/Callout.svelte +3 -3
- package/dist/components/molecules/CapBar.svelte +23 -22
- package/dist/components/molecules/Carousel.svelte +27 -17
- package/dist/components/molecules/ChatBubble.svelte +2 -2
- package/dist/components/molecules/CodeBlock.svelte +2 -2
- package/dist/components/molecules/Combobox.svelte +30 -1
- package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
- package/dist/components/molecules/Composer.svelte +2 -2
- package/dist/components/molecules/ConfirmModal.svelte +4 -0
- package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
- package/dist/components/molecules/CopyButton.svelte +46 -2
- package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Disclosure.svelte +23 -12
- package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
- package/dist/components/molecules/Drawer.svelte +43 -23
- package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
- package/dist/components/molecules/Dropzone.svelte +24 -8
- package/dist/components/molecules/EmojiPicker.svelte +27 -8
- package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
- package/dist/components/molecules/EmptyState.svelte +5 -5
- package/dist/components/molecules/Fieldset.svelte +8 -13
- package/dist/components/molecules/FileButton.svelte +57 -31
- package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
- package/dist/components/molecules/FilterInput.svelte +51 -11
- package/dist/components/molecules/FontScalePicker.svelte +20 -9
- package/dist/components/molecules/GitRef.svelte +9 -7
- package/dist/components/molecules/GlyphStack.svelte +38 -14
- package/dist/components/molecules/IconButton.svelte +6 -3
- package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
- package/dist/components/molecules/InputGroup.svelte +13 -12
- package/dist/components/molecules/LoadMore.svelte +6 -1
- package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
- package/dist/components/molecules/Menu.svelte +33 -7
- package/dist/components/molecules/Menu.svelte.d.ts +2 -0
- package/dist/components/molecules/Metric.svelte +11 -9
- package/dist/components/molecules/Modal.svelte +76 -26
- package/dist/components/molecules/Modal.svelte.d.ts +2 -0
- package/dist/components/molecules/OptionButton.svelte +28 -10
- package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
- package/dist/components/molecules/Pagination.svelte +26 -9
- package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
- package/dist/components/molecules/Popover.svelte +107 -51
- package/dist/components/molecules/Popover.svelte.d.ts +2 -0
- package/dist/components/molecules/Prose.svelte +1 -1
- package/dist/components/molecules/PullToRefresh.svelte +14 -10
- package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
- package/dist/components/molecules/RadialMenu.svelte +52 -19
- package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
- package/dist/components/molecules/RadioGroup.svelte +38 -23
- package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/molecules/SectionHeader.svelte +13 -8
- package/dist/components/molecules/SeekSlider.svelte +38 -27
- package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
- package/dist/components/molecules/SegmentedControl.svelte +139 -86
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
- package/dist/components/molecules/SelectButton.svelte +21 -6
- package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
- package/dist/components/molecules/SortableList.svelte +10 -11
- package/dist/components/molecules/SoundSettings.svelte +161 -0
- package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
- package/dist/components/molecules/SplitButton.svelte +30 -3
- package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
- package/dist/components/molecules/SwipeRow.svelte +18 -15
- package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
- package/dist/components/molecules/Tabs.svelte +92 -10
- package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
- package/dist/components/molecules/ThemePicker.svelte +25 -10
- package/dist/components/molecules/Timestamp.svelte +12 -7
- package/dist/components/molecules/Toaster.svelte +65 -14
- package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
- package/dist/components/molecules/Toggle.svelte +36 -13
- package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
- package/dist/components/molecules/Tooltip.svelte +41 -10
- package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
- package/dist/components/molecules/VolumeSlider.svelte +34 -14
- package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
- package/dist/components/molecules/WorkingDir.svelte +1 -0
- package/dist/components/organisms/ConversationFeed.svelte +4 -0
- package/dist/components/organisms/DataTable.svelte +37 -14
- package/dist/floating.js +26 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -1
- package/dist/sound-synth.d.ts +3 -0
- package/dist/sound-synth.js +91 -0
- package/dist/sound.d.ts +1 -0
- package/dist/sound.js +3 -0
- package/dist/stores/sound.svelte.d.ts +20 -0
- package/dist/stores/sound.svelte.js +150 -0
- package/dist/stores/toast.svelte.d.ts +9 -0
- package/dist/stores/toast.svelte.js +45 -9
- package/dist/styles/reset.css +19 -2
- package/dist/styles/tokens.css +89 -6
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const PALETTE = {
|
|
2
|
+
click: [
|
|
3
|
+
['n', 3000, 0, 0, 0.012, 0.18],
|
|
4
|
+
['sine', 620, 380, 0, 0.04, 0.22],
|
|
5
|
+
],
|
|
6
|
+
'toggle-on': [
|
|
7
|
+
['n', 2600, 0, 0, 0.012, 0.3],
|
|
8
|
+
['sine', 520, 520, 0, 0.03, 0.14],
|
|
9
|
+
['triangle', 660, 880, 0.035, 0.05, 0.3],
|
|
10
|
+
],
|
|
11
|
+
'toggle-off': [
|
|
12
|
+
['n', 2600, 0, 0, 0.012, 0.3],
|
|
13
|
+
['sine', 520, 520, 0, 0.03, 0.14],
|
|
14
|
+
['triangle', 880, 620, 0.035, 0.05, 0.3],
|
|
15
|
+
],
|
|
16
|
+
open: [['sine', 420, 760, 0, 0.09, 0.25]],
|
|
17
|
+
close: [['sine', 700, 380, 0, 0.08, 0.25]],
|
|
18
|
+
success: [
|
|
19
|
+
['sine', 698, 698, 0, 0.055, 0.25],
|
|
20
|
+
['sine', 1047, 1047, 0.058, 0.085, 0.2],
|
|
21
|
+
],
|
|
22
|
+
error: [
|
|
23
|
+
['triangle', 330, 300, 0, 0.065, 0.28],
|
|
24
|
+
['triangle', 262, 240, 0.06, 0.08, 0.28],
|
|
25
|
+
],
|
|
26
|
+
tick: [
|
|
27
|
+
['n', 3800, 0, 0, 0.008, 0.3],
|
|
28
|
+
['sine', 2400, 1900, 0, 0.012, 0.14],
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
let ctx;
|
|
32
|
+
let out;
|
|
33
|
+
let noise;
|
|
34
|
+
function context() {
|
|
35
|
+
ctx ??= new AudioContext();
|
|
36
|
+
if (ctx.state === 'suspended')
|
|
37
|
+
ctx.resume().catch(() => { });
|
|
38
|
+
return ctx;
|
|
39
|
+
}
|
|
40
|
+
// Call synchronously inside a user gesture so Safari lets the context start.
|
|
41
|
+
export function unlock() {
|
|
42
|
+
context();
|
|
43
|
+
}
|
|
44
|
+
export function play(name, volume) {
|
|
45
|
+
const voices = PALETTE[name];
|
|
46
|
+
if (!voices)
|
|
47
|
+
return;
|
|
48
|
+
const c = context();
|
|
49
|
+
if (!out) {
|
|
50
|
+
out = c.createGain();
|
|
51
|
+
out.connect(c.destination);
|
|
52
|
+
}
|
|
53
|
+
out.gain.value = volume;
|
|
54
|
+
const t = c.currentTime + 0.005;
|
|
55
|
+
for (const [wave, f0, f1, at, d, g] of voices) {
|
|
56
|
+
const env = c.createGain();
|
|
57
|
+
const s = t + at;
|
|
58
|
+
env.gain.setValueAtTime(0.0001, s);
|
|
59
|
+
env.gain.exponentialRampToValueAtTime(g, s + 0.004);
|
|
60
|
+
env.gain.exponentialRampToValueAtTime(0.0001, s + d);
|
|
61
|
+
env.connect(out);
|
|
62
|
+
let src;
|
|
63
|
+
if (wave === 'n') {
|
|
64
|
+
if (!noise) {
|
|
65
|
+
noise = c.createBuffer(1, c.sampleRate * 0.05, c.sampleRate);
|
|
66
|
+
const data = noise.getChannelData(0);
|
|
67
|
+
for (let i = 0; i < data.length; i++)
|
|
68
|
+
data[i] = Math.random() * 2 - 1;
|
|
69
|
+
}
|
|
70
|
+
const buf = c.createBufferSource();
|
|
71
|
+
buf.buffer = noise;
|
|
72
|
+
const bp = c.createBiquadFilter();
|
|
73
|
+
bp.type = 'bandpass';
|
|
74
|
+
bp.frequency.value = f0;
|
|
75
|
+
bp.Q.value = 1.2;
|
|
76
|
+
buf.connect(bp).connect(env);
|
|
77
|
+
src = buf;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const osc = c.createOscillator();
|
|
81
|
+
osc.type = wave;
|
|
82
|
+
osc.frequency.setValueAtTime(f0, s);
|
|
83
|
+
osc.frequency.exponentialRampToValueAtTime(f1, s + d);
|
|
84
|
+
osc.connect(env);
|
|
85
|
+
src = osc;
|
|
86
|
+
}
|
|
87
|
+
src.start(s);
|
|
88
|
+
src.stop(s + d + 0.02);
|
|
89
|
+
src.onended = () => env.disconnect();
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/sound.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { playSound, type SoundMuted, type SoundName } from './stores/sound.svelte';
|
package/dist/sound.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SoundName = 'click' | 'toggle-on' | 'toggle-off' | 'open' | 'close' | 'success' | 'error' | 'tick';
|
|
2
|
+
export type SoundMuted = 'off' | 'reduced-motion' | null;
|
|
3
|
+
/**
|
|
4
|
+
* Play a UI sound if the user enabled sounds. `allow` is the per-component
|
|
5
|
+
* opt-out (`sound={false}`). Safe to call during SSR: it is a no-op there.
|
|
6
|
+
*/
|
|
7
|
+
export declare function playSound(name: SoundName, allow?: boolean | undefined): void;
|
|
8
|
+
declare class Sound {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
volume: number;
|
|
11
|
+
constructor();
|
|
12
|
+
/** Why nothing would play right now, for a settings hint. */
|
|
13
|
+
get muted(): SoundMuted;
|
|
14
|
+
setEnabled(on: boolean): void;
|
|
15
|
+
setVolume(v: number): void;
|
|
16
|
+
play(name: SoundName): void;
|
|
17
|
+
private save;
|
|
18
|
+
}
|
|
19
|
+
export declare const sound: Sound;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { browser } from '../env';
|
|
2
|
+
import { readStorage, writeStorage } from './storage';
|
|
3
|
+
// OPT-IN UI sound effects. Off by default and persisted; while off nothing is
|
|
4
|
+
// imported, no AudioContext exists and no listener is attached. The synth is a
|
|
5
|
+
// separate chunk loaded on first play (or eagerly when enabling, so a gesture
|
|
6
|
+
// listener can unlock audio for Safari).
|
|
7
|
+
//
|
|
8
|
+
// With `prefers-reduced-motion: reduce` only the feedback sounds (success,
|
|
9
|
+
// error) play: they carry information, the decorative ones are muted.
|
|
10
|
+
// Calls inside one frame are coalesced and the highest priority wins
|
|
11
|
+
// (error > success > open/close > toggle > click > tick); the same sound plays
|
|
12
|
+
// at most once per 50 ms (tick: 40 ms). Hidden tabs are silent.
|
|
13
|
+
const KEY = 'tsumikit-sound';
|
|
14
|
+
const DEFAULT_VOLUME = 0.5;
|
|
15
|
+
// Lowest to highest priority; the last two are feedback and survive reduced motion.
|
|
16
|
+
const ORDER = [
|
|
17
|
+
'tick',
|
|
18
|
+
'click',
|
|
19
|
+
'toggle-on',
|
|
20
|
+
'toggle-off',
|
|
21
|
+
'open',
|
|
22
|
+
'close',
|
|
23
|
+
'success',
|
|
24
|
+
'error',
|
|
25
|
+
];
|
|
26
|
+
const GAP_MS = 50;
|
|
27
|
+
const TICK_GAP_MS = 40;
|
|
28
|
+
let synth = null;
|
|
29
|
+
let loading = null;
|
|
30
|
+
let armed = false;
|
|
31
|
+
let pending = null;
|
|
32
|
+
const last = new Map();
|
|
33
|
+
function clamp(v, fallback) {
|
|
34
|
+
return Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : fallback;
|
|
35
|
+
}
|
|
36
|
+
function reducedMotion() {
|
|
37
|
+
return !!globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
38
|
+
}
|
|
39
|
+
function load() {
|
|
40
|
+
loading ??= import('../sound-synth').then((m) => (synth = m));
|
|
41
|
+
return loading;
|
|
42
|
+
}
|
|
43
|
+
function unlock() {
|
|
44
|
+
if (!synth)
|
|
45
|
+
return;
|
|
46
|
+
synth.unlock();
|
|
47
|
+
listen(false);
|
|
48
|
+
}
|
|
49
|
+
function listen(on) {
|
|
50
|
+
if (armed === on)
|
|
51
|
+
return;
|
|
52
|
+
armed = on;
|
|
53
|
+
for (const type of ['pointerdown', 'keydown']) {
|
|
54
|
+
if (on)
|
|
55
|
+
addEventListener(type, unlock, { capture: true });
|
|
56
|
+
else
|
|
57
|
+
removeEventListener(type, unlock, { capture: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function flush() {
|
|
61
|
+
const name = pending;
|
|
62
|
+
pending = null;
|
|
63
|
+
if (!name || !sound.enabled)
|
|
64
|
+
return;
|
|
65
|
+
const now = performance.now();
|
|
66
|
+
if (now - (last.get(name) ?? -1e9) < (name === 'tick' ? TICK_GAP_MS : GAP_MS))
|
|
67
|
+
return;
|
|
68
|
+
last.set(name, now);
|
|
69
|
+
const volume = sound.volume;
|
|
70
|
+
if (synth)
|
|
71
|
+
synth.play(name, volume);
|
|
72
|
+
else
|
|
73
|
+
load().then((m) => m.play(name, volume), () => { });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Play a UI sound if the user enabled sounds. `allow` is the per-component
|
|
77
|
+
* opt-out (`sound={false}`). Safe to call during SSR: it is a no-op there.
|
|
78
|
+
*/
|
|
79
|
+
export function playSound(name, allow = true) {
|
|
80
|
+
if (!browser || allow === false || !sound.enabled || sound.volume <= 0)
|
|
81
|
+
return;
|
|
82
|
+
if (document.hidden)
|
|
83
|
+
return;
|
|
84
|
+
if (navigator.userActivation && !navigator.userActivation.hasBeenActive)
|
|
85
|
+
return;
|
|
86
|
+
const rank = ORDER.indexOf(name);
|
|
87
|
+
if (rank < ORDER.length - 2 && reducedMotion())
|
|
88
|
+
return;
|
|
89
|
+
if (pending === null) {
|
|
90
|
+
requestAnimationFrame(flush);
|
|
91
|
+
pending = name;
|
|
92
|
+
}
|
|
93
|
+
else if (rank > ORDER.indexOf(pending))
|
|
94
|
+
pending = name;
|
|
95
|
+
}
|
|
96
|
+
class Sound {
|
|
97
|
+
enabled = $state(false);
|
|
98
|
+
volume = $state(DEFAULT_VOLUME);
|
|
99
|
+
constructor() {
|
|
100
|
+
if (!browser)
|
|
101
|
+
return;
|
|
102
|
+
const raw = readStorage(KEY);
|
|
103
|
+
if (raw) {
|
|
104
|
+
try {
|
|
105
|
+
const saved = JSON.parse(raw);
|
|
106
|
+
if (typeof saved?.enabled === 'boolean')
|
|
107
|
+
this.enabled = saved.enabled;
|
|
108
|
+
if (typeof saved?.volume === 'number')
|
|
109
|
+
this.volume = clamp(saved.volume, DEFAULT_VOLUME);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
if (this.enabled) {
|
|
114
|
+
load().catch(() => { });
|
|
115
|
+
listen(true);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Why nothing would play right now, for a settings hint. */
|
|
119
|
+
get muted() {
|
|
120
|
+
if (!this.enabled)
|
|
121
|
+
return 'off';
|
|
122
|
+
return reducedMotion() ? 'reduced-motion' : null;
|
|
123
|
+
}
|
|
124
|
+
setEnabled(on) {
|
|
125
|
+
this.enabled = on;
|
|
126
|
+
this.save();
|
|
127
|
+
if (!browser)
|
|
128
|
+
return;
|
|
129
|
+
if (on) {
|
|
130
|
+
load().catch(() => { });
|
|
131
|
+
listen(true);
|
|
132
|
+
unlock();
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
listen(false);
|
|
136
|
+
}
|
|
137
|
+
setVolume(v) {
|
|
138
|
+
this.volume = clamp(v, this.volume);
|
|
139
|
+
this.save();
|
|
140
|
+
}
|
|
141
|
+
play(name) {
|
|
142
|
+
playSound(name);
|
|
143
|
+
}
|
|
144
|
+
save() {
|
|
145
|
+
if (!browser)
|
|
146
|
+
return;
|
|
147
|
+
writeStorage(KEY, JSON.stringify({ enabled: this.enabled, volume: this.volume }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export const sound = new Sound();
|
|
@@ -13,6 +13,8 @@ export interface Toast {
|
|
|
13
13
|
action?: ToastAction;
|
|
14
14
|
/** True while `action.run` is in flight; the Toaster shows the button loading. */
|
|
15
15
|
pending: boolean;
|
|
16
|
+
/** Dismissed: the Toaster plays the exit while the store holds it for `TOAST_EXIT_MS`. */
|
|
17
|
+
leaving: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface ToastOptions {
|
|
18
20
|
tone?: ToastToneInput;
|
|
@@ -22,14 +24,21 @@ export interface ToastOptions {
|
|
|
22
24
|
export declare const TOAST_MS = 4000;
|
|
23
25
|
/** Toasts with an action linger longer: the action is only reachable while visible. */
|
|
24
26
|
export declare const ACTION_TOAST_MS = 7000;
|
|
27
|
+
/** Matches `--dur-exit-slow`; the removal never waits on `transitionend` alone. */
|
|
28
|
+
export declare const TOAST_EXIT_MS = 200;
|
|
25
29
|
declare class Toasts {
|
|
26
30
|
#private;
|
|
27
31
|
items: Toast[];
|
|
28
32
|
show(message: string, opts?: ToastOptions): number;
|
|
33
|
+
/** Hold the auto-dismiss (pointer over the toast); `resume` restarts the remaining time. */
|
|
34
|
+
pause(id: number): void;
|
|
35
|
+
resume(id: number): void;
|
|
29
36
|
ok(message: string, duration?: number, action?: ToastAction): number;
|
|
30
37
|
error(message: string, duration?: number, action?: ToastAction): number;
|
|
31
38
|
info(message: string, duration?: number, action?: ToastAction): number;
|
|
32
39
|
dismiss(id: number): void;
|
|
40
|
+
/** Drop a toast at once, skipping the exit. */
|
|
41
|
+
remove(id: number): void;
|
|
33
42
|
/** Run a toast's action once: the toast stays (button loading) until run settles, then dismisses. */
|
|
34
43
|
act(id: number): Promise<void>;
|
|
35
44
|
}
|
|
@@ -5,22 +5,54 @@
|
|
|
5
5
|
export const TOAST_MS = 4000;
|
|
6
6
|
/** Toasts with an action linger longer: the action is only reachable while visible. */
|
|
7
7
|
export const ACTION_TOAST_MS = 7000;
|
|
8
|
+
/** Matches `--dur-exit-slow`; the removal never waits on `transitionend` alone. */
|
|
9
|
+
export const TOAST_EXIT_MS = 200;
|
|
8
10
|
let seq = 0;
|
|
9
11
|
class Toasts {
|
|
10
12
|
items = $state([]);
|
|
11
13
|
#timers = new Map();
|
|
14
|
+
#due = new Map();
|
|
15
|
+
#paused = new Map();
|
|
16
|
+
#arm(id, ms) {
|
|
17
|
+
this.#due.set(id, Date.now() + ms);
|
|
18
|
+
this.#timers.set(id, setTimeout(() => this.dismiss(id), ms));
|
|
19
|
+
}
|
|
20
|
+
#disarm(id) {
|
|
21
|
+
const timer = this.#timers.get(id);
|
|
22
|
+
if (timer)
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
this.#timers.delete(id);
|
|
25
|
+
this.#due.delete(id);
|
|
26
|
+
this.#paused.delete(id);
|
|
27
|
+
}
|
|
12
28
|
show(message, opts = {}) {
|
|
13
29
|
const id = ++seq;
|
|
14
30
|
const duration = opts.duration ?? (opts.action ? ACTION_TOAST_MS : TOAST_MS);
|
|
15
31
|
const tone = opts.tone === 'danger' ? 'error' : (opts.tone ?? 'neutral');
|
|
16
32
|
this.items = [
|
|
17
33
|
...this.items,
|
|
18
|
-
{ id, message, tone, duration, action: opts.action, pending: false },
|
|
34
|
+
{ id, message, tone, duration, action: opts.action, pending: false, leaving: false },
|
|
19
35
|
];
|
|
20
36
|
if (duration > 0)
|
|
21
|
-
this.#
|
|
37
|
+
this.#arm(id, duration);
|
|
22
38
|
return id;
|
|
23
39
|
}
|
|
40
|
+
/** Hold the auto-dismiss (pointer over the toast); `resume` restarts the remaining time. */
|
|
41
|
+
pause(id) {
|
|
42
|
+
const due = this.#due.get(id);
|
|
43
|
+
if (due === undefined)
|
|
44
|
+
return;
|
|
45
|
+
const left = Math.max(0, due - Date.now());
|
|
46
|
+
this.#disarm(id);
|
|
47
|
+
this.#paused.set(id, left);
|
|
48
|
+
}
|
|
49
|
+
resume(id) {
|
|
50
|
+
const left = this.#paused.get(id);
|
|
51
|
+
this.#paused.delete(id);
|
|
52
|
+
if (left === undefined || !this.items.some((t) => t.id === id && !t.leaving))
|
|
53
|
+
return;
|
|
54
|
+
this.#arm(id, left);
|
|
55
|
+
}
|
|
24
56
|
ok(message, duration, action) {
|
|
25
57
|
return this.show(message, { tone: 'ok', duration, action });
|
|
26
58
|
}
|
|
@@ -31,10 +63,16 @@ class Toasts {
|
|
|
31
63
|
return this.show(message, { tone: 'info', duration, action });
|
|
32
64
|
}
|
|
33
65
|
dismiss(id) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
this.#disarm(id);
|
|
67
|
+
const t = this.items.find((x) => x.id === id);
|
|
68
|
+
if (!t || t.leaving)
|
|
69
|
+
return;
|
|
70
|
+
this.items = this.items.map((x) => (x.id === id ? { ...x, leaving: true } : x));
|
|
71
|
+
setTimeout(() => this.remove(id), TOAST_EXIT_MS);
|
|
72
|
+
}
|
|
73
|
+
/** Drop a toast at once, skipping the exit. */
|
|
74
|
+
remove(id) {
|
|
75
|
+
this.#disarm(id);
|
|
38
76
|
this.items = this.items.filter((t) => t.id !== id);
|
|
39
77
|
}
|
|
40
78
|
/** Run a toast's action once: the toast stays (button loading) until run settles, then dismisses. */
|
|
@@ -42,9 +80,7 @@ class Toasts {
|
|
|
42
80
|
const t = this.items.find((x) => x.id === id);
|
|
43
81
|
if (!t?.action || t.pending)
|
|
44
82
|
return;
|
|
45
|
-
|
|
46
|
-
if (timer)
|
|
47
|
-
clearTimeout(timer);
|
|
83
|
+
this.#disarm(id);
|
|
48
84
|
this.items = this.items.map((x) => (x.id === id ? { ...x, pending: true } : x));
|
|
49
85
|
try {
|
|
50
86
|
await t.action.run();
|
package/dist/styles/reset.css
CHANGED
|
@@ -106,14 +106,31 @@ pre {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/* Reduced motion: fewer and gentler, not zero. Movement tokens go neutral and
|
|
110
|
+
transitions are limited to colour/opacity, so feedback still fades while
|
|
111
|
+
nothing moves. */
|
|
109
112
|
@media (prefers-reduced-motion: reduce) {
|
|
113
|
+
:root {
|
|
114
|
+
--press-scale: 1;
|
|
115
|
+
--press-scale-sm: 1;
|
|
116
|
+
--press-scale-lg: 1;
|
|
117
|
+
--enter-scale: 1;
|
|
118
|
+
--tip-scale: 1;
|
|
119
|
+
--shift-xs: 0px;
|
|
120
|
+
--shift-sm: 0px;
|
|
121
|
+
--shift-md: 0px;
|
|
122
|
+
--ease-spring: var(--ease-out);
|
|
123
|
+
--ease-spring-bounce: var(--ease-out);
|
|
124
|
+
}
|
|
110
125
|
*,
|
|
111
126
|
*::before,
|
|
112
127
|
*::after {
|
|
128
|
+
scroll-behavior: auto !important;
|
|
113
129
|
animation-duration: 0.01ms !important;
|
|
114
130
|
animation-iteration-count: 1 !important;
|
|
115
|
-
transition-
|
|
116
|
-
|
|
131
|
+
transition-property: opacity, color, background-color, border-color, outline-color, box-shadow, fill, stroke, text-decoration-color, visibility, display, overlay !important;
|
|
132
|
+
transition-duration: var(--dur-fast) !important;
|
|
133
|
+
transition-delay: 0ms !important;
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
package/dist/styles/tokens.css
CHANGED
|
@@ -123,6 +123,9 @@
|
|
|
123
123
|
ballooning UI. Default 1 = no scaling. */
|
|
124
124
|
--fs-scale: 1;
|
|
125
125
|
|
|
126
|
+
/* Type scale. 2xs is the floor (never go below 11px). 13 / 15 / 16 are
|
|
127
|
+
"UI small / UI / body": no more steps between them. */
|
|
128
|
+
--fs-2xs: calc(0.6875rem * var(--fs-scale)); /* 11 */
|
|
126
129
|
--fs-xs: calc(0.75rem * var(--fs-scale)); /* 12 */
|
|
127
130
|
--fs-sm: calc(0.8125rem * var(--fs-scale)); /* 13 */
|
|
128
131
|
--fs-base: calc(0.9375rem * var(--fs-scale)); /* 15 */
|
|
@@ -130,18 +133,28 @@
|
|
|
130
133
|
--fs-lg: calc(1.125rem * var(--fs-scale)); /* 18 */
|
|
131
134
|
--fs-xl: calc(1.375rem * var(--fs-scale)); /* 22 */
|
|
132
135
|
--fs-2xl: calc(1.75rem * var(--fs-scale)); /* 28 */
|
|
136
|
+
--fs-3xl: calc(2.25rem * var(--fs-scale)); /* 36 */
|
|
133
137
|
|
|
134
138
|
--fw-normal: 400;
|
|
135
139
|
--fw-medium: 500;
|
|
136
140
|
--fw-semibold: 600;
|
|
137
141
|
--fw-bold: 700;
|
|
138
142
|
|
|
143
|
+
/* Leading paired to the scale: none = single-line controls and glyphs,
|
|
144
|
+
snug = chips / captions / tooltips (2xs–sm), tight = fields and labels
|
|
145
|
+
(sm–md), normal = body prose (md–lg), heading = xl and up. */
|
|
146
|
+
--lh-none: 1;
|
|
147
|
+
--lh-heading: 1.2;
|
|
139
148
|
--lh-tight: 1.25;
|
|
149
|
+
--lh-snug: 1.375;
|
|
140
150
|
--lh-normal: 1.5;
|
|
141
151
|
|
|
142
152
|
/* ---- Spacing (4px base) ---- */
|
|
143
153
|
--sp-0: 0;
|
|
154
|
+
--sp-px: 1px;
|
|
155
|
+
--sp-0-5: 0.125rem;
|
|
144
156
|
--sp-1: 0.25rem;
|
|
157
|
+
--sp-1-5: 0.375rem;
|
|
145
158
|
--sp-2: 0.5rem;
|
|
146
159
|
--sp-3: 0.75rem;
|
|
147
160
|
--sp-4: 1rem;
|
|
@@ -152,6 +165,8 @@
|
|
|
152
165
|
--sp-12: 3rem;
|
|
153
166
|
|
|
154
167
|
/* ---- Radii ---- */
|
|
168
|
+
/* Nested corners stay concentric: inner = outer - (border + padding). */
|
|
169
|
+
--r-xs: 2px;
|
|
155
170
|
--r-sm: 6px;
|
|
156
171
|
--r-md: 10px;
|
|
157
172
|
--r-lg: 14px;
|
|
@@ -170,16 +185,46 @@
|
|
|
170
185
|
--control-height-default: 2.5rem;
|
|
171
186
|
--control-height-large: 3rem;
|
|
172
187
|
--control-height: 2.75rem;
|
|
173
|
-
/*
|
|
188
|
+
/* Per-tier control contract (sm 32 / md 40 / lg 48): height, label size,
|
|
189
|
+
side padding, icon size. Text buttons get roomier sides via --btn-px-*. */
|
|
190
|
+
--control-sm: var(--control-height-compact);
|
|
191
|
+
--control-md: var(--control-height-default);
|
|
192
|
+
--control-lg: var(--control-height-large);
|
|
193
|
+
--control-fs-sm: var(--fs-xs);
|
|
194
|
+
--control-fs-md: var(--fs-sm);
|
|
195
|
+
--control-fs-lg: var(--fs-base);
|
|
196
|
+
--control-px-sm: var(--sp-2);
|
|
197
|
+
--control-px-md: var(--sp-3);
|
|
198
|
+
--control-px-lg: var(--sp-4);
|
|
199
|
+
--control-icon-sm: var(--icon-sm);
|
|
200
|
+
--control-icon-md: var(--icon-md);
|
|
201
|
+
--control-icon-lg: var(--icon-lg);
|
|
202
|
+
--btn-px-sm: var(--sp-3);
|
|
203
|
+
--btn-px-md: var(--sp-4);
|
|
204
|
+
--btn-px-lg: var(--sp-5);
|
|
205
|
+
/* Chip-class heights (Badge, Switch track, Checkbox box) on the 4px grid. */
|
|
206
|
+
--chip-xs: 1rem;
|
|
207
|
+
--chip-sm: 1.25rem;
|
|
208
|
+
--chip-md: 1.5rem;
|
|
209
|
+
/* Icon glyph sizes follow the text scale, not the chrome. */
|
|
210
|
+
--icon-xs: calc(0.75rem * var(--fs-scale));
|
|
211
|
+
--icon-sm: calc(0.875rem * var(--fs-scale));
|
|
212
|
+
--icon-md: calc(1rem * var(--fs-scale));
|
|
213
|
+
--icon-lg: calc(1.25rem * var(--fs-scale));
|
|
214
|
+
--icon-xl: calc(1.5rem * var(--fs-scale));
|
|
215
|
+
--disabled-opacity: 0.45;
|
|
216
|
+
/* Keyboard focus indicator shared by every interactive atom. `-inset` is for
|
|
217
|
+
rows and clipped containers that cannot draw outside their box. */
|
|
218
|
+
--focus-ring-width: 2px;
|
|
174
219
|
--focus-ring: 2px solid var(--accent);
|
|
175
|
-
--focus-ring-offset:
|
|
220
|
+
--focus-ring-offset: 2px;
|
|
221
|
+
--focus-ring-offset-inset: calc(-1 * var(--focus-ring-width));
|
|
176
222
|
/* Square box scale for icon-only controls (IconButton, Button `box`/`square`,
|
|
177
|
-
SelectButton, Popover trigger, CopyButton, FileButton). md
|
|
178
|
-
|
|
179
|
-
lines up with text controls in the same row. */
|
|
223
|
+
SelectButton, Popover trigger, CopyButton, FileButton). sm/md/lg match the
|
|
224
|
+
control heights so a boxed control lines up with text controls in a row. */
|
|
180
225
|
--box-xs: 1.5rem;
|
|
181
226
|
--box-sm: var(--control-height-compact);
|
|
182
|
-
--box-md: 2.
|
|
227
|
+
--box-md: 2.5rem;
|
|
183
228
|
--box-lg: var(--control-height-default);
|
|
184
229
|
/* Minimum hit area on coarse pointers (WCAG 2.5.8); px so it never shrinks
|
|
185
230
|
under a small root font-size. */
|
|
@@ -212,5 +257,43 @@
|
|
|
212
257
|
--safe-left: env(safe-area-inset-left, 0px);
|
|
213
258
|
--safe-right: env(safe-area-inset-right, 0px);
|
|
214
259
|
|
|
260
|
+
/* ---- Motion ---- */
|
|
215
261
|
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
262
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
263
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
264
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
265
|
+
--ease-spring: var(--ease-out);
|
|
266
|
+
--ease-spring-bounce: cubic-bezier(0.34, 1.36, 0.64, 1);
|
|
267
|
+
|
|
268
|
+
--dur-instant: 0ms;
|
|
269
|
+
--dur-press: 100ms;
|
|
270
|
+
--dur-fast: 120ms;
|
|
271
|
+
--dur-base: 180ms;
|
|
272
|
+
--dur-slow: 240ms;
|
|
273
|
+
--dur-sheet: 280ms;
|
|
274
|
+
--dur-exit-fast: 80ms;
|
|
275
|
+
--dur-exit: 140ms;
|
|
276
|
+
--dur-exit-slow: 200ms;
|
|
277
|
+
--dur-spring: 250ms;
|
|
278
|
+
--dur-spring-bounce: 330ms;
|
|
279
|
+
|
|
280
|
+
--press-scale: 0.97;
|
|
281
|
+
--press-scale-sm: 0.92;
|
|
282
|
+
--press-scale-lg: 0.985;
|
|
283
|
+
--enter-scale: 0.96;
|
|
284
|
+
--tip-scale: 0.97;
|
|
285
|
+
--shift-xs: 2px;
|
|
286
|
+
--shift-sm: 4px;
|
|
287
|
+
--shift-md: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@supports (animation-timing-function: linear(0, 1)) {
|
|
291
|
+
:root {
|
|
292
|
+
/* stiffness 1200, damping 56, mass 1: zeta 0.81, 1.1% overshoot, settles 250ms */
|
|
293
|
+
/* prettier-ignore */
|
|
294
|
+
--ease-spring: linear(0, 0.007 1.2%, 0.023 2.4%, 0.055 4%, 0.098 5.6%, 0.188 8.4%, 0.53 18%, 0.629 21.2%, 0.704 24%, 0.778 27.2%, 0.839 30.4%, 0.887 33.6%, 0.928 37.2%, 0.959 40.8%, 0.982 44.8%, 0.998 49.2%, 1.007 54.4%, 1.011 64.4%, 1);
|
|
295
|
+
/* stiffness 1000, damping 40, mass 1: zeta 0.63, 7.4% overshoot, settles 330ms */
|
|
296
|
+
/* prettier-ignore */
|
|
297
|
+
--ease-spring-bounce: linear(0, 0.01 1.2%, 0.033 2.4%, 0.067 3.6%, 0.122 5.2%, 0.244 7.9%, 0.502 13%, 0.618 15.5%, 0.735 18.2%, 0.824 20.6%, 0.898 23%, 0.957 25.5%, 1.007 28.2%, 1.041 30.9%, 1.067 34.8%, 1.073 39.4%, 1.064 44.2%, 1.016 58.2%, 1 66.4%, 0.995 77.3%, 1);
|
|
298
|
+
}
|
|
216
299
|
}
|
package/package.json
CHANGED