New:Socket for Asana Is Now Available.Learn more
Sign In

kerfjs

Package Overview
Dependencies
Maintainers
1
Versions
54
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

kerfjs - npm Package Compare versions

Comparing version
4.3.0-beta.1
to
4.3.0
+1
-1
ai/manifest.json
{
"kerfjsVersion": "4.2.0",
"kerfjsVersion": "4.3.0",
"files": [

@@ -4,0 +4,0 @@ {

@@ -43,141 +43,2 @@ import { SafeHtml } from './jsx-runtime.js';

/**
* `toast()` for `kerfjs/overlay` — a non-modal, auto-dismissing notification that
* stacks in a shared body-level region. Split out of `overlay.ts` (KF-513) since
* it's a distinct transient-UI concern from the modal dialogs; re-exported from
* `overlay.ts` so the public `kerfjs/overlay` surface is unchanged. Structural
* only — kerf ships no CSS; you style the region / toast / animations.
*/
/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
type ToastContent = string | SafeHtml | (() => MountResult);
/** Accent variant for a {@link toast} — mapped to a `${className}--${variant}` class. */
type ToastVariant = 'info' | 'success' | 'warning';
/** Options for {@link toast}. */
interface ToastOptions {
/** Where toasts stack. Default: a lazily-created `<div class="kerf-toasts">` on `document.body`. */
container?: Element;
/** Class on the toast element. Default `'kerf-toast'`. */
className?: string;
/** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */
duration?: number;
/** ARIA role. Default `'status'`. */
role?: string;
/**
* `'stack'` (default) shows toasts stacked in the region; `'replace'` dismisses
* the region's current toast(s) first (collapse-to-latest for a rapid sequence).
*/
mode?: 'stack' | 'replace';
/**
* How `mode: 'replace'` drops the prior toast(s): `'fade'` (default) runs their
* full exit transition (nice for a STACKING region), or `'instant'` removes them
* synchronously with no exit — what a single, exactly-centered toast slot wants,
* so the outgoing and incoming messages never cross-fade in the same spot.
*/
collapse?: 'fade' | 'instant';
/** Accent variant — adds a `${className}--${variant}` class (kerf ships no CSS; you style it). */
variant?: ToastVariant;
/** Class added on the next animation frame after mount, so a CSS **entrance** transition can run. */
enterClass?: string;
/**
* Class added when dismissing, so CSS owns the **exit**. On dismiss the
* `enterClass` (if any) is also REMOVED, so `exitClass` doesn't have to
* out-specify it — and a symmetric single-class fade (entrance = add
* `enterClass`, exit = remove it) works by setting only `enterClass` +
* `exitDuration`. The node is removed `exitDuration` ms later.
*/
exitClass?: string;
/** 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`. */
exitDuration?: number;
}
/** Handle returned by {@link toast}. */
interface ToastHandle {
/** The toast element — inspect it, or run your own entrance/exit transitions. */
el: HTMLElement;
/**
* Dismiss it early. Default runs the `exitClass` transition (removed after
* `exitDuration`); pass `{ instant: true }` to remove it **synchronously** with
* no exit — for an action button that immediately shows a replacement toast in a
* single centered slot (no cross-fade). Idempotent.
*/
dismiss(options?: {
instant?: boolean;
}): void;
}
/**
* Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
* region (or your `container`). Returns a {@link ToastHandle} (`{ el, dismiss }`)
* so you can run entrance/exit transitions, wire an action button, or inspect the
* node. `mode: 'replace'` collapses a rapid sequence to the latest; `variant`
* adds an accent class; `enterClass`/`exitClass` let CSS own the animation.
*/
declare function toast(content: ToastContent, options?: ToastOptions): ToastHandle;
/** A user-initiated dismissal trigger. */
type DismissTrigger = 'escape' | 'backdrop' | 'outside';
/** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */
type OverlayContent = SafeHtml | (() => MountResult);
/** Options for {@link overlay}. */
interface OverlayOptions {
/** Where to append the overlay wrapper. Default `document.body`. */
container?: Element;
/** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */
className?: string;
/**
* Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.
* `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`
* = a click anywhere outside the wrapper (for anchored popovers). `false`
* disables user dismissal (close it programmatically).
*/
dismiss?: DismissTrigger | DismissTrigger[] | false;
/**
* Where focus lands on open: a selector, `true` (first focusable element, or
* the wrapper if none), or `false` (leave focus alone). Default `true`.
*/
initialFocus?: string | boolean;
/**
* Trap Tab / Shift+Tab within the overlay while open and mark it
* `role="dialog"` / `aria-modal="true"`. Default `true`. Set `false` for a
* non-modal popover.
*/
trap?: boolean;
/** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */
role?: string;
/** Called on any user-initiated dismissal (before `close()` runs). */
onDismiss?: () => void;
/** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */
outsideIgnore?: Element | readonly Element[];
/**
* Opt into the browser **top layer** (`docs/19-native-overlay-backing.md`).
* When `true` and the engine supports it, a modal overlay (`trap: true`) is
* hosted in a `<dialog>` opened with `.showModal()` — real inerting of the rest
* of the document + guaranteed stacking above any `z-index` — and a non-modal
* one (`trap: false`) uses the Popover API (`[popover]` + `showPopover()`).
* Feature-detected; falls back to today's plain `<div>` where unsupported.
*
* The `render` slot + promise API are unchanged — kerf just hosts your markup
* in a `<dialog>` / `[popover]` instead of a `<div>`. Two caveats: native
* `<dialog>` / `[popover]` carry **UA default styles** (a `::backdrop`,
* centering, border, padding) that kerf does not reset — style the element (and
* its `::backdrop`) via `className`; and `container` is effectively a **no-op**
* for visual position, since the top layer ignores where the element lives in
* the DOM. Default `false`.
*/
native?: boolean;
}
/** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */
interface OverlayHandle {
/** The wrapper element (mounted into, appended to `container`). */
el: HTMLElement;
/** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */
close(result?: unknown): void;
/** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */
result: Promise<unknown>;
}
/**
* Open an overlay: append a wrapper to `container`, `mount()` `content` inside
* it, wire the requested dismissals + (optionally) a focus trap, and return a
* handle. See {@link OverlayOptions}.
*/
declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
/**
* Wiring slots passed to a {@link ConfirmOptions.render} — spread `ok` / `cancel`

@@ -388,2 +249,164 @@ * onto your own clickable elements so `confirm()` still resolves them (they are

/**
* `toast()` for `kerfjs/overlay` — a non-modal, auto-dismissing notification that
* stacks in a shared body-level region. Split out of `overlay.ts` (KF-513) since
* it's a distinct transient-UI concern from the modal dialogs; re-exported from
* `overlay.ts` so the public `kerfjs/overlay` surface is unchanged. Structural
* only — kerf ships no CSS; you style the region / toast / animations.
*/
/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
type ToastContent = string | SafeHtml | (() => MountResult);
/** Accent variant for a {@link toast} — mapped to a `${className}--${variant}` class. */
type ToastVariant = 'info' | 'success' | 'warning';
/** Options for {@link toast}. */
interface ToastOptions {
/** Where toasts stack. Default: a lazily-created `<div class="kerf-toasts">` on `document.body`. */
container?: Element;
/** Class on the toast element. Default `'kerf-toast'`. */
className?: string;
/** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */
duration?: number;
/** ARIA role. Default `'status'`. */
role?: string;
/**
* `'stack'` (default) shows toasts stacked in the region; `'replace'` dismisses
* the region's current toast(s) first (collapse-to-latest for a rapid sequence).
*/
mode?: 'stack' | 'replace';
/**
* How `mode: 'replace'` drops the prior toast(s): `'fade'` (default) runs their
* full exit transition (nice for a STACKING region), or `'instant'` removes them
* synchronously with no exit — what a single, exactly-centered toast slot wants,
* so the outgoing and incoming messages never cross-fade in the same spot.
*/
collapse?: 'fade' | 'instant';
/** Accent variant — adds a `${className}--${variant}` class (kerf ships no CSS; you style it). */
variant?: ToastVariant;
/** Class added on the next animation frame after mount, so a CSS **entrance** transition can run. */
enterClass?: string;
/**
* Class added when dismissing, so CSS owns the **exit**. On dismiss the
* `enterClass` (if any) is also REMOVED, so `exitClass` doesn't have to
* out-specify it — and a symmetric single-class fade (entrance = add
* `enterClass`, exit = remove it) works by setting only `enterClass` +
* `exitDuration`. The node is removed `exitDuration` ms later.
*/
exitClass?: string;
/** 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`. */
exitDuration?: number;
}
/** Handle returned by {@link toast}. */
interface ToastHandle {
/** The toast element — inspect it, or run your own entrance/exit transitions. */
el: HTMLElement;
/**
* Dismiss it early. Default runs the `exitClass` transition (removed after
* `exitDuration`); pass `{ instant: true }` to remove it **synchronously** with
* no exit — for an action button that immediately shows a replacement toast in a
* single centered slot (no cross-fade). Idempotent.
*/
dismiss(options?: {
instant?: boolean;
}): void;
}
/**
* Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
* region (or your `container`). Returns a {@link ToastHandle} (`{ el, dismiss }`)
* so you can run entrance/exit transitions, wire an action button, or inspect the
* node. `mode: 'replace'` collapses a rapid sequence to the latest; `variant`
* adds an accent class; `enterClass`/`exitClass` let CSS own the animation.
*/
declare function toast(content: ToastContent, options?: ToastOptions): ToastHandle;
/**
* `kerfjs/overlay` — the modal / overlay + dismiss manager.
*
* Every real kerf app hand-rolls this: `toElement → body.appendChild → mount →
* wire dismissal → remove`, plus the fiddly parts (Escape, backdrop / outside
* click, focus trap, restoring focus on close). `window.confirm` is a no-op in
* Tauri WKWebViews, so a hand-built overlay is mandatory there. This subpath
* blesses the pattern as three functions over `mount()` — `overlay()`, and the
* `confirm()` / `toast()` conveniences built on it. No per-instance framework
* state: each call owns its DOM + listeners in a closure and returns a handle.
*
* import { overlay, confirm, toast } from 'kerfjs/overlay';
*
* const ok = await confirm('Delete this file?', { danger: true });
* toast('Saved');
* const dialog = overlay(<Settings />, { dismiss: ['escape', 'backdrop'] });
* // …later: dialog.close(); or await dialog.result;
*
* Structural only — kerf ships no CSS. The wrapper gets your `className`; style
* the backdrop / centering / animation yourself.
*/
/** A user-initiated dismissal trigger. */
type DismissTrigger = 'escape' | 'backdrop' | 'outside';
/** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */
type OverlayContent = SafeHtml | (() => MountResult);
/** Options for {@link overlay}. */
interface OverlayOptions {
/** Where to append the overlay wrapper. Default `document.body`. */
container?: Element;
/** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */
className?: string;
/**
* Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.
* `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`
* = a click anywhere outside the wrapper (for anchored popovers). `false`
* disables user dismissal (close it programmatically).
*/
dismiss?: DismissTrigger | DismissTrigger[] | false;
/**
* Where focus lands on open: a selector, `true` (first focusable element, or
* the wrapper if none), or `false` (leave focus alone). Default `true`.
*/
initialFocus?: string | boolean;
/**
* Trap Tab / Shift+Tab within the overlay while open and mark it
* `role="dialog"` / `aria-modal="true"`. Default `true`. Set `false` for a
* non-modal popover.
*/
trap?: boolean;
/** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */
role?: string;
/** Called on any user-initiated dismissal (before `close()` runs). */
onDismiss?: () => void;
/** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */
outsideIgnore?: Element | readonly Element[];
/**
* Opt into the browser **top layer** (`docs/19-native-overlay-backing.md`).
* When `true` and the engine supports it, a modal overlay (`trap: true`) is
* hosted in a `<dialog>` opened with `.showModal()` — real inerting of the rest
* of the document + guaranteed stacking above any `z-index` — and a non-modal
* one (`trap: false`) uses the Popover API (`[popover]` + `showPopover()`).
* Feature-detected; falls back to today's plain `<div>` where unsupported.
*
* The `render` slot + promise API are unchanged — kerf just hosts your markup
* in a `<dialog>` / `[popover]` instead of a `<div>`. Two caveats: native
* `<dialog>` / `[popover]` carry **UA default styles** (a `::backdrop`,
* centering, border, padding) that kerf does not reset — style the element (and
* its `::backdrop`) via `className`; and `container` is effectively a **no-op**
* for visual position, since the top layer ignores where the element lives in
* the DOM. Default `false`.
*/
native?: boolean;
}
/** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */
interface OverlayHandle {
/** The wrapper element (mounted into, appended to `container`). */
el: HTMLElement;
/** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */
close(result?: unknown): void;
/** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */
result: Promise<unknown>;
}
/**
* Open an overlay: append a wrapper to `container`, `mount()` `content` inside
* it, wire the requested dismissals + (optionally) a focus trap, and return a
* handle. See {@link OverlayOptions}.
*/
declare function overlay(content: OverlayContent, options?: OverlayOptions): OverlayHandle;
/** Options for {@link popover}. */

@@ -390,0 +413,0 @@ interface PopoverOptions {

@@ -40,224 +40,3 @@ import { mount } from './chunk-SRWQKB33.js';

// src/overlay-toast.ts
var TOAST_SET = /* @__PURE__ */ Symbol("kerf.toasts");
function toastRegion(container) {
if (container !== void 0) return container;
const existing = document.querySelector(".kerf-toasts");
if (existing !== null) return existing;
const region = document.createElement("div");
region.className = "kerf-toasts";
region.setAttribute("aria-live", "polite");
document.body.appendChild(region);
return region;
}
function toast(content, options = {}) {
const {
container,
className = "kerf-toast",
duration = 4e3,
role = "status",
mode = "stack",
collapse = "fade",
variant,
enterClass,
exitClass,
exitDuration = 0
} = options;
const region = toastRegion(container);
const active = region[TOAST_SET] ??= /* @__PURE__ */ new Set();
if (mode === "replace") {
for (const d of [...active]) {
if (collapse === "instant") d.removeNow();
else d();
}
}
const el = document.createElement("div");
el.className = className;
if (variant !== void 0) el.classList.add(`${className}--${variant}`);
el.setAttribute("role", role);
region.appendChild(el);
const disposeMount = mount(el, typeof content === "function" ? content : () => content);
const state = { dismissed: false, removed: false, timer: void 0, exitTimer: void 0 };
if (enterClass !== void 0) {
globalThis.requestAnimationFrame(() => {
if (!state.dismissed) el.classList.add(enterClass);
});
}
const remove = () => {
if (state.removed) return;
state.removed = true;
state.dismissed = true;
if (state.exitTimer !== void 0) clearTimeout(state.exitTimer);
disposeMount();
el.remove();
active.delete(dismiss);
};
const removeNow = () => {
if (state.timer !== void 0) clearTimeout(state.timer);
remove();
};
const fadeOut = () => {
if (state.dismissed) return;
state.dismissed = true;
if (state.timer !== void 0) clearTimeout(state.timer);
if (enterClass !== void 0) el.classList.remove(enterClass);
if (exitClass !== void 0) el.classList.add(exitClass);
if (exitClass !== void 0 || exitDuration > 0) {
state.exitTimer = setTimeout(remove, exitDuration);
} else {
remove();
}
};
function dismiss(options2) {
if (options2?.instant === true) removeNow();
else fadeOut();
}
dismiss.removeNow = removeNow;
active.add(dismiss);
if (duration > 0) state.timer = setTimeout(fadeOut, duration);
return { el, dismiss };
}
// src/overlay.ts
var FOCUSABLE = 'a[href],area[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),iframe,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
function focusable(root) {
return Array.from(root.querySelectorAll(FOCUSABLE)).filter(
(el) => !el.hasAttribute("hidden")
);
}
function supportsDialog() {
return typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
}
function supportsPopover() {
return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
}
function overlay(content, options = {}) {
const {
container = document.body,
className = "kerf-overlay",
dismiss = ["escape", "backdrop"],
initialFocus = true,
trap = true,
role = "dialog",
onDismiss,
outsideIgnore,
native = false
} = options;
const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
const restoreTo = document.activeElement;
const useDialog = native && trap && supportsDialog();
const usePopover = native && !trap && supportsPopover();
const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div");
wrapper.className = className;
if (usePopover) wrapper.setAttribute("popover", "manual");
if (trap && !useDialog) {
wrapper.setAttribute("role", role);
wrapper.setAttribute("aria-modal", "true");
}
container.appendChild(wrapper);
const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content);
let nativeOpened = false;
if (useDialog) {
wrapper.showModal();
nativeOpened = true;
} else if (usePopover) {
wrapper.showPopover();
wrapper.style.inset = "auto";
nativeOpened = true;
}
const removers = [];
const resultBox = {};
const result = new Promise((resolve) => {
resultBox.resolve = resolve;
});
const state = { closed: false };
function close(value) {
if (state.closed) return;
state.closed = true;
for (const remove of removers) remove();
disposeMount();
if (nativeOpened) {
nativeOpened = false;
if (useDialog) wrapper.close();
else wrapper.hidePopover();
}
wrapper.remove();
if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
resultBox.resolve?.(value);
}
function userDismiss() {
onDismiss?.();
close();
}
const wantEscape = triggers.includes("escape");
if (useDialog) {
const onCancel = (event) => {
event.preventDefault();
if (wantEscape) userDismiss();
};
wrapper.addEventListener("cancel", onCancel);
removers.push(() => wrapper.removeEventListener("cancel", onCancel));
} else if (wantEscape || trap) {
const onKeydown = (event) => {
if (wantEscape && event.key === "Escape") {
event.stopPropagation();
userDismiss();
return;
}
if (trap && event.key === "Tab") {
const items = focusable(wrapper);
if (items.length === 0) {
event.preventDefault();
return;
}
const first = items[0];
const last = items[items.length - 1];
const active = document.activeElement;
const outside = !wrapper.contains(active);
if (event.shiftKey && (active === first || outside)) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && (active === last || outside)) {
event.preventDefault();
first.focus();
}
}
};
document.addEventListener("keydown", onKeydown, true);
removers.push(() => document.removeEventListener("keydown", onKeydown, true));
}
if (triggers.includes("backdrop")) {
const onClick = (event) => {
if (event.target === wrapper) userDismiss();
};
wrapper.addEventListener("click", onClick);
removers.push(() => wrapper.removeEventListener("click", onClick));
}
if (triggers.includes("outside")) {
const ignore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];
const onDocClick = (event) => {
const target = event.target;
if (target === null) return;
if (wrapper.contains(target)) return;
if (ignore.some((el) => el === target || el.contains(target))) return;
userDismiss();
};
document.addEventListener("click", onDocClick, true);
removers.push(() => document.removeEventListener("click", onDocClick, true));
}
if (initialFocus !== false) {
if (typeof initialFocus === "string") {
wrapper.querySelector(initialFocus)?.focus();
} else {
const first = focusable(wrapper)[0];
if (first !== void 0) {
first.focus();
} else {
wrapper.tabIndex = -1;
wrapper.focus();
}
}
}
return { el: wrapper, close, result };
}
// src/overlay-dialogs.ts
function confirm(message, options = {}) {

@@ -550,2 +329,225 @@ const {

}
// src/overlay-toast.ts
var TOAST_SET = /* @__PURE__ */ Symbol("kerf.toasts");
function toastRegion(container) {
if (container !== void 0) return container;
const existing = document.querySelector(".kerf-toasts");
if (existing !== null) return existing;
const region = document.createElement("div");
region.className = "kerf-toasts";
region.setAttribute("aria-live", "polite");
document.body.appendChild(region);
return region;
}
function toast(content, options = {}) {
const {
container,
className = "kerf-toast",
duration = 4e3,
role = "status",
mode = "stack",
collapse = "fade",
variant,
enterClass,
exitClass,
exitDuration = 0
} = options;
const region = toastRegion(container);
const active = region[TOAST_SET] ??= /* @__PURE__ */ new Set();
if (mode === "replace") {
for (const d of [...active]) {
if (collapse === "instant") d.removeNow();
else d();
}
}
const el = document.createElement("div");
el.className = className;
if (variant !== void 0) el.classList.add(`${className}--${variant}`);
el.setAttribute("role", role);
region.appendChild(el);
const disposeMount = mount(el, typeof content === "function" ? content : () => content);
const state = { dismissed: false, removed: false, timer: void 0, exitTimer: void 0 };
if (enterClass !== void 0) {
globalThis.requestAnimationFrame(() => {
if (!state.dismissed) el.classList.add(enterClass);
});
}
const remove = () => {
if (state.removed) return;
state.removed = true;
state.dismissed = true;
if (state.exitTimer !== void 0) clearTimeout(state.exitTimer);
disposeMount();
el.remove();
active.delete(dismiss);
};
const removeNow = () => {
if (state.timer !== void 0) clearTimeout(state.timer);
remove();
};
const fadeOut = () => {
if (state.dismissed) return;
state.dismissed = true;
if (state.timer !== void 0) clearTimeout(state.timer);
if (enterClass !== void 0) el.classList.remove(enterClass);
if (exitClass !== void 0) el.classList.add(exitClass);
if (exitClass !== void 0 || exitDuration > 0) {
state.exitTimer = setTimeout(remove, exitDuration);
} else {
remove();
}
};
function dismiss(options2) {
if (options2?.instant === true) removeNow();
else fadeOut();
}
dismiss.removeNow = removeNow;
active.add(dismiss);
if (duration > 0) state.timer = setTimeout(fadeOut, duration);
return { el, dismiss };
}
// src/overlay.ts
var FOCUSABLE = 'a[href],area[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),iframe,[tabindex]:not([tabindex="-1"]),[contenteditable="true"]';
function focusable(root) {
return Array.from(root.querySelectorAll(FOCUSABLE)).filter(
(el) => !el.hasAttribute("hidden")
);
}
function supportsDialog() {
return typeof HTMLDialogElement !== "undefined" && typeof HTMLDialogElement.prototype.showModal === "function";
}
function supportsPopover() {
return typeof HTMLElement !== "undefined" && typeof HTMLElement.prototype.showPopover === "function";
}
function overlay(content, options = {}) {
const {
container = document.body,
className = "kerf-overlay",
dismiss = ["escape", "backdrop"],
initialFocus = true,
trap = true,
role = "dialog",
onDismiss,
outsideIgnore,
native = false
} = options;
const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
const restoreTo = document.activeElement;
const useDialog = native && trap && supportsDialog();
const usePopover = native && !trap && supportsPopover();
const wrapper = useDialog ? document.createElement("dialog") : document.createElement("div");
wrapper.className = className;
if (usePopover) wrapper.setAttribute("popover", "manual");
if (trap && !useDialog) {
wrapper.setAttribute("role", role);
wrapper.setAttribute("aria-modal", "true");
}
container.appendChild(wrapper);
const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content);
let nativeOpened = false;
if (useDialog) {
wrapper.showModal();
nativeOpened = true;
} else if (usePopover) {
wrapper.showPopover();
wrapper.style.inset = "auto";
nativeOpened = true;
}
const removers = [];
const resultBox = {};
const result = new Promise((resolve) => {
resultBox.resolve = resolve;
});
const state = { closed: false };
function close(value) {
if (state.closed) return;
state.closed = true;
for (const remove of removers) remove();
disposeMount();
if (nativeOpened) {
nativeOpened = false;
if (useDialog) wrapper.close();
else wrapper.hidePopover();
}
wrapper.remove();
if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
resultBox.resolve?.(value);
}
function userDismiss() {
onDismiss?.();
close();
}
const wantEscape = triggers.includes("escape");
if (useDialog) {
const onCancel = (event) => {
event.preventDefault();
if (wantEscape) userDismiss();
};
wrapper.addEventListener("cancel", onCancel);
removers.push(() => wrapper.removeEventListener("cancel", onCancel));
} else if (wantEscape || trap) {
const onKeydown = (event) => {
if (wantEscape && event.key === "Escape") {
event.stopPropagation();
userDismiss();
return;
}
if (trap && event.key === "Tab") {
const items = focusable(wrapper);
if (items.length === 0) {
event.preventDefault();
return;
}
const first = items[0];
const last = items[items.length - 1];
const active = document.activeElement;
const outside = !wrapper.contains(active);
if (event.shiftKey && (active === first || outside)) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && (active === last || outside)) {
event.preventDefault();
first.focus();
}
}
};
document.addEventListener("keydown", onKeydown, true);
removers.push(() => document.removeEventListener("keydown", onKeydown, true));
}
if (triggers.includes("backdrop")) {
const onClick = (event) => {
if (event.target === wrapper) userDismiss();
};
wrapper.addEventListener("click", onClick);
removers.push(() => wrapper.removeEventListener("click", onClick));
}
if (triggers.includes("outside")) {
const ignore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];
const onDocClick = (event) => {
const target = event.target;
if (target === null) return;
if (wrapper.contains(target)) return;
if (ignore.some((el) => el === target || el.contains(target))) return;
userDismiss();
};
document.addEventListener("click", onDocClick, true);
removers.push(() => document.removeEventListener("click", onDocClick, true));
}
if (initialFocus !== false) {
if (typeof initialFocus === "string") {
wrapper.querySelector(initialFocus)?.focus();
} else {
const first = focusable(wrapper)[0];
if (first !== void 0) {
first.focus();
} else {
wrapper.tabIndex = -1;
wrapper.focus();
}
}
}
return { el: wrapper, close, result };
}
function popover(anchor, content, options = {}) {

@@ -552,0 +554,0 @@ const {

@@ -5,3 +5,3 @@ # kerf

kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children — partial-update on huge lists is O(changes), not O(rows). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `renderDocument`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). An optional subpath at `kerfjs/dev` installs the development diagnostics — kerf does NOT infer dev mode, so you import it behind your own build's dev flag (`if (import.meta.env.DEV) await import('kerfjs/dev');`); omitting it is production and sheds ~4.7 KB min+gzip. Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step. A family of optional, tree-shakeable **companion-utility** subpaths cover patterns real apps hand-roll: `kerfjs/list` (`bindList` — a keyed list with per-row fine-grained mounts and fixed / declared / measured-height viewport virtualization, plus `observeRowHeights`), `kerfjs/overlay` (`overlay` / `confirm` / `prompt` / `form` / `choice` / `popover` / `tooltip` / `toast` + `positionAnchored` / `autoReposition`), `kerfjs/scope` (`disposeScope` / `disposeSubtree` / `observeRemovals`), `kerfjs/async` (`resource` async-state with a stale-response guard + SWR cache), `kerfjs/timing` (`debounce` / `throttle` / `debouncedSignal`), `kerfjs/remount` (`remountOn` — key-driven wholesale subtree replacement), `kerfjs/attach` (`attach` — bind a non-kerf widget's lifecycle to one node), and `kerfjs/actions` (`action` / `delegateActions` — the delegated `data-action` table idiom).
kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children — partial-update on huge lists is O(changes), not O(rows). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `renderDocument`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). An optional subpath at `kerfjs/dev` installs the development diagnostics — kerf does NOT infer dev mode, so you import it behind your own build's dev flag (`if (import.meta.env.DEV) await import('kerfjs/dev');`); omitting it is production and sheds ~4.7 KB min+gzip. Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step. A family of optional, tree-shakeable **companion-utility** subpaths cover patterns real apps hand-roll: `kerfjs/list` (`bindList` — a keyed list with per-row fine-grained mounts and fixed / declared / measured-height viewport virtualization, plus `observeRowHeights`), `kerfjs/overlay` (`overlay` / `confirm` / `prompt` / `form` / `choice` / `popover` / `tooltip` / `toast` + `positionAnchored` / `autoReposition`, with opt-in `native: true` top-layer backing via `<dialog>` / the Popover API), `kerfjs/scope` (`disposeScope` / `disposeSubtree` / `observeRemovals`), `kerfjs/async` (`resource` async-state with a stale-response guard + SWR cache), `kerfjs/timing` (`debounce` / `throttle` / `debouncedSignal`), `kerfjs/remount` (`remountOn` — key-driven wholesale subtree replacement), `kerfjs/attach` (`attach` — bind a non-kerf widget's lifecycle to one node), and `kerfjs/actions` (`action` / `delegateActions` — the delegated `data-action` table idiom).

@@ -40,4 +40,5 @@ ## For humans new to the codebase

- [List identity](https://github.com/brianwestphal/kerf/blob/main/docs/16-list-identity.md): why an `each()` list's call-order identity is not stable, what the source guard fixes and what it doesn't, the five constraints any scheme must survive, and the explicit-key recommendation now shipped as `each(items, render, { key })`.
- [List virtualization](https://github.com/brianwestphal/kerf/blob/main/docs/17-list-virtualization.md): `bindList`'s virtualization height models — fixed `number`, app-declared `(item, index) => number`, and measured `{ estimate }` + `setHeight` — with kerf owning the cumulative-offset math and scroll anchoring while the app owns measurement (the `observeRowHeights` helper), plus the `minRows` render-all threshold and the container/resize ergonomics.
- [List virtualization](https://github.com/brianwestphal/kerf/blob/main/docs/17-list-virtualization.md): `bindList`'s virtualization — the `window` (default) height models (fixed `number`, app-declared `(item, index) => number`, and measured `{ estimate }` + `setHeight`) with kerf owning the cumulative-offset math and scroll anchoring while the app owns measurement (the `observeRowHeights` helper), the `minRows` render-all threshold and container/resize ergonomics, and the `content-visibility` mode that keeps every row in the DOM (full find-in-page / a11y) while the browser skips off-screen layout, plus the findability/a11y tradeoff of the default `window` mode.
- [State-preserving moves](https://github.com/brianwestphal/kerf/blob/main/docs/18-state-preserving-moves.md): connected-row reorders (every `each()` / `bindList` / `morph` move site) use `Node.prototype.moveBefore()` where the engine supports it — an atomic move that keeps focus, selection, `<iframe>` state, playing media, and running CSS animations across the reorder — falling back to `insertBefore()` otherwise. Transparent internal `moveNode` helper; no API change.
- [Native overlay backing](https://github.com/brianwestphal/kerf/blob/main/docs/19-native-overlay-backing.md): opt-in `native: true` on every `kerfjs/overlay` surface hosts the overlay in the browser top layer — a `<dialog>.showModal()` for modal surfaces, the Popover API for non-modal — feature-detected, falling back to today's plain `<div>` where unsupported. Fixes stacking (top layer beats any `z-index`), real inerting, and native light-dismiss; opt-in because the native elements carry UA styles kerf's zero-CSS contract won't reset.

@@ -44,0 +45,0 @@ ## Examples

{
"name": "kerfjs",
"version": "4.3.0-beta.1",
"version": "4.3.0",
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -56,3 +56,3 @@ <p align="center">

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.
8. **Batteries on their own subpaths.** Eight 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/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.

@@ -59,0 +59,0 @@ 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.

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display