@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, the `<table>`
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), NavBar (horizontal route bar — `<nav>` + `<a aria-current="page">`,
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 `inert` so focus stays in the drawer.
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 { Snippet } from 'svelte';
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) open = false; // desktop: drawer concept doesn't apply
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' && (open = false)} />
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={() => (open = !open)}
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={() => (open = false)}
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 { Snippet } from 'svelte';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",