@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.
Files changed (137) hide show
  1. package/README.md +38 -1
  2. package/dist/autoresize.d.ts +2 -0
  3. package/dist/autoresize.js +8 -1
  4. package/dist/components/atoms/Artwork.svelte +12 -5
  5. package/dist/components/atoms/Avatar.svelte +1 -1
  6. package/dist/components/atoms/Badge.svelte +59 -23
  7. package/dist/components/atoms/Button.svelte +130 -96
  8. package/dist/components/atoms/Button.svelte.d.ts +2 -0
  9. package/dist/components/atoms/Card.svelte +10 -4
  10. package/dist/components/atoms/Checkbox.svelte +36 -9
  11. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  12. package/dist/components/atoms/Gauge.svelte +20 -14
  13. package/dist/components/atoms/Heading.svelte +1 -1
  14. package/dist/components/atoms/Icon.svelte +1 -0
  15. package/dist/components/atoms/Input.svelte +67 -13
  16. package/dist/components/atoms/Kbd.svelte +3 -3
  17. package/dist/components/atoms/Link.svelte +7 -2
  18. package/dist/components/atoms/Progress.svelte +12 -6
  19. package/dist/components/atoms/Scrim.svelte +3 -8
  20. package/dist/components/atoms/SegmentedProgress.svelte +5 -4
  21. package/dist/components/atoms/Select.svelte +58 -27
  22. package/dist/components/atoms/Skeleton.svelte +14 -8
  23. package/dist/components/atoms/Slider.svelte +79 -22
  24. package/dist/components/atoms/Slider.svelte.d.ts +2 -0
  25. package/dist/components/atoms/Spinner.svelte +464 -5
  26. package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
  27. package/dist/components/atoms/Swatch.svelte +19 -8
  28. package/dist/components/atoms/Switch.svelte +34 -21
  29. package/dist/components/atoms/Switch.svelte.d.ts +2 -0
  30. package/dist/components/atoms/Text.svelte +1 -1
  31. package/dist/components/atoms/Textarea.svelte +34 -12
  32. package/dist/components/atoms/TimeDisplay.svelte +4 -2
  33. package/dist/components/layouts/AppShell.svelte +26 -15
  34. package/dist/components/layouts/MasterDetail.svelte +23 -21
  35. package/dist/components/layouts/NavBar.svelte +11 -8
  36. package/dist/components/layouts/NavItem.svelte +15 -7
  37. package/dist/components/layouts/NavSection.svelte +1 -1
  38. package/dist/components/layouts/ResizablePanel.svelte +37 -16
  39. package/dist/components/layouts/TabBar.svelte +24 -14
  40. package/dist/components/layouts/TabBar.svelte.d.ts +2 -0
  41. package/dist/components/molecules/Accordion.svelte +4 -0
  42. package/dist/components/molecules/Accordion.svelte.d.ts +2 -0
  43. package/dist/components/molecules/AttachmentList.svelte +14 -11
  44. package/dist/components/molecules/Breadcrumb.svelte +13 -9
  45. package/dist/components/molecules/Callout.svelte +3 -3
  46. package/dist/components/molecules/CapBar.svelte +23 -22
  47. package/dist/components/molecules/Carousel.svelte +27 -17
  48. package/dist/components/molecules/ChatBubble.svelte +2 -2
  49. package/dist/components/molecules/CodeBlock.svelte +2 -2
  50. package/dist/components/molecules/Combobox.svelte +30 -1
  51. package/dist/components/molecules/Combobox.svelte.d.ts +4 -0
  52. package/dist/components/molecules/Composer.svelte +2 -2
  53. package/dist/components/molecules/ConfirmModal.svelte +4 -0
  54. package/dist/components/molecules/ConfirmModal.svelte.d.ts +2 -0
  55. package/dist/components/molecules/CopyButton.svelte +46 -2
  56. package/dist/components/molecules/CopyButton.svelte.d.ts +2 -0
  57. package/dist/components/molecules/Disclosure.svelte +23 -12
  58. package/dist/components/molecules/Disclosure.svelte.d.ts +2 -0
  59. package/dist/components/molecules/Drawer.svelte +43 -23
  60. package/dist/components/molecules/Drawer.svelte.d.ts +2 -0
  61. package/dist/components/molecules/Dropzone.svelte +24 -8
  62. package/dist/components/molecules/EmojiPicker.svelte +27 -8
  63. package/dist/components/molecules/EmojiPicker.svelte.d.ts +2 -0
  64. package/dist/components/molecules/EmptyState.svelte +5 -5
  65. package/dist/components/molecules/Fieldset.svelte +8 -13
  66. package/dist/components/molecules/FileButton.svelte +57 -31
  67. package/dist/components/molecules/FileButton.svelte.d.ts +2 -0
  68. package/dist/components/molecules/FilterInput.svelte +51 -11
  69. package/dist/components/molecules/FontScalePicker.svelte +20 -9
  70. package/dist/components/molecules/GitRef.svelte +9 -7
  71. package/dist/components/molecules/GlyphStack.svelte +38 -14
  72. package/dist/components/molecules/IconButton.svelte +6 -3
  73. package/dist/components/molecules/IconButton.svelte.d.ts +2 -0
  74. package/dist/components/molecules/InputGroup.svelte +13 -12
  75. package/dist/components/molecules/LoadMore.svelte +6 -1
  76. package/dist/components/molecules/LoadMore.svelte.d.ts +2 -0
  77. package/dist/components/molecules/Menu.svelte +33 -7
  78. package/dist/components/molecules/Menu.svelte.d.ts +2 -0
  79. package/dist/components/molecules/Metric.svelte +11 -9
  80. package/dist/components/molecules/Modal.svelte +76 -26
  81. package/dist/components/molecules/Modal.svelte.d.ts +2 -0
  82. package/dist/components/molecules/OptionButton.svelte +28 -10
  83. package/dist/components/molecules/OptionButton.svelte.d.ts +2 -0
  84. package/dist/components/molecules/Pagination.svelte +26 -9
  85. package/dist/components/molecules/Pagination.svelte.d.ts +2 -0
  86. package/dist/components/molecules/Popover.svelte +107 -51
  87. package/dist/components/molecules/Popover.svelte.d.ts +2 -0
  88. package/dist/components/molecules/Prose.svelte +1 -1
  89. package/dist/components/molecules/PullToRefresh.svelte +14 -10
  90. package/dist/components/molecules/PullToRefresh.svelte.d.ts +2 -0
  91. package/dist/components/molecules/RadialMenu.svelte +52 -19
  92. package/dist/components/molecules/RadialMenu.svelte.d.ts +2 -0
  93. package/dist/components/molecules/RadioGroup.svelte +38 -23
  94. package/dist/components/molecules/RadioGroup.svelte.d.ts +2 -0
  95. package/dist/components/molecules/SectionHeader.svelte +13 -8
  96. package/dist/components/molecules/SeekSlider.svelte +38 -27
  97. package/dist/components/molecules/SeekSlider.svelte.d.ts +2 -0
  98. package/dist/components/molecules/SegmentedControl.svelte +139 -86
  99. package/dist/components/molecules/SegmentedControl.svelte.d.ts +2 -0
  100. package/dist/components/molecules/SelectButton.svelte +21 -6
  101. package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
  102. package/dist/components/molecules/SortableList.svelte +10 -11
  103. package/dist/components/molecules/SoundSettings.svelte +161 -0
  104. package/dist/components/molecules/SoundSettings.svelte.d.ts +17 -0
  105. package/dist/components/molecules/SplitButton.svelte +30 -3
  106. package/dist/components/molecules/SplitButton.svelte.d.ts +2 -0
  107. package/dist/components/molecules/SwipeRow.svelte +18 -15
  108. package/dist/components/molecules/SwipeRow.svelte.d.ts +2 -0
  109. package/dist/components/molecules/Tabs.svelte +92 -10
  110. package/dist/components/molecules/Tabs.svelte.d.ts +2 -0
  111. package/dist/components/molecules/ThemePicker.svelte +25 -10
  112. package/dist/components/molecules/Timestamp.svelte +12 -7
  113. package/dist/components/molecules/Toaster.svelte +65 -14
  114. package/dist/components/molecules/Toaster.svelte.d.ts +2 -0
  115. package/dist/components/molecules/Toggle.svelte +36 -13
  116. package/dist/components/molecules/Toggle.svelte.d.ts +2 -0
  117. package/dist/components/molecules/Tooltip.svelte +41 -10
  118. package/dist/components/molecules/Tooltip.svelte.d.ts +1 -1
  119. package/dist/components/molecules/VolumeSlider.svelte +34 -14
  120. package/dist/components/molecules/VolumeSlider.svelte.d.ts +2 -0
  121. package/dist/components/molecules/WorkingDir.svelte +1 -0
  122. package/dist/components/organisms/ConversationFeed.svelte +4 -0
  123. package/dist/components/organisms/DataTable.svelte +37 -14
  124. package/dist/floating.js +26 -0
  125. package/dist/index.d.ts +4 -0
  126. package/dist/index.js +4 -1
  127. package/dist/sound-synth.d.ts +3 -0
  128. package/dist/sound-synth.js +91 -0
  129. package/dist/sound.d.ts +1 -0
  130. package/dist/sound.js +3 -0
  131. package/dist/stores/sound.svelte.d.ts +20 -0
  132. package/dist/stores/sound.svelte.js +150 -0
  133. package/dist/stores/toast.svelte.d.ts +9 -0
  134. package/dist/stores/toast.svelte.js +45 -9
  135. package/dist/styles/reset.css +19 -2
  136. package/dist/styles/tokens.css +89 -6
  137. 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
+ }
@@ -0,0 +1 @@
1
+ export { playSound, type SoundMuted, type SoundName } from './stores/sound.svelte';
package/dist/sound.js ADDED
@@ -0,0 +1,3 @@
1
+ // What components import to play a sound. No audio code lives here: the synth
2
+ // is a separate chunk that `playSound` imports on demand once sounds are on.
3
+ export { playSound } from './stores/sound.svelte';
@@ -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.#timers.set(id, setTimeout(() => this.dismiss(id), duration));
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
- const timer = this.#timers.get(id);
35
- if (timer)
36
- clearTimeout(timer);
37
- this.#timers.delete(id);
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
- const timer = this.#timers.get(id);
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();
@@ -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-duration: 0.01ms !important;
116
- scroll-behavior: auto !important;
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
 
@@ -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
- /* Keyboard focus indicator shared by every interactive atom. */
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: 1px;
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 is the classic
178
- 2.25rem icon button; sm/lg reuse the control heights so a boxed control
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.25rem;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.73.2",
3
+ "version": "0.75.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",