@dorsk/tsumikit 0.69.0 → 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
CHANGED
|
@@ -348,9 +348,11 @@ hooks on head/row/cell; `responsive="stack"` turns rows into cards below
|
|
|
348
348
|
`stackBelow` (48rem, measured on the table's own box) using `Column.role`
|
|
349
349
|
`title | detail | meta | actions | hidden` — detail cells get a `data-label`
|
|
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
|
|
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>`
|
|
352
353
|
stays a table for assistive tech), FilterSearchBar /
|
|
353
|
-
FilterInput (`size="sm"` compact bar on `--control-height-compact`,
|
|
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,
|
|
354
356
|
`shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
|
|
355
357
|
outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
|
|
356
358
|
`key="cwd"` puts FilterInput in single-key mode — `value` is the bare value
|
|
@@ -363,7 +365,11 @@ sidebar on desktop, overlay drawer on mobile, optionally resizable;
|
|
|
363
365
|
the content column only, `stickySidebar` pins it to the viewport, and
|
|
364
366
|
`sidebarPadding="none" | "sm" | "md"` sets the aside gutter — the header and its
|
|
365
367
|
children are `min-width: 0` so a wide title/actions row can't widen the grid on
|
|
366
|
-
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">`,
|
|
367
373
|
icon/emoji + label + count badge; `placement="bar"` pins it to the bottom edge
|
|
368
374
|
over `--z-nav` with `--safe-bottom` padding, `orientation` stacks or inlines the
|
|
369
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
|
}
|
package/package.json
CHANGED