@dorsk/tsumikit 0.68.1 → 0.70.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 +13 -4
- package/dist/components/layouts/AppShell.svelte +51 -9
- package/dist/components/layouts/AppShell.svelte.d.ts +9 -2
- package/dist/components/molecules/FilterInput.svelte +8 -0
- package/dist/components/organisms/DataTable.svelte +26 -2
- package/dist/components/organisms/DataTable.svelte.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -334,7 +334,8 @@ bindable, `onselect`/`onclose(reason)` report picks; `findTrigger`/`applyTrigger
|
|
|
334
334
|
are exported for trigger-char detection and token replacement).
|
|
335
335
|
|
|
336
336
|
**Organisms:** ConversationPanel / ConversationFeed (chat column + sticky
|
|
337
|
-
feed, see below), DataTable (generic `<T>`, typed columns + cell snippets;
|
|
337
|
+
feed, see below), DataTable (generic `<T>`, typed columns + cell snippets; `headerSnippets[key]`
|
|
338
|
+
replaces a header cell (a select-all checkbox);
|
|
338
339
|
`layout="fixed"` makes column widths authoritative, `Column.truncate` /
|
|
339
340
|
`nowrap` / `hideBelow="sm|md|lg"` (container-query on the table's own box),
|
|
340
341
|
`hideHeader` clips the header but keeps it for assistive tech, `rowTone(row)`
|
|
@@ -346,8 +347,12 @@ for a hover/focus-revealed trailing cell (always visible on touch; under
|
|
|
346
347
|
hooks on head/row/cell; `responsive="stack"` turns rows into cards below
|
|
347
348
|
`stackBelow` (48rem, measured on the table's own box) using `Column.role`
|
|
348
349
|
`title | detail | meta | actions | hidden` — detail cells get a `data-label`
|
|
349
|
-
prefix,
|
|
350
|
-
|
|
350
|
+
prefix, `Column.stackLabel: false` drops it so metas join on one `·` line
|
|
351
|
+
and details share lines, `--dt-meta-font` restyles meta values (e.g. `var(--font-sans)`; a
|
|
352
|
+
literal `inherit` falls back to mono), the `<table>`
|
|
353
|
+
stays a table for assistive tech), FilterSearchBar /
|
|
354
|
+
FilterInput (`size="sm"` compact bar on `--control-height-compact`, text
|
|
355
|
+
floored at 16px under `pointer: coarse` so iOS does not zoom on focus,
|
|
351
356
|
`shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
|
|
352
357
|
outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
|
|
353
358
|
`key="cwd"` puts FilterInput in single-key mode — `value` is the bare value
|
|
@@ -360,7 +365,11 @@ sidebar on desktop, overlay drawer on mobile, optionally resizable;
|
|
|
360
365
|
the content column only, `stickySidebar` pins it to the viewport, and
|
|
361
366
|
`sidebarPadding="none" | "sm" | "md"` sets the aside gutter — the header and its
|
|
362
367
|
children are `min-width: 0` so a wide title/actions row can't widen the grid on
|
|
363
|
-
mobile
|
|
368
|
+
mobile; the mobile drawer's state is `bind:open` with `onOpenChange(open)`, and
|
|
369
|
+
`closeOnNavigate` (default `true`) closes it when a plain same-origin link inside
|
|
370
|
+
it is clicked — a router can also close it, e.g. SvelteKit
|
|
371
|
+
`afterNavigate(() => (navOpen = false))` with `<AppShell bind:open={navOpen}>`
|
|
372
|
+
to cover `goto` and back/forward), NavBar (horizontal route bar — `<nav>` + `<a aria-current="page">`,
|
|
364
373
|
icon/emoji + label + count badge; `placement="bar"` pins it to the bottom edge
|
|
365
374
|
over `--z-nav` with `--safe-bottom` padding, `orientation` stacks or inlines the
|
|
366
375
|
glyph, `maxWidth` centres the row; items without `href` are buttons that set a
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// nav content collapse to icons.
|
|
8
8
|
// • mobile: the sidebar becomes an off-canvas drawer that slides in over a
|
|
9
9
|
// scrim; AppShell renders the hamburger toggle for you (shown only on
|
|
10
|
-
// mobile), locks body scroll while open, closes on Escape / scrim click
|
|
11
|
-
// and marks the rest of the page
|
|
10
|
+
// mobile), locks body scroll while open, closes on Escape / scrim click /
|
|
11
|
+
// a same-origin link click inside it, and marks the rest of the page
|
|
12
|
+
// `inert` so focus stays in the drawer. `bind:open` to drive it yourself.
|
|
12
13
|
// Switching to desktop auto-closes the drawer. Safe-area insets are honored.
|
|
13
|
-
import type
|
|
14
|
+
import { untrack, type Snippet } from 'svelte';
|
|
14
15
|
import { browser } from '../../env';
|
|
15
16
|
import IconButton from '../molecules/IconButton.svelte';
|
|
16
17
|
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
layout = 'header-top',
|
|
29
30
|
stickySidebar = false,
|
|
30
31
|
sidebarPadding = 'md',
|
|
32
|
+
open = $bindable(false),
|
|
33
|
+
onOpenChange,
|
|
34
|
+
closeOnNavigate = true,
|
|
31
35
|
class: klass = '',
|
|
32
36
|
style: styleProp = '',
|
|
33
37
|
}: {
|
|
@@ -54,12 +58,42 @@
|
|
|
54
58
|
stickySidebar?: boolean;
|
|
55
59
|
/** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
|
|
56
60
|
sidebarPadding?: 'none' | 'sm' | 'md';
|
|
61
|
+
/** Mobile drawer state; reset to `false` when the viewport reaches 48rem. */
|
|
62
|
+
open?: boolean;
|
|
63
|
+
/** Fires when AppShell itself opens or closes the drawer. */
|
|
64
|
+
onOpenChange?: (open: boolean) => void;
|
|
65
|
+
/** Close the mobile drawer when a same-origin link inside it is clicked
|
|
66
|
+
* (plain left click, no modifier keys, no `target` / `download`). */
|
|
67
|
+
closeOnNavigate?: boolean;
|
|
57
68
|
class?: string;
|
|
58
69
|
style?: string;
|
|
59
70
|
} = $props();
|
|
60
71
|
|
|
61
|
-
let open = $state(false);
|
|
62
72
|
let isMobile = $state(false);
|
|
73
|
+
let aside = $state<HTMLElement>();
|
|
74
|
+
|
|
75
|
+
function setOpen(next: boolean) {
|
|
76
|
+
if (open === next) return;
|
|
77
|
+
open = next;
|
|
78
|
+
onOpenChange?.(next);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onSidebarClick(e: MouseEvent) {
|
|
82
|
+
if (!closeOnNavigate || !open) return;
|
|
83
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
84
|
+
const link = (e.target as Element | null)?.closest?.('a[href]');
|
|
85
|
+
if (!link) return;
|
|
86
|
+
const target = link.getAttribute('target');
|
|
87
|
+
if ((target && target !== '_self') || link.hasAttribute('download')) return;
|
|
88
|
+
let url: URL;
|
|
89
|
+
try {
|
|
90
|
+
url = new URL(link.getAttribute('href') ?? '', document.baseURI);
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (url.origin !== location.origin) return;
|
|
95
|
+
setOpen(false);
|
|
96
|
+
}
|
|
63
97
|
|
|
64
98
|
// --- resizable sidebar (desktop) ---
|
|
65
99
|
function loadW(): number | null {
|
|
@@ -108,13 +142,20 @@
|
|
|
108
142
|
const mq = window.matchMedia('(max-width: 47.999rem)');
|
|
109
143
|
const sync = () => {
|
|
110
144
|
isMobile = mq.matches;
|
|
111
|
-
if (!isMobile)
|
|
145
|
+
if (!isMobile) setOpen(false);
|
|
112
146
|
};
|
|
113
|
-
sync
|
|
147
|
+
untrack(sync);
|
|
114
148
|
mq.addEventListener('change', sync);
|
|
115
149
|
return () => mq.removeEventListener('change', sync);
|
|
116
150
|
});
|
|
117
151
|
|
|
152
|
+
$effect(() => {
|
|
153
|
+
const el = aside;
|
|
154
|
+
if (!el) return;
|
|
155
|
+
el.addEventListener('click', onSidebarClick);
|
|
156
|
+
return () => el.removeEventListener('click', onSidebarClick);
|
|
157
|
+
});
|
|
158
|
+
|
|
118
159
|
// Lock body scroll while the mobile drawer is open.
|
|
119
160
|
$effect(() => {
|
|
120
161
|
if (!browser) return;
|
|
@@ -128,7 +169,7 @@
|
|
|
128
169
|
const bgInert = $derived(open && isMobile);
|
|
129
170
|
</script>
|
|
130
171
|
|
|
131
|
-
<svelte:window onkeydown={(e) => e.key === 'Escape' && (
|
|
172
|
+
<svelte:window onkeydown={(e) => e.key === 'Escape' && setOpen(false)} />
|
|
132
173
|
|
|
133
174
|
<div
|
|
134
175
|
class="shell {klass}"
|
|
@@ -147,7 +188,7 @@
|
|
|
147
188
|
icon="menu"
|
|
148
189
|
label="Toggle navigation"
|
|
149
190
|
aria-expanded={open}
|
|
150
|
-
onclick={() => (
|
|
191
|
+
onclick={() => setOpen(!open)}
|
|
151
192
|
/>
|
|
152
193
|
</div>
|
|
153
194
|
{/if}
|
|
@@ -160,7 +201,7 @@
|
|
|
160
201
|
class:show={open && isMobile}
|
|
161
202
|
tabindex="-1"
|
|
162
203
|
aria-hidden="true"
|
|
163
|
-
onclick={() => (
|
|
204
|
+
onclick={() => setOpen(false)}
|
|
164
205
|
></button>
|
|
165
206
|
<aside
|
|
166
207
|
class="shell-sidebar"
|
|
@@ -168,6 +209,7 @@
|
|
|
168
209
|
data-padding={sidebarPadding}
|
|
169
210
|
aria-label={navLabel}
|
|
170
211
|
inert={isMobile && !open ? true : undefined}
|
|
212
|
+
bind:this={aside}
|
|
171
213
|
>
|
|
172
214
|
{@render sidebar()}
|
|
173
215
|
{#if resizableSidebar}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
header?: Snippet;
|
|
4
4
|
sidebar?: Snippet;
|
|
@@ -23,9 +23,16 @@ type $$ComponentProps = {
|
|
|
23
23
|
stickySidebar?: boolean;
|
|
24
24
|
/** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
|
|
25
25
|
sidebarPadding?: 'none' | 'sm' | 'md';
|
|
26
|
+
/** Mobile drawer state; reset to `false` when the viewport reaches 48rem. */
|
|
27
|
+
open?: boolean;
|
|
28
|
+
/** Fires when AppShell itself opens or closes the drawer. */
|
|
29
|
+
onOpenChange?: (open: boolean) => void;
|
|
30
|
+
/** Close the mobile drawer when a same-origin link inside it is clicked
|
|
31
|
+
* (plain left click, no modifier keys, no `target` / `download`). */
|
|
32
|
+
closeOnNavigate?: boolean;
|
|
26
33
|
class?: string;
|
|
27
34
|
style?: string;
|
|
28
35
|
};
|
|
29
|
-
declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
36
|
+
declare const AppShell: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
30
37
|
type AppShell = ReturnType<typeof AppShell>;
|
|
31
38
|
export default AppShell;
|
|
@@ -524,6 +524,14 @@
|
|
|
524
524
|
outline: none;
|
|
525
525
|
min-width: 0;
|
|
526
526
|
}
|
|
527
|
+
@media (pointer: coarse) {
|
|
528
|
+
.fi__input {
|
|
529
|
+
font-size: max(16px, var(--fs-md));
|
|
530
|
+
}
|
|
531
|
+
.fi--sm .fi__input {
|
|
532
|
+
font-size: max(16px, var(--fs-sm));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
527
535
|
.fi__input::placeholder {
|
|
528
536
|
color: var(--text-faint);
|
|
529
537
|
}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
/** Placement inside the stacked card (`responsive="stack"` only). Columns
|
|
22
22
|
* without a role render as `label: value` detail lines. */
|
|
23
23
|
role?: 'title' | 'detail' | 'meta' | 'actions' | 'hidden';
|
|
24
|
+
/** `false` drops the stacked label: meta cells join on one `·` line and
|
|
25
|
+
* detail cells share lines (`responsive="stack"` only, default true). */
|
|
26
|
+
stackLabel?: boolean;
|
|
24
27
|
}
|
|
25
28
|
</script>
|
|
26
29
|
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
onrowclick,
|
|
41
44
|
onsort,
|
|
42
45
|
cellSnippets = {},
|
|
46
|
+
headerSnippets = {},
|
|
43
47
|
empty = 'No data.',
|
|
44
48
|
stickyHeader = false,
|
|
45
49
|
stickyOffset,
|
|
@@ -69,6 +73,8 @@
|
|
|
69
73
|
* table only reflects the indicator and emits; it does not reorder rows. */
|
|
70
74
|
onsort?: (key: string, dir: 'asc' | 'desc') => void;
|
|
71
75
|
cellSnippets?: Record<string, Snippet<[T]>>;
|
|
76
|
+
/** Replaces a column's header content (e.g. a select-all checkbox). */
|
|
77
|
+
headerSnippets?: Record<string, Snippet<[Column<T>]>>;
|
|
72
78
|
empty?: string | Snippet;
|
|
73
79
|
stickyHeader?: boolean;
|
|
74
80
|
/** Distance from the scroll container's top for the sticky header, e.g.
|
|
@@ -210,7 +216,9 @@
|
|
|
210
216
|
: 'none'
|
|
211
217
|
: undefined}
|
|
212
218
|
>
|
|
213
|
-
{#if col.
|
|
219
|
+
{#if headerSnippets[col.key]}
|
|
220
|
+
{@render headerSnippets[col.key](col)}
|
|
221
|
+
{:else if col.sortable}
|
|
214
222
|
<button type="button" class="dt-sort" onclick={() => toggleSort(col)}>
|
|
215
223
|
<span>{col.label}</span>
|
|
216
224
|
<span class="dt-arrow" class:active={sortKey === col.key}>
|
|
@@ -264,6 +272,7 @@
|
|
|
264
272
|
data-hide-below={col.hideBelow}
|
|
265
273
|
data-role={responsive === 'stack' ? (col.role ?? 'detail') : undefined}
|
|
266
274
|
data-label={responsive === 'stack' ? col.label : undefined}
|
|
275
|
+
data-stack-label={responsive === 'stack' && col.stackLabel === false ? 'false' : undefined}
|
|
267
276
|
class:truncate={col.truncate}
|
|
268
277
|
class:nowrap={col.nowrap}
|
|
269
278
|
style:text-align={stacked ? undefined : (col.align ?? 'left')}
|
|
@@ -528,12 +537,27 @@
|
|
|
528
537
|
[data-stacked] td[data-role='meta'] {
|
|
529
538
|
flex: none;
|
|
530
539
|
order: 1;
|
|
531
|
-
font-family: var(--font-mono);
|
|
540
|
+
font-family: var(--dt-meta-font, var(--font-mono));
|
|
532
541
|
font-size: var(--fs-xs);
|
|
533
542
|
color: var(--text-muted);
|
|
534
543
|
}
|
|
535
544
|
[data-stacked] td[data-role='meta']::before {
|
|
536
545
|
content: attr(data-label) ' ';
|
|
546
|
+
font-family: var(--font-sans);
|
|
547
|
+
color: var(--text-faint);
|
|
548
|
+
}
|
|
549
|
+
[data-stacked] td[data-role='detail'][data-stack-label='false'] {
|
|
550
|
+
flex: 0 1 auto;
|
|
551
|
+
}
|
|
552
|
+
[data-stacked] td[data-stack-label='false']::before {
|
|
553
|
+
content: none;
|
|
554
|
+
}
|
|
555
|
+
[data-stacked] td[data-role='meta'] ~ td[data-role='meta'][data-stack-label='false'] {
|
|
556
|
+
margin-inline-start: calc(var(--sp-1) - var(--sp-3));
|
|
557
|
+
}
|
|
558
|
+
[data-stacked] td[data-role='meta'] ~ td[data-role='meta'][data-stack-label='false']::before {
|
|
559
|
+
content: '·';
|
|
560
|
+
margin-inline-end: var(--sp-1);
|
|
537
561
|
color: var(--text-faint);
|
|
538
562
|
}
|
|
539
563
|
[data-stacked] td[data-role='actions'],
|
|
@@ -19,6 +19,9 @@ export interface Column<T> {
|
|
|
19
19
|
/** Placement inside the stacked card (`responsive="stack"` only). Columns
|
|
20
20
|
* without a role render as `label: value` detail lines. */
|
|
21
21
|
role?: 'title' | 'detail' | 'meta' | 'actions' | 'hidden';
|
|
22
|
+
/** `false` drops the stacked label: meta cells join on one `·` line and
|
|
23
|
+
* detail cells share lines (`responsive="stack"` only, default true). */
|
|
24
|
+
stackLabel?: boolean;
|
|
22
25
|
}
|
|
23
26
|
import type { Snippet } from 'svelte';
|
|
24
27
|
declare function $$render<T>(): {
|
|
@@ -32,6 +35,8 @@ declare function $$render<T>(): {
|
|
|
32
35
|
* table only reflects the indicator and emits; it does not reorder rows. */
|
|
33
36
|
onsort?: (key: string, dir: "asc" | "desc") => void;
|
|
34
37
|
cellSnippets?: Record<string, Snippet<[T]>>;
|
|
38
|
+
/** Replaces a column's header content (e.g. a select-all checkbox). */
|
|
39
|
+
headerSnippets?: Record<string, Snippet<[Column<T>]>>;
|
|
35
40
|
empty?: string | Snippet;
|
|
36
41
|
stickyHeader?: boolean;
|
|
37
42
|
/** Distance from the scroll container's top for the sticky header, e.g.
|
package/package.json
CHANGED