@dorsk/tsumikit 0.71.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 +54 -5
- package/dist/autoresize.js +13 -0
- package/dist/clipboard.js +12 -0
- package/dist/components/atoms/Artwork.svelte +4 -2
- package/dist/components/atoms/Avatar.svelte +4 -3
- package/dist/components/atoms/Badge.svelte +13 -0
- package/dist/components/atoms/Button.svelte +25 -9
- package/dist/components/atoms/Card.svelte +2 -0
- package/dist/components/atoms/Dot.svelte +1 -1
- package/dist/components/atoms/Icon.svelte +7 -0
- package/dist/components/atoms/Icon.svelte.d.ts +7 -0
- package/dist/components/atoms/Kbd.svelte +28 -16
- package/dist/components/atoms/Link.svelte +6 -4
- package/dist/components/atoms/Scrim.svelte +18 -1
- package/dist/components/atoms/Slider.svelte +11 -1
- package/dist/components/atoms/Swatch.svelte +12 -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 +72 -40
- package/dist/components/layouts/Container.svelte +25 -3
- package/dist/components/layouts/MasterDetail.svelte +59 -33
- package/dist/components/layouts/MasterDetail.svelte.d.ts +1 -1
- package/dist/components/layouts/NavItem.svelte +8 -4
- package/dist/components/layouts/NavSection.svelte +8 -2
- package/dist/components/layouts/ResizablePanel.svelte +19 -4
- package/dist/components/layouts/TabBar.svelte +217 -0
- package/dist/components/layouts/TabBar.svelte.d.ts +27 -0
- package/dist/components/layouts/Toolbar.svelte +5 -0
- package/dist/components/layouts/toolbar-roving.d.ts +16 -0
- package/dist/components/layouts/toolbar-roving.js +47 -1
- 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 +17 -1
- package/dist/components/molecules/FilterInput.svelte.d.ts +2 -0
- 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/PullToRefresh.svelte +205 -0
- package/dist/components/molecules/PullToRefresh.svelte.d.ts +18 -0
- 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/SwipeRow.svelte +349 -0
- package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
- 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/components/organisms/ConversationFeed.svelte +22 -8
- package/dist/components/organisms/ConversationFeed.svelte.d.ts +2 -0
- package/dist/components/organisms/DataTable.svelte +22 -5
- package/dist/components/organisms/DataTable.svelte.d.ts +4 -2
- package/dist/components/organisms/FilterSearchBar.svelte +4 -0
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/markdown.js +15 -7
- 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/dist/styles/reset.css +30 -0
- package/dist/styles/syntax.css +0 -29
- package/package.json +1 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { IconName } from '../atoms/Icon.svelte';
|
|
3
|
+
import type { Tone } from '../../tone';
|
|
4
|
+
|
|
5
|
+
export interface SwipeAction {
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
onaction: () => void;
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
/**
|
|
15
|
+
* List row with a hidden action on each side, revealed by a horizontal swipe.
|
|
16
|
+
* A swipe past `threshold` (fraction of the row width) fires the action on
|
|
17
|
+
* release; a shorter one snaps back. The axis is locked after a few pixels:
|
|
18
|
+
* a drag that is more vertical than horizontal is left to the scroller.
|
|
19
|
+
* The same actions are reachable as visually hidden buttons for keyboard
|
|
20
|
+
* and screen-reader users.
|
|
21
|
+
*/
|
|
22
|
+
import type { Snippet } from 'svelte';
|
|
23
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
24
|
+
import Icon from '../atoms/Icon.svelte';
|
|
25
|
+
import { canonicalTone } from '../../tone';
|
|
26
|
+
|
|
27
|
+
type Own = {
|
|
28
|
+
children: Snippet;
|
|
29
|
+
/** Revealed on a rightward swipe. */
|
|
30
|
+
leadingAction?: SwipeAction;
|
|
31
|
+
/** Revealed on a leftward swipe. */
|
|
32
|
+
trailingAction?: SwipeAction;
|
|
33
|
+
/** Custom pane content; defaults to the action's icon and label. */
|
|
34
|
+
leading?: Snippet<[SwipeAction, boolean]>;
|
|
35
|
+
trailing?: Snippet<[SwipeAction, boolean]>;
|
|
36
|
+
/** Fraction of the row width the content must travel to fire. */
|
|
37
|
+
threshold?: number;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
class?: string;
|
|
40
|
+
style?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
children,
|
|
45
|
+
leadingAction,
|
|
46
|
+
trailingAction,
|
|
47
|
+
leading,
|
|
48
|
+
trailing,
|
|
49
|
+
threshold = 0.35,
|
|
50
|
+
disabled = false,
|
|
51
|
+
class: klass = '',
|
|
52
|
+
style: styleProp,
|
|
53
|
+
...rest
|
|
54
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
55
|
+
|
|
56
|
+
const SLOP = 8;
|
|
57
|
+
const FALLBACK_WIDTH = 320;
|
|
58
|
+
|
|
59
|
+
let offset = $state(0);
|
|
60
|
+
let dragging = $state(false);
|
|
61
|
+
let settling = $state(false);
|
|
62
|
+
|
|
63
|
+
let pointerId: number | null = null;
|
|
64
|
+
let startX = 0;
|
|
65
|
+
let startY = 0;
|
|
66
|
+
let width = $state(0);
|
|
67
|
+
let locked: 'x' | 'y' | null = null;
|
|
68
|
+
let swallowClick = false;
|
|
69
|
+
let settleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
|
|
71
|
+
const armed = $derived(width > 0 && Math.abs(offset) >= width * threshold);
|
|
72
|
+
const side = $derived(offset > 0 ? 'leading' : offset < 0 ? 'trailing' : null);
|
|
73
|
+
const active = $derived(side === 'leading' ? leadingAction : side === 'trailing' ? trailingAction : undefined);
|
|
74
|
+
const tone = $derived(canonicalTone(active?.tone ?? 'neutral'));
|
|
75
|
+
|
|
76
|
+
function reducedMotion() {
|
|
77
|
+
return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function release(el: HTMLElement) {
|
|
81
|
+
if (pointerId !== null && typeof el.releasePointerCapture === 'function') {
|
|
82
|
+
try {
|
|
83
|
+
el.releasePointerCapture(pointerId);
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
pointerId = null;
|
|
87
|
+
locked = null;
|
|
88
|
+
dragging = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function settle() {
|
|
92
|
+
clearTimeout(settleTimer);
|
|
93
|
+
if (reducedMotion()) {
|
|
94
|
+
offset = 0;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
settling = true;
|
|
98
|
+
offset = 0;
|
|
99
|
+
settleTimer = setTimeout(() => (settling = false), 220);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onpointerdown(e: PointerEvent) {
|
|
103
|
+
swallowClick = false;
|
|
104
|
+
if (disabled || e.button !== 0 || pointerId !== null) return;
|
|
105
|
+
if (!leadingAction && !trailingAction) return;
|
|
106
|
+
const el = e.currentTarget as HTMLElement;
|
|
107
|
+
pointerId = e.pointerId;
|
|
108
|
+
startX = e.clientX;
|
|
109
|
+
startY = e.clientY;
|
|
110
|
+
width = el.clientWidth || FALLBACK_WIDTH;
|
|
111
|
+
locked = null;
|
|
112
|
+
clearTimeout(settleTimer);
|
|
113
|
+
settling = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onpointermove(e: PointerEvent) {
|
|
117
|
+
if (pointerId !== e.pointerId) return;
|
|
118
|
+
const el = e.currentTarget as HTMLElement;
|
|
119
|
+
const dx = e.clientX - startX;
|
|
120
|
+
const dy = e.clientY - startY;
|
|
121
|
+
if (locked === null) {
|
|
122
|
+
if (Math.hypot(dx, dy) < SLOP) return;
|
|
123
|
+
if (Math.abs(dy) >= Math.abs(dx)) {
|
|
124
|
+
locked = 'y';
|
|
125
|
+
release(el);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
locked = 'x';
|
|
129
|
+
dragging = true;
|
|
130
|
+
swallowClick = true;
|
|
131
|
+
if (typeof el.setPointerCapture === 'function') {
|
|
132
|
+
try {
|
|
133
|
+
el.setPointerCapture(e.pointerId);
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (locked !== 'x') return;
|
|
138
|
+
const max = leadingAction ? width : 0;
|
|
139
|
+
const min = trailingAction ? -width : 0;
|
|
140
|
+
offset = Math.max(min, Math.min(max, dx));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onpointerup(e: PointerEvent) {
|
|
144
|
+
if (pointerId !== e.pointerId) return;
|
|
145
|
+
const el = e.currentTarget as HTMLElement;
|
|
146
|
+
const fire = locked === 'x' && armed ? active : undefined;
|
|
147
|
+
release(el);
|
|
148
|
+
settle();
|
|
149
|
+
fire?.onaction();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function onpointercancel(e: PointerEvent) {
|
|
153
|
+
if (pointerId !== e.pointerId) return;
|
|
154
|
+
release(e.currentTarget as HTMLElement);
|
|
155
|
+
settle();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onclickcapture(e: MouseEvent) {
|
|
159
|
+
if (!swallowClick) return;
|
|
160
|
+
swallowClick = false;
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onlostpointercapture() {
|
|
166
|
+
if (pointerId === null) return;
|
|
167
|
+
pointerId = null;
|
|
168
|
+
locked = null;
|
|
169
|
+
dragging = false;
|
|
170
|
+
settle();
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<div
|
|
175
|
+
data-tsu="SwipeRow"
|
|
176
|
+
class="swiperow {klass}"
|
|
177
|
+
class:swiperow--dragging={dragging}
|
|
178
|
+
class:swiperow--settling={settling}
|
|
179
|
+
class:swiperow--armed={armed}
|
|
180
|
+
class:swiperow--leading={side === 'leading'}
|
|
181
|
+
class:swiperow--trailing={side === 'trailing'}
|
|
182
|
+
class:swiperow-tone-accent={tone === 'accent'}
|
|
183
|
+
class:swiperow-tone-success={tone === 'ok'}
|
|
184
|
+
class:swiperow-tone-info={tone === 'info'}
|
|
185
|
+
class:swiperow-tone-warn={tone === 'warn'}
|
|
186
|
+
class:swiperow-tone-danger={tone === 'danger'}
|
|
187
|
+
style="--swiperow-x: {offset}px;{styleProp ?? ''}"
|
|
188
|
+
{onpointerdown}
|
|
189
|
+
{onpointermove}
|
|
190
|
+
{onpointerup}
|
|
191
|
+
{onpointercancel}
|
|
192
|
+
{onlostpointercapture}
|
|
193
|
+
{onclickcapture}
|
|
194
|
+
{...rest}
|
|
195
|
+
>
|
|
196
|
+
{#if leadingAction}
|
|
197
|
+
<div class="swiperow-pane swiperow-pane--leading" aria-hidden="true" data-part="leading">
|
|
198
|
+
{#if leading}
|
|
199
|
+
{@render leading(leadingAction, side === 'leading' && armed)}
|
|
200
|
+
{:else}
|
|
201
|
+
{#if leadingAction.icon}<Icon name={leadingAction.icon} />{/if}
|
|
202
|
+
<span class="swiperow-pane-label">{leadingAction.label}</span>
|
|
203
|
+
{/if}
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
{#if trailingAction}
|
|
207
|
+
<div class="swiperow-pane swiperow-pane--trailing" aria-hidden="true" data-part="trailing">
|
|
208
|
+
{#if trailing}
|
|
209
|
+
{@render trailing(trailingAction, side === 'trailing' && armed)}
|
|
210
|
+
{:else}
|
|
211
|
+
{#if trailingAction.icon}<Icon name={trailingAction.icon} />{/if}
|
|
212
|
+
<span class="swiperow-pane-label">{trailingAction.label}</span>
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
216
|
+
<div class="swiperow-content" data-part="content">
|
|
217
|
+
{@render children()}
|
|
218
|
+
</div>
|
|
219
|
+
{#if leadingAction || trailingAction}
|
|
220
|
+
<div class="swiperow-actions" data-part="actions">
|
|
221
|
+
{#if leadingAction}
|
|
222
|
+
<button type="button" class="swiperow-sr" data-part="leading-button" {disabled} onclick={() => leadingAction?.onaction()}>
|
|
223
|
+
{leadingAction.label}
|
|
224
|
+
</button>
|
|
225
|
+
{/if}
|
|
226
|
+
{#if trailingAction}
|
|
227
|
+
<button type="button" class="swiperow-sr" data-part="trailing-button" {disabled} onclick={() => trailingAction?.onaction()}>
|
|
228
|
+
{trailingAction.label}
|
|
229
|
+
</button>
|
|
230
|
+
{/if}
|
|
231
|
+
</div>
|
|
232
|
+
{/if}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<style>
|
|
236
|
+
.swiperow {
|
|
237
|
+
--swiperow-tone: var(--bg-elevated-2);
|
|
238
|
+
--swiperow-ink: var(--text);
|
|
239
|
+
position: relative;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
touch-action: pan-y;
|
|
242
|
+
user-select: none;
|
|
243
|
+
-webkit-user-select: none;
|
|
244
|
+
}
|
|
245
|
+
.swiperow-tone-accent {
|
|
246
|
+
--swiperow-tone: var(--accent);
|
|
247
|
+
--swiperow-ink: var(--text-on-accent);
|
|
248
|
+
}
|
|
249
|
+
.swiperow-tone-success {
|
|
250
|
+
--swiperow-tone: var(--ok);
|
|
251
|
+
--swiperow-ink: var(--bg);
|
|
252
|
+
}
|
|
253
|
+
.swiperow-tone-info {
|
|
254
|
+
--swiperow-tone: var(--info);
|
|
255
|
+
--swiperow-ink: var(--bg);
|
|
256
|
+
}
|
|
257
|
+
.swiperow-tone-warn {
|
|
258
|
+
--swiperow-tone: var(--warn);
|
|
259
|
+
--swiperow-ink: var(--bg);
|
|
260
|
+
}
|
|
261
|
+
.swiperow-tone-danger {
|
|
262
|
+
--swiperow-tone: var(--danger);
|
|
263
|
+
--swiperow-ink: var(--bg);
|
|
264
|
+
}
|
|
265
|
+
.swiperow-pane {
|
|
266
|
+
position: absolute;
|
|
267
|
+
inset: 0;
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: var(--sp-2);
|
|
271
|
+
padding: 0 var(--sp-4);
|
|
272
|
+
background: var(--swiperow-tone);
|
|
273
|
+
color: var(--swiperow-ink);
|
|
274
|
+
font-size: var(--fs-sm);
|
|
275
|
+
font-weight: 600;
|
|
276
|
+
visibility: hidden;
|
|
277
|
+
pointer-events: none;
|
|
278
|
+
}
|
|
279
|
+
.swiperow-pane--leading {
|
|
280
|
+
justify-content: flex-start;
|
|
281
|
+
}
|
|
282
|
+
.swiperow-pane--trailing {
|
|
283
|
+
justify-content: flex-end;
|
|
284
|
+
}
|
|
285
|
+
.swiperow--leading .swiperow-pane--leading,
|
|
286
|
+
.swiperow--trailing .swiperow-pane--trailing {
|
|
287
|
+
visibility: visible;
|
|
288
|
+
}
|
|
289
|
+
.swiperow-pane :global(svg) {
|
|
290
|
+
transition: transform 120ms var(--ease);
|
|
291
|
+
}
|
|
292
|
+
.swiperow--armed .swiperow-pane :global(svg) {
|
|
293
|
+
transform: scale(1.2);
|
|
294
|
+
}
|
|
295
|
+
.swiperow-content {
|
|
296
|
+
position: relative;
|
|
297
|
+
background: var(--bg);
|
|
298
|
+
transform: translate3d(var(--swiperow-x), 0, 0);
|
|
299
|
+
will-change: transform;
|
|
300
|
+
}
|
|
301
|
+
.swiperow--settling .swiperow-content {
|
|
302
|
+
transition: transform 200ms var(--ease);
|
|
303
|
+
}
|
|
304
|
+
.swiperow--dragging .swiperow-content {
|
|
305
|
+
cursor: grabbing;
|
|
306
|
+
}
|
|
307
|
+
.swiperow-actions {
|
|
308
|
+
position: absolute;
|
|
309
|
+
inset: 0 0 0 auto;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
gap: var(--sp-1);
|
|
313
|
+
padding: 0 var(--sp-2);
|
|
314
|
+
pointer-events: none;
|
|
315
|
+
}
|
|
316
|
+
.swiperow-sr {
|
|
317
|
+
position: absolute;
|
|
318
|
+
width: 1px;
|
|
319
|
+
height: 1px;
|
|
320
|
+
padding: 0;
|
|
321
|
+
margin: -1px;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
clip: rect(0, 0, 0, 0);
|
|
324
|
+
white-space: nowrap;
|
|
325
|
+
border: 0;
|
|
326
|
+
}
|
|
327
|
+
.swiperow-sr:focus-visible {
|
|
328
|
+
position: static;
|
|
329
|
+
width: auto;
|
|
330
|
+
height: auto;
|
|
331
|
+
margin: 0;
|
|
332
|
+
padding: var(--sp-1) var(--sp-2);
|
|
333
|
+
clip: auto;
|
|
334
|
+
pointer-events: auto;
|
|
335
|
+
font: inherit;
|
|
336
|
+
font-size: var(--fs-sm);
|
|
337
|
+
color: var(--swiperow-ink);
|
|
338
|
+
background: var(--swiperow-tone);
|
|
339
|
+
border-radius: var(--r-sm);
|
|
340
|
+
outline: 2px solid var(--accent);
|
|
341
|
+
outline-offset: 1px;
|
|
342
|
+
}
|
|
343
|
+
@media (prefers-reduced-motion: reduce) {
|
|
344
|
+
.swiperow--settling .swiperow-content,
|
|
345
|
+
.swiperow-pane :global(svg) {
|
|
346
|
+
transition: none;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IconName } from '../atoms/Icon.svelte';
|
|
2
|
+
import type { Tone } from '../../tone';
|
|
3
|
+
export interface SwipeAction {
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: IconName;
|
|
6
|
+
tone?: Tone;
|
|
7
|
+
onaction: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* List row with a hidden action on each side, revealed by a horizontal swipe.
|
|
11
|
+
* A swipe past `threshold` (fraction of the row width) fires the action on
|
|
12
|
+
* release; a shorter one snaps back. The axis is locked after a few pixels:
|
|
13
|
+
* a drag that is more vertical than horizontal is left to the scroller.
|
|
14
|
+
* The same actions are reachable as visually hidden buttons for keyboard
|
|
15
|
+
* and screen-reader users.
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
19
|
+
type Own = {
|
|
20
|
+
children: Snippet;
|
|
21
|
+
/** Revealed on a rightward swipe. */
|
|
22
|
+
leadingAction?: SwipeAction;
|
|
23
|
+
/** Revealed on a leftward swipe. */
|
|
24
|
+
trailingAction?: SwipeAction;
|
|
25
|
+
/** Custom pane content; defaults to the action's icon and label. */
|
|
26
|
+
leading?: Snippet<[SwipeAction, boolean]>;
|
|
27
|
+
trailing?: Snippet<[SwipeAction, boolean]>;
|
|
28
|
+
/** Fraction of the row width the content must travel to fire. */
|
|
29
|
+
threshold?: number;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
class?: string;
|
|
32
|
+
style?: string;
|
|
33
|
+
};
|
|
34
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
35
|
+
declare const SwipeRow: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
36
|
+
type SwipeRow = ReturnType<typeof SwipeRow>;
|
|
37
|
+
export default SwipeRow;
|
|
@@ -86,6 +86,12 @@
|
|
|
86
86
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
87
87
|
const baseId = `tabs-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
88
|
const panelId = $derived(panel ? `${baseId}-panel` : undefined);
|
|
89
|
+
// The roving tab stop: the selected tab, or the first enabled one when the
|
|
90
|
+
// selection is unknown or disabled, so the strip never drops out of the Tab order.
|
|
91
|
+
const stop = $derived(
|
|
92
|
+
tabs.find((t) => t.id === value && !t.disabled) ?? tabs.find((t) => !t.disabled)
|
|
93
|
+
);
|
|
94
|
+
const tabId = (id: string) => `${baseId}-tab-${id}`;
|
|
89
95
|
|
|
90
96
|
function canClose(t: TabItem): boolean {
|
|
91
97
|
return t.closable ?? closable;
|
|
@@ -95,7 +101,7 @@
|
|
|
95
101
|
if (tabs.find((t) => t.id === id)?.disabled) return;
|
|
96
102
|
value = id;
|
|
97
103
|
queueMicrotask(() => {
|
|
98
|
-
const el = listEl?.
|
|
104
|
+
const el = listEl?.ownerDocument.getElementById(tabId(id));
|
|
99
105
|
// The list scrolls when it is too narrow, so keyboard selection has to
|
|
100
106
|
// bring the tab back into view or it moves somewhere unseen.
|
|
101
107
|
el?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
@@ -129,7 +135,7 @@
|
|
|
129
135
|
close(id);
|
|
130
136
|
}
|
|
131
137
|
function onkeydown(e: KeyboardEvent) {
|
|
132
|
-
const i = tabs.
|
|
138
|
+
const i = stop ? tabs.indexOf(stop) : -1;
|
|
133
139
|
if (i < 0) return;
|
|
134
140
|
if (e.key === 'Delete') {
|
|
135
141
|
if (!canClose(tabs[i])) return;
|
|
@@ -169,7 +175,7 @@
|
|
|
169
175
|
aria-selected={value === t.id}
|
|
170
176
|
aria-controls={panelId}
|
|
171
177
|
aria-disabled={t.disabled || undefined}
|
|
172
|
-
tabindex={
|
|
178
|
+
tabindex={stop?.id === t.id ? 0 : -1}
|
|
173
179
|
class="tab closable"
|
|
174
180
|
class:selected={value === t.id}
|
|
175
181
|
class:disabled={t.disabled}
|
|
@@ -202,7 +208,7 @@
|
|
|
202
208
|
aria-selected={value === t.id}
|
|
203
209
|
aria-controls={panelId}
|
|
204
210
|
disabled={t.disabled}
|
|
205
|
-
tabindex={
|
|
211
|
+
tabindex={stop?.id === t.id ? 0 : -1}
|
|
206
212
|
class="tab"
|
|
207
213
|
class:selected={value === t.id}
|
|
208
214
|
onclick={() => select(t.id)}
|
|
@@ -217,7 +223,7 @@
|
|
|
217
223
|
{/if}
|
|
218
224
|
</div>
|
|
219
225
|
{#if panel}
|
|
220
|
-
<div role="tabpanel" id={panelId} tabindex="0" class="tabpanel {panelClass}" class:pad-none={panelPadding === 'none'} class:pad-sm={panelPadding === 'sm'}>
|
|
226
|
+
<div role="tabpanel" id={panelId} aria-labelledby={value === undefined ? undefined : tabId(value)} tabindex="0" class="tabpanel {panelClass}" class:pad-none={panelPadding === 'none'} class:pad-sm={panelPadding === 'sm'}>
|
|
221
227
|
{#if value !== undefined}{@render panel(value)}{/if}
|
|
222
228
|
</div>
|
|
223
229
|
{/if}
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
<!-- Unparseable input: degrade to an inert dash rather than empty text. -->
|
|
131
131
|
<time class="{cls} ts-invalid" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar}>—</time>
|
|
132
132
|
{:else if showPopover}
|
|
133
|
-
<Popover label="
|
|
133
|
+
<Popover label="{label}, details" placement="bottom-start" bare>
|
|
134
134
|
{#snippet trigger()}
|
|
135
135
|
<time class="{cls} ts-trigger" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
136
136
|
{/snippet}
|
|
@@ -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
|
}
|