kerfjs 4.3.0-beta.1 → 4.4.0-beta.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - **New `kerfjs/router` subpath — a client-side router (the "postcard router").** `createRouter({ routes, mode?, base?, interceptLinks? })` returns a handle over three things kerf already has: a reactive `route` signal (`{ path, params, query, hash }`), `delegate()`-based `<a href>` link interception, and a keyed **outlet** — `router.outlet()` renders the matched route in a `data-key`ed wrapper, so kerf's keyed morph **replaces the page wholesale on a route change** (fresh DOM) and **reconciles in place on a same-route param change** (preserving scroll / focus). Route patterns are static, `:param`, a trailing `*rest` wildcard, and `*` catch-all; the handle also gives `navigate(path, { replace?, state? })`, `back()`/`forward()`, `match(pattern)` / `activeClass(pattern, className)` reactive active-link helpers, hash **or** history mode, an optional base path, and `dispose()`. Link interception is automatic (same-origin, left-click, no modifier/`target`/`download`, opt out per-link with `data-router-ignore` / `rel="external"` or globally with `interceptLinks: false`). **Deliberately scoped** — no nested layouts, data loaders, lazy routes, guards, or SSR matching; compose those with kerf primitives (`resource` for loading, an `effect` on `route` for guards). The kerf **core stays router-free** — this is opt-in and tree-shakeable, adding nothing to the main barrel until imported, and docs/1's "Not a router" is about the runtime. See [`docs/20-router.md`](docs/20-router.md).
10
+
11
+ ## [4.3.0] - 2026-08-22
12
+
13
+
14
+
15
+ - KF-529: refresh README for the 4.3 cycle (prep-major-release) (`909c82a`)
16
+ - KF-530: extract promise-dialog helpers into overlay-dialogs.ts (`5d4ac38`)
17
+ - KF-528: sync llms.txt with docs/19 + 4.3 features (`1d9c004`)
18
+
9
19
  - **`kerfjs/overlay` gains opt-in native top-layer backing (`native: true`).** Every overlay surface (`overlay`, `confirm`, `prompt`, `form`, `choice`, `popover`, `tooltip`) now takes `native?: boolean` (default `false`). When `true` and the engine supports it, a **modal** surface (`trap: true`) is hosted in a `<dialog>` opened with `.showModal()` — real document inerting (pointer + focus + AT) and guaranteed stacking above any `z-index` — and a **non-modal** surface (`trap: false`) uses the **Popover API** (`[popover]` + `showPopover()`). Feature-detected (`HTMLDialogElement.prototype.showModal`, `HTMLElement.prototype.showPopover`), falling back to today's plain `<div>` where unsupported, so `native: true` is always safe to pass. The `render` slot + promise API are unchanged — kerf just hosts your markup in a `<dialog>` / `[popover]`. **Opt-in on purpose:** the native elements carry UA default styles (a `::backdrop`, centering, border, padding) that kerf does **not** reset (a reset would violate the zero-CSS contract) — style the element and its `::backdrop` via `className` (`.kerf-overlay::backdrop { … }`, the stable contract); and `container` becomes a visual no-op in native mode (the top layer ignores DOM position). See [`docs/19-native-overlay-backing.md`](docs/19-native-overlay-backing.md).
10
20
  - **`bindList` virtualization gains a `content-visibility` mode.** `virtualize: { rowHeight, mode: 'content-visibility' }` is a second virtualization strategy alongside the default `mode: 'window'` (today's JS windowing). It keeps **every** row in the DOM and sets `content-visibility: auto` + `contain-intrinsic-size: 0 <rowHeight>px` on each row, so a supporting engine (Chromium, Safari 18) skips the *layout/paint* of off-screen rows while **all rows stay findable** — find-in-page (Cmd/Ctrl+F), the accessibility tree, and anchor links / `scrollIntoView` all work on any row (the exact guarantee `mode: 'window'` can't give, since it removes off-window rows from the DOM). The `mode` choice is the app's and it's about list size: pick `'content-visibility'` for medium lists where findability beats the node ceiling, keep `'window'` for very large (100k-row) lists. In this mode `rowHeight` is only the `contain-intrinsic-size` placeholder (no windowing math), `setHeight` / `observeRowHeights` are no-ops (the browser owns measurement), `minRows` is ignored (all rows already render), and no scroll listener / `ResizeObserver` is installed — while `handle.container` / `containerClass` / `containerId` still work. There is deliberately **no feature detection**: on an engine without `content-visibility` the CSS is inert, so all rows still render (correct, still findable) — only the off-screen-skip optimization is absent. See [`docs/17-list-virtualization.md`](docs/17-list-virtualization.md) §17.11.
11
21
  - **Docs: `bindList` virtualization findability/a11y tradeoff is now a first-class caveat.** Off-window rows are removed from the DOM (not just hidden), so with `virtualize` set, find-in-page (Cmd/Ctrl+F), screen readers / the accessibility tree, and anchor links / `scrollIntoView` reach only the visible window. New `docs/17-list-virtualization.md` §17.10 spells out the consequences and the guidance (don't virtualize, or use `minRows` above the list length, when full findability matters more than the DOM node ceiling), and the `bindList` JSDoc + `docs/8-api-reference.md` §8.11 carry the same note. Behavior unchanged — documentation only.
package/README.md CHANGED
@@ -53,7 +53,7 @@ Here's the whole development loop — write a component, run the dev server, cli
53
53
 
54
54
  7. **Small public API.** ~18 exports from the main barrel (plus `arraySignal`, the `html` tagged template, and the companion-utility subpaths below — each opt-in, none in the core). No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
55
55
 
56
- 8. **Batteries on their own subpaths.** Nine optional, tree-shakeable subpaths cover the patterns every real app otherwise hand-rolls — **`kerfjs/list`** (a keyed list with per-row fine-grained mounts and fixed / app-declared / measured-height viewport **virtualization**), **`kerfjs/overlay`** (modals, `confirm` / `prompt` / `form` / `choice`, anchored popovers + tooltips, toasts), **`kerfjs/async`** (`resource` async-state with a built-in stale-response guard + SWR cache), **`kerfjs/scope`** (dispose-scopes that tie teardown to a DOM node's lifetime), plus `timing`, `remount`, `attach`, and `actions`. None of them grows the ~12 KB core until you import it.
56
+ 8. **Batteries on their own subpaths.** Nine optional, tree-shakeable subpaths cover the patterns every real app otherwise hand-rolls — **`kerfjs/list`** (a keyed list with per-row fine-grained mounts and fixed / app-declared / measured-height viewport **virtualization**, plus a `content-visibility` mode that keeps every row find-in-page-able), **`kerfjs/router`** (a "postcard **router**": route matching, `navigate`, auto `<a>` link interception, and a keyed outlet — the *core* stays router-free, this is opt-in), **`kerfjs/overlay`** (modals, `confirm` / `prompt` / `form` / `choice`, anchored popovers + tooltips, toasts — with opt-in native **top-layer** backing that stacks above any `z-index`), **`kerfjs/async`** (`resource` async-state with a built-in stale-response guard + SWR cache), **`kerfjs/scope`** (dispose-scopes that tie teardown to a DOM node's lifetime), plus `timing`, `remount`, `attach`, and `actions`. None of them grows the ~12 KB core until you import it.
57
57
 
58
58
  9. **Plain TS, plain JSX, plain ESM.** Drops into anything using esbuild / Vite / tsup. No plugin chain. And with the `html` tagged template (`import { html } from 'kerfjs/html'` — identical runtime semantics to JSX), a CDN / importmap project needs no build step at all.
59
59
 
@@ -227,7 +227,25 @@ const list = bindList(scrollEl, messages, {
227
227
  observeRowHeights(list); // one ResizeObserver → kerf anchor-corrects scroll
228
228
  ```
229
229
 
230
- Each subpath adds nothing to the main barrel until it's imported. See [`docs/8-api-reference.md`](./docs/8-api-reference.md) for the full list (`list`, `overlay`, `scope`, `async`, `timing`, `remount`, `attach`, `actions`).
230
+ And a whole client-side router in one call `kerfjs/router`, the "postcard router." A route table, a keyed `outlet()`, and automatic `<a href>` interception; the *core* stays router-free (this is opt-in):
231
+
232
+ ```ts
233
+ import { createRouter } from 'kerfjs/router';
234
+
235
+ const router = createRouter({
236
+ routes: [
237
+ { path: '/', component: () => <Home /> },
238
+ { path: '/users/:id', component: ({ id }) => <User id={id} /> },
239
+ { path: '*', component: () => <NotFound /> },
240
+ ],
241
+ });
242
+
243
+ mount(app, () => <div><nav>{/* <a href> links, auto-intercepted */}</nav>{router.outlet()}</div>);
244
+ ```
245
+
246
+ `router.route` is a signal; `router.outlet()` swaps the page wholesale across routes and morphs in place within one. Deliberately small — no nested layouts, loaders, or guards; compose those with the primitives above.
247
+
248
+ Each subpath adds nothing to the main barrel until it's imported. See [`docs/8-api-reference.md`](./docs/8-api-reference.md) for the full list (`list`, `router`, `overlay`, `scope`, `async`, `timing`, `remount`, `attach`, `actions`).
231
249
 
232
250
  ## Install
233
251
 
package/ai/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "kerfjsVersion": "4.2.0",
2
+ "kerfjsVersion": "4.3.0",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
package/dist/overlay.d.ts CHANGED
@@ -39,145 +39,6 @@ declare function positionAnchored(el: HTMLElement, anchor: Element, options?: An
39
39
  */
40
40
  declare function autoReposition(el: HTMLElement, anchor: Element, options?: AnchorPositionOptions): () => void;
41
41
 
42
- /**
43
- * `toast()` for `kerfjs/overlay` — a non-modal, auto-dismissing notification that
44
- * stacks in a shared body-level region. Split out of `overlay.ts` (KF-513) since
45
- * it's a distinct transient-UI concern from the modal dialogs; re-exported from
46
- * `overlay.ts` so the public `kerfjs/overlay` surface is unchanged. Structural
47
- * only — kerf ships no CSS; you style the region / toast / animations.
48
- */
49
-
50
- /** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
51
- type ToastContent = string | SafeHtml | (() => MountResult);
52
- /** Accent variant for a {@link toast} — mapped to a `${className}--${variant}` class. */
53
- type ToastVariant = 'info' | 'success' | 'warning';
54
- /** Options for {@link toast}. */
55
- interface ToastOptions {
56
- /** Where toasts stack. Default: a lazily-created `<div class="kerf-toasts">` on `document.body`. */
57
- container?: Element;
58
- /** Class on the toast element. Default `'kerf-toast'`. */
59
- className?: string;
60
- /** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */
61
- duration?: number;
62
- /** ARIA role. Default `'status'`. */
63
- role?: string;
64
- /**
65
- * `'stack'` (default) shows toasts stacked in the region; `'replace'` dismisses
66
- * the region's current toast(s) first (collapse-to-latest for a rapid sequence).
67
- */
68
- mode?: 'stack' | 'replace';
69
- /**
70
- * How `mode: 'replace'` drops the prior toast(s): `'fade'` (default) runs their
71
- * full exit transition (nice for a STACKING region), or `'instant'` removes them
72
- * synchronously with no exit — what a single, exactly-centered toast slot wants,
73
- * so the outgoing and incoming messages never cross-fade in the same spot.
74
- */
75
- collapse?: 'fade' | 'instant';
76
- /** Accent variant — adds a `${className}--${variant}` class (kerf ships no CSS; you style it). */
77
- variant?: ToastVariant;
78
- /** Class added on the next animation frame after mount, so a CSS **entrance** transition can run. */
79
- enterClass?: string;
80
- /**
81
- * Class added when dismissing, so CSS owns the **exit**. On dismiss the
82
- * `enterClass` (if any) is also REMOVED, so `exitClass` doesn't have to
83
- * out-specify it — and a symmetric single-class fade (entrance = add
84
- * `enterClass`, exit = remove it) works by setting only `enterClass` +
85
- * `exitDuration`. The node is removed `exitDuration` ms later.
86
- */
87
- exitClass?: string;
88
- /** ms to wait before removing the node on dismiss — applies when `exitClass` is set OR when it's > 0 (to let a removed `enterClass` transition out). Default `0`. */
89
- exitDuration?: number;
90
- }
91
- /** Handle returned by {@link toast}. */
92
- interface ToastHandle {
93
- /** The toast element — inspect it, or run your own entrance/exit transitions. */
94
- el: HTMLElement;
95
- /**
96
- * Dismiss it early. Default runs the `exitClass` transition (removed after
97
- * `exitDuration`); pass `{ instant: true }` to remove it **synchronously** with
98
- * no exit — for an action button that immediately shows a replacement toast in a
99
- * single centered slot (no cross-fade). Idempotent.
100
- */
101
- dismiss(options?: {
102
- instant?: boolean;
103
- }): void;
104
- }
105
- /**
106
- * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
107
- * region (or your `container`). Returns a {@link ToastHandle} (`{ el, dismiss }`)
108
- * so you can run entrance/exit transitions, wire an action button, or inspect the
109
- * node. `mode: 'replace'` collapses a rapid sequence to the latest; `variant`
110
- * adds an accent class; `enterClass`/`exitClass` let CSS own the animation.
111
- */
112
- declare function toast(content: ToastContent, options?: ToastOptions): ToastHandle;
113
-
114
- /** A user-initiated dismissal trigger. */
115
- type DismissTrigger = 'escape' | 'backdrop' | 'outside';
116
- /** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */
117
- type OverlayContent = SafeHtml | (() => MountResult);
118
- /** Options for {@link overlay}. */
119
- interface OverlayOptions {
120
- /** Where to append the overlay wrapper. Default `document.body`. */
121
- container?: Element;
122
- /** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */
123
- className?: string;
124
- /**
125
- * Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.
126
- * `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`
127
- * = a click anywhere outside the wrapper (for anchored popovers). `false`
128
- * disables user dismissal (close it programmatically).
129
- */
130
- dismiss?: DismissTrigger | DismissTrigger[] | false;
131
- /**
132
- * Where focus lands on open: a selector, `true` (first focusable element, or
133
- * the wrapper if none), or `false` (leave focus alone). Default `true`.
134
- */
135
- initialFocus?: string | boolean;
136
- /**
137
- * Trap Tab / Shift+Tab within the overlay while open and mark it
138
- * `role="dialog"` / `aria-modal="true"`. Default `true`. Set `false` for a
139
- * non-modal popover.
140
- */
141
- trap?: boolean;
142
- /** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */
143
- role?: string;
144
- /** Called on any user-initiated dismissal (before `close()` runs). */
145
- onDismiss?: () => void;
146
- /** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */
147
- outsideIgnore?: Element | readonly Element[];
148
- /**
149
- * Opt into the browser **top layer** (`docs/19-native-overlay-backing.md`).
150
- * When `true` and the engine supports it, a modal overlay (`trap: true`) is
151
- * hosted in a `<dialog>` opened with `.showModal()` — real inerting of the rest
152
- * of the document + guaranteed stacking above any `z-index` — and a non-modal
153
- * one (`trap: false`) uses the Popover API (`[popover]` + `showPopover()`).
154
- * Feature-detected; falls back to today's plain `<div>` where unsupported.
155
- *
156
- * The `render` slot + promise API are unchanged — kerf just hosts your markup
157
- * in a `<dialog>` / `[popover]` instead of a `<div>`. Two caveats: native
158
- * `<dialog>` / `[popover]` carry **UA default styles** (a `::backdrop`,
159
- * centering, border, padding) that kerf does not reset — style the element (and
160
- * its `::backdrop`) via `className`; and `container` is effectively a **no-op**
161
- * for visual position, since the top layer ignores where the element lives in
162
- * the DOM. Default `false`.
163
- */
164
- native?: boolean;
165
- }
166
- /** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */
167
- interface OverlayHandle {
168
- /** The wrapper element (mounted into, appended to `container`). */
169
- el: HTMLElement;
170
- /** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */
171
- close(result?: unknown): void;
172
- /** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */
173
- result: Promise<unknown>;
174
- }
175
- /**
176
- * Open an overlay: append a wrapper to `container`, `mount()` `content` inside
177
- * it, wire the requested dismissals + (optionally) a focus trap, and return a
178
- * handle. See {@link OverlayOptions}.
179
- */
180
- declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
181
42
  /**
182
43
  * Wiring slots passed to a {@link ConfirmOptions.render} — spread `ok` / `cancel`
183
44
  * onto your own clickable elements so `confirm()` still resolves them (they are
@@ -384,6 +245,168 @@ interface ChoiceOptions<R> {
384
245
  */
385
246
  declare function choice<R>(message: string, actions: ReadonlyArray<ChoiceAction<R>>, options?: ChoiceOptions<R>): Promise<R | null>;
386
247
 
248
+ /**
249
+ * `toast()` for `kerfjs/overlay` — a non-modal, auto-dismissing notification that
250
+ * stacks in a shared body-level region. Split out of `overlay.ts` (KF-513) since
251
+ * it's a distinct transient-UI concern from the modal dialogs; re-exported from
252
+ * `overlay.ts` so the public `kerfjs/overlay` surface is unchanged. Structural
253
+ * only — kerf ships no CSS; you style the region / toast / animations.
254
+ */
255
+
256
+ /** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
257
+ type ToastContent = string | SafeHtml | (() => MountResult);
258
+ /** Accent variant for a {@link toast} — mapped to a `${className}--${variant}` class. */
259
+ type ToastVariant = 'info' | 'success' | 'warning';
260
+ /** Options for {@link toast}. */
261
+ interface ToastOptions {
262
+ /** Where toasts stack. Default: a lazily-created `<div class="kerf-toasts">` on `document.body`. */
263
+ container?: Element;
264
+ /** Class on the toast element. Default `'kerf-toast'`. */
265
+ className?: string;
266
+ /** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */
267
+ duration?: number;
268
+ /** ARIA role. Default `'status'`. */
269
+ role?: string;
270
+ /**
271
+ * `'stack'` (default) shows toasts stacked in the region; `'replace'` dismisses
272
+ * the region's current toast(s) first (collapse-to-latest for a rapid sequence).
273
+ */
274
+ mode?: 'stack' | 'replace';
275
+ /**
276
+ * How `mode: 'replace'` drops the prior toast(s): `'fade'` (default) runs their
277
+ * full exit transition (nice for a STACKING region), or `'instant'` removes them
278
+ * synchronously with no exit — what a single, exactly-centered toast slot wants,
279
+ * so the outgoing and incoming messages never cross-fade in the same spot.
280
+ */
281
+ collapse?: 'fade' | 'instant';
282
+ /** Accent variant — adds a `${className}--${variant}` class (kerf ships no CSS; you style it). */
283
+ variant?: ToastVariant;
284
+ /** Class added on the next animation frame after mount, so a CSS **entrance** transition can run. */
285
+ enterClass?: string;
286
+ /**
287
+ * Class added when dismissing, so CSS owns the **exit**. On dismiss the
288
+ * `enterClass` (if any) is also REMOVED, so `exitClass` doesn't have to
289
+ * out-specify it — and a symmetric single-class fade (entrance = add
290
+ * `enterClass`, exit = remove it) works by setting only `enterClass` +
291
+ * `exitDuration`. The node is removed `exitDuration` ms later.
292
+ */
293
+ exitClass?: string;
294
+ /** ms to wait before removing the node on dismiss — applies when `exitClass` is set OR when it's > 0 (to let a removed `enterClass` transition out). Default `0`. */
295
+ exitDuration?: number;
296
+ }
297
+ /** Handle returned by {@link toast}. */
298
+ interface ToastHandle {
299
+ /** The toast element — inspect it, or run your own entrance/exit transitions. */
300
+ el: HTMLElement;
301
+ /**
302
+ * Dismiss it early. Default runs the `exitClass` transition (removed after
303
+ * `exitDuration`); pass `{ instant: true }` to remove it **synchronously** with
304
+ * no exit — for an action button that immediately shows a replacement toast in a
305
+ * single centered slot (no cross-fade). Idempotent.
306
+ */
307
+ dismiss(options?: {
308
+ instant?: boolean;
309
+ }): void;
310
+ }
311
+ /**
312
+ * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
313
+ * region (or your `container`). Returns a {@link ToastHandle} (`{ el, dismiss }`)
314
+ * so you can run entrance/exit transitions, wire an action button, or inspect the
315
+ * node. `mode: 'replace'` collapses a rapid sequence to the latest; `variant`
316
+ * adds an accent class; `enterClass`/`exitClass` let CSS own the animation.
317
+ */
318
+ declare function toast(content: ToastContent, options?: ToastOptions): ToastHandle;
319
+
320
+ /**
321
+ * `kerfjs/overlay` — the modal / overlay + dismiss manager.
322
+ *
323
+ * Every real kerf app hand-rolls this: `toElement → body.appendChild → mount →
324
+ * wire dismissal → remove`, plus the fiddly parts (Escape, backdrop / outside
325
+ * click, focus trap, restoring focus on close). `window.confirm` is a no-op in
326
+ * Tauri WKWebViews, so a hand-built overlay is mandatory there. This subpath
327
+ * blesses the pattern as three functions over `mount()` — `overlay()`, and the
328
+ * `confirm()` / `toast()` conveniences built on it. No per-instance framework
329
+ * state: each call owns its DOM + listeners in a closure and returns a handle.
330
+ *
331
+ * import { overlay, confirm, toast } from 'kerfjs/overlay';
332
+ *
333
+ * const ok = await confirm('Delete this file?', { danger: true });
334
+ * toast('Saved');
335
+ * const dialog = overlay(<Settings />, { dismiss: ['escape', 'backdrop'] });
336
+ * // …later: dialog.close(); or await dialog.result;
337
+ *
338
+ * Structural only — kerf ships no CSS. The wrapper gets your `className`; style
339
+ * the backdrop / centering / animation yourself.
340
+ */
341
+
342
+ /** A user-initiated dismissal trigger. */
343
+ type DismissTrigger = 'escape' | 'backdrop' | 'outside';
344
+ /** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */
345
+ type OverlayContent = SafeHtml | (() => MountResult);
346
+ /** Options for {@link overlay}. */
347
+ interface OverlayOptions {
348
+ /** Where to append the overlay wrapper. Default `document.body`. */
349
+ container?: Element;
350
+ /** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */
351
+ className?: string;
352
+ /**
353
+ * Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.
354
+ * `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`
355
+ * = a click anywhere outside the wrapper (for anchored popovers). `false`
356
+ * disables user dismissal (close it programmatically).
357
+ */
358
+ dismiss?: DismissTrigger | DismissTrigger[] | false;
359
+ /**
360
+ * Where focus lands on open: a selector, `true` (first focusable element, or
361
+ * the wrapper if none), or `false` (leave focus alone). Default `true`.
362
+ */
363
+ initialFocus?: string | boolean;
364
+ /**
365
+ * Trap Tab / Shift+Tab within the overlay while open and mark it
366
+ * `role="dialog"` / `aria-modal="true"`. Default `true`. Set `false` for a
367
+ * non-modal popover.
368
+ */
369
+ trap?: boolean;
370
+ /** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */
371
+ role?: string;
372
+ /** Called on any user-initiated dismissal (before `close()` runs). */
373
+ onDismiss?: () => void;
374
+ /** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */
375
+ outsideIgnore?: Element | readonly Element[];
376
+ /**
377
+ * Opt into the browser **top layer** (`docs/19-native-overlay-backing.md`).
378
+ * When `true` and the engine supports it, a modal overlay (`trap: true`) is
379
+ * hosted in a `<dialog>` opened with `.showModal()` — real inerting of the rest
380
+ * of the document + guaranteed stacking above any `z-index` — and a non-modal
381
+ * one (`trap: false`) uses the Popover API (`[popover]` + `showPopover()`).
382
+ * Feature-detected; falls back to today's plain `<div>` where unsupported.
383
+ *
384
+ * The `render` slot + promise API are unchanged — kerf just hosts your markup
385
+ * in a `<dialog>` / `[popover]` instead of a `<div>`. Two caveats: native
386
+ * `<dialog>` / `[popover]` carry **UA default styles** (a `::backdrop`,
387
+ * centering, border, padding) that kerf does not reset — style the element (and
388
+ * its `::backdrop`) via `className`; and `container` is effectively a **no-op**
389
+ * for visual position, since the top layer ignores where the element lives in
390
+ * the DOM. Default `false`.
391
+ */
392
+ native?: boolean;
393
+ }
394
+ /** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */
395
+ interface OverlayHandle {
396
+ /** The wrapper element (mounted into, appended to `container`). */
397
+ el: HTMLElement;
398
+ /** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */
399
+ close(result?: unknown): void;
400
+ /** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */
401
+ result: Promise<unknown>;
402
+ }
403
+ /**
404
+ * Open an overlay: append a wrapper to `container`, `mount()` `content` inside
405
+ * it, wire the requested dismissals + (optionally) a focus trap, and return a
406
+ * handle. See {@link OverlayOptions}.
407
+ */
408
+ declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
409
+
387
410
  /** Options for {@link popover}. */
388
411
  interface PopoverOptions {
389
412
  /** Where to append the popover wrapper. Default `document.body`. */