@dorsk/tsumikit 0.72.0 → 0.73.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 +21 -0
- package/dist/components/atoms/Icon.svelte +5 -0
- package/dist/components/atoms/Icon.svelte.d.ts +5 -0
- package/dist/components/atoms/TimeDisplay.svelte +98 -0
- package/dist/components/atoms/TimeDisplay.svelte.d.ts +24 -0
- package/dist/components/layouts/AppShell.svelte +3 -2
- package/dist/components/layouts/ResizablePanel.svelte +5 -4
- package/dist/components/molecules/Accordion.svelte +2 -1
- package/dist/components/molecules/AttachmentList.svelte +1 -0
- package/dist/components/molecules/Breadcrumb.svelte +7 -6
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -1
- package/dist/components/molecules/CapBar.svelte +3 -1
- package/dist/components/molecules/Composer.svelte +27 -5
- package/dist/components/molecules/Drawer.svelte +10 -1
- package/dist/components/molecules/Dropzone.svelte +2 -1
- package/dist/components/molecules/FilterInput.svelte +10 -1
- package/dist/components/molecules/Metric.svelte +28 -3
- package/dist/components/molecules/Modal.svelte +20 -4
- package/dist/components/molecules/Popover.svelte +7 -3
- package/dist/components/molecules/SectionHeader.svelte +29 -14
- package/dist/components/molecules/SeekSlider.svelte +370 -0
- package/dist/components/molecules/SeekSlider.svelte.d.ts +37 -0
- package/dist/components/molecules/SegmentedControl.svelte +10 -6
- package/dist/components/molecules/SelectButton.svelte +33 -22
- package/dist/components/molecules/Tabs.svelte +11 -5
- package/dist/components/molecules/Timestamp.svelte +1 -1
- package/dist/components/molecules/Toaster.svelte +70 -13
- package/dist/components/molecules/Tooltip.svelte +19 -10
- package/dist/components/molecules/VolumeSlider.svelte +367 -0
- package/dist/components/molecules/VolumeSlider.svelte.d.ts +30 -0
- package/dist/components/molecules/combobox-keyboard.js +2 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/media-time.d.ts +6 -0
- package/dist/media-time.js +25 -0
- package/dist/query/edit.js +1 -16
- package/dist/query/parser.js +68 -28
- package/dist/query/query.js +9 -5
- package/dist/query/quotes.d.ts +24 -0
- package/dist/query/quotes.js +78 -0
- package/dist/query/single.js +1 -3
- package/dist/query/suggest.js +25 -32
- package/dist/stores/fontscale.svelte.js +3 -2
- package/dist/stores/storage.d.ts +2 -0
- package/dist/stores/storage.js +16 -0
- package/dist/stores/theme.svelte.js +4 -7
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Renders the toast queue bottom-centered in two live regions: polite for
|
|
3
3
|
// ok/info/neutral, assertive (role=alert) for errors. Mount once near the
|
|
4
|
-
// app root. The stack lives in a manual popover so it paints in the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// app root. The stack lives in a manual popover so it paints in the top
|
|
5
|
+
// layer; without the Popover API the attribute is ignored and the
|
|
6
|
+
// fixed/z-index positioning applies instead. While a modal <dialog> is open
|
|
7
|
+
// everything outside it is inert, top layer included, so the stack moves
|
|
8
|
+
// into the topmost modal dialog and back to its host when it closes.
|
|
7
9
|
import { toasts } from '../../stores/toast.svelte';
|
|
8
10
|
import Button from '../atoms/Button.svelte';
|
|
9
11
|
import Card from '../atoms/Card.svelte';
|
|
@@ -11,16 +13,61 @@
|
|
|
11
13
|
|
|
12
14
|
let { class: klass = '', style: styleProp = '' }: { class?: string; style?: string } = $props();
|
|
13
15
|
|
|
16
|
+
let host: HTMLDivElement | undefined = $state();
|
|
14
17
|
let el: HTMLDivElement | undefined = $state();
|
|
15
18
|
const polite = $derived(toasts.items.filter((t) => t.tone !== 'error'));
|
|
16
19
|
const assertive = $derived(toasts.items.filter((t) => t.tone === 'error'));
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
function topModal(): HTMLDialogElement | null {
|
|
22
|
+
let found: HTMLDialogElement | null = null;
|
|
23
|
+
for (const d of document.querySelectorAll<HTMLDialogElement>('dialog[open]')) {
|
|
24
|
+
try {
|
|
25
|
+
if (d.matches(':modal')) found = d;
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let shown = false;
|
|
32
|
+
function show(stack: HTMLDivElement) {
|
|
33
|
+
if (typeof stack.showPopover !== 'function') return;
|
|
20
34
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
stack.showPopover();
|
|
36
|
+
shown = true;
|
|
23
37
|
} catch {}
|
|
38
|
+
}
|
|
39
|
+
function hide(stack: HTMLDivElement) {
|
|
40
|
+
if (!shown || typeof stack.hidePopover !== 'function') return;
|
|
41
|
+
shown = false;
|
|
42
|
+
try {
|
|
43
|
+
stack.hidePopover();
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
const stack = el;
|
|
49
|
+
const home = host;
|
|
50
|
+
if (!stack || !home) return;
|
|
51
|
+
if (!toasts.items.length) {
|
|
52
|
+
hide(stack);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const place = () => {
|
|
56
|
+
const target = topModal() ?? home;
|
|
57
|
+
if (stack.parentNode !== target) {
|
|
58
|
+
hide(stack);
|
|
59
|
+
target.appendChild(stack);
|
|
60
|
+
}
|
|
61
|
+
show(stack);
|
|
62
|
+
};
|
|
63
|
+
place();
|
|
64
|
+
const observer = new MutationObserver(place);
|
|
65
|
+
observer.observe(document.body, { attributes: true, attributeFilter: ['open'], subtree: true });
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
$effect(() => () => {
|
|
70
|
+
if (el && host && el.parentNode !== host) host.appendChild(el);
|
|
24
71
|
});
|
|
25
72
|
</script>
|
|
26
73
|
|
|
@@ -62,16 +109,21 @@
|
|
|
62
109
|
{/if}
|
|
63
110
|
{/snippet}
|
|
64
111
|
|
|
65
|
-
<div class="toaster
|
|
66
|
-
<div class="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
<div class="toaster-host" bind:this={host}>
|
|
113
|
+
<div class="toaster {klass}" style={styleProp} bind:this={el} popover="manual" data-tsu="Toaster">
|
|
114
|
+
<div class="region" role="status" aria-live="polite">
|
|
115
|
+
{#each polite as t (t.id)}{@render toast(t)}{/each}
|
|
116
|
+
</div>
|
|
117
|
+
<div class="region" role="alert" aria-live="assertive">
|
|
118
|
+
{#each assertive as t (t.id)}{@render toast(t)}{/each}
|
|
119
|
+
</div>
|
|
71
120
|
</div>
|
|
72
121
|
</div>
|
|
73
122
|
|
|
74
123
|
<style>
|
|
124
|
+
.toaster-host {
|
|
125
|
+
display: contents;
|
|
126
|
+
}
|
|
75
127
|
.toaster {
|
|
76
128
|
position: fixed;
|
|
77
129
|
inset: auto;
|
|
@@ -142,4 +194,9 @@
|
|
|
142
194
|
opacity: 0;
|
|
143
195
|
}
|
|
144
196
|
}
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.toaster :global(.toast) {
|
|
199
|
+
animation: none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
145
202
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Lightweight tooltip. Shows on hover (after a short delay) and on keyboard
|
|
3
|
-
// focus of the trigger; hides on blur / mouseleave / Escape. The trigger is
|
|
3
|
+
// focus of the trigger; hides on blur / mouseleave / Escape (anywhere). The trigger is
|
|
4
4
|
// any snippet; an action wires `aria-describedby` onto its first focusable
|
|
5
5
|
// element so screen readers announce the text.
|
|
6
6
|
//
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
if (wrapEl && tipEl) place(wrapEl, tipEl, `${placement}-center`, 6);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Escape must dismiss a hover-opened bubble even while focus sits elsewhere.
|
|
67
|
+
function onEscape(e: KeyboardEvent) {
|
|
68
|
+
if (e.key === 'Escape') hideNow();
|
|
69
|
+
}
|
|
70
|
+
|
|
66
71
|
function show() {
|
|
67
72
|
clearTimeout(hideTimer); // pointer came back within the grace period
|
|
68
73
|
clearTimeout(showTimer);
|
|
@@ -72,15 +77,20 @@
|
|
|
72
77
|
reposition();
|
|
73
78
|
addEventListener('scroll', reposition, true);
|
|
74
79
|
addEventListener('resize', reposition);
|
|
80
|
+
document.addEventListener('keydown', onEscape);
|
|
75
81
|
}, delay);
|
|
76
82
|
}
|
|
77
83
|
function hideNow() {
|
|
78
84
|
clearTimeout(showTimer);
|
|
79
85
|
clearTimeout(hideTimer);
|
|
80
|
-
|
|
86
|
+
try {
|
|
87
|
+
if (tipEl?.matches(':popover-open')) tipEl.hidePopover();
|
|
88
|
+
} catch {}
|
|
81
89
|
removeEventListener('scroll', reposition, true);
|
|
82
90
|
removeEventListener('resize', reposition);
|
|
91
|
+
document.removeEventListener('keydown', onEscape);
|
|
83
92
|
}
|
|
93
|
+
$effect(() => () => hideNow());
|
|
84
94
|
// Debounced close: cancelled if the pointer/focus enters the panel (rich
|
|
85
95
|
// mode) or returns to the trigger before `closeDelay` elapses.
|
|
86
96
|
function scheduleHide() {
|
|
@@ -104,21 +114,23 @@
|
|
|
104
114
|
node.tabIndex = 0;
|
|
105
115
|
target = node;
|
|
106
116
|
}
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
// The trigger may already be described (a Field hint/error): append our
|
|
118
|
+
// id to the token list and take back only that token on destroy.
|
|
119
|
+
const describedBy = () => (target.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean);
|
|
120
|
+
target.setAttribute('aria-describedby', [...describedBy().filter((t) => t !== id), id].join(' '));
|
|
109
121
|
node.addEventListener('mouseenter', show);
|
|
110
122
|
node.addEventListener('mouseleave', scheduleHide);
|
|
111
123
|
node.addEventListener('focusin', show);
|
|
112
124
|
node.addEventListener('focusout', scheduleHide);
|
|
113
|
-
node.addEventListener('keydown', onkey);
|
|
114
125
|
return {
|
|
115
126
|
destroy() {
|
|
116
|
-
|
|
127
|
+
const rest = describedBy().filter((t) => t !== id);
|
|
128
|
+
if (rest.length) target.setAttribute('aria-describedby', rest.join(' '));
|
|
129
|
+
else target.removeAttribute('aria-describedby');
|
|
117
130
|
node.removeEventListener('mouseenter', show);
|
|
118
131
|
node.removeEventListener('mouseleave', scheduleHide);
|
|
119
132
|
node.removeEventListener('focusin', show);
|
|
120
133
|
node.removeEventListener('focusout', scheduleHide);
|
|
121
|
-
node.removeEventListener('keydown', onkey);
|
|
122
134
|
}
|
|
123
135
|
};
|
|
124
136
|
}
|
|
@@ -126,19 +138,16 @@
|
|
|
126
138
|
// Action for the panel: in rich mode it accepts the pointer/focus, which
|
|
127
139
|
// cancels the pending close (hovercard persistence).
|
|
128
140
|
function panel(node: HTMLElement) {
|
|
129
|
-
const onkey = (e: KeyboardEvent) => e.key === 'Escape' && hideNow();
|
|
130
141
|
node.addEventListener('mouseenter', cancelHide);
|
|
131
142
|
node.addEventListener('mouseleave', scheduleHide);
|
|
132
143
|
node.addEventListener('focusin', cancelHide);
|
|
133
144
|
node.addEventListener('focusout', scheduleHide);
|
|
134
|
-
node.addEventListener('keydown', onkey);
|
|
135
145
|
return {
|
|
136
146
|
destroy() {
|
|
137
147
|
node.removeEventListener('mouseenter', cancelHide);
|
|
138
148
|
node.removeEventListener('mouseleave', scheduleHide);
|
|
139
149
|
node.removeEventListener('focusin', cancelHide);
|
|
140
150
|
node.removeEventListener('focusout', scheduleHide);
|
|
141
|
-
node.removeEventListener('keydown', onkey);
|
|
142
151
|
}
|
|
143
152
|
};
|
|
144
153
|
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Compact volume control: a mute toggle whose glyph tracks the level, and a
|
|
4
|
+
* short slider. `oninput` fires on every change, `onchange` once per
|
|
5
|
+
* release or keyboard/wheel step. The slider hides on touch devices by
|
|
6
|
+
* default (`hideOnTouch`), where the OS owns volume; the mute button stays.
|
|
7
|
+
*/
|
|
8
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
9
|
+
import type { IconName } from '../atoms/Icon.svelte';
|
|
10
|
+
import IconButton from './IconButton.svelte';
|
|
11
|
+
|
|
12
|
+
type Own = {
|
|
13
|
+
/** Level 0–1. */
|
|
14
|
+
value?: number;
|
|
15
|
+
muted?: boolean;
|
|
16
|
+
orientation?: 'horizontal' | 'vertical';
|
|
17
|
+
size?: 'sm' | 'md';
|
|
18
|
+
/** Level per arrow key or wheel notch. */
|
|
19
|
+
step?: number;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
hideOnTouch?: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
muteLabel?: string;
|
|
24
|
+
unmuteLabel?: string;
|
|
25
|
+
oninput?: (value: number) => void;
|
|
26
|
+
onchange?: (value: number) => void;
|
|
27
|
+
onmutechange?: (muted: boolean) => void;
|
|
28
|
+
class?: string;
|
|
29
|
+
style?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
value = $bindable(1),
|
|
34
|
+
muted = $bindable(false),
|
|
35
|
+
orientation = 'horizontal',
|
|
36
|
+
size = 'sm',
|
|
37
|
+
step = 0.05,
|
|
38
|
+
disabled = false,
|
|
39
|
+
hideOnTouch = true,
|
|
40
|
+
label = 'Volume',
|
|
41
|
+
muteLabel = 'Mute',
|
|
42
|
+
unmuteLabel = 'Unmute',
|
|
43
|
+
oninput,
|
|
44
|
+
onchange,
|
|
45
|
+
onmutechange,
|
|
46
|
+
class: klass = '',
|
|
47
|
+
style: styleProp,
|
|
48
|
+
...rest
|
|
49
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
50
|
+
|
|
51
|
+
let dragging = $state(false);
|
|
52
|
+
let pointerId: number | null = null;
|
|
53
|
+
|
|
54
|
+
const clamp = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4;
|
|
55
|
+
const level = $derived(muted ? 0 : clamp(value));
|
|
56
|
+
const pct = $derived(Math.round(level * 100));
|
|
57
|
+
const vertical = $derived(orientation === 'vertical');
|
|
58
|
+
const icon = $derived<IconName>(
|
|
59
|
+
muted ? 'volume-mute' : level === 0 ? 'volume' : level < 0.5 ? 'volume-low' : 'volume-high'
|
|
60
|
+
);
|
|
61
|
+
const valueText = $derived(muted ? 'Muted' : `${pct}%`);
|
|
62
|
+
|
|
63
|
+
function setMuted(m: boolean) {
|
|
64
|
+
if (m === muted) return;
|
|
65
|
+
muted = m;
|
|
66
|
+
onmutechange?.(m);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function apply(v: number) {
|
|
70
|
+
const next = clamp(v);
|
|
71
|
+
value = next;
|
|
72
|
+
if (next > 0) setMuted(false);
|
|
73
|
+
oninput?.(next);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function commit(v: number) {
|
|
78
|
+
onchange?.(apply(v));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fracAt(el: HTMLElement, e: PointerEvent) {
|
|
82
|
+
const r = el.getBoundingClientRect();
|
|
83
|
+
let f: number;
|
|
84
|
+
if (vertical) {
|
|
85
|
+
f = r.height ? 1 - (e.clientY - r.top) / r.height : 0;
|
|
86
|
+
} else {
|
|
87
|
+
f = r.width ? (e.clientX - r.left) / r.width : 0;
|
|
88
|
+
if (getComputedStyle(el).direction === 'rtl') f = 1 - f;
|
|
89
|
+
}
|
|
90
|
+
return clamp(f);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function release(el: HTMLElement) {
|
|
94
|
+
if (pointerId !== null && typeof el.releasePointerCapture === 'function') {
|
|
95
|
+
try {
|
|
96
|
+
el.releasePointerCapture(pointerId);
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
pointerId = null;
|
|
100
|
+
dragging = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onpointerdown(e: PointerEvent) {
|
|
104
|
+
if (disabled || e.button !== 0 || pointerId !== null) return;
|
|
105
|
+
const el = e.currentTarget as HTMLElement;
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
pointerId = e.pointerId;
|
|
108
|
+
dragging = true;
|
|
109
|
+
if (typeof el.setPointerCapture === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
el.setPointerCapture(e.pointerId);
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
el.focus?.({ preventScroll: true });
|
|
115
|
+
apply(fracAt(el, e));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onpointermove(e: PointerEvent) {
|
|
119
|
+
if (pointerId !== e.pointerId) return;
|
|
120
|
+
apply(fracAt(e.currentTarget as HTMLElement, e));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onpointerup(e: PointerEvent) {
|
|
124
|
+
if (pointerId !== e.pointerId) return;
|
|
125
|
+
release(e.currentTarget as HTMLElement);
|
|
126
|
+
onchange?.(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onpointercancel(e: PointerEvent) {
|
|
130
|
+
if (pointerId !== e.pointerId) return;
|
|
131
|
+
release(e.currentTarget as HTMLElement);
|
|
132
|
+
onchange?.(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function onlostpointercapture(e: PointerEvent) {
|
|
136
|
+
if (pointerId === null || pointerId !== e.pointerId) return;
|
|
137
|
+
pointerId = null;
|
|
138
|
+
dragging = false;
|
|
139
|
+
onchange?.(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function onkeydown(e: KeyboardEvent) {
|
|
143
|
+
if (disabled) return;
|
|
144
|
+
const base = level;
|
|
145
|
+
let next: number | null = null;
|
|
146
|
+
switch (e.key) {
|
|
147
|
+
case 'ArrowRight':
|
|
148
|
+
case 'ArrowUp':
|
|
149
|
+
next = base + step;
|
|
150
|
+
break;
|
|
151
|
+
case 'ArrowLeft':
|
|
152
|
+
case 'ArrowDown':
|
|
153
|
+
next = base - step;
|
|
154
|
+
break;
|
|
155
|
+
case 'PageUp':
|
|
156
|
+
next = base + step * 2;
|
|
157
|
+
break;
|
|
158
|
+
case 'PageDown':
|
|
159
|
+
next = base - step * 2;
|
|
160
|
+
break;
|
|
161
|
+
case 'Home':
|
|
162
|
+
next = 0;
|
|
163
|
+
break;
|
|
164
|
+
case 'End':
|
|
165
|
+
next = 1;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (next === null) return;
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
commit(next);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function wheel(el: HTMLElement) {
|
|
174
|
+
const onwheel = (e: WheelEvent) => {
|
|
175
|
+
if (disabled) return;
|
|
176
|
+
const d = e.deltaY || e.deltaX;
|
|
177
|
+
if (!d) return;
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
commit(level + (d < 0 ? step : -step));
|
|
180
|
+
};
|
|
181
|
+
el.addEventListener('wheel', onwheel, { passive: false });
|
|
182
|
+
return () => el.removeEventListener('wheel', onwheel);
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<div
|
|
187
|
+
data-tsu="VolumeSlider"
|
|
188
|
+
class="vol vol--{size} vol--{orientation} {klass}"
|
|
189
|
+
class:vol--hide-touch={hideOnTouch}
|
|
190
|
+
class:vol--dragging={dragging}
|
|
191
|
+
class:vol--muted={muted}
|
|
192
|
+
style="--pct: {pct};{styleProp ?? ''}"
|
|
193
|
+
{...rest}
|
|
194
|
+
>
|
|
195
|
+
<IconButton
|
|
196
|
+
{icon}
|
|
197
|
+
label={muted ? unmuteLabel : muteLabel}
|
|
198
|
+
pressed={muted}
|
|
199
|
+
box="sm"
|
|
200
|
+
size={size === 'sm' ? 16 : 18}
|
|
201
|
+
{disabled}
|
|
202
|
+
onclick={() => setMuted(!muted)}
|
|
203
|
+
data-part="mute"
|
|
204
|
+
/>
|
|
205
|
+
<div
|
|
206
|
+
class="vol-slab"
|
|
207
|
+
data-part="slab"
|
|
208
|
+
role="slider"
|
|
209
|
+
tabindex={disabled ? -1 : 0}
|
|
210
|
+
aria-label={label}
|
|
211
|
+
aria-orientation={orientation}
|
|
212
|
+
aria-valuemin={0}
|
|
213
|
+
aria-valuemax={100}
|
|
214
|
+
aria-valuenow={pct}
|
|
215
|
+
aria-valuetext={valueText}
|
|
216
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
217
|
+
{onpointerdown}
|
|
218
|
+
{onpointermove}
|
|
219
|
+
{onpointerup}
|
|
220
|
+
{onpointercancel}
|
|
221
|
+
{onlostpointercapture}
|
|
222
|
+
{onkeydown}
|
|
223
|
+
{@attach wheel}
|
|
224
|
+
>
|
|
225
|
+
<span class="vol-track" data-part="track">
|
|
226
|
+
<i class="vol-fill" data-part="fill"></i>
|
|
227
|
+
</span>
|
|
228
|
+
<span class="vol-anchor">
|
|
229
|
+
<i class="vol-thumb" data-part="thumb"></i>
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<style>
|
|
235
|
+
.vol {
|
|
236
|
+
--vol-track: 3px;
|
|
237
|
+
--vol-thumb: 10px;
|
|
238
|
+
--vol-length: 5rem;
|
|
239
|
+
--vol-accent: var(--slider-accent, var(--accent));
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: var(--sp-1);
|
|
243
|
+
vertical-align: middle;
|
|
244
|
+
}
|
|
245
|
+
.vol--md {
|
|
246
|
+
--vol-track: 4px;
|
|
247
|
+
--vol-thumb: 12px;
|
|
248
|
+
--vol-length: 7rem;
|
|
249
|
+
}
|
|
250
|
+
.vol--vertical {
|
|
251
|
+
flex-direction: column-reverse;
|
|
252
|
+
}
|
|
253
|
+
.vol-slab {
|
|
254
|
+
position: relative;
|
|
255
|
+
flex: none;
|
|
256
|
+
width: var(--vol-length);
|
|
257
|
+
min-height: 1.25rem;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
touch-action: none;
|
|
260
|
+
user-select: none;
|
|
261
|
+
-webkit-user-select: none;
|
|
262
|
+
outline: none;
|
|
263
|
+
border-radius: var(--r-sm);
|
|
264
|
+
}
|
|
265
|
+
.vol--vertical .vol-slab {
|
|
266
|
+
width: 1.25rem;
|
|
267
|
+
height: var(--vol-length);
|
|
268
|
+
min-height: 0;
|
|
269
|
+
}
|
|
270
|
+
@media (pointer: coarse) {
|
|
271
|
+
.vol-slab {
|
|
272
|
+
min-height: var(--touch-target-compact);
|
|
273
|
+
}
|
|
274
|
+
.vol--vertical .vol-slab {
|
|
275
|
+
min-width: var(--touch-target-compact);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
@media (hover: none) and (pointer: coarse) {
|
|
279
|
+
.vol--hide-touch .vol-slab {
|
|
280
|
+
display: none;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
.vol-slab[aria-disabled='true'] {
|
|
284
|
+
cursor: default;
|
|
285
|
+
opacity: 0.45;
|
|
286
|
+
}
|
|
287
|
+
.vol-slab:focus-visible {
|
|
288
|
+
outline: var(--focus-ring);
|
|
289
|
+
outline-offset: var(--focus-ring-offset);
|
|
290
|
+
}
|
|
291
|
+
.vol-track {
|
|
292
|
+
position: absolute;
|
|
293
|
+
inset-inline: 0;
|
|
294
|
+
top: 50%;
|
|
295
|
+
height: var(--vol-track);
|
|
296
|
+
margin-top: calc(var(--vol-track) / -2);
|
|
297
|
+
border-radius: var(--r-pill);
|
|
298
|
+
background: var(--bg-elevated-2);
|
|
299
|
+
overflow: hidden;
|
|
300
|
+
}
|
|
301
|
+
.vol--vertical .vol-track {
|
|
302
|
+
inset-inline: auto;
|
|
303
|
+
inset-block: 0;
|
|
304
|
+
left: 50%;
|
|
305
|
+
top: auto;
|
|
306
|
+
width: var(--vol-track);
|
|
307
|
+
height: auto;
|
|
308
|
+
margin-top: 0;
|
|
309
|
+
margin-left: calc(var(--vol-track) / -2);
|
|
310
|
+
}
|
|
311
|
+
.vol-fill {
|
|
312
|
+
position: absolute;
|
|
313
|
+
inset-block: 0;
|
|
314
|
+
inset-inline-start: 0;
|
|
315
|
+
width: calc(var(--pct) * 1%);
|
|
316
|
+
background: var(--vol-accent);
|
|
317
|
+
}
|
|
318
|
+
.vol--vertical .vol-fill {
|
|
319
|
+
inset-inline: 0;
|
|
320
|
+
inset-block: auto 0;
|
|
321
|
+
width: auto;
|
|
322
|
+
height: calc(var(--pct) * 1%);
|
|
323
|
+
}
|
|
324
|
+
.vol-anchor {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 50%;
|
|
327
|
+
inset-inline-start: calc(var(--pct) * 1%);
|
|
328
|
+
width: 0;
|
|
329
|
+
height: 0;
|
|
330
|
+
pointer-events: none;
|
|
331
|
+
}
|
|
332
|
+
.vol--vertical .vol-anchor {
|
|
333
|
+
top: auto;
|
|
334
|
+
bottom: calc(var(--pct) * 1%);
|
|
335
|
+
inset-inline-start: 50%;
|
|
336
|
+
}
|
|
337
|
+
.vol-thumb {
|
|
338
|
+
position: absolute;
|
|
339
|
+
left: 0;
|
|
340
|
+
top: 0;
|
|
341
|
+
width: var(--vol-thumb);
|
|
342
|
+
height: var(--vol-thumb);
|
|
343
|
+
border-radius: 50%;
|
|
344
|
+
background: var(--vol-accent);
|
|
345
|
+
border: 2px solid var(--bg);
|
|
346
|
+
box-shadow: var(--shadow-sm);
|
|
347
|
+
transform: translate(-50%, -50%) scale(0.6);
|
|
348
|
+
opacity: 0;
|
|
349
|
+
transition:
|
|
350
|
+
opacity 120ms var(--ease),
|
|
351
|
+
transform 120ms var(--ease);
|
|
352
|
+
}
|
|
353
|
+
.vol-slab:hover .vol-thumb,
|
|
354
|
+
.vol-slab:focus-visible .vol-thumb,
|
|
355
|
+
.vol--dragging .vol-thumb {
|
|
356
|
+
opacity: 1;
|
|
357
|
+
transform: translate(-50%, -50%) scale(1);
|
|
358
|
+
}
|
|
359
|
+
.vol--muted .vol-fill {
|
|
360
|
+
background: var(--text-faint);
|
|
361
|
+
}
|
|
362
|
+
@media (prefers-reduced-motion: reduce) {
|
|
363
|
+
.vol-thumb {
|
|
364
|
+
transition: none;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact volume control: a mute toggle whose glyph tracks the level, and a
|
|
3
|
+
* short slider. `oninput` fires on every change, `onchange` once per
|
|
4
|
+
* release or keyboard/wheel step. The slider hides on touch devices by
|
|
5
|
+
* default (`hideOnTouch`), where the OS owns volume; the mute button stays.
|
|
6
|
+
*/
|
|
7
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
8
|
+
type Own = {
|
|
9
|
+
/** Level 0–1. */
|
|
10
|
+
value?: number;
|
|
11
|
+
muted?: boolean;
|
|
12
|
+
orientation?: 'horizontal' | 'vertical';
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
/** Level per arrow key or wheel notch. */
|
|
15
|
+
step?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
hideOnTouch?: boolean;
|
|
18
|
+
label?: string;
|
|
19
|
+
muteLabel?: string;
|
|
20
|
+
unmuteLabel?: string;
|
|
21
|
+
oninput?: (value: number) => void;
|
|
22
|
+
onchange?: (value: number) => void;
|
|
23
|
+
onmutechange?: (muted: boolean) => void;
|
|
24
|
+
class?: string;
|
|
25
|
+
style?: string;
|
|
26
|
+
};
|
|
27
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
28
|
+
declare const VolumeSlider: import("svelte").Component<$$ComponentProps, {}, "muted" | "value">;
|
|
29
|
+
type VolumeSlider = ReturnType<typeof VolumeSlider>;
|
|
30
|
+
export default VolumeSlider;
|
|
@@ -157,8 +157,9 @@ export function caretRect(el) {
|
|
|
157
157
|
}
|
|
158
158
|
mirror.textContent = el.value.slice(0, caret);
|
|
159
159
|
const marker = doc.createElement('span');
|
|
160
|
-
marker.textContent = el.value
|
|
160
|
+
marker.textContent = el.value[caret] ?? '.';
|
|
161
161
|
mirror.appendChild(marker);
|
|
162
|
+
mirror.appendChild(doc.createTextNode(el.value.slice(caret + 1)));
|
|
162
163
|
doc.body.appendChild(mirror);
|
|
163
164
|
const m = mirror.getBoundingClientRect();
|
|
164
165
|
const s = marker.getBoundingClientRect();
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { default as Swatch } from './components/atoms/Swatch.svelte';
|
|
|
30
30
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
|
31
31
|
export { default as Text } from './components/atoms/Text.svelte';
|
|
32
32
|
export { default as Textarea } from './components/atoms/Textarea.svelte';
|
|
33
|
+
export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
|
|
33
34
|
export { default as AppShell } from './components/layouts/AppShell.svelte';
|
|
34
35
|
export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
35
36
|
export { default as Cluster } from './components/layouts/Cluster.svelte';
|
|
@@ -84,6 +85,7 @@ export { default as PullToRefresh } from './components/molecules/PullToRefresh.s
|
|
|
84
85
|
export { default as RadialMenu, type RadialArc, } from './components/molecules/RadialMenu.svelte';
|
|
85
86
|
export { default as RadioGroup, type RadioOption, } from './components/molecules/RadioGroup.svelte';
|
|
86
87
|
export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
|
|
88
|
+
export { default as SeekSlider } from './components/molecules/SeekSlider.svelte';
|
|
87
89
|
export { default as SegmentedControl, type SegmentOption, } from './components/molecules/SegmentedControl.svelte';
|
|
88
90
|
export { default as SelectButton } from './components/molecules/SelectButton.svelte';
|
|
89
91
|
export { default as SortableList, SORTABLE_LIST_MESSAGES, type SortableListMessages, type SortableListMove, type SortableListRowContext, } from './components/molecules/SortableList.svelte';
|
|
@@ -97,6 +99,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
|
|
|
97
99
|
export { default as Toggle } from './components/molecules/Toggle.svelte';
|
|
98
100
|
export { default as Tooltip } from './components/molecules/Tooltip.svelte';
|
|
99
101
|
export { default as Truncate } from './components/molecules/Truncate.svelte';
|
|
102
|
+
export { default as VolumeSlider } from './components/molecules/VolumeSlider.svelte';
|
|
100
103
|
export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
|
|
101
104
|
export { default as ConversationFeed } from './components/organisms/ConversationFeed.svelte';
|
|
102
105
|
export { default as ConversationPanel } from './components/organisms/ConversationPanel.svelte';
|
|
@@ -106,6 +109,7 @@ export { formatCount, hasCount } from './count';
|
|
|
106
109
|
export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
|
|
107
110
|
export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
108
111
|
export { escapeHtml, type Highlight, type RenderMarkdownOptions, renderMarkdown, stripAnsi, } from './markdown';
|
|
112
|
+
export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
|
|
109
113
|
export * as filterQuery from './query';
|
|
110
114
|
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';
|
|
111
115
|
export { type OptionSection, sectionOptions } from './select-options';
|
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 TimeDisplay } from './components/atoms/TimeDisplay.svelte';
|
|
37
38
|
// ---- layouts ----
|
|
38
39
|
export { default as AppShell } from './components/layouts/AppShell.svelte';
|
|
39
40
|
export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
@@ -92,6 +93,7 @@ export { default as PullToRefresh } from './components/molecules/PullToRefresh.s
|
|
|
92
93
|
export { default as RadialMenu, } from './components/molecules/RadialMenu.svelte';
|
|
93
94
|
export { default as RadioGroup, } from './components/molecules/RadioGroup.svelte';
|
|
94
95
|
export { default as SectionHeader } from './components/molecules/SectionHeader.svelte';
|
|
96
|
+
export { default as SeekSlider } from './components/molecules/SeekSlider.svelte';
|
|
95
97
|
export { default as SegmentedControl, } from './components/molecules/SegmentedControl.svelte';
|
|
96
98
|
export { default as SelectButton } from './components/molecules/SelectButton.svelte';
|
|
97
99
|
export { default as SortableList, SORTABLE_LIST_MESSAGES, } from './components/molecules/SortableList.svelte';
|
|
@@ -105,6 +107,7 @@ export { default as Toaster } from './components/molecules/Toaster.svelte';
|
|
|
105
107
|
export { default as Toggle } from './components/molecules/Toggle.svelte';
|
|
106
108
|
export { default as Tooltip } from './components/molecules/Tooltip.svelte';
|
|
107
109
|
export { default as Truncate } from './components/molecules/Truncate.svelte';
|
|
110
|
+
export { default as VolumeSlider } from './components/molecules/VolumeSlider.svelte';
|
|
108
111
|
export { default as WorkingDir } from './components/molecules/WorkingDir.svelte';
|
|
109
112
|
// ---- organisms ----
|
|
110
113
|
export { default as ConversationFeed } from './components/organisms/ConversationFeed.svelte';
|
|
@@ -115,6 +118,7 @@ export { formatCount, hasCount } from './count';
|
|
|
115
118
|
export { EMOJI_GROUPS, searchEmoji } from './emoji';
|
|
116
119
|
export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
|
|
117
120
|
export { escapeHtml, renderMarkdown, stripAnsi, } from './markdown';
|
|
121
|
+
export { describeMediaTime, formatMediaTime, isLiveDuration } from './media-time';
|
|
118
122
|
// ---- query core (headless: schema / parser / AST / suggest / compilers) ----
|
|
119
123
|
export * as filterQuery from './query';
|
|
120
124
|
export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Seconds as `m:ss`, or `h:mm:ss` once the value (or `ref`) reaches an hour. */
|
|
2
|
+
export declare function formatMediaTime(seconds: number, ref?: number): string;
|
|
3
|
+
/** True when a duration cannot be scrubbed: unknown, live, or empty. */
|
|
4
|
+
export declare function isLiveDuration(duration: number | null | undefined): boolean;
|
|
5
|
+
/** Spoken position: "1:02 of 23:45", or just the position when live. */
|
|
6
|
+
export declare function describeMediaTime(value: number, duration: number | null | undefined, format?: (s: number, ref?: number) => string): string;
|