@dorsk/tsumikit 0.74.0 → 0.76.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 +4 -2
- 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/Slider.svelte +79 -22
- package/dist/components/atoms/Slider.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 +46 -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/RichSelect.svelte +583 -0
- package/dist/components/molecules/RichSelect.svelte.d.ts +68 -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/molecules/rich-select-keyboard.d.ts +82 -0
- package/dist/components/molecules/rich-select-keyboard.js +114 -0
- package/dist/components/organisms/ConversationFeed.svelte +24 -1
- package/dist/components/organisms/DataTable.svelte +37 -14
- package/dist/floating.js +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/sound-synth.js +17 -12
- package/dist/stores/toast.svelte.d.ts +9 -0
- package/dist/stores/toast.svelte.js +45 -9
- package/dist/styles/reset.css +3 -3
- package/package.json +1 -1
|
@@ -7,11 +7,21 @@
|
|
|
7
7
|
// everything outside it is inert, top layer included, so the stack moves
|
|
8
8
|
// into the topmost modal dialog and back to its host when it closes.
|
|
9
9
|
import { toasts } from '../../stores/toast.svelte';
|
|
10
|
+
import { playSound } from '../../sound';
|
|
10
11
|
import Button from '../atoms/Button.svelte';
|
|
11
12
|
import Card from '../atoms/Card.svelte';
|
|
12
13
|
import Icon from '../atoms/Icon.svelte';
|
|
13
14
|
|
|
14
|
-
let {
|
|
15
|
+
let {
|
|
16
|
+
class: klass = '',
|
|
17
|
+
style: styleProp = '',
|
|
18
|
+
sound = true
|
|
19
|
+
}: {
|
|
20
|
+
class?: string;
|
|
21
|
+
style?: string;
|
|
22
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
23
|
+
sound?: boolean;
|
|
24
|
+
} = $props();
|
|
15
25
|
|
|
16
26
|
let host: HTMLDivElement | undefined = $state();
|
|
17
27
|
let el: HTMLDivElement | undefined = $state();
|
|
@@ -69,12 +79,45 @@
|
|
|
69
79
|
$effect(() => () => {
|
|
70
80
|
if (el && host && el.parentNode !== host) host.appendChild(el);
|
|
71
81
|
});
|
|
82
|
+
|
|
83
|
+
// Feedback tones sound once, when the toast appears; the queue present at
|
|
84
|
+
// mount is a sync, not an arrival.
|
|
85
|
+
let seen: Set<number> | null = null;
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const items = toasts.items;
|
|
88
|
+
if (!seen) {
|
|
89
|
+
seen = new Set(items.map((t) => t.id));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const t of items) {
|
|
93
|
+
if (seen.has(t.id)) continue;
|
|
94
|
+
seen.add(t.id);
|
|
95
|
+
if (t.tone === 'ok') playSound('success', sound);
|
|
96
|
+
else if (t.tone === 'error') playSound('error', sound);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Hovering a toast holds its timer; only a real pointer counts.
|
|
101
|
+
function hold(e: PointerEvent, id: number) {
|
|
102
|
+
if (e.pointerType === 'mouse') toasts.pause(id);
|
|
103
|
+
}
|
|
104
|
+
function release(e: PointerEvent, id: number) {
|
|
105
|
+
if (e.pointerType === 'mouse') toasts.resume(id);
|
|
106
|
+
}
|
|
72
107
|
</script>
|
|
73
108
|
|
|
74
109
|
{#snippet toast(t: (typeof toasts.items)[number])}
|
|
75
110
|
{@const tone = t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
|
|
76
111
|
{#if t.action}
|
|
77
|
-
<Card
|
|
112
|
+
<Card
|
|
113
|
+
as="div"
|
|
114
|
+
surface="raised"
|
|
115
|
+
class="toast"
|
|
116
|
+
{tone}
|
|
117
|
+
data-leaving={t.leaving || undefined}
|
|
118
|
+
onpointerenter={(e: PointerEvent) => hold(e, t.id)}
|
|
119
|
+
onpointerleave={(e: PointerEvent) => release(e, t.id)}
|
|
120
|
+
>
|
|
78
121
|
<button
|
|
79
122
|
type="button"
|
|
80
123
|
class="toast-text"
|
|
@@ -98,6 +141,9 @@
|
|
|
98
141
|
{tone}
|
|
99
142
|
type="button"
|
|
100
143
|
aria-label="Dismiss: {t.message}"
|
|
144
|
+
data-leaving={t.leaving || undefined}
|
|
145
|
+
onpointerenter={(e: PointerEvent) => hold(e, t.id)}
|
|
146
|
+
onpointerleave={(e: PointerEvent) => release(e, t.id)}
|
|
101
147
|
onclick={() => toasts.dismiss(t.id)}
|
|
102
148
|
>
|
|
103
149
|
<span aria-hidden="true" class="toast-icon">
|
|
@@ -160,7 +206,23 @@
|
|
|
160
206
|
width: 100%;
|
|
161
207
|
text-align: left;
|
|
162
208
|
font-size: var(--fs-sm);
|
|
163
|
-
|
|
209
|
+
transition:
|
|
210
|
+
opacity var(--dur-sheet) var(--ease-out),
|
|
211
|
+
translate var(--dur-sheet) var(--ease-out),
|
|
212
|
+
scale var(--dur-sheet) var(--ease-out);
|
|
213
|
+
}
|
|
214
|
+
@starting-style {
|
|
215
|
+
.toaster :global(.toast) {
|
|
216
|
+
opacity: 0;
|
|
217
|
+
translate: 0 var(--shift-md);
|
|
218
|
+
scale: 0.98;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
.toaster :global(.toast[data-leaving]) {
|
|
222
|
+
opacity: 0;
|
|
223
|
+
scale: 0.96;
|
|
224
|
+
pointer-events: none;
|
|
225
|
+
transition-duration: var(--dur-exit-slow);
|
|
164
226
|
}
|
|
165
227
|
.toaster :global(.toast.card-ok) {
|
|
166
228
|
color: var(--ok);
|
|
@@ -188,15 +250,4 @@
|
|
|
188
250
|
.msg {
|
|
189
251
|
flex: 1;
|
|
190
252
|
}
|
|
191
|
-
@keyframes toast-in {
|
|
192
|
-
from {
|
|
193
|
-
transform: translateY(8px);
|
|
194
|
-
opacity: 0;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
@media (prefers-reduced-motion: reduce) {
|
|
198
|
-
.toaster :global(.toast) {
|
|
199
|
-
animation: none;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
253
|
</style>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
2
|
class?: string;
|
|
3
3
|
style?: string;
|
|
4
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
5
|
+
sound?: boolean;
|
|
4
6
|
};
|
|
5
7
|
declare const Toaster: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
8
|
type Toaster = ReturnType<typeof Toaster>;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// <button> + scoped styles rather than specializing the Button atom.
|
|
11
11
|
import type { Snippet } from 'svelte';
|
|
12
12
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
13
|
+
import { playSound } from '../../sound';
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
pressed = false,
|
|
@@ -20,6 +21,8 @@
|
|
|
20
21
|
shrink = true,
|
|
21
22
|
type = 'button',
|
|
22
23
|
disabled = false,
|
|
24
|
+
sound = true,
|
|
25
|
+
onclick,
|
|
23
26
|
class: klass = '',
|
|
24
27
|
children,
|
|
25
28
|
...rest
|
|
@@ -32,8 +35,17 @@
|
|
|
32
35
|
grow?: boolean;
|
|
33
36
|
// `shrink={false}` keeps the chip at its intrinsic width (flex: none).
|
|
34
37
|
shrink?: boolean;
|
|
38
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
39
|
+
sound?: boolean;
|
|
35
40
|
children?: Snippet;
|
|
36
41
|
} = $props();
|
|
42
|
+
|
|
43
|
+
function click(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
|
44
|
+
const next = !pressed;
|
|
45
|
+
onclick?.(e);
|
|
46
|
+
if (e.defaultPrevented) return;
|
|
47
|
+
playSound(next ? 'toggle-on' : 'toggle-off', sound);
|
|
48
|
+
}
|
|
37
49
|
</script>
|
|
38
50
|
|
|
39
51
|
<button
|
|
@@ -41,6 +53,7 @@
|
|
|
41
53
|
{...rest}
|
|
42
54
|
{type}
|
|
43
55
|
{disabled}
|
|
56
|
+
onclick={click}
|
|
44
57
|
class="toggle {klass}"
|
|
45
58
|
class:pill
|
|
46
59
|
class:struck
|
|
@@ -60,34 +73,38 @@
|
|
|
60
73
|
display: inline-flex;
|
|
61
74
|
align-items: center;
|
|
62
75
|
justify-content: center;
|
|
63
|
-
gap:
|
|
76
|
+
gap: var(--sp-1);
|
|
64
77
|
box-sizing: border-box;
|
|
65
|
-
height: var(--toggle-size, var(--
|
|
66
|
-
padding: 0 var(--
|
|
78
|
+
height: var(--toggle-size, var(--control-sm));
|
|
79
|
+
padding: 0 var(--control-px-sm);
|
|
67
80
|
border-radius: var(--r-sm);
|
|
68
|
-
font-size: var(--fs-
|
|
81
|
+
font-size: var(--control-fs-sm);
|
|
69
82
|
font-weight: var(--fw-medium);
|
|
70
|
-
line-height:
|
|
83
|
+
line-height: var(--lh-none);
|
|
84
|
+
--icon-size: var(--control-icon-sm);
|
|
71
85
|
background: var(--bg-elevated-2);
|
|
72
86
|
color: var(--text-muted);
|
|
73
87
|
border: 1px solid var(--border);
|
|
74
88
|
white-space: nowrap;
|
|
75
89
|
user-select: none;
|
|
76
90
|
transition:
|
|
77
|
-
background
|
|
78
|
-
border-color
|
|
79
|
-
color
|
|
91
|
+
background var(--dur-fast) var(--ease),
|
|
92
|
+
border-color var(--dur-fast) var(--ease),
|
|
93
|
+
color var(--dur-fast) var(--ease),
|
|
94
|
+
scale var(--dur-fast) var(--ease-out);
|
|
80
95
|
}
|
|
81
96
|
.toggle:disabled {
|
|
82
|
-
opacity:
|
|
97
|
+
opacity: var(--disabled-opacity);
|
|
83
98
|
cursor: not-allowed;
|
|
84
99
|
}
|
|
85
100
|
.toggle.pill {
|
|
86
101
|
border-radius: var(--r-pill);
|
|
87
102
|
}
|
|
88
103
|
.toggle.md {
|
|
89
|
-
height: var(--toggle-size, var(--control-
|
|
90
|
-
|
|
104
|
+
height: var(--toggle-size, var(--control-md));
|
|
105
|
+
padding: 0 var(--control-px-md);
|
|
106
|
+
font-size: var(--control-fs-md);
|
|
107
|
+
--icon-size: var(--control-icon-md);
|
|
91
108
|
}
|
|
92
109
|
.toggle.grow {
|
|
93
110
|
flex: 1 1 0;
|
|
@@ -96,8 +113,14 @@
|
|
|
96
113
|
.toggle.no-shrink {
|
|
97
114
|
flex: none;
|
|
98
115
|
}
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
@media (hover: hover) and (pointer: fine) {
|
|
117
|
+
.toggle:hover:not(:disabled) {
|
|
118
|
+
border-color: var(--border-strong);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
.toggle:active:not(:disabled) {
|
|
122
|
+
scale: var(--press-scale);
|
|
123
|
+
transition-duration: var(--dur-press);
|
|
101
124
|
}
|
|
102
125
|
.toggle.on {
|
|
103
126
|
--tc: var(--toggle-accent, var(--accent));
|
|
@@ -8,6 +8,8 @@ type $$ComponentProps = HTMLButtonAttributes & {
|
|
|
8
8
|
size?: ControlSize;
|
|
9
9
|
grow?: boolean;
|
|
10
10
|
shrink?: boolean;
|
|
11
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
12
|
+
sound?: boolean;
|
|
11
13
|
children?: Snippet;
|
|
12
14
|
};
|
|
13
15
|
declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
let lastClosed = -Infinity;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
6
|
// Lightweight tooltip. Shows on hover (after a short delay) and on keyboard
|
|
3
7
|
// focus of the trigger; hides on blur / mouseleave / Escape (anywhere). The trigger is
|
|
@@ -15,7 +19,7 @@
|
|
|
15
19
|
// by `closeDelay` and cancelled when the pointer (or focus) enters the panel,
|
|
16
20
|
// so users can move in to select/copy text or click a button. Note the
|
|
17
21
|
// role="tooltip" caveat: `aria-describedby` flattens the panel to its text.
|
|
18
|
-
import type
|
|
22
|
+
import { flushSync, type Snippet } from 'svelte';
|
|
19
23
|
import { place } from '../../floating';
|
|
20
24
|
|
|
21
25
|
let {
|
|
@@ -53,6 +57,8 @@
|
|
|
53
57
|
style?: string;
|
|
54
58
|
} = $props();
|
|
55
59
|
|
|
60
|
+
// A tooltip closed moments ago makes the next one open at once, unanimated.
|
|
61
|
+
const WARM_MS = 300;
|
|
56
62
|
const id = `tip-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
63
|
let wrapEl = $state<HTMLElement | null>(null);
|
|
58
64
|
let tipEl = $state<HTMLElement | null>(null);
|
|
@@ -68,23 +74,32 @@
|
|
|
68
74
|
if (e.key === 'Escape') hideNow();
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
let instant = $state(false);
|
|
71
78
|
function show() {
|
|
72
79
|
clearTimeout(hideTimer); // pointer came back within the grace period
|
|
73
80
|
clearTimeout(showTimer);
|
|
74
|
-
|
|
81
|
+
const warm = performance.now() - lastClosed < WARM_MS;
|
|
82
|
+
const reveal = () => {
|
|
75
83
|
if (!tipEl || tipEl.matches(':popover-open')) return; // re-entry guard
|
|
84
|
+
instant = warm;
|
|
85
|
+
flushSync();
|
|
76
86
|
tipEl.showPopover(); // top layer — displayed before we measure it
|
|
77
87
|
reposition();
|
|
78
88
|
addEventListener('scroll', reposition, true);
|
|
79
89
|
addEventListener('resize', reposition);
|
|
80
90
|
document.addEventListener('keydown', onEscape);
|
|
81
|
-
}
|
|
91
|
+
};
|
|
92
|
+
if (warm) reveal();
|
|
93
|
+
else showTimer = setTimeout(reveal, delay);
|
|
82
94
|
}
|
|
83
95
|
function hideNow() {
|
|
84
96
|
clearTimeout(showTimer);
|
|
85
97
|
clearTimeout(hideTimer);
|
|
86
98
|
try {
|
|
87
|
-
if (tipEl?.matches(':popover-open'))
|
|
99
|
+
if (tipEl?.matches(':popover-open')) {
|
|
100
|
+
tipEl.hidePopover();
|
|
101
|
+
lastClosed = performance.now();
|
|
102
|
+
}
|
|
88
103
|
} catch {}
|
|
89
104
|
removeEventListener('scroll', reposition, true);
|
|
90
105
|
removeEventListener('resize', reposition);
|
|
@@ -164,6 +179,7 @@
|
|
|
164
179
|
popover="manual"
|
|
165
180
|
class="tip"
|
|
166
181
|
class:rich={!!content}
|
|
182
|
+
data-instant={instant ? '' : undefined}
|
|
167
183
|
style:width
|
|
168
184
|
style:max-width={maxWidth}
|
|
169
185
|
use:panel
|
|
@@ -194,9 +210,16 @@
|
|
|
194
210
|
border-radius: var(--r-sm);
|
|
195
211
|
box-shadow: var(--shadow-md);
|
|
196
212
|
font-size: var(--fs-xs);
|
|
197
|
-
line-height:
|
|
213
|
+
line-height: var(--lh-snug);
|
|
198
214
|
white-space: normal;
|
|
199
215
|
pointer-events: none; /* non-interactive: never steals hover/clicks */
|
|
216
|
+
transform-origin: var(--tsu-origin, bottom center);
|
|
217
|
+
opacity: 0;
|
|
218
|
+
scale: var(--tip-scale);
|
|
219
|
+
transition:
|
|
220
|
+
opacity var(--dur-exit-fast) var(--ease-out),
|
|
221
|
+
display var(--dur-exit-fast) allow-discrete,
|
|
222
|
+
overlay var(--dur-exit-fast) allow-discrete;
|
|
200
223
|
}
|
|
201
224
|
/* Rich panel: interactive + selectable so the user can move the pointer in
|
|
202
225
|
to copy an id or click a button (the close grace period allows the trip). */
|
|
@@ -207,14 +230,22 @@
|
|
|
207
230
|
padding: var(--sp-2);
|
|
208
231
|
border-radius: var(--r-md);
|
|
209
232
|
}
|
|
210
|
-
/* Fade/scale in when shown (skipped under reduced-motion via the global rule). */
|
|
211
233
|
.tip:popover-open {
|
|
212
|
-
|
|
234
|
+
opacity: 1;
|
|
235
|
+
scale: 1;
|
|
236
|
+
transition:
|
|
237
|
+
opacity var(--dur-fast) var(--ease-out),
|
|
238
|
+
scale var(--dur-fast) var(--ease-out),
|
|
239
|
+
display var(--dur-fast) allow-discrete,
|
|
240
|
+
overlay var(--dur-fast) allow-discrete;
|
|
213
241
|
}
|
|
214
|
-
@
|
|
215
|
-
|
|
242
|
+
@starting-style {
|
|
243
|
+
.tip:popover-open {
|
|
216
244
|
opacity: 0;
|
|
217
|
-
|
|
245
|
+
scale: var(--tip-scale);
|
|
218
246
|
}
|
|
219
247
|
}
|
|
248
|
+
.tip[data-instant] {
|
|
249
|
+
transition-duration: var(--dur-instant);
|
|
250
|
+
}
|
|
220
251
|
</style>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
9
9
|
import type { IconName } from '../atoms/Icon.svelte';
|
|
10
10
|
import IconButton from './IconButton.svelte';
|
|
11
|
+
import { playSound } from '../../sound';
|
|
11
12
|
|
|
12
13
|
type Own = {
|
|
13
14
|
/** Level 0–1. */
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
label?: string;
|
|
23
24
|
muteLabel?: string;
|
|
24
25
|
unmuteLabel?: string;
|
|
26
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
27
|
+
sound?: boolean;
|
|
25
28
|
oninput?: (value: number) => void;
|
|
26
29
|
onchange?: (value: number) => void;
|
|
27
30
|
onmutechange?: (muted: boolean) => void;
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
label = 'Volume',
|
|
41
44
|
muteLabel = 'Mute',
|
|
42
45
|
unmuteLabel = 'Unmute',
|
|
46
|
+
sound = true,
|
|
43
47
|
oninput,
|
|
44
48
|
onchange,
|
|
45
49
|
onmutechange,
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
muted ? 'volume-mute' : level === 0 ? 'volume' : level < 0.5 ? 'volume-low' : 'volume-high'
|
|
60
64
|
);
|
|
61
65
|
const valueText = $derived(muted ? 'Muted' : `${pct}%`);
|
|
66
|
+
const discrete = $derived(1 / step <= 20);
|
|
62
67
|
|
|
63
68
|
function setMuted(m: boolean) {
|
|
64
69
|
if (m === muted) return;
|
|
@@ -75,7 +80,8 @@
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
function commit(v: number) {
|
|
78
|
-
|
|
83
|
+
const next = apply(v);
|
|
84
|
+
onchange?.(next);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
function fracAt(el: HTMLElement, e: PointerEvent) {
|
|
@@ -117,6 +123,7 @@
|
|
|
117
123
|
|
|
118
124
|
function onpointermove(e: PointerEvent) {
|
|
119
125
|
if (pointerId !== e.pointerId) return;
|
|
126
|
+
if (discrete) playSound('tick', sound);
|
|
120
127
|
apply(fracAt(e.currentTarget as HTMLElement, e));
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -167,6 +174,7 @@
|
|
|
167
174
|
}
|
|
168
175
|
if (next === null) return;
|
|
169
176
|
e.preventDefault();
|
|
177
|
+
playSound('tick', sound);
|
|
170
178
|
commit(next);
|
|
171
179
|
}
|
|
172
180
|
|
|
@@ -176,6 +184,7 @@
|
|
|
176
184
|
const d = e.deltaY || e.deltaX;
|
|
177
185
|
if (!d) return;
|
|
178
186
|
e.preventDefault();
|
|
187
|
+
playSound('tick', sound);
|
|
179
188
|
commit(level + (d < 0 ? step : -step));
|
|
180
189
|
};
|
|
181
190
|
el.addEventListener('wheel', onwheel, { passive: false });
|
|
@@ -199,6 +208,7 @@
|
|
|
199
208
|
box="sm"
|
|
200
209
|
size={size === 'sm' ? 16 : 18}
|
|
201
210
|
{disabled}
|
|
211
|
+
{sound}
|
|
202
212
|
onclick={() => setMuted(!muted)}
|
|
203
213
|
data-part="mute"
|
|
204
214
|
/>
|
|
@@ -237,6 +247,7 @@
|
|
|
237
247
|
--vol-thumb: 10px;
|
|
238
248
|
--vol-length: 5rem;
|
|
239
249
|
--vol-accent: var(--slider-accent, var(--accent));
|
|
250
|
+
--vol-halo: 0 0 0 4px color-mix(in srgb, var(--vol-accent) 20%, transparent);
|
|
240
251
|
display: inline-flex;
|
|
241
252
|
align-items: center;
|
|
242
253
|
gap: var(--sp-1);
|
|
@@ -281,8 +292,8 @@
|
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
.vol-slab[aria-disabled='true'] {
|
|
284
|
-
cursor:
|
|
285
|
-
opacity:
|
|
295
|
+
cursor: not-allowed;
|
|
296
|
+
opacity: var(--disabled-opacity);
|
|
286
297
|
}
|
|
287
298
|
.vol-slab:focus-visible {
|
|
288
299
|
outline: var(--focus-ring);
|
|
@@ -340,28 +351,37 @@
|
|
|
340
351
|
top: 0;
|
|
341
352
|
width: var(--vol-thumb);
|
|
342
353
|
height: var(--vol-thumb);
|
|
343
|
-
border-radius:
|
|
354
|
+
border-radius: var(--r-pill);
|
|
344
355
|
background: var(--vol-accent);
|
|
345
356
|
border: 2px solid var(--bg);
|
|
346
357
|
box-shadow: var(--shadow-sm);
|
|
347
|
-
transform: translate(-50%, -50%)
|
|
358
|
+
transform: translate(-50%, -50%);
|
|
359
|
+
scale: 0.6;
|
|
348
360
|
opacity: 0;
|
|
349
361
|
transition:
|
|
350
|
-
opacity
|
|
351
|
-
|
|
362
|
+
opacity var(--dur-fast) var(--ease),
|
|
363
|
+
scale var(--dur-press) var(--ease-out),
|
|
364
|
+
box-shadow var(--dur-fast) var(--ease);
|
|
352
365
|
}
|
|
353
|
-
.vol-slab:hover .vol-thumb,
|
|
354
366
|
.vol-slab:focus-visible .vol-thumb,
|
|
355
367
|
.vol--dragging .vol-thumb {
|
|
356
368
|
opacity: 1;
|
|
357
|
-
|
|
369
|
+
scale: 1;
|
|
358
370
|
}
|
|
359
|
-
.vol--
|
|
360
|
-
|
|
371
|
+
.vol--dragging .vol-thumb {
|
|
372
|
+
scale: 1.15;
|
|
361
373
|
}
|
|
362
|
-
@media (
|
|
363
|
-
.vol-thumb {
|
|
364
|
-
|
|
374
|
+
@media (hover: hover) and (pointer: fine) {
|
|
375
|
+
.vol-slab:not([aria-disabled='true']):hover .vol-thumb {
|
|
376
|
+
opacity: 1;
|
|
377
|
+
scale: 1;
|
|
378
|
+
box-shadow: var(--shadow-sm), var(--vol-halo);
|
|
365
379
|
}
|
|
380
|
+
.vol--dragging .vol-thumb {
|
|
381
|
+
scale: 1.15;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
.vol--muted .vol-fill {
|
|
385
|
+
background: var(--text-faint);
|
|
366
386
|
}
|
|
367
387
|
</style>
|
|
@@ -18,6 +18,8 @@ type Own = {
|
|
|
18
18
|
label?: string;
|
|
19
19
|
muteLabel?: string;
|
|
20
20
|
unmuteLabel?: string;
|
|
21
|
+
/** Play UI sounds when enabled globally; false silences this instance. */
|
|
22
|
+
sound?: boolean;
|
|
21
23
|
oninput?: (value: number) => void;
|
|
22
24
|
onchange?: (value: number) => void;
|
|
23
25
|
onmutechange?: (muted: boolean) => void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{ type: 'open', index?: number }
|
|
3
|
+
* | { type: 'close' }
|
|
4
|
+
* | { type: 'select' }
|
|
5
|
+
* | { type: 'move', index: number }
|
|
6
|
+
* | { type: 'change', index: number }
|
|
7
|
+
* | { type: 'reorder', from: number, to: number }
|
|
8
|
+
* | { type: 'trailing' }
|
|
9
|
+
* | { type: 'type', char: string }} RichSelectAction
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Map a keydown on the trigger to what the select does with it. Closed, the
|
|
13
|
+
* arrows step the value like a native select and Enter/Space/Alt+ArrowDown open.
|
|
14
|
+
* Open, the arrows and Home/End move the active option, Enter/Space select,
|
|
15
|
+
* Escape closes, Alt+arrows reorder (when `reorder` is set), and Tab or
|
|
16
|
+
* ArrowRight reach the active option's trailing actions (when it has any).
|
|
17
|
+
* Printable characters feed the typeahead in both states. `undefined` leaves
|
|
18
|
+
* the key alone.
|
|
19
|
+
*
|
|
20
|
+
* @param {{ key: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} e
|
|
21
|
+
* @param {{ open: boolean, count: number, index: number, reorder?: boolean, trailing?: boolean }} state
|
|
22
|
+
* @returns {RichSelectAction | undefined}
|
|
23
|
+
*/
|
|
24
|
+
export function richSelectAction(e: {
|
|
25
|
+
key: string;
|
|
26
|
+
altKey?: boolean;
|
|
27
|
+
ctrlKey?: boolean;
|
|
28
|
+
metaKey?: boolean;
|
|
29
|
+
shiftKey?: boolean;
|
|
30
|
+
}, { open, count, index, reorder, trailing }: {
|
|
31
|
+
open: boolean;
|
|
32
|
+
count: number;
|
|
33
|
+
index: number;
|
|
34
|
+
reorder?: boolean;
|
|
35
|
+
trailing?: boolean;
|
|
36
|
+
}): RichSelectAction | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Index of the first label, searching forward from `from` and wrapping, that
|
|
39
|
+
* starts with `query` (case- and accent-insensitive). A query of one repeated
|
|
40
|
+
* character cycles through the labels starting with it, as native selects do.
|
|
41
|
+
*
|
|
42
|
+
* @param {readonly string[]} labels
|
|
43
|
+
* @param {string} query
|
|
44
|
+
* @param {number} from index of the current option, or -1
|
|
45
|
+
* @returns {number | undefined}
|
|
46
|
+
*/
|
|
47
|
+
export function typeaheadIndex(labels: readonly string[], query: string, from: number): number | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Accumulates typed characters into a query that resets after `timeout` ms of
|
|
50
|
+
* silence.
|
|
51
|
+
*
|
|
52
|
+
* @param {number} [timeout]
|
|
53
|
+
* @param {() => number} [now]
|
|
54
|
+
*/
|
|
55
|
+
export function createTypeahead(timeout?: number, now?: () => number): {
|
|
56
|
+
/** @param {string} char */
|
|
57
|
+
push(char: string): string;
|
|
58
|
+
reset(): void;
|
|
59
|
+
};
|
|
60
|
+
export type RichSelectAction = {
|
|
61
|
+
type: "open";
|
|
62
|
+
index?: number;
|
|
63
|
+
} | {
|
|
64
|
+
type: "close";
|
|
65
|
+
} | {
|
|
66
|
+
type: "select";
|
|
67
|
+
} | {
|
|
68
|
+
type: "move";
|
|
69
|
+
index: number;
|
|
70
|
+
} | {
|
|
71
|
+
type: "change";
|
|
72
|
+
index: number;
|
|
73
|
+
} | {
|
|
74
|
+
type: "reorder";
|
|
75
|
+
from: number;
|
|
76
|
+
to: number;
|
|
77
|
+
} | {
|
|
78
|
+
type: "trailing";
|
|
79
|
+
} | {
|
|
80
|
+
type: "type";
|
|
81
|
+
char: string;
|
|
82
|
+
};
|