Sign In

kerfjs

Package Overview
Dependencies
Maintainers
1
Versions
50
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.1.1
to
4.2.0-beta.1
+72
dist/actions.d.ts
import { A as AttrSpec } from './attrSelector-Cmu2ZoGO.js';
import { D as DelegateOptions } from './delegate-CL9VTZFb.js';
/**
* `kerfjs/actions` — the delegated action-table helper.
*
* The most-reinvented idiom across real kerf apps: one table of `data-action`
* attribute specs used as the single source of truth for BOTH the JSX attribute
* and the delegate selector, plus a hand-rolled `switch (dataset.action)`
* dispatcher. This subpath blesses it as two thin helpers over the existing
* `attr()` + `delegate()` — it does NOT replace them.
*
* import { action, delegateActions } from 'kerfjs/actions';
*
* const A = {
* select: action('select-file'),
* remove: action('remove-file'),
* };
*
* // JSX — spread the attr (rename-safe; no hardcoded attribute name):
* // <button {...A.select.attrs} data-id={id}>…</button>
*
* // Wire the whole table with ONE delegated listener; returns a disposer:
* const dispose = delegateActions(root, 'click', {
* [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),
* [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),
* });
*
* Contract: `delegateActions` returns a `() => void` disposer and holds no
* per-instance state — the same shape as `delegate()`, which it builds on (so
* it inherits the single-listener dispatch and the capture auto-promotion for
* well-known non-bubbling event types). One event type per call, mirroring
* `delegate()`; collect the disposers for a root that needs several.
*/
/**
* A handler in a {@link delegateActions} table. Receives the DOM event and the
* matched element (walk-up `closest()` match by default) — the same shape as a
* `delegate()` handler.
*/
type ActionHandler<E extends Element = Element> = (event: Event, el: E) => void;
/**
* `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization
* of `attr('data-action', value)`: spread its `.attrs` in JSX and use its
* `.value` as the handler-table key, so the action name lives in exactly one
* place and can't drift between the markup and the dispatcher.
*/
declare function action<V extends string>(value: V): AttrSpec<'data-action', V>;
/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */
interface DelegateActionsOptions extends DelegateOptions {
/**
* The attribute the table keys on. Default `'data-action'`. Override it only
* if you also author the specs with `attr(yourName, …)` instead of `action()`.
*/
attr?: string;
}
/**
* Wire a whole table of action handlers with ONE delegated listener.
*
* On `eventType`, the nearest element carrying the action attribute (walk-up
* `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)
* is looked up in `table` by its attribute value, and the matching handler
* runs. An element whose action is absent from the table is ignored — the same
* behavior as a `switch (dataset.action)` with no matching `case`.
*
* Returns a `() => void` disposer. One event type per call (the smallest
* surface, mirroring `delegate()`); collect the disposers when a root needs
* several event types.
*/
declare function delegateActions<E extends Element = Element>(root: HTMLElement, eventType: string, table: Readonly<Record<string, ActionHandler<E>>>, options?: DelegateActionsOptions): () => void;
export { type ActionHandler, type DelegateActionsOptions, action, delegateActions };
import { attr } from './chunk-U32TFTGZ.js';
import { delegate } from './chunk-KEZTD6H4.js';
import './chunk-VVDJLWMP.js';
// src/actions.ts
var DEFAULT_ACTION_ATTR = "data-action";
function action(value) {
return attr(DEFAULT_ACTION_ATTR, value);
}
function delegateActions(root, eventType, table, options) {
const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;
return delegate(
root,
eventType,
`[${attrName}]`,
(event, el) => {
const handler = table[el.getAttribute(attrName)];
if (handler !== void 0) handler(event, el);
},
options
);
}
export { action, delegateActions };
//# sourceMappingURL=actions.js.map
//# sourceMappingURL=actions.js.map
{"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAerB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAW,CAAA;AACzD,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attrSelector.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (event: Event, el: E) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const handler = table[el.getAttribute(attrName) as string];\n if (handler !== undefined) handler(event, el);\n },\n options,\n );\n}\n"]}
/** The lifecycle status of a {@link Resource}. */
type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';
/** Optional progress for a long-running fetch (uploads, chunked work). */
interface ResourceProgress {
completed: number;
total: number;
}
/** The reactive state a {@link Resource} exposes. */
interface ResourceState<T> {
status: ResourceStatus;
/** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */
data: T | undefined;
/** The rejection from the most recent failed run. */
error: unknown;
/** Latest reported progress while running, or `undefined`. */
progress: ResourceProgress | undefined;
}
/**
* The fetcher passed to {@link Resource.run}. You own the transport. It receives
* a `report(completed, total)` callback for optional progress — ignore it if you
* don't need progress (a plain `() => Promise<T>` is assignable here).
*/
type ResourceFetcher<T> = (report: (completed: number, total: number) => void) => Promise<T>;
/** An async-state container. Its `value` is a tracking read; drive UI off `value.status`. */
interface Resource<T> {
/** Tracking read of the current {@link ResourceState}. */
readonly value: ResourceState<T>;
/**
* Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding
* against stale responses (only the latest run resolves the state). Never
* rejects — a failure lands in `value.error`; resolves with the data (or
* `undefined` on failure) for callers who want to await it.
*/
run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;
/** Reset to `idle` (clearing data/error/progress) and invalidate any in-flight run. */
reset(): void;
}
/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */
declare function resource<T>(): Resource<T>;
export { type Resource, type ResourceFetcher, type ResourceProgress, type ResourceState, type ResourceStatus, resource };
import { signal } from './chunk-3APBEVHF.js';
import './chunk-VVDJLWMP.js';
// src/async.ts
var IDLE = () => ({
status: "idle",
data: void 0,
error: void 0,
progress: void 0
});
function resource() {
const state = signal(IDLE());
let generation = 0;
function run(fetcher) {
const gen = ++generation;
state.value = { ...state.value, status: "running", error: void 0, progress: void 0 };
const report = (completed, total) => {
if (gen === generation) {
state.value = { ...state.value, progress: { completed, total } };
}
};
return fetcher(report).then(
(data) => {
if (gen === generation) {
state.value = { status: "completed", data, error: void 0, progress: void 0 };
}
return data;
},
(error) => {
if (gen === generation) {
state.value = { ...state.value, status: "failed", error, progress: void 0 };
}
return void 0;
}
);
}
function reset() {
generation++;
state.value = IDLE();
}
return {
get value() {
return state.value;
},
run,
reset
};
}
export { resource };
//# sourceMappingURL=async.js.map
//# sourceMappingURL=async.js.map
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":";;;;AA+DA,IAAM,OAAO,OAA4B;AAAA,EACvC,MAAA,EAAQ,MAAA;AAAA,EACR,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,MAAA;AAAA,EACP,QAAA,EAAU;AACZ,CAAA,CAAA;AAGO,SAAS,QAAA,GAA2B;AACzC,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAyB,IAAA,EAAS,CAAA;AAEhD,EAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,EAAA,SAAS,IAAI,OAAA,EAAqD;AAChE,IAAA,MAAM,MAAM,EAAE,UAAA;AACd,IAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,KAAA,EAAO,QAAQ,SAAA,EAAW,KAAA,EAAO,MAAA,EAAW,QAAA,EAAU,MAAA,EAAU;AAEzF,IAAA,MAAM,MAAA,GAAS,CAAC,SAAA,EAAmB,KAAA,KAAwB;AACzD,MAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,QAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,OAAO,QAAA,EAAU,EAAE,SAAA,EAAW,KAAA,EAAM,EAAE;AAAA,MACjE;AAAA,IACF,CAAA;AAEA,IAAA,OAAO,OAAA,CAAQ,MAAM,CAAA,CAAE,IAAA;AAAA,MACrB,CAAC,IAAA,KAAS;AACR,QAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,UAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,MAAA,EAAQ,WAAA,EAAa,MAAM,KAAA,EAAO,MAAA,EAAW,UAAU,MAAA,EAAU;AAAA,QACnF;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAA;AAAA,MACA,CAAC,KAAA,KAAmB;AAClB,QAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,UAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,OAAO,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,QAAA,EAAU,MAAA,EAAU;AAAA,QAC/E;AACA,QAAA,OAAO,MAAA;AAAA,MACT;AAAA,KACF;AAAA,EACF;AAEA,EAAA,SAAS,KAAA,GAAc;AACrB,IAAA,UAAA,EAAA;AACA,IAAA,KAAA,CAAM,QAAQ,IAAA,EAAQ;AAAA,EACxB;AAEA,EAAA,OAAO;AAAA,IACL,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,KAAA,CAAM,KAAA;AAAA,IACf,CAAA;AAAA,IACA,GAAA;AAAA,IACA;AAAA,GACF;AACF","file":"async.js","sourcesContent":["/**\n * `kerfjs/async` — model async state, with the stale-response guard built in.\n *\n * Every real kerf app reproduces the same shape — `{ status, data, error }` —\n * for loading/error UI, each paired with a hand-rolled generation counter so a\n * slow response can't overwrite a newer one. This subpath blesses exactly that,\n * and no more: you still write the fetch (Node `fetch` for SSR, browser `fetch`\n * client-side), and `.run()` owns the status transitions plus the stale guard.\n *\n * import { resource } from 'kerfjs/async';\n *\n * const users = resource<User[]>();\n * users.run(() => fetch('/api/users').then((r) => r.json()));\n * // render off users.value.status: 'idle' | 'running' | 'completed' | 'failed'\n *\n * Only the LATEST run may resolve the state, so out-of-order responses are\n * dropped automatically. Optional progress: declare the `report` parameter on\n * your fetcher and call it (e.g. from an upload's progress events).\n */\nimport { signal } from './reactive.js';\n\n/** The lifecycle status of a {@link Resource}. */\nexport type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';\n\n/** Optional progress for a long-running fetch (uploads, chunked work). */\nexport interface ResourceProgress {\n completed: number;\n total: number;\n}\n\n/** The reactive state a {@link Resource} exposes. */\nexport interface ResourceState<T> {\n status: ResourceStatus;\n /** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */\n data: T | undefined;\n /** The rejection from the most recent failed run. */\n error: unknown;\n /** Latest reported progress while running, or `undefined`. */\n progress: ResourceProgress | undefined;\n}\n\n/**\n * The fetcher passed to {@link Resource.run}. You own the transport. It receives\n * a `report(completed, total)` callback for optional progress — ignore it if you\n * don't need progress (a plain `() => Promise<T>` is assignable here).\n */\nexport type ResourceFetcher<T> = (report: (completed: number, total: number) => void) => Promise<T>;\n\n/** An async-state container. Its `value` is a tracking read; drive UI off `value.status`. */\nexport interface Resource<T> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T>;\n /**\n * Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding\n * against stale responses (only the latest run resolves the state). Never\n * rejects — a failure lands in `value.error`; resolves with the data (or\n * `undefined` on failure) for callers who want to await it.\n */\n run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress) and invalidate any in-flight run. */\n reset(): void;\n}\n\nconst IDLE = <T>(): ResourceState<T> => ({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n});\n\n/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */\nexport function resource<T>(): Resource<T> {\n const state = signal<ResourceState<T>>(IDLE<T>());\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(fetcher: ResourceFetcher<T>): Promise<T | undefined> {\n const gen = ++generation;\n state.value = { ...state.value, status: 'running', error: undefined, progress: undefined };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.value, progress: { completed, total } };\n }\n };\n\n return fetcher(report).then(\n (data) => {\n if (gen === generation) {\n state.value = { status: 'completed', data, error: undefined, progress: undefined };\n }\n return data;\n },\n (error: unknown) => {\n if (gen === generation) {\n state.value = { ...state.value, status: 'failed', error, progress: undefined };\n }\n return undefined;\n },\n );\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n state.value = IDLE<T>();\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n };\n}\n"]}
/**
* `attr(name, value)` — create a pre-computed attribute descriptor (static form).
* `attr(name)` — create a per-render factory for dynamic attribute values (dynamic form).
*
* **Static form** — best for fixed action names, filter keys, role values, etc.
* Escapes once at module-load time; produces a full {@link AttrSpec} with
* `.name`, `.value`, `.selector`, and `.attrs`.
*
* const ACTIONS = {
* toggle: attr('data-action', 'toggle'),
* remove: attr('data-action', 'remove'),
* } as const satisfies Record<string, AttrSpec<'data-action'>>;
*
* // In JSX — spread .attrs (rename-safe; no hardcoded attribute name):
* <button {...ACTIONS.toggle.attrs}>Toggle</button>
*
* // In delegate — use the pre-computed selector:
* delegate(root, 'click', ACTIONS.toggle.selector, handler);
*
* **Dynamic form** — best for per-row data like `data-id`, where the value
* changes per item but the attribute name is constant.
* The name is validated and pre-escaped at definition time; calling the
* returned factory is cheap (it just freezes a one-key object — the value is
* escaped later by the JSX attribute renderer when the result is spread).
*
* const ITEM = { id: attr('data-id') } as const;
*
* // In JSX — call the factory inline:
* <li {...ITEM.id(String(item.id))}>…</li>
*
* For ad-hoc compound selectors, concatenate `.selector` strings:
*
* delegate(root, 'click',
* ACTIONS.toggle.selector + attr('data-id', id).selector,
* handler);
*
* Escaping:
* - Attribute name: escaped as a CSS identifier via `cssEscapeIdent`, which is
* an SSR-safe (no `CSS.escape`) adaptation of the Mathias Bynens polyfill
* (https://github.com/mathiasbynens/CSS.escape, MIT licensed — see the
* Acknowledgements section of LICENSE). Handles
* control chars, leading digits, non-ASCII, and CSS metacharacters.
* - Attribute value: embedded in double quotes as a CSS string. Backslashes and
* double-quote characters are backslash-escaped; control characters are
* hex-escaped per CSS Syntax Level 3 §3.4.
*
* Throws on an empty attribute name (not a valid CSS identifier).
*/
/** Descriptor created by the static {@link attr} overload. */
interface AttrSpec<N extends string = string, V extends string = string> {
/** The raw attribute name passed to `attr()`. */
readonly name: N;
/** The raw attribute value passed to `attr()`. */
readonly value: V;
/** Pre-computed `[name="value"]` CSS selector string, safe to pass to `delegate()`. */
readonly selector: string;
/** Spreadable JSX object — `{ [name]: value }` — keeps the attribute name out of JSX literals. */
readonly attrs: {
readonly [K in N]: V;
};
}
/**
* Static overload — pre-computes the full descriptor at definition time.
* Returns an {@link AttrSpec} with `.name`, `.value`, `.selector`, and `.attrs`.
*/
declare function attr<N extends string, V extends string>(name: N, value: V): AttrSpec<N, V>;
/**
* Dynamic overload — pre-validates and pre-escapes the attribute name, returns a
* factory that accepts a per-render value and produces a frozen spreadable object.
* Use for per-row attributes like `data-id` where the value changes per item.
* The optional `V` generic constrains which values the factory accepts:
* `attr<'data-id', 'a'|'b'>('data-id')` → `(value: 'a'|'b') => { 'data-id': 'a'|'b' }`.
* Leaving both generics off infers `N` from the argument and defaults `V` to `string`.
*/
declare function attr<N extends string, V extends string = string>(name: N): (value: V) => {
readonly [K in N]: V;
};
export { type AttrSpec as A, attr as a };
import { itemVersion } from './chunk-QIP723L4.js';
import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js';
import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-FSAQR6IU.js';
import { effect } from './chunk-3APBEVHF.js';
import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/list-render-state.ts
function deriveListRenderState(bindingCount) {
if (bindingCount === void 0) return "unbound";
return bindingCount === 0 ? "empty" : "bound";
}
function decideListPath(state, patches, snapshotLength, previousBindingCount) {
if (state === "unbound") return { path: "snapshot", reason: "first-render" };
if (state === "empty") return { path: "snapshot", reason: "empty-binding" };
if (patches.length === 0) return { path: "snapshot", reason: "no-patches" };
let netDelta = 0;
for (const p of patches) {
if (p.type === "insert") netDelta += 1;
else if (p.type === "remove") netDelta -= 1;
else if (p.type === "replace") return { path: "snapshot", reason: "replace" };
}
const count = previousBindingCount ?? 0;
if (count + netDelta !== snapshotLength) {
return { path: "snapshot", reason: "count-drift" };
}
return { path: "granular" };
}
// src/each.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
function isArraySignal(value) {
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
}
var context = null;
var renderingRow = false;
function inRowScope(fn) {
const prev = renderingRow;
renderingRow = true;
try {
return fn();
} finally {
renderingRow = prev;
}
}
function _setRenderContext(c) {
context = c;
}
function _resetCallOrderListState(ctx) {
const isCallOrderId = (id) => !id.startsWith("k:");
for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) {
for (const id of Array.from(map.keys())) {
if (isCallOrderId(id)) map.delete(id);
}
}
}
function isEachOptions(v) {
return typeof v === "object" && v !== null;
}
var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/;
function assertValidKey(key) {
if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) {
throw new Error(
`each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.`
);
}
}
function claimKey(ctx, key) {
assertValidKey(key);
if (renderingRow) {
throw new Error(
`each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.`
);
}
if (ctx.keysThisRender.has(key)) {
throw new Error(
`each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.`
);
}
ctx.keysThisRender.add(key);
return `k:${key}`;
}
function each(items, render, cacheKeyOrOptions) {
const useOptions = isEachOptions(cacheKeyOrOptions);
const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions;
const listKey = useOptions ? cacheKeyOrOptions.key : void 0;
if (isArraySignal(items) && context !== null) {
return eachGranular(items, render, cacheKey, listKey);
}
const snapshotItems = isArraySignal(items) ? items.value : items;
return eachSnapshot(snapshotItems, render, cacheKey, listKey);
}
function eachSnapshot(items, render, cacheKey, listKey) {
let id;
if (context !== null) {
id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++);
} else {
id = "orphan";
}
return eachSnapshotById(items, render, cacheKey, id);
}
function assertObjectItem(item, index) {
if (typeof item !== "object" || item === null) {
throw new Error(
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
);
}
}
function eachGranular(sig, render, cacheKey, listKey) {
const ctx = context;
const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++);
const previousBindingCount = ctx.bindingCounts.get(id);
const patches = sig._consumePatches();
const snapshot = sig.value;
const previousSource = ctx.bindingSources.get(id);
const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig;
if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id);
const decision = sourceReused ? { path: "snapshot" } : decideListPath(
deriveListRenderState(previousBindingCount),
patches,
snapshot.length,
previousBindingCount
);
if (decision.path === "snapshot") {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
let staleIndexShift = false;
if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
const rendered = [];
for (let i = 0; i < previousBindingCount; i++) rendered.push(i);
for (const p of patches) {
if (p.type === "insert") rendered.splice(p.index, 0, p.index);
else if (p.type === "remove") rendered.splice(p.index, 1);
else if (p.type === "move") {
const [moved] = rendered.splice(p.from, 1);
rendered.splice(p.to, 0, moved);
}
}
for (let i = 0; i < rendered.length; i++) {
if (rendered[i] !== i) {
staleIndexShift = true;
break;
}
}
}
if (cacheKey !== void 0) {
const cache2 = ctx.caches.get(id);
for (let i = 0; i < snapshot.length; i++) {
const item = snapshot[i];
const k = cacheKey(item, i);
const cached = cache2.get(item);
if (cached !== void 0 && cached.cacheKey !== k) {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
}
}
const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => {
const out = render(item, index);
return isSafeHtml(out) ? out.toString() : out;
}));
const internalPatches = new Array(patches.length);
const cache = ctx.caches.get(id);
try {
for (let i = 0; i < patches.length; i++) {
const p = patches[i];
if (p.type === "insert" || p.type === "update") {
assertObjectItem(p.item, p.index);
const { html, bindings } = renderRow(p.item, p.index);
internalPatches[i] = {
type: p.type,
index: p.index,
item: p.item,
html,
bindings
};
cache?.set(p.item, {
cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0,
html,
bindings,
version: itemVersion(p.item),
index: p.index
});
} else {
internalPatches[i] = p;
}
}
} catch {
ctx.bindingCounts.delete(id);
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
if (staleIndexShift) devHooks.staleIndex?.(id);
return granularListSafeHtml(id, [], internalPatches, sig);
}
function eachSnapshotById(items, render, cacheKey, id, source) {
let cache = null;
if (context !== null) {
let c = context.caches.get(id);
if (c === void 0) {
c = /* @__PURE__ */ new WeakMap();
context.caches.set(id, c);
}
cache = c;
}
const segItems = new Array(items.length);
const seen = /* @__PURE__ */ new Set();
for (let i = 0; i < items.length; i++) {
const item = items[i];
assertObjectItem(item, i);
if (seen.has(item)) {
throw new Error(
`each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
);
}
seen.add(item);
const k = cacheKey ? cacheKey(item, i) : void 0;
const version = itemVersion(item);
let html;
let bindings;
const cached = cache !== null ? cache.get(item) : void 0;
if (cached !== void 0 && cached.cacheKey === k && cached.version === version) {
html = cached.html;
bindings = cached.bindings;
if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
devHooks.staleIndex?.(id);
}
} else {
const captured = captureRowBindings(() => inRowScope(() => {
const out = render(item, i);
return isSafeHtml(out) ? out.toString() : out;
}));
html = captured.html;
bindings = captured.bindings;
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i });
}
segItems[i] = { ref: item, cacheKey: k, html, bindings };
}
if (cacheKey !== void 0) {
devHooks.duplicateCacheKeys?.(id, segItems);
}
return listSafeHtml(id, segItems, source);
}
// src/list-reconcile-focus.ts
function captureFocus(liveParent) {
const active = document.activeElement;
if (active === null || active === document.body) return null;
if (!liveParent.contains(active)) return null;
const el = active;
let selStart = null;
let selEnd = null;
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
try {
selStart = el.selectionStart;
selEnd = el.selectionEnd;
} catch {
}
}
return { el, selStart, selEnd };
}
function restoreFocus(snap) {
if (document.activeElement === snap.el) return;
if (!snap.el.isConnected) return;
snap.el.focus();
if (snap.selStart !== null && snap.selEnd !== null) {
try {
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
} catch {
}
}
}
// src/morph.ts
var ID_KEY_PREFIX = "id:";
var DATA_KEY_PREFIX = "data-key:";
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function getNodeKey(node) {
if (node.nodeType !== ELEMENT_NODE) return void 0;
const el = node;
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
if (el.dataset !== void 0 && el.dataset.key !== void 0) {
return `${DATA_KEY_PREFIX}${el.dataset.key}`;
}
return void 0;
}
var EMPTY_OWNED = /* @__PURE__ */ new Set();
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
if (liveRoot == null) {
throw new Error(
'morph: liveRoot is null/undefined \u2014 pass the live element, e.g. morph(document.getElementById("app")!, template). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say Element.'
);
}
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
const focusSnap = captureFocus(liveRoot);
morphChildren(liveRoot, templateEl, ownedItems);
if (focusSnap !== null) restoreFocus(focusSnap);
}
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
morphElement(fromEl, toEl, ownedItems);
}
function isElementNode(t) {
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
}
function parseTemplate(liveRoot, template) {
const el = liveRoot.cloneNode(false);
el.innerHTML = String(template);
return el;
}
function protectionTag(node) {
const { dataset } = node;
return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : "");
}
var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX];
function isMarker(node) {
if (node.nodeType !== COMMENT_NODE) return false;
const { data } = node;
return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix));
}
function markersPairable(a, b) {
if (!isMarker(a) && !isMarker(b)) return true;
return a.data === b.data;
}
function skipOwned(node, ownedItems) {
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
node = node.nextSibling;
}
return node;
}
function isListMarker(node) {
return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX);
}
function afterListRegion(marker, ownedItems) {
let last = marker;
for (let r = marker.nextSibling; r !== null; r = r.nextSibling) {
if (isListMarker(r)) break;
if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r;
}
return last.nextSibling;
}
function morphChildren(fromParent, toParent, ownedItems) {
const keyed = /* @__PURE__ */ new Map();
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
const k = getNodeKey(c);
if (k !== void 0) keyed.set(k, c);
}
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
let toChild = toParent.firstChild;
while (toChild !== null) {
const toNext = toChild.nextSibling;
let matched = null;
const toKey = getNodeKey(toChild);
if (toKey !== void 0 && keyed.has(toKey)) {
matched = keyed.get(toKey);
keyed.delete(toKey);
if (matched !== fromChild) {
fromParent.insertBefore(matched, fromChild);
} else {
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
}
}
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) {
matched = fromChild;
fromChild = skipOwned(
isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling,
ownedItems
);
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
const owned = boundTextNodeOf(matched);
if (owned !== null && fromChild === owned) {
fromChild = skipOwned(owned.nextSibling, ownedItems);
}
}
}
if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) {
const toTag = toChild.tagName;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== ELEMENT_NODE) continue;
const el = scan;
if (ownedItems.has(el)) continue;
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
if (protectionTag(el) !== protectionTag(toChild)) continue;
matched = el;
fromParent.insertBefore(el, fromChild);
break;
}
}
if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) {
const wantData = toChild.data;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue;
const regionEnd = afterListRegion(scan, ownedItems);
const run = [];
for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) {
run.push(r);
}
const focusSnap = captureFocus(fromParent);
for (const node of run) fromParent.insertBefore(node, fromChild);
if (focusSnap !== null) restoreFocus(focusSnap);
matched = scan;
break;
}
}
if (matched !== null) {
morphNode(matched, toChild, ownedItems);
} else {
const cloned = toChild.cloneNode(true);
fromParent.insertBefore(cloned, fromChild);
}
toChild = toNext;
}
while (fromChild !== null) {
const next = fromChild.nextSibling;
if (fromChild.nodeType === ELEMENT_NODE) {
const el = fromChild;
if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) {
fromParent.removeChild(fromChild);
}
} else {
fromParent.removeChild(fromChild);
}
fromChild = next;
}
}
function morphNode(fromNode, toNode, ownedItems) {
if (fromNode.nodeType === ELEMENT_NODE) {
morphElement(fromNode, toNode, ownedItems);
return;
}
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
const fromText = fromNode;
const toText = toNode;
if (fromText.data !== toText.data) fromText.data = toText.data;
}
}
function morphElement(fromEl, toEl, ownedItems) {
if (fromEl.tagName !== toEl.tagName) {
const replacement = toEl.cloneNode(true);
fromEl.parentNode?.replaceChild(replacement, fromEl);
return;
}
if (fromEl.dataset.morphSkip !== void 0) return;
if (fromEl.isEqualNode(toEl)) return;
if (fromEl === document.activeElement) {
const ce = fromEl.getAttribute("contenteditable");
if (ce !== null && ce.toLowerCase() !== "false") return;
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
}
morphAttributes(fromEl, toEl);
if (fromEl.dataset.morphSkipChildren !== void 0) return;
const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent;
morphChildren(fromEl, toEl, ownedItems);
if (syncTextareaValue) {
fromEl.value = toEl.textContent;
}
}
function isUserAgentOwnedAttr(tagName, name) {
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
}
function morphAttributes(fromEl, toEl) {
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
const value = attr.value;
if (ns !== null) {
if (fromEl.getAttributeNS(ns, name) !== value) {
fromEl.setAttributeNS(ns, attr.name, value);
}
} else if (fromEl.getAttribute(name) !== value) {
fromEl.setAttribute(name, value);
syncFormProp(fromEl, name, value, true);
}
}
const fromAttrs = fromEl.attributes;
const fromTag = fromEl.tagName;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
const ns = attr.namespaceURI;
const name = attr.localName;
if (ns !== null) {
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
fromEl.removeAttribute(name);
syncFormProp(fromEl, name, "", false);
}
}
}
function isTextInputOrTextarea(el) {
if (el.tagName === "TEXTAREA") return true;
if (el.tagName === "INPUT") {
const type = el.type;
return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === "";
}
return false;
}
function preserveTextEntryState(fromEl, toEl) {
if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") {
const fromInput = fromEl;
const toInput = toEl;
toInput.value = fromInput.value;
try {
toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);
} catch {
}
}
}
// src/list-binding.ts
function endAnchor(binding) {
if (binding.items.length > 0) {
return binding.items[binding.items.length - 1].node.nextSibling;
}
return binding.marker.nextSibling;
}
// src/list-reconcile-fast-paths.ts
var LT = 60;
var GT = 62;
var DQUOTE = 34;
var SQUOTE = 39;
var AMP = 38;
var EQ = 61;
var SLASH = 47;
var TEXT_NODE2 = 3;
var ELEMENT_NODE2 = 1;
function isWhitespace(cc) {
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
}
function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
const oldGt = oldHtml.indexOf(">");
const newGt = newHtml.indexOf(">");
if (oldGt === -1 || newGt === -1) return false;
if (oldHtml.length - oldGt !== newHtml.length - newGt) return false;
if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false;
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
const oldTag = parseOpeningTag(oldHtml, oldGt);
const newTag = parseOpeningTag(newHtml, newGt);
if (oldTag === null || newTag === null) return false;
if (oldTag.tagName !== newTag.tagName) return false;
for (const name of oldTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
for (const name of newTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
const liveTagUpper = liveNode.tagName;
for (const [name, rawValue] of newTag.attrs) {
const oldValue = oldTag.attrs.get(name);
if (oldValue === rawValue) continue;
const value = unescapeAttrValue(rawValue);
liveNode.setAttribute(name, value);
syncFormProp(liveNode, name, value, true);
}
for (const name of oldTag.attrs.keys()) {
if (newTag.attrs.has(name)) continue;
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
liveNode.removeAttribute(name);
syncFormProp(liveNode, name, "", false);
}
return true;
}
function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
let p = 0;
const minLen = Math.min(oldHtml.length, newHtml.length);
while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++;
let s = 0;
const maxS = minLen - p;
while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) {
s++;
}
const oldWinEnd = oldHtml.length - s;
const newWinEnd = newHtml.length - s;
if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false;
if (!isPureTextWindow(newHtml, p, newWinEnd)) return false;
if (p === 0) return false;
const boundaryCc = oldHtml.charCodeAt(p - 1);
if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false;
const textStart = lastIndexOfChar(oldHtml, GT, p - 1);
if (textStart === -1) return false;
const textEnd = oldHtml.indexOf("<", p);
if (textEnd === -1) return false;
if (textEnd < oldWinEnd) return false;
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
const oldText = oldHtml.slice(textStart + 1, textEnd);
const newText = newHtml.slice(textStart + 1, newTextEnd);
if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false;
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
if (targetNode === null) return false;
if (targetNode.nodeValue !== oldText) return false;
targetNode.nodeValue = newText;
const host = targetNode.parentNode;
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
host.value = newText;
}
return true;
}
function containsDataMorphSkip(html) {
return html.indexOf("data-morph-skip") !== -1;
}
function isPureTextWindow(html, start, end) {
for (let i = start; i < end; i++) {
const cc = html.charCodeAt(i);
if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false;
}
return true;
}
function lastIndexOfChar(html, target, beforeInclusive) {
for (let i = beforeInclusive; i >= 0; i--) {
if (html.charCodeAt(i) === target) return i;
}
return -1;
}
function countTextNodesBefore(html, beforePos) {
let count = 0;
let i = 0;
while (i < beforePos) {
if (html.charCodeAt(i) === LT) {
while (i < beforePos && html.charCodeAt(i) !== GT) i++;
i++;
} else {
const start = i;
while (i < beforePos && html.charCodeAt(i) !== LT) i++;
if (i > start) count++;
}
}
return count;
}
function nthTextNodeDescendant(root, n) {
let count = 0;
let result = null;
function walk(node) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (result !== null) return;
if (c.nodeType === TEXT_NODE2) {
if (count === n) {
result = c;
return;
}
count++;
} else if (c.nodeType === ELEMENT_NODE2) {
walk(c);
}
}
}
walk(root);
return result;
}
function parseOpeningTag(html, gtPos) {
if (html.charCodeAt(0) !== LT) return null;
let i = 1;
let end = gtPos;
if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1;
const nameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (isWhitespace(cc)) break;
i++;
}
const tagName = html.slice(nameStart, i);
if (tagName.length === 0) return null;
const attrs = /* @__PURE__ */ new Map();
while (i < end) {
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) break;
const aNameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (cc === EQ || isWhitespace(cc)) break;
i++;
}
const aName = html.slice(aNameStart, i);
if (aName.length === 0) return null;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i < end && html.charCodeAt(i) === EQ) {
i++;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) return null;
const q = html.charCodeAt(i);
if (q !== DQUOTE && q !== SQUOTE) return null;
i++;
const vStart = i;
while (i < end && html.charCodeAt(i) !== q) i++;
if (i >= end) return null;
attrs.set(aName, html.slice(vStart, i));
i++;
} else {
attrs.set(aName, "");
}
}
return { tagName, attrs };
}
function unescapeAttrValue(s) {
if (s.indexOf("&") === -1) return s;
return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
function isUserAgentOwnedAttr2(tagNameUpper, name) {
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
}
// src/list-reconcile-granular.ts
function reconcileGranular(binding, patches) {
const { liveParent } = binding;
const items = binding.items;
const focusSnap = captureFocus(liveParent);
let i = 0;
while (i < patches.length) {
const patch = patches[i];
if (patch.type === "replace") {
i += 1;
continue;
}
if (patch.type === "update") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "update") {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleUpdate(liveParent, items, patch);
} else {
applyBulkUpdate(liveParent, items, patches, i, runEnd);
}
i = runEnd;
continue;
}
if (patch.type === "insert") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleInsert(liveParent, items, patch, endAnchor(binding));
} else {
applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
}
i = runEnd;
continue;
}
if (patch.type === "remove") {
const entry = items[patch.index];
disposeRowBindings(entry.bindingDisposers);
liveParent.removeChild(entry.node);
items.splice(patch.index, 1);
i += 1;
continue;
}
if (patch.type === "move") {
const moved = items[patch.from];
let anchorIdx = patch.to;
if (patch.from < patch.to) anchorIdx += 1;
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
liveParent.insertBefore(moved.node, anchor);
items.splice(patch.from, 1);
items.splice(patch.to, 0, moved);
i += 1;
continue;
}
}
if (focusSnap !== null) restoreFocus(focusSnap);
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
}
function applySingleInsert(liveParent, items, patch, tailAnchor) {
const { html } = patch;
const newNode = parseSingleRow(html, patch.index, liveParent);
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
liveParent.insertBefore(newNode, anchor);
items.splice(patch.index, 0, {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
// KF-294: wire the inserted row's fine-grained bindings to its new node.
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
});
}
function wireRowIfBound(node, bindings) {
return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0;
}
function applySingleUpdate(liveParent, items, patch) {
const { html } = patch;
const oldEntry = items[patch.index];
if (html === oldEntry.html) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
const newNode = parseSingleRow(html, patch.index, liveParent);
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
}
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
const oldEntry = items[patch.index];
if (oldEntry.node.tagName === newNode.tagName) {
_morphElement(oldEntry.node, newNode);
items[patch.index] = reuseBound(patch, html, oldEntry);
} else {
disposeRowBindings(oldEntry.bindingDisposers);
liveParent.replaceChild(newNode, oldEntry.node);
items[patch.index] = {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
};
}
}
function reuseBound(patch, html, oldEntry) {
const kept = carryOrRewireRowBindings(
oldEntry.node,
oldEntry.bindings,
oldEntry.bindingDisposers,
patch.bindings
);
return {
ref: patch.item,
cacheKey: void 0,
html,
node: oldEntry.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
function applyBulkUpdate(liveParent, items, patches, start, end) {
const morphChanges = [];
for (let k = start; k < end; k++) {
const p = patches[k];
const oldEntry = items[p.index];
if (p.html === oldEntry.html) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
morphChanges.push({ patchIdx: k, html: p.html });
}
if (morphChanges.length === 0) return;
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
if (count !== morphChanges.length) {
throw findOffendingChange(patches, morphChanges, liveParent);
}
const newNodes = collectTemplateChildren(content, morphChanges.length);
for (let k = 0; k < morphChanges.length; k++) {
const c = morphChanges[k];
const p = patches[c.patchIdx];
applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]);
}
}
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
const startIdx = patches[start].index;
const htmls = new Array(end - start);
for (let k = start; k < end; k++) {
htmls[k - start] = patches[k].html;
}
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
if (count !== htmls.length) {
throw findOffendingInsert(patches, start, htmls, liveParent);
}
const newNodes = collectTemplateChildren(content, end - start);
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
liveParent.insertBefore(content, anchor);
const newEntries = new Array(end - start);
for (let k = 0; k < newEntries.length; k++) {
const p = patches[start + k];
newEntries[k] = {
ref: p.item,
cacheKey: void 0,
html: htmls[k],
node: newNodes[k],
bindings: p.bindings,
bindingDisposers: wireRowIfBound(newNodes[k], p.bindings)
// KF-294
};
}
items.splice(startIdx, 0, ...newEntries);
}
function findOffendingInsert(patches, start, htmls, liveParent) {
for (let i = 0; i < htmls.length; i++) {
if (parseRowTemplate(htmls[i], liveParent).count !== 1) {
return rowContractError(patches[start + i].index, htmls[i], liveParent);
}
}
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
}
function findOffendingChange(patches, changes, liveParent) {
for (const c of changes) {
if (parseRowTemplate(c.html, liveParent).count !== 1) {
return rowContractError(patches[c.patchIdx].index, c.html, liveParent);
}
}
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
}
// src/list-reconcile-inplace.ts
function tryInPlaceContentUpdate(binding, listSeg) {
const oldItems = binding.items;
const items = listSeg.items;
const n = items.length;
if (n === 0 || n !== oldItems.length) return false;
for (let i = 0; i < n; i++) {
if (items[i].ref !== oldItems[i].ref) return false;
}
const { liveParent } = binding;
const newRecord = new Array(n);
const focusSnap = captureFocus(liveParent);
for (let i = 0; i < n; i++) {
newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i);
}
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
return true;
}
function updateRowInPlace(liveParent, old, ni, index) {
if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) {
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
const newNode = parseSingleRow(ni.html, index, liveParent);
if (old.node.tagName === newNode.tagName) {
_morphElement(old.node, newNode);
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
disposeRowBindings(old.bindingDisposers);
liveParent.replaceChild(newNode, old.node);
const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: newNode,
bindings: fresh.bindings,
bindingDisposers: fresh.bindingDisposers
};
}
// src/list-reconcile-snapshot.ts
function reconcileSnapshot(binding, listSeg) {
if (tryInPlaceContentUpdate(binding, listSeg)) return;
const { liveParent } = binding;
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
const tailAnchor = endAnchor(binding);
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
const focusSnap = captureFocus(liveParent);
removeOldNodes(liveParent, removedItems);
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
if (newRecord.length > 0) {
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
}
}
function classifyItems(oldItems, listSeg) {
const oldByRef = /* @__PURE__ */ new Map();
for (let i = 0; i < oldItems.length; i++) {
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
}
const newRecord = new Array(listSeg.items.length);
const prevIdx = new Array(listSeg.items.length);
const removedItems = [];
const freshIndices = [];
const freshHtmls = [];
for (let i = 0; i < listSeg.items.length; i++) {
const ni = listSeg.items[i];
const oi = oldByRef.get(ni.ref);
if (oi !== void 0) {
oldByRef.delete(ni.ref);
if (oi[0].html === ni.html) {
newRecord[i] = oi[0];
prevIdx[i] = oi[1];
continue;
}
removedItems.push(oi[0]);
}
newRecord[i] = {
// `node` placeholder is filled by `buildFreshNodes`; its parse-count
// check guarantees every fresh index gets a real element before use.
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: null,
bindings: ni.bindings
};
prevIdx[i] = -1;
freshIndices.push(i);
freshHtmls.push(ni.html);
}
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
}
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
if (freshHtmls.length === 0) return;
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
if (count !== freshHtmls.length) {
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
}
let node = content.firstElementChild;
for (const idx of freshIndices) {
const next = node.nextElementSibling;
const item = newRecord[idx];
item.node = node;
if (item.bindings !== void 0 && item.bindings.length > 0) {
item.bindingDisposers = wireRowBindings(item.node, item.bindings);
}
node = next;
}
}
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
for (let i = 0; i < freshHtmls.length; i++) {
if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) {
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent);
}
}
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
}
function removeOldNodes(liveParent, removedItems) {
for (const item of removedItems) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement === liveParent) liveParent.removeChild(item.node);
}
}
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
let nextSibling = tailAnchor;
for (let i = newRecord.length - 1; i >= 0; i--) {
const node = newRecord[i].node;
if (prevIdx[i] === -1 || !stable.has(i)) {
liveParent.insertBefore(node, nextSibling);
}
nextSibling = node;
}
}
function lis(arr) {
const tails = [];
const tailIdx = [];
const prev = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
if (v === -1) {
prev[i] = -1;
continue;
}
let lo = 0;
let hi = tails.length;
while (lo < hi) {
const mid = lo + hi >> 1;
if (tails[mid] < v) lo = mid + 1;
else hi = mid;
}
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
tails[lo] = v;
tailIdx[lo] = i;
}
const out = /* @__PURE__ */ new Set();
let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;
while (k !== -1) {
out.add(k);
k = prev[k];
}
return out;
}
// src/list-reconcile.ts
function reconcileList(binding, listSeg) {
if (listSeg.patches !== void 0 && binding.items.length > 0) {
reconcileGranular(binding, listSeg.patches);
return;
}
reconcileSnapshot(binding, listSeg);
}
// src/mount.ts
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
var NESTED_MOUNT_MSG = "mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts.";
function isMounted(el) {
return el[MOUNTED_MARKER] === true;
}
function setMounted(el, on) {
if (on) {
el[MOUNTED_MARKER] = true;
} else {
delete el[MOUNTED_MARKER];
}
}
function describeEl(el) {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : "";
return `<${tag}${id}>`;
}
function assertNotInsideMountedTree(rootEl) {
if (isMounted(rootEl)) {
throw new Error(
`mount: ${describeEl(rootEl)} is already mounted. Call the disposer returned by the first mount() before mounting again. kerf supports one mount per element \u2014 compose with plain functions that return JSX instead of nesting mounts.`
);
}
let ancestor = rootEl.parentElement;
while (ancestor !== null) {
if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG);
ancestor = ancestor.parentElement;
}
const stack = [];
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
while (stack.length > 0) {
const cur = stack.pop();
if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG);
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
}
}
function mount(rootEl, render) {
if (rootEl == null) {
throw new Error(
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
);
}
const owner = rootEl.ownerDocument;
if (owner !== document) {
if (owner.defaultView === null) document.adoptNode(rootEl);
}
assertNotInsideMountedTree(rootEl);
setMounted(rootEl, true);
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
const bindings = /* @__PURE__ */ new Map();
const renderCtx = {
counter: 0,
caches: /* @__PURE__ */ new Map(),
bindingCounts: /* @__PURE__ */ new Map(),
bindingSources: /* @__PURE__ */ new Map(),
keysThisRender: /* @__PURE__ */ new Set(),
shiftCandidates: [],
warnedShiftIds: /* @__PURE__ */ new Set(),
rebuiltLists: /* @__PURE__ */ new Set()
};
const bindingCtx = newBindingContext();
let bindingDisposers = [];
let prevWiredBindings = [];
let isFirst = true;
let prevStaticHtml = "";
const valueOnlyWarnCtx = { warned: false };
const runRenderPass = () => {
renderCtx.counter = 0;
renderCtx.keysThisRender.clear();
renderCtx.shiftCandidates.length = 0;
bindingCtx.counter = 0;
bindingCtx.list = [];
_setRenderContext(renderCtx);
_setBindingContext(bindingCtx);
try {
return render();
} finally {
_setRenderContext(null);
_setBindingContext(null);
}
};
const disposeEffect = effect(() => {
let result = runRenderPass();
const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter;
if (countChanged) {
for (const id of renderCtx.shiftCandidates) {
if (renderCtx.warnedShiftIds.has(id)) continue;
renderCtx.warnedShiftIds.add(id);
devHooks.listIdShift?.(id);
}
_resetCallOrderListState(renderCtx);
result = runRenderPass();
}
let segment = resultToSegment(result);
if (isFirst) {
runFirstRender(rootEl, segment, bindings);
prevStaticHtml = flattenWithoutListItems(segment);
devHooks.parserRepair?.(prevStaticHtml);
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
isFirst = false;
} else {
let nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) {
for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id);
result = runRenderPass();
segment = resultToSegment(result);
nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
}
if (nextStaticHtml !== prevStaticHtml) {
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
} else {
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
}
prevStaticHtml = nextStaticHtml;
}
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
for (const listSeg of collectLists(segment).values()) {
const binding = bindings.get(listSeg.id);
if (binding === void 0) {
throw new Error(
"mount: an each() list appeared in the render output but its marker never reached the live DOM. The most common cause is an each() introduced inside a data-morph-skip subtree on a re-render \u2014 the morph leaves that subtree untouched, so the list can never bind. Move the each() outside the skipped subtree, or remove data-morph-skip from its ancestor."
);
}
reconcileList(binding, listSeg);
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
renderCtx.bindingSources.set(listSeg.id, listSeg.source);
expectedCounts?.set(
listSeg.id,
listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length
);
}
renderCtx.previousCallCount = renderCtx.counter;
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
});
return () => {
disposeEffect();
for (const d of bindingDisposers) d();
bindingDisposers = [];
for (const b of bindings.values()) {
for (const item of b.items) disposeRowBindings(item.bindingDisposers);
}
listenerWarnObserver?.disconnect();
setMounted(rootEl, false);
};
}
function runFirstRender(rootEl, segment, bindings) {
rootEl.innerHTML = flatten(segment, true);
bindListsFromMarkers(rootEl, segment, bindings, true);
}
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
renderCtx.rebuiltLists.clear();
const currentStaticHtml = flattenWithoutListItems(segment);
if (currentStaticHtml === prevStaticHtml) {
return prevStaticHtml;
}
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
cleanupOrphanBindings(segment, bindings, renderCtx);
const template = rootEl.cloneNode(false);
template.innerHTML = currentStaticHtml;
morph(rootEl, template, collectOwnedItems(bindings));
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
return currentStaticHtml;
}
function coerceRenderResult(result) {
if (result === null || result === void 0) return "";
if (result === false || result === true) return "";
return String(result);
}
function resultToSegment(result) {
return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
}
function anyRebuiltListIsGranular(segment, rebuilt) {
if (rebuilt.size === 0) return false;
const lists = collectLists(segment);
for (const id of rebuilt) {
if (lists.get(id)?.patches !== void 0) return true;
}
return false;
}
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) {
const lists = collectLists(segment);
const found = [];
collectComments(rootEl, found);
for (const marker of found) {
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
const existing = bindings.get(id);
if (existing !== void 0) {
if (existing.marker === marker && rootEl.contains(existing.marker)) continue;
for (const item of existing.items) {
disposeRowBindings(item.bindingDisposers);
if (rootEl.contains(item.node)) {
item.node.parentElement?.removeChild(item.node);
}
}
bindings.delete(id);
rebuiltLists?.add(id);
devHooks.listRebind?.(id, marker.parentElement);
}
const listSeg = lists.get(id);
const liveParent = marker.parentElement;
const items = [];
if (inlinedItems) {
let next = marker.nextElementSibling;
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
const rowBindings = listSeg.items[i].bindings;
const bound = {
ref: listSeg.items[i].ref,
cacheKey: listSeg.items[i].cacheKey,
html: listSeg.items[i].html,
node: next,
bindings: rowBindings
};
if (rowBindings !== void 0 && rowBindings.length > 0) {
bound.bindingDisposers = wireRowBindings(next, rowBindings);
}
items.push(bound);
next = next.nextElementSibling;
}
}
const binding = { liveParent, items, marker };
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
bindings.set(id, binding);
}
}
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
if (boundEl.outerHTML === expectedHtml) return;
const { content, count } = parseRowTemplate(expectedHtml, liveParent);
if (count !== 1) throw rowContractError(index, expectedHtml, liveParent);
const expectedTag = content.firstElementChild.tagName;
if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag);
}
function rowStructureError(index, gotTag, wantTag) {
const got = gotTag.toLowerCase();
const want = wantTag.toLowerCase();
return new Error(
`each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.`
);
}
function collectOwnedItems(bindings) {
const owned = /* @__PURE__ */ new Set();
for (const b of bindings.values()) {
for (const item of b.items) owned.add(item.node);
}
return owned;
}
function cleanupOrphanBindings(segment, bindings, renderCtx) {
const liveIds = collectLists(segment);
for (const [id, binding] of bindings) {
if (liveIds.has(id)) continue;
for (const item of binding.items) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement !== null) {
item.node.parentElement.removeChild(item.node);
}
}
if (binding.marker.parentElement !== null) {
binding.marker.parentElement.removeChild(binding.marker);
}
bindings.delete(id);
renderCtx.bindingCounts.delete(id);
renderCtx.bindingSources.delete(id);
renderCtx.caches.delete(id);
}
}
function collectComments(node, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) out.push(c);
else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out);
}
}
export { each, morph, mount };
//# sourceMappingURL=chunk-4MY2656S.js.map
//# sourceMappingURL=chunk-4MY2656S.js.map

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

import { effect, isSignal } from './chunk-3APBEVHF.js';
import { flatten, wrapWithTags, mergeChildSegments } from './chunk-GY4XV2UV.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/utils/syncFormProp.ts
function syncFormProp(el, name, value, present) {
const tag = el.tagName;
if (name === "checked") {
if (tag === "INPUT") el.checked = present;
} else if (name === "value") {
if (tag === "INPUT" && el !== document.activeElement) {
el.value = present ? value : "";
}
} else if (name === "selected") {
if (tag === "OPTION") el.selected = present;
}
}
// src/utils/urlScreen.ts
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
var DANGEROUS_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript"]);
var INERT_JAVASCRIPT_URLS = /* @__PURE__ */ new Set([
"javascript:",
"javascript:;",
"javascript:void(0)",
"javascript:void(0);",
"javascript:void 0",
"javascript:void 0;"
]);
var CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
function normalizeUrl(value) {
return value.replace(CONTROL_CHARS, "").replace(/^\s+/, "");
}
function schemeOf(normalized) {
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(normalized);
return m ? m[1].toLowerCase() : null;
}
function isDangerousDataUrl(normalized) {
const media = /^data:([^;,]*)/.exec(normalized.toLowerCase())?.[1].trim() ?? "";
if (media === "" || media === "text/plain" || media === "text/css") return false;
if (media === "image/svg+xml") return true;
if (media.startsWith("image/")) return false;
if (media.startsWith("font/") || media.startsWith("application/font")) return false;
if (media.startsWith("audio/") || media.startsWith("video/")) return false;
return true;
}
function isDangerousUrlValue(name, value) {
if (!URL_ATTRS.has(name)) return false;
const normalized = normalizeUrl(value);
const scheme = schemeOf(normalized);
if (scheme === null) return false;
if (DANGEROUS_SCHEMES.has(scheme)) {
return !INERT_JAVASCRIPT_URLS.has(normalized.trimEnd().toLowerCase());
}
return scheme === "data" && isDangerousDataUrl(normalized);
}
function dangerousUrlWarning(name, value) {
return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
}
function reportDangerousUrl(context2, name, value) {
const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
devHooks.urlScreenThrow?.(message);
console.warn(message);
}
// src/bindings.ts
var BIND_ATTR = "data-kfb";
var TEXT_MARKER_PREFIX = "kfb:";
var BIND_ATTR_ROW = "data-kfbrow";
var ROW_TEXT_PREFIX = "kfbr:";
var context = null;
var rowSink = null;
var rowCounter = 0;
var NO_DISPOSERS = Object.freeze([]);
function newBindingContext() {
return { counter: 0, list: [] };
}
function _setBindingContext(c) {
context = c;
}
function captureRowBindings(renderRow) {
const prevSink = rowSink;
const prevCounter = rowCounter;
rowSink = [];
rowCounter = 0;
try {
const html = renderRow();
return { html, bindings: rowSink };
} finally {
rowSink = prevSink;
rowCounter = prevCounter;
}
}
function bindAttr(attr, signal) {
if (rowSink !== null) {
const id = `a${rowCounter++}`;
rowSink.push({ kind: "attr", id, attr, signal });
return id;
}
if (context !== null) {
const id = `a${context.counter++}`;
context.list.push({ kind: "attr", id, attr, signal });
return id;
}
return null;
}
function bindMarkerAttr() {
return rowSink !== null ? BIND_ATTR_ROW : BIND_ATTR;
}
function bindText(signal) {
if (rowSink !== null) {
const id = `t${rowCounter++}`;
rowSink.push({ kind: "text", id, signal });
return `<!--${ROW_TEXT_PREFIX}${id}-->`;
}
if (context !== null) {
const id = `t${context.counter++}`;
context.list.push({ kind: "text", id, signal });
return `<!--${TEXT_MARKER_PREFIX}${id}-->`;
}
return null;
}
function wireBindings(rootEl, ctx, prevDisposers) {
for (const d of prevDisposers) d();
if (ctx.list.length === 0) return NO_DISPOSERS;
const disposers = [];
wireInto(rootEl, ctx.list, disposers);
return disposers;
}
function wireRowBindings(rowNode, bindings) {
const disposers = new Array(bindings.length);
const rootIds = rowNode.getAttribute(BIND_ATTR_ROW);
let rootIdSet = null;
let descIndex = null;
let textMarkers = null;
for (let i = 0; i < bindings.length; i++) {
const b = bindings[i];
if (b.kind === "attr") {
let onRoot = false;
if (rootIds !== null) {
if (rootIds === b.id) {
onRoot = true;
} else if (rootIds.indexOf(",") !== -1) {
rootIdSet ??= new Set(rootIds.split(","));
onRoot = rootIdSet.has(b.id);
}
}
let el;
if (onRoot) {
el = rowNode;
} else {
descIndex ??= indexAttrEls(rowNode, BIND_ATTR_ROW);
el = descIndex.get(b.id);
}
if (el === void 0) continue;
disposers[i] = attachAttrEffect(el, b.attr, b.signal);
} else {
if (textMarkers === null) {
textMarkers = /* @__PURE__ */ new Map();
collectComments(rowNode, ROW_TEXT_PREFIX, textMarkers);
}
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers[i] = attachTextEffect(marker, b.signal);
}
}
return disposers;
}
function disposeRowBindings(disposers) {
if (disposers === void 0) return;
for (const d of disposers) d();
}
function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
const newLen = newBindings === void 0 ? 0 : newBindings.length;
if (oldLen === newLen) {
let same = true;
for (let i = 0; i < newLen; i++) {
if (oldBindings[i].signal !== newBindings[i].signal) {
same = false;
break;
}
}
if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
}
disposeRowBindings(oldDisposers);
return {
bindings: newBindings,
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
};
}
function wireInto(scope, bindings, disposers) {
const attrEls = indexAttrEls(scope, BIND_ATTR);
const textMarkers = /* @__PURE__ */ new Map();
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
for (const b of bindings) {
if (b.kind === "attr") {
const el = attrEls.get(b.id);
if (el === void 0) continue;
disposers.push(attachAttrEffect(el, b.attr, b.signal));
} else {
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers.push(attachTextEffect(marker, b.signal));
}
}
}
function indexAttrEls(scope, attrName) {
const map = /* @__PURE__ */ new Map();
for (const el of scope.querySelectorAll(`[${attrName}]`)) {
for (const id of el.getAttribute(attrName).split(",")) map.set(id, el);
}
return map;
}
function attachAttrEffect(el, attr, signal) {
return effect(() => setBoundAttr(el, attr, signal.value));
}
var insertedTextNodes = /* @__PURE__ */ new WeakMap();
function boundTextNodeOf(marker) {
const t = insertedTextNodes.get(marker);
return t !== void 0 && marker.nextSibling === t ? t : null;
}
function attachTextEffect(marker, signal) {
let text = insertedTextNodes.get(marker);
if (text === void 0 || marker.nextSibling !== text) {
text = marker.ownerDocument.createTextNode("");
marker.parentNode.insertBefore(text, marker.nextSibling);
insertedTextNodes.set(marker, text);
}
const node = text;
return effect(() => {
node.data = coerceText(signal.value);
});
}
function setBoundAttr(el, name, value) {
if (value == null || value === false) {
el.removeAttribute(name);
syncFormProp(el, name, "", false);
return;
}
if (value === true) {
el.setAttribute(name, "");
syncFormProp(el, name, "", true);
return;
}
if (isSafeHtmlValue(value)) {
el.setAttribute(name, value.__html);
syncFormProp(el, name, value.__html, true);
return;
}
const str = String(value);
if (isDangerousUrlValue(name, str)) {
reportDangerousUrl("kerf binding", name, str);
el.removeAttribute(name);
return;
}
el.setAttribute(name, str);
syncFormProp(el, name, str, true);
}
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
function isSafeHtmlValue(v) {
return typeof v === "object" && v !== null && v[SAFE_HTML_BRAND] === true;
}
function coerceText(value) {
if (value == null || typeof value === "boolean") return "";
return String(value);
}
function collectComments(node, prefix, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) {
const data = c.data;
if (data.startsWith(prefix)) out.set(data.slice(prefix.length), c);
} else if (c.nodeType === Node.ELEMENT_NODE) {
collectComments(c, prefix, out);
}
}
}
// src/utils/escapeHtml.ts
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function escapeAttr(str) {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// src/utils/jsx-attr-aliases.ts
var ATTR_ALIASES = {
// HTML attributes
className: "class",
htmlFor: "for",
httpEquiv: "http-equiv",
acceptCharset: "accept-charset",
accessKey: "accesskey",
autoCapitalize: "autocapitalize",
autoComplete: "autocomplete",
autoFocus: "autofocus",
autoPlay: "autoplay",
colSpan: "colspan",
contentEditable: "contenteditable",
crossOrigin: "crossorigin",
dateTime: "datetime",
defaultChecked: "checked",
defaultSelected: "selected",
defaultValue: "value",
encType: "enctype",
formAction: "formaction",
formEncType: "formenctype",
formMethod: "formmethod",
formNoValidate: "formnovalidate",
formTarget: "formtarget",
hrefLang: "hreflang",
inputMode: "inputmode",
maxLength: "maxlength",
minLength: "minlength",
noModule: "nomodule",
noValidate: "novalidate",
readOnly: "readonly",
referrerPolicy: "referrerpolicy",
rowSpan: "rowspan",
spellCheck: "spellcheck",
srcDoc: "srcdoc",
srcLang: "srclang",
srcSet: "srcset",
tabIndex: "tabindex",
useMap: "usemap",
// SVG presentation attributes (camelCase → kebab-case)
strokeWidth: "stroke-width",
strokeLinecap: "stroke-linecap",
strokeLinejoin: "stroke-linejoin",
strokeDasharray: "stroke-dasharray",
strokeDashoffset: "stroke-dashoffset",
strokeMiterlimit: "stroke-miterlimit",
strokeOpacity: "stroke-opacity",
fillOpacity: "fill-opacity",
fillRule: "fill-rule",
clipPath: "clip-path",
clipRule: "clip-rule",
colorInterpolation: "color-interpolation",
colorInterpolationFilters: "color-interpolation-filters",
floodColor: "flood-color",
floodOpacity: "flood-opacity",
lightingColor: "lighting-color",
stopColor: "stop-color",
stopOpacity: "stop-opacity",
shapeRendering: "shape-rendering",
imageRendering: "image-rendering",
textRendering: "text-rendering",
pointerEvents: "pointer-events",
vectorEffect: "vector-effect",
paintOrder: "paint-order",
// SVG text/font attributes
fontFamily: "font-family",
fontSize: "font-size",
fontStyle: "font-style",
fontVariant: "font-variant",
fontWeight: "font-weight",
fontStretch: "font-stretch",
textAnchor: "text-anchor",
textDecoration: "text-decoration",
dominantBaseline: "dominant-baseline",
alignmentBaseline: "alignment-baseline",
baselineShift: "baseline-shift",
letterSpacing: "letter-spacing",
wordSpacing: "word-spacing",
writingMode: "writing-mode",
// SVG marker attributes
markerStart: "marker-start",
markerMid: "marker-mid",
markerEnd: "marker-end",
// SVG xlink (legacy but still used)
xlinkHref: "xlink:href",
xlinkShow: "xlink:show",
xlinkActuate: "xlink:actuate",
xlinkType: "xlink:type",
xlinkRole: "xlink:role",
xlinkTitle: "xlink:title",
xlinkArcrole: "xlink:arcrole",
xmlBase: "xml:base",
xmlLang: "xml:lang",
xmlSpace: "xml:space",
xmlnsXlink: "xmlns:xlink"
};
// src/jsx-runtime.ts
var SAFE_HTML_BRAND2 = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
var SafeHtml = class {
__html;
__segment;
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
[SAFE_HTML_BRAND2] = true;
constructor(input) {
if (typeof input === "string") {
this.__segment = { kind: "static", html: input };
this.__html = input;
} else {
this.__segment = input;
this.__html = flatten(input, false);
}
}
toString() {
return this.__html;
}
};
function isSafeHtml(value) {
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND2] === true;
}
function raw(html) {
return new SafeHtml(html);
}
function trustedRaw(html) {
return new SafeHtml(html);
}
function listSafeHtml(id, items, source) {
return new SafeHtml({ kind: "list", id, items, source });
}
function granularListSafeHtml(id, items, patches, source) {
return new SafeHtml({ kind: "list", id, items, patches, source });
}
var VOID_TAGS = /* @__PURE__ */ new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
]);
function toSegment(child) {
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
if (isSignal(child)) {
const marker = bindText(child);
if (marker !== null) return { kind: "static", html: marker };
const v = child.value;
return { kind: "static", html: v == null || typeof v === "boolean" ? "" : escapeHtml(String(v)) };
}
if (isSafeHtml(child)) {
return child.__segment ?? { kind: "static", html: child.__html };
}
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
if (typeof child === "number") return { kind: "static", html: String(child) };
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
const maybeNode = child;
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
throw new Error(
"JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). Build the tree in one JSX expression and use querySelector after toElement() to get element refs."
);
}
throw new Error(
`JSX: unsupported child of type ${describeValue(child)}. Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), passing a function (call it first), or passing a Promise (await it before render).`
);
}
function describeValue(v) {
if (Array.isArray(v)) return "array";
if (typeof v === "object" && v !== null) {
const ctor = v.constructor?.name;
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
}
return typeof v;
}
var SAFE_ATTR_NAME = /^[A-Za-z_:][\w.:-]*$/;
function assertEmittableAttrName(key, name, isFn) {
if (/^on[a-z]/i.test(name)) {
if (isFn) {
throw new Error(
`JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX \u2192 HTML-string runtime. Use event delegation from the mount root instead:
delegate(rootEl, 'click', '[data-action="..."]', (evt, target) => { ... });
<button data-action="...">click</button>
See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
);
}
throw new Error(
`JSX: event-handler attribute ${JSON.stringify(key)} is not allowed \u2014 an \`on*\` attribute (whether a string emitted into HTML or a signal bound via setAttribute) installs a live inline handler, an XSS vector. kerf uses event delegation: delegate(rootEl, 'click', '[data-action="..."]', handler). See docs/5-event-delegation.md.`
);
}
if (!SAFE_ATTR_NAME.test(name)) {
throw new Error(
`JSX: invalid attribute name ${JSON.stringify(key)}. Attribute names must be a letter/underscore/colon followed by letters, digits, or "_.:-" (e.g. class, data-id, aria-label, xlink:href). This usually means an untrusted object was spread into JSX ({...obj}) with attacker-controlled keys \u2014 validate keys first.`
);
}
}
function renderAttr(key, value) {
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
}
function renderAttrNamed(key, name, value) {
if (value == null || value === false) return "";
assertEmittableAttrName(key, name, typeof value === "function");
if (value === true) return ` ${name}`;
let strValue;
if (isSafeHtml(value)) {
strValue = value.__html;
} else if (typeof value === "number") {
strValue = String(value);
} else if (typeof value === "string") {
if (isDangerousUrlValue(name, value)) {
reportDangerousUrl("JSX", name, value);
return "";
}
strValue = escapeAttr(value);
} else {
throw new Error(
`JSX: unsupported value for attribute "${key}" \u2014 got ${describeValue(value)}. Attribute values must be string, number, boolean, null, undefined, or SafeHtml. Did you mean to read .value off a Signal, or stringify the object first?`
);
}
return ` ${name}="${strValue}"`;
}
function jsx(tag, props) {
if (typeof tag === "function") return tag(props);
const { children, ...attrs } = props;
let attrStr = "";
let bindIds = null;
for (const [k, v] of Object.entries(attrs)) {
if (isSignal(v)) {
const name = ATTR_ALIASES[k] ?? k;
assertEmittableAttrName(k, name, false);
const id = bindAttr(name, v);
if (id !== null) {
(bindIds ??= []).push(id);
continue;
}
attrStr += renderAttr(k, v.value);
continue;
}
attrStr += renderAttr(k, v);
}
if (bindIds !== null) attrStr += ` ${bindMarkerAttr()}="${bindIds.join(",")}"`;
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
}
function Fragment({ children }) {
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
}
function _toSegment(child) {
return toSegment(child);
}
function _renderAttrVerbatim(name, value) {
return renderAttrNamed(name, name, value);
}
export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, trustedRaw, wireBindings, wireRowBindings };
//# sourceMappingURL=chunk-FSAQR6IU.js.map
//# sourceMappingURL=chunk-FSAQR6IU.js.map

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

import { devHooks } from './chunk-VVDJLWMP.js';
// src/delegate.ts
var NON_BUBBLING = /* @__PURE__ */ new Set([
"focus",
"blur",
"scroll",
"load",
"error",
"mouseenter",
"mouseleave"
]);
function assertValidSelector(selector, fn) {
try {
document.createElement("div").matches(selector);
} catch {
throw new Error(
`${fn}: invalid selector "${selector}". Pass a valid CSS selector (e.g. '[data-action="add"]', '.btn', 'input').`
);
}
}
function makeListener(rootEl, selector, handler, match) {
return (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const matched = match === "direct" ? target.matches(selector) ? target : null : target.closest(selector);
if (matched !== null && rootEl.contains(matched)) {
handler(event, matched);
}
};
}
function delegate(rootEl, type, selector, handler, options) {
assertValidSelector(selector, "delegate");
devHooks.delegateInEffect?.("delegate");
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
const capture = NON_BUBBLING.has(type);
rootEl.addEventListener(type, listener, capture);
return () => {
rootEl.removeEventListener(type, listener, capture);
};
}
function delegateCapture(rootEl, type, selector, handler, options) {
assertValidSelector(selector, "delegateCapture");
devHooks.delegateInEffect?.("delegateCapture");
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
rootEl.addEventListener(type, listener, true);
return () => {
rootEl.removeEventListener(type, listener, true);
};
}
export { delegate, delegateCapture };
//# sourceMappingURL=chunk-KEZTD6H4.js.map
//# sourceMappingURL=chunk-KEZTD6H4.js.map
{"version":3,"sources":["../src/delegate.ts"],"names":[],"mappings":";;;AA+CA,IAAM,YAAA,uBAAmB,GAAA,CAAY;AAAA,EACnC,OAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAC,CAAA;AAqBD,SAAS,mBAAA,CAAoB,UAAkB,EAAA,EAAkB;AAC/D,EAAA,IAAI;AACF,IAAA,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,EAChD,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,EAAE,CAAA,oBAAA,EAAuB,QAAQ,CAAA,2EAAA;AAAA,KAEtC;AAAA,EACF;AACF;AAQA,SAAS,YAAA,CACP,MAAA,EACA,QAAA,EACA,OAAA,EACA,KAAA,EACwB;AACxB,EAAA,OAAO,CAAC,KAAA,KAAuB;AAC7B,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,EAAE,kBAAkB,OAAA,CAAA,EAAU;AAClC,IAAA,MAAM,OAAA,GAAU,KAAA,KAAU,QAAA,GACrB,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,GAAI,MAAA,GAAS,IAAA,GACrC,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA;AAC3B,IAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AAChD,MAAA,OAAA,CAAQ,OAAO,OAAY,CAAA;AAAA,IAC7B;AAAA,EACF,CAAA;AACF;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,UAAU,CAAA;AACxC,EAAA,QAAA,CAAS,mBAAmB,UAAU,CAAA;AACtC,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AACrC,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAC/C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,OAAO,CAAA;AAAA,EACpD,CAAA;AACF;AAuBO,SAAS,eAAA,CACd,MAAA,EACA,IAAA,EACA,QAAA,EACA,SACA,OAAA,EACY;AACZ,EAAA,mBAAA,CAAoB,UAAU,iBAAiB,CAAA;AAC/C,EAAA,QAAA,CAAS,mBAAmB,iBAAiB,CAAA;AAC7C,EAAA,MAAM,WAAW,YAAA,CAAa,MAAA,EAAQ,UAAU,OAAA,EAAS,OAAA,EAAS,SAAS,SAAS,CAAA;AACpF,EAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAC5C,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,IAAA,EAAM,QAAA,EAAU,IAAI,CAAA;AAAA,EACjD,CAAA;AACF","file":"chunk-KEZTD6H4.js","sourcesContent":["/**\n * Tiny event-delegation helpers. Replace per-element `addEventListener` calls\n * (which don't survive morph re-renders for nodes the diff creates) with one\n * listener at the morph-root that dispatches via `closest()`.\n *\n * Three-tier listener model:\n *\n * - Tier 1 (bubbling events) — use `delegate()`.\n * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,\n * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.\n *\n * `delegate()` also auto-promotes the well-known non-bubbling event\n * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,\n * `mouseleave`) to the capture phase under the hood, so the call site\n * looks identical for \"interactive thing happens on a descendant\"\n * regardless of whether that event bubbles. Selector matching stays\n * `closest()`-style — the same as for bubbling events — so a wrapper\n * selector like `'.field-row'` still matches when the event fires on\n * a descendant `<input>`.\n *\n * - Tier 2 (explicit capture) — use `delegateCapture()`.\n * The escape hatch for cases the auto-promotion list doesn't cover\n * (custom non-bubbling events) or when you want capture-phase\n * interception. Selector matching is `closest()`-style by default —\n * the same walk-up as `delegate()`, and it passes the matched ancestor\n * (not the raw target) to the handler — so a click on any descendant of\n * the selected element climbs to it. Pass `{ match: 'direct' }` to opt\n * into strict `matches()`-style matching (fire only when the event lands\n * on the exact element the selector identifies).\n *\n * - Tier 3 (per-element instances / library-owned subtrees) — mark the\n * host element with `data-morph-skip` and manage the library's\n * lifecycle directly. No delegation helper applies.\n */\n\nimport { devHooks } from './dev-hooks.js';\n\n/**\n * Event types that don't bubble and so wouldn't reach a root-level\n * bubble-phase listener. `delegate()` flips to capture for these; the\n * caller doesn't need to know or care.\n *\n * Membership is conservative — it covers the cases that \"should obviously\n * work\" with delegate() but otherwise don't. For exotic non-bubbling events\n * (custom events, less-common DOM events) the explicit `delegateCapture()`\n * remains the escape hatch.\n */\nconst NON_BUBBLING = new Set<string>([\n 'focus',\n 'blur',\n 'scroll',\n 'load',\n 'error',\n 'mouseenter',\n 'mouseleave',\n]);\n\n/**\n * How the selector is matched against the event's target:\n *\n * - `'closest'` (the default for both helpers) — walk UP from `event.target`\n * via `closest(selector)`, firing for the nearest matching ancestor inside\n * `rootEl`. This is the delegation behavior you almost always want: a click\n * on an icon inside a button fires the button's handler.\n * - `'direct'` — strict `matches()` match: fire only when `event.target`\n * itself matches the selector, with no walk-up.\n */\nexport interface DelegateOptions {\n match?: 'closest' | 'direct';\n}\n\n/**\n * Validate a CSS selector at registration time, so a typo throws immediately\n * with the bad selector quoted instead of producing a cryptic DOMException\n * the first time a matching event fires.\n */\nfunction assertValidSelector(selector: string, fn: string): void {\n try {\n document.createElement('div').matches(selector);\n } catch {\n throw new Error(\n `${fn}: invalid selector \"${selector}\". `\n + 'Pass a valid CSS selector (e.g. \\'[data-action=\"add\"]\\', \\'.btn\\', \\'input\\').',\n );\n }\n}\n\n/**\n * Build the shared root-level listener used by both helpers. Resolves the\n * event's target to a matched element (walk-up `closest()` or strict\n * `matches()`, per `match`), requires the match to be inside `rootEl`, then\n * fires `handler(event, matched)`.\n */\nfunction makeListener<T extends Element>(\n rootEl: HTMLElement,\n selector: string,\n handler: (event: Event, target: T) => void,\n match: 'closest' | 'direct',\n): (event: Event) => void {\n return (event: Event): void => {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const matched = match === 'direct'\n ? (target.matches(selector) ? target : null)\n : target.closest(selector);\n if (matched !== null && rootEl.contains(matched)) {\n handler(event, matched as T);\n }\n };\n}\n\n/**\n * Delegation that \"just works\" for both bubbling and the common non-bubbling\n * events. Installs ONE listener on `rootEl`; for known non-bubblers (see\n * `NON_BUBBLING` above) the listener is registered on the capture phase so\n * it actually reaches the target, otherwise on the bubble phase. Either way,\n * matching walks up from `event.target` via `closest(selector)` and fires\n * `handler(event, matched)` if the match is inside `rootEl`.\n *\n * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches\n * the selector (no walk-up); the default is `'closest'`.\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`\n * — so consumers can avoid casts. Defaults to `Element` for untyped calls.\n *\n * Returns a disposer that removes the listener.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegate(rootEl, 'click', '[data-action=\"add\"]', handlerFn);\n * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture\n */\nexport function delegate<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegate');\n devHooks.delegateInEffect?.('delegate');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n const capture = NON_BUBBLING.has(type);\n rootEl.addEventListener(type, listener, capture);\n return () => {\n rootEl.removeEventListener(type, listener, capture);\n };\n}\n\n/**\n * Capture-phase delegation — the escape hatch for custom non-bubbling events\n * (ones `delegate()`'s auto-promotion list doesn't know about) and for\n * capture-phase interception (run before any descendant's bubble-phase\n * handler). Reaches descendants of `rootEl` that match `selector` regardless\n * of how many times the diff has rebuilt them.\n *\n * Selector matching is `closest()`-style by default — the same walk-up as\n * `delegate()`, and it passes the matched ancestor (not the raw target) to\n * the handler — so a click on any descendant of the selected element climbs\n * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style\n * matching (fire only when the event lands on the exact element the selector\n * identifies, with no walk-up).\n *\n * The generic `T` narrows the second handler argument to the expected element\n * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.\n *\n * Usage (pseudo-code — see examples for live ones):\n * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);\n * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });\n */\nexport function delegateCapture<T extends Element = Element>(\n rootEl: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n): () => void {\n assertValidSelector(selector, 'delegateCapture');\n devHooks.delegateInEffect?.('delegateCapture');\n const listener = makeListener(rootEl, selector, handler, options?.match ?? 'closest');\n rootEl.addEventListener(type, listener, true);\n return () => {\n rootEl.removeEventListener(type, listener, true);\n };\n}\n"]}
import { bumpItemVersion } from './chunk-QIP723L4.js';
import { signal } from './chunk-3APBEVHF.js';
// src/array-signal.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
var ArraySignal = class {
_items;
_version;
_patches;
// Branded so `isArraySignal()` recognizes instances from any copy of this module.
[ARRAY_SIGNAL_BRAND] = true;
constructor(initial = []) {
this._items = [...initial];
this._version = signal(0);
this._patches = [];
}
/** Read-only snapshot. Reads inside an effect/computed register a dependency. */
get value() {
void this._version.value;
return this._items;
}
/**
* Replace the item at `index` with `fn(currentItem)`. Emits one `update`
* patch. Both styles work: returning a fresh object (idiomatic) invalidates
* the row by identity, and mutating `item` in place and returning it works
* too — a per-item content version (KF-418) makes the same-ref change visible
* to every consumer's row memo.
*/
update(index, fn) {
if (index < 0 || index >= this._items.length) {
throw new Error(
`arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`
);
}
const next = fn(this._items[index]);
this._items[index] = next;
this._patches.push({ type: "update", index, item: next });
bumpItemVersion(next);
this._version.value++;
}
/** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */
insert(index, item) {
if (index < 0 || index > this._items.length) {
throw new Error(
`arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`
);
}
this._items.splice(index, 0, item);
this._patches.push({ type: "insert", index, item });
this._version.value++;
}
/** Append `item` at the end. Sugar for `insert(items.length, item)`. */
push(item) {
this.insert(this._items.length, item);
}
/** Remove and return the item at `index`. Emits one `remove` patch. */
remove(index) {
if (index < 0 || index >= this._items.length) {
throw new Error(
`arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`
);
}
const [removed] = this._items.splice(index, 1);
this._patches.push({ type: "remove", index });
this._version.value++;
return removed;
}
/** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */
move(from, to) {
if (from === to) return;
if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {
throw new Error(
`arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`
);
}
const [item] = this._items.splice(from, 1);
this._items.splice(to, 0, item);
this._patches.push({ type: "move", from, to });
this._version.value++;
}
/** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */
replace(items) {
this._items = [...items];
this._patches.push({ type: "replace", items: this._items });
this._version.value++;
}
/**
* @internal Used by `each()` when binding this signal to a list. Returns
* the queue of granular patches issued since the previous call, then
* clears the queue. Best paired with a single binding — a second consumer
* in the same render gets an empty array (which forces the snapshot
* fall-back path, which is correct but slower).
*/
_consumePatches() {
const out = this._patches;
this._patches = [];
return out;
}
};
function arraySignal(initial = []) {
return new ArraySignal(initial);
}
export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal };
//# sourceMappingURL=chunk-MRYM3O3V.js.map
//# sourceMappingURL=chunk-MRYM3O3V.js.map
{"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"chunk-MRYM3O3V.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport { bumpItemVersion } from './item-version.js';\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognizes instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /**\n * Replace the item at `index` with `fn(currentItem)`. Emits one `update`\n * patch. Both styles work: returning a fresh object (idiomatic) invalidates\n * the row by identity, and mutating `item` in place and returning it works\n * too — a per-item content version (KF-418) makes the same-ref change visible\n * to every consumer's row memo.\n */\n update(index: number, fn: (item: T) => T): void {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n // KF-418: a same-ref update (fn mutates and returns the same object) is\n // invisible to the row memo, which is keyed on object identity. Bump the\n // item's content version so every consumer — this list, another list over\n // this signal, a second mount, a plain-array filter() view — re-renders it.\n // Non-object items (an arraySignal<number> used as a plain signal) are\n // skipped by bumpItemVersion — they can't be each() rows (KF-419).\n bumpItemVersion(next);\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (index < 0 || index > this._items.length) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (from === to) return;\n if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
// src/attrSelector.ts
function cssEscapeIdent(value) {
if (value === "") {
throw new Error("attr: attribute name must not be empty");
}
const str = String(value);
let result = "";
for (let i = 0; i < str.length; i++) {
const cp = str.charCodeAt(i);
const ch = str.charAt(i);
if (cp === 0) {
result += "\uFFFD";
continue;
}
if (cp >= 1 && cp <= 31 || cp === 127) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (i === 0 && cp >= 48 && cp <= 57) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (i === 1 && cp >= 48 && cp <= 57 && str.charCodeAt(0) === 45) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (cp >= 128 || cp === 45 || // `-`
cp === 95 || // `_`
cp >= 48 && cp <= 57 || // 0-9
cp >= 65 && cp <= 90 || // A-Z
cp >= 97 && cp <= 122) {
result += ch;
continue;
}
result += "\\" + ch;
}
return result;
}
function escapeCSSString(value) {
let result = "";
for (let i = 0; i < value.length; i++) {
const cp = value.charCodeAt(i);
const ch = value.charAt(i);
if (cp === 0) {
result += "\uFFFD";
} else if (cp >= 1 && cp <= 31 || cp === 127) {
result += "\\" + cp.toString(16) + " ";
} else if (cp === 92) {
result += "\\\\";
} else if (cp === 34) {
result += '\\"';
} else {
result += ch;
}
}
return result;
}
function attr(name, value) {
const escapedName = cssEscapeIdent(name);
if (value !== void 0) {
const selector = `[${escapedName}="${escapeCSSString(value)}"]`;
return Object.freeze({
name,
value,
selector,
attrs: Object.freeze({ [name]: value })
});
}
return (v) => Object.freeze({ [name]: v });
}
export { attr };
//# sourceMappingURL=chunk-U32TFTGZ.js.map
//# sourceMappingURL=chunk-U32TFTGZ.js.map
{"version":3,"sources":["../src/attrSelector.ts"],"names":[],"mappings":"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","file":"chunk-U32TFTGZ.js","sourcesContent":["/**\n * `attr(name, value)` — create a pre-computed attribute descriptor (static form).\n * `attr(name)` — create a per-render factory for dynamic attribute values (dynamic form).\n *\n * **Static form** — best for fixed action names, filter keys, role values, etc.\n * Escapes once at module-load time; produces a full {@link AttrSpec} with\n * `.name`, `.value`, `.selector`, and `.attrs`.\n *\n * const ACTIONS = {\n * toggle: attr('data-action', 'toggle'),\n * remove: attr('data-action', 'remove'),\n * } as const satisfies Record<string, AttrSpec<'data-action'>>;\n *\n * // In JSX — spread .attrs (rename-safe; no hardcoded attribute name):\n * <button {...ACTIONS.toggle.attrs}>Toggle</button>\n *\n * // In delegate — use the pre-computed selector:\n * delegate(root, 'click', ACTIONS.toggle.selector, handler);\n *\n * **Dynamic form** — best for per-row data like `data-id`, where the value\n * changes per item but the attribute name is constant.\n * The name is validated and pre-escaped at definition time; calling the\n * returned factory is cheap (it just freezes a one-key object — the value is\n * escaped later by the JSX attribute renderer when the result is spread).\n *\n * const ITEM = { id: attr('data-id') } as const;\n *\n * // In JSX — call the factory inline:\n * <li {...ITEM.id(String(item.id))}>…</li>\n *\n * For ad-hoc compound selectors, concatenate `.selector` strings:\n *\n * delegate(root, 'click',\n * ACTIONS.toggle.selector + attr('data-id', id).selector,\n * handler);\n *\n * Escaping:\n * - Attribute name: escaped as a CSS identifier via `cssEscapeIdent`, which is\n * an SSR-safe (no `CSS.escape`) adaptation of the Mathias Bynens polyfill\n * (https://github.com/mathiasbynens/CSS.escape, MIT licensed — see the\n * Acknowledgements section of LICENSE). Handles\n * control chars, leading digits, non-ASCII, and CSS metacharacters.\n * - Attribute value: embedded in double quotes as a CSS string. Backslashes and\n * double-quote characters are backslash-escaped; control characters are\n * hex-escaped per CSS Syntax Level 3 §3.4.\n *\n * Throws on an empty attribute name (not a valid CSS identifier).\n */\n\n/** Descriptor created by the static {@link attr} overload. */\nexport interface AttrSpec<N extends string = string, V extends string = string> {\n /** The raw attribute name passed to `attr()`. */\n readonly name: N;\n /** The raw attribute value passed to `attr()`. */\n readonly value: V;\n /** Pre-computed `[name=\"value\"]` CSS selector string, safe to pass to `delegate()`. */\n readonly selector: string;\n /** Spreadable JSX object — `{ [name]: value }` — keeps the attribute name out of JSX literals. */\n readonly attrs: { readonly [K in N]: V };\n}\n\n/**\n * Escape `value` as a CSS identifier (for attribute names, id fragments, etc.).\n * Adapted from the CSS.escape polyfill by Mathias Bynens (MIT).\n */\nfunction cssEscapeIdent(value: string): string {\n if (value === '') {\n throw new Error('attr: attribute name must not be empty');\n }\n const str = String(value);\n let result = '';\n for (let i = 0; i < str.length; i++) {\n const cp = str.charCodeAt(i);\n const ch = str.charAt(i);\n\n // U+0000 NULL → replacement character\n if (cp === 0x0000) {\n result += '�';\n continue;\n }\n // Control characters and DEL: hex-escape\n if ((cp >= 0x0001 && cp <= 0x001F) || cp === 0x007F) {\n result += '\\\\' + cp.toString(16) + ' ';\n continue;\n }\n // Leading digit: hex-escape to avoid \"-NN\" / \"3px\"-style ambiguity\n if (i === 0 && cp >= 0x0030 && cp <= 0x0039) {\n result += '\\\\' + cp.toString(16) + ' ';\n continue;\n }\n // Second char is a digit when first is '-' (e.g. \"-3foo\"): hex-escape digit\n if (\n i === 1 &&\n cp >= 0x0030 && cp <= 0x0039 &&\n str.charCodeAt(0) === 0x002D\n ) {\n result += '\\\\' + cp.toString(16) + ' ';\n continue;\n }\n // Non-ASCII, safe identifier chars (letters, digits, underscore, hyphen)\n if (\n cp >= 0x0080 ||\n cp === 0x002D || // `-`\n cp === 0x005F || // `_`\n (cp >= 0x0030 && cp <= 0x0039) || // 0-9\n (cp >= 0x0041 && cp <= 0x005A) || // A-Z\n (cp >= 0x0061 && cp <= 0x007A) // a-z\n ) {\n result += ch;\n continue;\n }\n // Everything else: backslash-escape\n result += '\\\\' + ch;\n }\n return result;\n}\n\n/**\n * Escape `value` as a CSS double-quoted string (for attribute values in\n * `[attr=\"value\"]` selectors).\n */\nfunction escapeCSSString(value: string): string {\n let result = '';\n for (let i = 0; i < value.length; i++) {\n const cp = value.charCodeAt(i);\n const ch = value.charAt(i);\n if (cp === 0x0000) {\n result += '�';\n } else if ((cp >= 0x0001 && cp <= 0x001F) || cp === 0x007F) {\n // Control chars: hex-escape\n result += '\\\\' + cp.toString(16) + ' ';\n } else if (cp === 0x005C) {\n // Backslash\n result += '\\\\\\\\';\n } else if (cp === 0x0022) {\n // Double quote (the string delimiter we use)\n result += '\\\\\"';\n } else {\n result += ch;\n }\n }\n return result;\n}\n\n/**\n * Static overload — pre-computes the full descriptor at definition time.\n * Returns an {@link AttrSpec} with `.name`, `.value`, `.selector`, and `.attrs`.\n */\nexport function attr<N extends string, V extends string>(name: N, value: V): AttrSpec<N, V>;\n\n/**\n * Dynamic overload — pre-validates and pre-escapes the attribute name, returns a\n * factory that accepts a per-render value and produces a frozen spreadable object.\n * Use for per-row attributes like `data-id` where the value changes per item.\n * The optional `V` generic constrains which values the factory accepts:\n * `attr<'data-id', 'a'|'b'>('data-id')` → `(value: 'a'|'b') => { 'data-id': 'a'|'b' }`.\n * Leaving both generics off infers `N` from the argument and defaults `V` to `string`.\n */\nexport function attr<N extends string, V extends string = string>(name: N): (value: V) => { readonly [K in N]: V };\n\nexport function attr<N extends string, V extends string>(\n name: N,\n value?: V,\n): AttrSpec<N, V> | ((value: string) => { readonly [K in N]: string }) {\n const escapedName = cssEscapeIdent(name); // validates + pre-escapes name in both paths\n if (value !== undefined) {\n const selector = `[${escapedName}=\"${escapeCSSString(value)}\"]`;\n return Object.freeze({\n name,\n value,\n selector,\n attrs: Object.freeze({ [name]: value }) as { readonly [K in N]: V },\n }) as AttrSpec<N, V>;\n }\n return (v: string): { readonly [K in N]: string } =>\n Object.freeze({ [name]: v }) as { readonly [K in N]: string };\n}\n"]}
/**
* Tiny event-delegation helpers. Replace per-element `addEventListener` calls
* (which don't survive morph re-renders for nodes the diff creates) with one
* listener at the morph-root that dispatches via `closest()`.
*
* Three-tier listener model:
*
* - Tier 1 (bubbling events) — use `delegate()`.
* click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
* drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
*
* `delegate()` also auto-promotes the well-known non-bubbling event
* types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,
* `mouseleave`) to the capture phase under the hood, so the call site
* looks identical for "interactive thing happens on a descendant"
* regardless of whether that event bubbles. Selector matching stays
* `closest()`-style — the same as for bubbling events — so a wrapper
* selector like `'.field-row'` still matches when the event fires on
* a descendant `<input>`.
*
* - Tier 2 (explicit capture) — use `delegateCapture()`.
* The escape hatch for cases the auto-promotion list doesn't cover
* (custom non-bubbling events) or when you want capture-phase
* interception. Selector matching is `closest()`-style by default —
* the same walk-up as `delegate()`, and it passes the matched ancestor
* (not the raw target) to the handler — so a click on any descendant of
* the selected element climbs to it. Pass `{ match: 'direct' }` to opt
* into strict `matches()`-style matching (fire only when the event lands
* on the exact element the selector identifies).
*
* - Tier 3 (per-element instances / library-owned subtrees) — mark the
* host element with `data-morph-skip` and manage the library's
* lifecycle directly. No delegation helper applies.
*/
/**
* How the selector is matched against the event's target:
*
* - `'closest'` (the default for both helpers) — walk UP from `event.target`
* via `closest(selector)`, firing for the nearest matching ancestor inside
* `rootEl`. This is the delegation behavior you almost always want: a click
* on an icon inside a button fires the button's handler.
* - `'direct'` — strict `matches()` match: fire only when `event.target`
* itself matches the selector, with no walk-up.
*/
interface DelegateOptions {
match?: 'closest' | 'direct';
}
/**
* Delegation that "just works" for both bubbling and the common non-bubbling
* events. Installs ONE listener on `rootEl`; for known non-bubblers (see
* `NON_BUBBLING` above) the listener is registered on the capture phase so
* it actually reaches the target, otherwise on the bubble phase. Either way,
* matching walks up from `event.target` via `closest(selector)` and fires
* `handler(event, matched)` if the match is inside `rootEl`.
*
* Pass `{ match: 'direct' }` to fire only when `event.target` itself matches
* the selector (no walk-up); the default is `'closest'`.
*
* The generic `T` narrows the second handler argument to the expected element
* type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`
* — so consumers can avoid casts. Defaults to `Element` for untyped calls.
*
* Returns a disposer that removes the listener.
*
* Usage (pseudo-code — see examples for live ones):
* delegate(rootEl, 'click', '[data-action="add"]', handlerFn);
* delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture
*/
declare function delegate<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
/**
* Capture-phase delegation — the escape hatch for custom non-bubbling events
* (ones `delegate()`'s auto-promotion list doesn't know about) and for
* capture-phase interception (run before any descendant's bubble-phase
* handler). Reaches descendants of `rootEl` that match `selector` regardless
* of how many times the diff has rebuilt them.
*
* Selector matching is `closest()`-style by default — the same walk-up as
* `delegate()`, and it passes the matched ancestor (not the raw target) to
* the handler — so a click on any descendant of the selected element climbs
* to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style
* matching (fire only when the event lands on the exact element the selector
* identifies, with no walk-up).
*
* The generic `T` narrows the second handler argument to the expected element
* type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.
*
* Usage (pseudo-code — see examples for live ones):
* delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);
* delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });
*/
declare function delegateCapture<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
export { type DelegateOptions as D, delegateCapture as a, delegate as d };
import { M as MountResult } from './mount-Bo2qOx25.js';
import './jsx-runtime.js';
import '@preact/signals-core';
import './bindings-CYwoJpQb.js';
/** A row's stable key. */
type ListKey = string | number;
/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */
interface ListSource<T> {
readonly value: readonly T[];
}
/** Options for {@link bindList}. */
interface BindListOptions<T> {
/** Stable per-row key. Rows are matched, moved, and reused by this. */
key: (item: T) => ListKey;
/** Renders a row's content into its (individually mounted) row element. Read signals here for per-row reactivity. */
render: (item: T) => MountResult;
/** Row element tag. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). */
tag?: string;
/**
* Turn on viewport virtualization. `rowHeight` is the fixed pixel height of
* every row; `overscan` (default 3) is how many extra rows to render above and
* below the viewport. `parent` must be a scroll container (your CSS: a fixed
* height + `overflow: auto`).
*/
virtualize?: {
rowHeight: number;
overscan?: number;
};
}
/**
* Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a
* `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears
* down every row mount, the scroll listener (if virtualized), and the source
* subscription.
*/
declare function bindList<T>(parent: HTMLElement, source: ListSource<T>, options: BindListOptions<T>): () => void;
export { type BindListOptions, type ListKey, type ListSource, bindList };
import { ARRAY_SIGNAL_BRAND } from './chunk-MRYM3O3V.js';
import { mount } from './chunk-4MY2656S.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import './chunk-FSAQR6IU.js';
import { effect } from './chunk-3APBEVHF.js';
import './chunk-GY4XV2UV.js';
import './chunk-VVDJLWMP.js';
// src/list.ts
function bindList(parent, source, options) {
const { key, render, tag = "div", virtualize } = options;
const overscan = virtualize?.overscan ?? 3;
const rows = /* @__PURE__ */ new Map();
const order = [];
let items = [];
let disposed = false;
let rafPending = false;
let firstRender = true;
const patchSource = source;
const granularEligible = virtualize === void 0 && patchSource[ARRAY_SIGNAL_BRAND] === true;
const container = virtualize === void 0 ? parent : document.createElement("div");
if (virtualize !== void 0) parent.appendChild(container);
const makeRow = (item) => {
const el = document.createElement(tag);
if (virtualize !== void 0) el.style.height = `${virtualize.rowHeight}px`;
const dispose = mount(el, () => render(item));
return { el, item, dispose };
};
const syncRows = (visible) => {
const wanted = /* @__PURE__ */ new Set();
for (const item of visible) wanted.add(key(item));
for (const [k, row] of rows) {
if (!wanted.has(k)) {
row.dispose();
row.el.remove();
rows.delete(k);
}
}
order.length = 0;
for (const item of visible) {
const k = key(item);
let row = rows.get(k);
if (row !== void 0 && row.item !== item) {
row.dispose();
row.el.remove();
rows.delete(k);
row = void 0;
}
if (row === void 0) {
row = makeRow(item);
rows.set(k, row);
}
order.push(row);
}
let ref = null;
for (let i = order.length - 1; i >= 0; i--) {
const el = order[i].el;
if (el.parentNode !== container || el.nextSibling !== ref) {
container.insertBefore(el, ref);
}
ref = el;
}
};
const applyPatches = (patches) => {
for (const patch of patches) {
if (patch.type === "insert") {
const row = makeRow(patch.item);
rows.set(key(patch.item), row);
order.splice(patch.index, 0, row);
container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);
} else if (patch.type === "remove") {
const [row] = order.splice(patch.index, 1);
row.dispose();
row.el.remove();
rows.delete(key(row.item));
} else if (patch.type === "move") {
const [row] = order.splice(patch.from, 1);
order.splice(patch.to, 0, row);
container.insertBefore(row.el, order[patch.to + 1]?.el ?? null);
} else if (patch.type === "update") {
const current = order[patch.index];
if (current.item !== patch.item) {
current.dispose();
current.el.remove();
rows.delete(key(current.item));
const row = makeRow(patch.item);
rows.set(key(patch.item), row);
order[patch.index] = row;
container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);
}
}
}
};
const renderWindow = () => {
if (virtualize === void 0) {
if (granularEligible) {
const patches = patchSource._consumePatches();
if (!firstRender && patches.length > 0 && !patches.some((p) => p.type === "replace")) {
applyPatches(patches);
return;
}
}
syncRows(items);
firstRender = false;
return;
}
const { rowHeight } = virtualize;
const total = items.length;
const start = Math.max(0, Math.floor(parent.scrollTop / rowHeight) - overscan);
const end = Math.min(total, Math.ceil((parent.scrollTop + parent.clientHeight) / rowHeight) + overscan);
syncRows(items.slice(start, end));
container.style.paddingTop = `${start * rowHeight}px`;
container.style.paddingBottom = `${Math.max(0, total - end) * rowHeight}px`;
};
const stopEffect = effect(() => {
items = source.value;
renderWindow();
});
const onScroll = () => {
if (rafPending) return;
rafPending = true;
globalThis.requestAnimationFrame(() => {
rafPending = false;
if (!disposed) renderWindow();
});
};
if (virtualize !== void 0) parent.addEventListener("scroll", onScroll);
return () => {
disposed = true;
stopEffect();
for (const row of rows.values()) {
row.dispose();
if (virtualize === void 0) row.el.remove();
}
rows.clear();
if (virtualize !== void 0) {
parent.removeEventListener("scroll", onScroll);
container.remove();
}
};
}
export { bindList };
//# sourceMappingURL=list.js.map
//# sourceMappingURL=list.js.map
{"version":3,"sources":["../src/list.ts"],"names":[],"mappings":"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","file":"list.js","sourcesContent":["/**\n * `kerfjs/list` — `bindList`, a keyed list with a live per-row mount and\n * optional viewport virtualization.\n *\n * This is a DELIBERATE second list API, distinct from `each()`. It does two\n * things `each()` structurally cannot:\n * 1. **Per-row reactivity.** Every row is individually `mount()`ed, so a signal\n * the row's `render` reads updates just that row (fine-grained binding or a\n * one-row morph) without touching its siblings — no full-list pass.\n * 2. **Virtualization.** With `{ virtualize: { rowHeight } }` only the rows in\n * the scroll viewport are rendered; padding on the scroll container keeps\n * `scrollHeight` honest.\n *\n * `each()` stays the choice for item-owned-state lists rendered to HTML strings;\n * reach for `bindList` when you need surgical per-row updates or windowing.\n *\n * import { bindList } from 'kerfjs/list';\n *\n * const dispose = bindList(listEl, itemsSignal, {\n * key: (row) => row.id,\n * render: (row) => <span class={selected} data-id={row.id}>{row.label}</span>,\n * tag: 'li',\n * virtualize: { rowHeight: 32 },\n * });\n *\n * `render` reads signals for reactivity (external state like a `selectedId`, or\n * signals the item carries) — keep the item OBJECTS stable across renders and\n * drive structure (add/remove/move) through `itemsSignal`. A row whose item\n * object identity changes is rebuilt (same rule as `each()`'s memo). `bindList`\n * OWNS `parent`'s children. It reads `itemsSignal.value`, so a plain\n * `signal<T[]>` or an `arraySignal<T>` both work.\n */\nimport { ARRAY_SIGNAL_BRAND, type ArrayPatch } from './array-signal.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A row's stable key. */\nexport type ListKey = string | number;\n\n/** Anything with a tracking `.value` array read — a `signal<readonly T[]>` or an `arraySignal<T>`. */\nexport interface ListSource<T> {\n readonly value: readonly T[];\n}\n\n/** Options for {@link bindList}. */\nexport interface BindListOptions<T> {\n /** Stable per-row key. Rows are matched, moved, and reused by this. */\n key: (item: T) => ListKey;\n /** Renders a row's content into its (individually mounted) row element. Read signals here for per-row reactivity. */\n render: (item: T) => MountResult;\n /** Row element tag. Default `'div'` (use `'li'` inside a `<ul>`, `'tr'` inside a `<tbody>`, …). */\n tag?: string;\n /**\n * Turn on viewport virtualization. `rowHeight` is the fixed pixel height of\n * every row; `overscan` (default 3) is how many extra rows to render above and\n * below the viewport. `parent` must be a scroll container (your CSS: a fixed\n * height + `overflow: auto`).\n */\n virtualize?: { rowHeight: number; overscan?: number };\n}\n\ninterface Row<T> {\n el: HTMLElement;\n item: T;\n dispose: () => void;\n}\n\n/**\n * Bind a keyed, per-row-reactive list to `parent`, driven by `source` (a\n * `signal<readonly T[]>` or an `arraySignal<T>`). Returns a disposer that tears\n * down every row mount, the scroll listener (if virtualized), and the source\n * subscription.\n */\nexport function bindList<T>(\n parent: HTMLElement,\n source: ListSource<T>,\n options: BindListOptions<T>,\n): () => void {\n const { key, render, tag = 'div', virtualize } = options;\n const overscan = virtualize?.overscan ?? 3;\n\n const rows = new Map<ListKey, Row<T>>();\n // The current DOM order of rows, kept in step by both the keyed-diff and the\n // granular patch paths so index-based patches can address rows directly.\n const order: Array<Row<T>> = [];\n let items: readonly T[] = [];\n let disposed = false;\n let rafPending = false;\n let firstRender = true;\n\n // Granular fast path (KF-478): when the source is an `arraySignal` and the\n // list is NOT virtualized, apply its insert/remove/move/update patches\n // directly in O(patches) instead of diffing the whole snapshot. Virtualized\n // lists keep the keyed diff — their visible set is just the window (cheap),\n // and absolute-index patches don't compose with a shifting window. A plain\n // `signal<T[]>` has no patches, so it always uses the keyed diff.\n const patchSource = source as {\n [ARRAY_SIGNAL_BRAND]?: boolean;\n _consumePatches?: () => ArrayPatch<T>[];\n };\n const granularEligible = virtualize === undefined && patchSource[ARRAY_SIGNAL_BRAND] === true;\n\n // Virtualized lists put the windowing padding + rows on an INNER sizer, so the\n // padding never inflates the scroll container's clientHeight (padding counts\n // toward clientHeight). `parent` stays the clean scroll viewport; `container`\n // holds the rows. Non-virtualized lists render straight into `parent`.\n const container: HTMLElement = virtualize === undefined ? parent : document.createElement('div');\n if (virtualize !== undefined) parent.appendChild(container);\n\n const makeRow = (item: T): Row<T> => {\n const el = document.createElement(tag);\n // bindList knows the fixed row height, so it sizes rows itself — no\n // consumer CSS needed for the windowing math to line up.\n if (virtualize !== undefined) el.style.height = `${virtualize.rowHeight}px`;\n const dispose = mount(el, () => render(item));\n return { el, item, dispose };\n };\n\n // Reconcile the live rows to exactly `visible`, in order, keyed.\n const syncRows = (visible: readonly T[]): void => {\n const wanted = new Set<ListKey>();\n for (const item of visible) wanted.add(key(item));\n\n // Remove rows that are gone from the window.\n for (const [k, row] of rows) {\n if (!wanted.has(k)) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n }\n }\n\n // Create missing rows (and rebuild a row whose item OBJECT changed identity).\n order.length = 0;\n for (const item of visible) {\n const k = key(item);\n let row = rows.get(k);\n if (row !== undefined && row.item !== item) {\n row.dispose();\n row.el.remove();\n rows.delete(k);\n row = undefined;\n }\n if (row === undefined) {\n row = makeRow(item);\n rows.set(k, row);\n }\n order.push(row);\n }\n\n // Reverse pass: move only rows that are out of position.\n let ref: Node | null = null;\n for (let i = order.length - 1; i >= 0; i--) {\n const el = order[i].el;\n if (el.parentNode !== container || el.nextSibling !== ref) {\n container.insertBefore(el, ref);\n }\n ref = el;\n }\n };\n\n // Apply arraySignal structural patches directly to `order` + the DOM, in\n // O(patches). Indices are always valid by construction: `order` reflects the\n // last-rendered state and the patches are exactly the delta from it (bindList\n // drains the queue every render, and `replace` is filtered out by the caller,\n // which snapshots instead). The `splice()`s mirror `arraySignal`'s own\n // `_items` mutations exactly.\n const applyPatches = (patches: readonly ArrayPatch<T>[]): void => {\n for (const patch of patches) {\n if (patch.type === 'insert') {\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order.splice(patch.index, 0, row);\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);\n } else if (patch.type === 'remove') {\n const [row] = order.splice(patch.index, 1);\n row.dispose();\n row.el.remove();\n rows.delete(key(row.item));\n } else if (patch.type === 'move') {\n const [row] = order.splice(patch.from, 1);\n order.splice(patch.to, 0, row);\n container.insertBefore(row.el, order[patch.to + 1]?.el ?? null);\n } else if (patch.type === 'update') {\n // Decision #3: an item whose OBJECT identity changed rebuilds the row\n // (matching the keyed-diff rule). A same-ref update needs nothing here —\n // the row's own mount reacts to whatever signals its render reads.\n const current = order[patch.index];\n if (current.item !== patch.item) {\n current.dispose();\n current.el.remove();\n rows.delete(key(current.item));\n const row = makeRow(patch.item);\n rows.set(key(patch.item), row);\n order[patch.index] = row;\n container.insertBefore(row.el, order[patch.index + 1]?.el ?? null);\n }\n }\n // 'replace' never reaches here — the caller snapshots on it.\n }\n };\n\n const renderWindow = (): void => {\n if (virtualize === undefined) {\n if (granularEligible) {\n // Always drain to keep the single patch queue clean (so patches never\n // double-apply). Take the granular path past the first render, when\n // there are patches, and none is a `replace` (which reshapes the whole\n // array — snapshot instead). Otherwise fall through to a keyed diff.\n const patches = patchSource._consumePatches!();\n if (\n !firstRender\n && patches.length > 0\n && !patches.some((p) => p.type === 'replace')\n ) {\n applyPatches(patches);\n return;\n }\n }\n syncRows(items);\n firstRender = false;\n return;\n }\n const { rowHeight } = virtualize;\n const total = items.length;\n const start = Math.max(0, Math.floor(parent.scrollTop / rowHeight) - overscan);\n const end = Math.min(total, Math.ceil((parent.scrollTop + parent.clientHeight) / rowHeight) + overscan);\n syncRows(items.slice(start, end));\n container.style.paddingTop = `${start * rowHeight}px`;\n container.style.paddingBottom = `${Math.max(0, total - end) * rowHeight}px`;\n };\n\n const stopEffect = effect(() => {\n items = source.value; // tracking read — re-runs on any structural change\n renderWindow();\n });\n\n const onScroll = (): void => {\n if (rafPending) return;\n rafPending = true;\n globalThis.requestAnimationFrame(() => {\n rafPending = false;\n if (!disposed) renderWindow();\n });\n };\n if (virtualize !== undefined) parent.addEventListener('scroll', onScroll);\n\n return () => {\n disposed = true;\n stopEffect();\n for (const row of rows.values()) {\n row.dispose();\n if (virtualize === undefined) row.el.remove();\n }\n rows.clear();\n if (virtualize !== undefined) {\n parent.removeEventListener('scroll', onScroll);\n container.remove(); // removes the inner sizer and its rows in one go\n }\n };\n}\n"]}
import { SafeHtml } from './jsx-runtime.js';
/**
* `mount(rootEl, render)` — kerf's render primitive.
*
* Wraps `effect()` so that whenever any signal read inside `render()`
* changes, we re-run `render()` and apply the minimum DOM mutations against
* the live tree. Element identity (and thus focus, selection, in-flight
* pointer interactions, and event listeners on preserved nodes) is preserved
* wherever the keyed/positional diff matches.
*
* Two phases per render:
*
* - Static surrounds (everything outside `each()` lists): kerf's native
* `morph()` reconciler walks a freshly-built template against the live
* tree. Conventions: id/data-key matching, `data-morph-skip`, focus
* preservation.
*
* - List interiors (children of every `each()` parent): native keyed
* reconciler operates directly on the live parent's children. No
* re-parse, no morph walk for cache-hit rows. Cost is O(changes), not
* O(rows).
*
* Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,
* the user-visible win is that an `<input>` the user is typing into
* survives an unrelated re-render — its DOM node, focus state, and cursor
* position are not destroyed and recreated on each tick.
*/
/** What `mount()`'s render function may return; non-SafeHtml values coerce (nullish/boolean → render nothing). */
type MountResult = SafeHtml | string | number | boolean | null | undefined;
/**
* Bind `render()` to the children of `rootEl`. Re-runs whenever any signal
* read inside `render()` changes. Returns a disposer that tears down the
* effect; call it when the host element is removed from the DOM.
*
* Conventions:
*
* - Diff keys: `id` and `data-key` are matched across the morph by key
* rather than positionally, so list reorders move existing nodes instead
* of churning unrelated siblings.
* - `data-morph-skip`: any element with this attribute is left untouched
* inside on subsequent renders. Used for library-owned subtrees (xterm-
* style widgets, charts, third-party editors) where the library's own
* lifecycle manages the children.
* - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)
* keep their current value + selection range across morphs while focused.
* The user never sees their cursor jump mid-keystroke.
* - Focused `[contenteditable]` elements have their entire subtree
* skipped (same mechanism as `data-morph-skip`). The user's in-progress
* edit — typed content, caret position, multi-range selections, anything
* else they did to the DOM — survives verbatim. The next render after
* blur catches up.
*/
declare function mount(rootEl: HTMLElement, render: () => MountResult): () => void;
export { type MountResult as M, mount as m };
import { SafeHtml } from './jsx-runtime.js';
import { M as MountResult } from './mount-Bo2qOx25.js';
import '@preact/signals-core';
import './bindings-CYwoJpQb.js';
/** 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[];
}
/** 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 confirm}. */
interface ConfirmOptions {
/** Where to append the overlay. Default `document.body`. */
container?: Element;
/** Wrapper class. Default `'kerf-overlay'`. */
className?: string;
/** Optional heading above the message. */
title?: string;
/** Confirm button label. Default `'OK'`. */
okText?: string;
/** Cancel button label. Default `'Cancel'`. */
cancelText?: string;
/** Add a `kerf-confirm--danger` class to the wrapper for destructive actions. */
danger?: boolean;
}
/**
* A promise-based `window.confirm` replacement (that global is a no-op in Tauri
* webviews). Renders a two-button dialog and resolves `true` for OK, `false`
* for Cancel or any dismissal (Escape / backdrop). Message + labels are
* auto-escaped (rendered through the JSX runtime).
*/
declare function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
type ToastContent = string | SafeHtml | (() => MountResult);
/** 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;
}
/**
* Show a non-modal, auto-dismissing notification. Stacks in a shared body-level
* region (or your `container`). Returns a `() => void` that dismisses it early.
*/
declare function toast(content: ToastContent, options?: ToastOptions): () => void;
export { type ConfirmOptions, type DismissTrigger, type OverlayContent, type OverlayHandle, type OverlayOptions, type ToastContent, type ToastOptions, confirm, overlay, toast };
import { delegate } from './chunk-KEZTD6H4.js';
import { mount } from './chunk-4MY2656S.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import { jsx } from './chunk-FSAQR6IU.js';
import './chunk-3APBEVHF.js';
import './chunk-GY4XV2UV.js';
import './chunk-VVDJLWMP.js';
// 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 overlay(content, options = {}) {
const {
container = document.body,
className = "kerf-overlay",
dismiss = ["escape", "backdrop"],
initialFocus = true,
trap = true,
role = "dialog",
onDismiss,
outsideIgnore
} = options;
const triggers = dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];
const restoreTo = document.activeElement;
const wrapper = document.createElement("div");
wrapper.className = className;
if (trap) {
wrapper.setAttribute("role", role);
wrapper.setAttribute("aria-modal", "true");
}
container.appendChild(wrapper);
const disposeMount = mount(wrapper, typeof content === "function" ? content : () => content);
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();
wrapper.remove();
if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();
resultBox.resolve?.(value);
}
function userDismiss() {
onDismiss?.();
close();
}
const wantEscape = triggers.includes("escape");
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 confirm(message, options = {}) {
const {
container,
className = "kerf-overlay",
title,
okText = "OK",
cancelText = "Cancel",
danger = false
} = options;
const body = jsx("div", {
class: "kerf-confirm",
children: [
title !== void 0 ? jsx("h2", { class: "kerf-confirm__title", children: title }) : "",
jsx("p", { class: "kerf-confirm__message", children: message }),
jsx("div", {
class: "kerf-confirm__actions",
children: [
jsx("button", { type: "button", "data-confirm": "cancel", children: cancelText }),
jsx("button", {
type: "button",
"data-confirm": "ok",
class: "kerf-confirm__ok",
children: okText
})
]
})
]
});
const handle = overlay(body, {
container,
className: danger ? `${className} kerf-confirm--danger` : className,
dismiss: ["escape", "backdrop"],
initialFocus: ".kerf-confirm__ok",
trap: true
});
delegate(handle.el, "click", "[data-confirm]", (_event, el) => {
handle.close(el.getAttribute("data-confirm") === "ok");
});
return handle.result.then((value) => value === true);
}
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" } = options;
const el = document.createElement("div");
el.className = className;
el.setAttribute("role", role);
toastRegion(container).appendChild(el);
const disposeMount = mount(el, typeof content === "function" ? content : () => content);
const state = {
dismissed: false,
timer: void 0
};
function dismiss() {
if (state.dismissed) return;
state.dismissed = true;
if (state.timer !== void 0) clearTimeout(state.timer);
disposeMount();
el.remove();
}
if (duration > 0) state.timer = setTimeout(dismiss, duration);
return dismiss;
}
export { confirm, overlay, toast };
//# sourceMappingURL=overlay.js.map
//# sourceMappingURL=overlay.js.map
{"version":3,"sources":["../src/overlay.ts"],"names":[],"mappings":"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","file":"overlay.js","sourcesContent":["/**\n * `kerfjs/overlay` — the modal / overlay + dismiss manager.\n *\n * Every real kerf app hand-rolls this: `toElement → body.appendChild → mount →\n * wire dismissal → remove`, plus the fiddly parts (Escape, backdrop / outside\n * click, focus trap, restoring focus on close). `window.confirm` is a no-op in\n * Tauri WKWebViews, so a hand-built overlay is mandatory there. This subpath\n * blesses the pattern as three functions over `mount()` — `overlay()`, and the\n * `confirm()` / `toast()` conveniences built on it. No per-instance framework\n * state: each call owns its DOM + listeners in a closure and returns a handle.\n *\n * import { overlay, confirm, toast } from 'kerfjs/overlay';\n *\n * const ok = await confirm('Delete this file?', { danger: true });\n * toast('Saved');\n * const dialog = overlay(<Settings />, { dismiss: ['escape', 'backdrop'] });\n * // …later: dialog.close(); or await dialog.result;\n *\n * Structural only — kerf ships no CSS. The wrapper gets your `className`; style\n * the backdrop / centering / animation yourself.\n */\nimport { delegate } from './delegate.js';\nimport { jsx, type SafeHtml } from './jsx-runtime.js';\nimport { mount, type MountResult } from './mount.js';\n\n/** A user-initiated dismissal trigger. */\nexport type DismissTrigger = 'escape' | 'backdrop' | 'outside';\n\n/** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */\nexport type OverlayContent = SafeHtml | (() => MountResult);\n\n/** Options for {@link overlay}. */\nexport interface OverlayOptions {\n /** Where to append the overlay wrapper. Default `document.body`. */\n container?: Element;\n /** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */\n className?: string;\n /**\n * Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.\n * `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`\n * = a click anywhere outside the wrapper (for anchored popovers). `false`\n * disables user dismissal (close it programmatically).\n */\n dismiss?: DismissTrigger | DismissTrigger[] | false;\n /**\n * Where focus lands on open: a selector, `true` (first focusable element, or\n * the wrapper if none), or `false` (leave focus alone). Default `true`.\n */\n initialFocus?: string | boolean;\n /**\n * Trap Tab / Shift+Tab within the overlay while open and mark it\n * `role=\"dialog\"` / `aria-modal=\"true\"`. Default `true`. Set `false` for a\n * non-modal popover.\n */\n trap?: boolean;\n /** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */\n role?: string;\n /** Called on any user-initiated dismissal (before `close()` runs). */\n onDismiss?: () => void;\n /** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */\n outsideIgnore?: Element | readonly Element[];\n}\n\n/** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */\nexport interface OverlayHandle {\n /** The wrapper element (mounted into, appended to `container`). */\n el: HTMLElement;\n /** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */\n close(result?: unknown): void;\n /** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */\n result: Promise<unknown>;\n}\n\nconst FOCUSABLE =\n 'a[href],area[href],button:not([disabled]),input:not([disabled]),'\n + 'select:not([disabled]),textarea:not([disabled]),iframe,'\n + '[tabindex]:not([tabindex=\"-1\"]),[contenteditable=\"true\"]';\n\nfunction focusable(root: Element): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hasAttribute('hidden'),\n );\n}\n\n/**\n * Open an overlay: append a wrapper to `container`, `mount()` `content` inside\n * it, wire the requested dismissals + (optionally) a focus trap, and return a\n * handle. See {@link OverlayOptions}.\n */\nexport function overlay(content: OverlayContent, options: OverlayOptions = {}): OverlayHandle {\n const {\n container = document.body,\n className = 'kerf-overlay',\n dismiss = ['escape', 'backdrop'],\n initialFocus = true,\n trap = true,\n role = 'dialog',\n onDismiss,\n outsideIgnore,\n } = options;\n\n const triggers: readonly DismissTrigger[] =\n dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];\n const restoreTo = document.activeElement;\n\n const wrapper = document.createElement('div');\n wrapper.className = className;\n if (trap) {\n wrapper.setAttribute('role', role);\n wrapper.setAttribute('aria-modal', 'true');\n }\n container.appendChild(wrapper);\n\n const disposeMount = mount(wrapper, typeof content === 'function' ? content : () => content);\n\n const removers: Array<() => void> = [];\n const resultBox: { resolve?: (value: unknown) => void } = {};\n const result = new Promise<unknown>((resolve) => {\n resultBox.resolve = resolve;\n });\n const state = { closed: false };\n\n function close(value?: unknown): void {\n if (state.closed) return;\n state.closed = true;\n for (const remove of removers) remove();\n disposeMount();\n wrapper.remove();\n if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();\n resultBox.resolve?.(value);\n }\n\n function userDismiss(): void {\n onDismiss?.();\n close();\n }\n\n const wantEscape = triggers.includes('escape');\n if (wantEscape || trap) {\n const onKeydown = (event: KeyboardEvent): void => {\n if (wantEscape && event.key === 'Escape') {\n event.stopPropagation();\n userDismiss();\n return;\n }\n if (trap && event.key === 'Tab') {\n const items = focusable(wrapper);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n const active = document.activeElement;\n const outside = !wrapper.contains(active);\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n };\n document.addEventListener('keydown', onKeydown, true);\n removers.push(() => document.removeEventListener('keydown', onKeydown, true));\n }\n\n if (triggers.includes('backdrop')) {\n const onClick = (event: Event): void => {\n if (event.target === wrapper) userDismiss();\n };\n wrapper.addEventListener('click', onClick);\n removers.push(() => wrapper.removeEventListener('click', onClick));\n }\n\n if (triggers.includes('outside')) {\n const ignore = outsideIgnore === undefined\n ? []\n : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];\n // Capture phase: the click that opened this overlay already passed\n // document's capture phase, so this never fires for that opening click.\n const onDocClick = (event: Event): void => {\n const target = event.target as Node | null;\n if (target === null) return;\n if (wrapper.contains(target)) return;\n if (ignore.some((el) => el === target || el.contains(target))) return;\n userDismiss();\n };\n document.addEventListener('click', onDocClick, true);\n removers.push(() => document.removeEventListener('click', onDocClick, true));\n }\n\n if (initialFocus !== false) {\n if (typeof initialFocus === 'string') {\n wrapper.querySelector<HTMLElement>(initialFocus)?.focus();\n } else {\n const first = focusable(wrapper)[0];\n if (first !== undefined) {\n first.focus();\n } else {\n wrapper.tabIndex = -1;\n wrapper.focus();\n }\n }\n }\n\n return { el: wrapper, close, result };\n}\n\n/** Options for {@link confirm}. */\nexport interface ConfirmOptions {\n /** Where to append the overlay. Default `document.body`. */\n container?: Element;\n /** Wrapper class. Default `'kerf-overlay'`. */\n className?: string;\n /** Optional heading above the message. */\n title?: string;\n /** Confirm button label. Default `'OK'`. */\n okText?: string;\n /** Cancel button label. Default `'Cancel'`. */\n cancelText?: string;\n /** Add a `kerf-confirm--danger` class to the wrapper for destructive actions. */\n danger?: boolean;\n}\n\n/**\n * A promise-based `window.confirm` replacement (that global is a no-op in Tauri\n * webviews). Renders a two-button dialog and resolves `true` for OK, `false`\n * for Cancel or any dismissal (Escape / backdrop). Message + labels are\n * auto-escaped (rendered through the JSX runtime).\n */\nexport function confirm(message: string, options: ConfirmOptions = {}): Promise<boolean> {\n const {\n container,\n className = 'kerf-overlay',\n title,\n okText = 'OK',\n cancelText = 'Cancel',\n danger = false,\n } = options;\n\n const body: SafeHtml = jsx('div', {\n class: 'kerf-confirm',\n children: [\n title !== undefined ? jsx('h2', { class: 'kerf-confirm__title', children: title }) : '',\n jsx('p', { class: 'kerf-confirm__message', children: message }),\n jsx('div', {\n class: 'kerf-confirm__actions',\n children: [\n jsx('button', { type: 'button', 'data-confirm': 'cancel', children: cancelText }),\n jsx('button', {\n type: 'button',\n 'data-confirm': 'ok',\n class: 'kerf-confirm__ok',\n children: okText,\n }),\n ],\n }),\n ],\n });\n\n const handle = overlay(body, {\n container,\n className: danger ? `${className} kerf-confirm--danger` : className,\n dismiss: ['escape', 'backdrop'],\n initialFocus: '.kerf-confirm__ok',\n trap: true,\n });\n\n delegate(handle.el, 'click', '[data-confirm]', (_event, el) => {\n handle.close(el.getAttribute('data-confirm') === 'ok');\n });\n\n return handle.result.then((value) => value === true);\n}\n\n/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */\nexport type ToastContent = string | SafeHtml | (() => MountResult);\n\n/** Options for {@link toast}. */\nexport interface ToastOptions {\n /** Where toasts stack. Default: a lazily-created `<div class=\"kerf-toasts\">` on `document.body`. */\n container?: Element;\n /** Class on the toast element. Default `'kerf-toast'`. */\n className?: string;\n /** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */\n duration?: number;\n /** ARIA role. Default `'status'`. */\n role?: string;\n}\n\n/** The singleton toast region lives in the DOM (queried, not held in a module variable). */\nfunction toastRegion(container?: Element): Element {\n if (container !== undefined) return container;\n const existing = document.querySelector('.kerf-toasts');\n if (existing !== null) return existing;\n const region = document.createElement('div');\n region.className = 'kerf-toasts';\n region.setAttribute('aria-live', 'polite');\n document.body.appendChild(region);\n return region;\n}\n\n/**\n * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level\n * region (or your `container`). Returns a `() => void` that dismisses it early.\n */\nexport function toast(content: ToastContent, options: ToastOptions = {}): () => void {\n const { container, className = 'kerf-toast', duration = 4000, role = 'status' } = options;\n\n const el = document.createElement('div');\n el.className = className;\n el.setAttribute('role', role);\n toastRegion(container).appendChild(el);\n\n const disposeMount = mount(el, typeof content === 'function' ? content : () => content);\n const state: { dismissed: boolean; timer: ReturnType<typeof setTimeout> | undefined } = {\n dismissed: false,\n timer: undefined,\n };\n\n function dismiss(): void {\n if (state.dismissed) return;\n state.dismissed = true;\n if (state.timer !== undefined) clearTimeout(state.timer);\n disposeMount();\n el.remove();\n }\n\n if (duration > 0) state.timer = setTimeout(dismiss, duration);\n return dismiss;\n}\n"]}
import { D as DelegateOptions } from './delegate-CL9VTZFb.js';
import { M as MountResult } from './mount-Bo2qOx25.js';
import './jsx-runtime.js';
import '@preact/signals-core';
import './bindings-CYwoJpQb.js';
/**
* `kerfjs/scope` — tie a set of disposers to a DOM element's lifetime.
*
* kerf hands out disposers (`mount()` / `effect()` / `delegate()` all return
* `() => void`), but nothing scopes them to a subtree's lifetime — so an
* append-heavy app (a feed, a list of cards) leaks detached-but-subscribed
* effects, listeners, and observers. Every such app hand-rolls the same
* `WeakMap<Element, disposers[]>` swept on removal. This subpath blesses it.
*
* import { disposeScope, disposeSubtree, observeRemovals } from 'kerfjs/scope';
*
* const s = disposeScope(card);
* s.mount(card, renderCard); // mounts AND registers its disposer
* s.effect(() => syncCard(card));
* s.delegate(card, 'click', '.del', del);
* s.add(() => observer.disconnect()); // any () => void disposer
* // …when the card goes away:
* disposeSubtree(feed); // runs every scope in feed (incl. feed)
* feed.remove();
*
* Or install one observer and let removals auto-dispose:
* observeRemovals(document.body);
*
* No module-level mutable state: scopes live in a `WeakMap` (GC-tied, keyed by
* element), and `disposeSubtree` finds them by walking the subtree.
*/
/** A per-element teardown scope. Calling `disposeScope(el)` again returns the SAME scope. */
interface Scope {
/** Register any `() => void` disposer (a `mount`/`effect`/`delegate` return, a listener remover, …). Returns it. */
add(dispose: () => void): () => void;
/** `mount()` into `el` and register its disposer in one step. Returns the disposer. */
mount(el: HTMLElement, render: () => MountResult): () => void;
/** `effect(fn)` and register its disposer in one step. Returns the disposer. */
effect(fn: () => void | (() => void)): () => void;
/** `delegate(...)` and register its disposer in one step. Returns the disposer. */
delegate<T extends Element = Element>(root: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
/** Run every registered disposer (best-effort — a throwing one won't strand the rest) and reset. Idempotent. */
dispose(): void;
}
/**
* Get (or create) the teardown {@link Scope} for `el`. Repeated calls for the
* same element return the same scope, so disparate code paths can register into
* one place. After `dispose()`, a later `disposeScope(el)` starts fresh.
*/
declare function disposeScope(el: Element): Scope;
/**
* Dispose every scope within `root` (including `root`'s own), then leave the DOM
* to you. Call it right before removing a subtree. Finds scopes by walking the
* subtree against the `WeakMap` — no marker attributes are added to your DOM.
*/
declare function disposeSubtree(root: Element): void;
/**
* Install a `MutationObserver` on `root` that auto-disposes a node's scope when
* that node (or an ancestor) is removed from the subtree. One observer covers
* the whole tree. Returns a disconnect function. Note: `MutationObserver` fires
* asynchronously, so disposal runs a microtask after the removal.
*/
declare function observeRemovals(root: Element): () => void;
export { type Scope, disposeScope, disposeSubtree, observeRemovals };
import { delegate } from './chunk-KEZTD6H4.js';
import { mount } from './chunk-4MY2656S.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
import './chunk-FSAQR6IU.js';
import { effect } from './chunk-3APBEVHF.js';
import './chunk-GY4XV2UV.js';
import './chunk-VVDJLWMP.js';
// src/scope.ts
var scopes = /* @__PURE__ */ new WeakMap();
function disposeScope(el) {
const existing = scopes.get(el);
if (existing !== void 0) return existing.scope;
const disposers = [];
const scope = {
add(dispose) {
disposers.push(dispose);
return dispose;
},
mount(target, render) {
const dispose = mount(target, render);
disposers.push(dispose);
return dispose;
},
effect(fn) {
const dispose = effect(fn);
disposers.push(dispose);
return dispose;
},
delegate(root, type, selector, handler, options) {
const dispose = delegate(root, type, selector, handler, options);
disposers.push(dispose);
return dispose;
},
dispose() {
scopes.delete(el);
for (const d of disposers.splice(0)) {
try {
d();
} catch {
}
}
}
};
scopes.set(el, { scope, disposers });
return scope;
}
function disposeSubtree(root) {
const own = scopes.get(root);
if (own !== void 0) own.scope.dispose();
for (const el of root.querySelectorAll("*")) {
const state = scopes.get(el);
if (state !== void 0) state.scope.dispose();
}
}
function observeRemovals(root) {
const observer = new MutationObserver((records) => {
for (const record of records) {
for (const node of record.removedNodes) {
if (node instanceof Element) disposeSubtree(node);
}
}
});
observer.observe(root, { childList: true, subtree: true });
return () => observer.disconnect();
}
export { disposeScope, disposeSubtree, observeRemovals };
//# sourceMappingURL=scope.js.map
//# sourceMappingURL=scope.js.map
{"version":3,"sources":["../src/scope.ts"],"names":[],"mappings":";;;;;;;;;;AAyDA,IAAM,MAAA,uBAAa,OAAA,EAA6B;AAOzC,SAAS,aAAa,EAAA,EAAoB;AAC/C,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAC9B,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAO,QAAA,CAAS,KAAA;AAE5C,EAAA,MAAM,YAA+B,EAAC;AACtC,EAAA,MAAM,KAAA,GAAe;AAAA,IACnB,IAAI,OAAA,EAAS;AACX,MAAA,SAAA,CAAU,KAAK,OAAO,CAAA;AACtB,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,KAAA,CAAM,QAAQ,MAAA,EAAQ;AACpB,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,MAAA,EAAQ,MAAM,CAAA;AACpC,MAAA,SAAA,CAAU,KAAK,OAAO,CAAA;AACtB,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,OAAO,EAAA,EAAI;AACT,MAAA,MAAM,OAAA,GAAU,OAAO,EAAE,CAAA;AACzB,MAAA,SAAA,CAAU,KAAK,OAAO,CAAA;AACtB,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,QAAA,CAAS,IAAA,EAAM,IAAA,EAAM,QAAA,EAAU,SAAS,OAAA,EAAS;AAC/C,MAAA,MAAM,UAAU,QAAA,CAAS,IAAA,EAAM,IAAA,EAAM,QAAA,EAAU,SAAS,OAAO,CAAA;AAC/D,MAAA,SAAA,CAAU,KAAK,OAAO,CAAA;AACtB,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,OAAA,GAAU;AACR,MAAA,MAAA,CAAO,OAAO,EAAE,CAAA;AAEhB,MAAA,KAAA,MAAW,CAAA,IAAK,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,EAAG;AACnC,QAAA,IAAI;AACF,UAAA,CAAA,EAAE;AAAA,QACJ,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,GACF;AACA,EAAA,MAAA,CAAO,GAAA,CAAI,EAAA,EAAI,EAAE,KAAA,EAAO,WAAW,CAAA;AACnC,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,eAAe,IAAA,EAAqB;AAGlD,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAC3B,EAAA,IAAI,GAAA,KAAQ,MAAA,EAAW,GAAA,CAAI,KAAA,CAAM,OAAA,EAAQ;AACzC,EAAA,KAAA,MAAW,EAAA,IAAM,IAAA,CAAK,gBAAA,CAAiB,GAAG,CAAA,EAAG;AAC3C,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAC3B,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,KAAA,CAAM,KAAA,CAAM,OAAA,EAAQ;AAAA,EAC/C;AACF;AAQO,SAAS,gBAAgB,IAAA,EAA2B;AACzD,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACjD,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,KAAA,MAAW,IAAA,IAAQ,OAAO,YAAA,EAAc;AACtC,QAAA,IAAI,IAAA,YAAgB,OAAA,EAAS,cAAA,CAAe,IAAI,CAAA;AAAA,MAClD;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,QAAQ,IAAA,EAAM,EAAE,WAAW,IAAA,EAAM,OAAA,EAAS,MAAM,CAAA;AACzD,EAAA,OAAO,MAAM,SAAS,UAAA,EAAW;AACnC","file":"scope.js","sourcesContent":["/**\n * `kerfjs/scope` — tie a set of disposers to a DOM element's lifetime.\n *\n * kerf hands out disposers (`mount()` / `effect()` / `delegate()` all return\n * `() => void`), but nothing scopes them to a subtree's lifetime — so an\n * append-heavy app (a feed, a list of cards) leaks detached-but-subscribed\n * effects, listeners, and observers. Every such app hand-rolls the same\n * `WeakMap<Element, disposers[]>` swept on removal. This subpath blesses it.\n *\n * import { disposeScope, disposeSubtree, observeRemovals } from 'kerfjs/scope';\n *\n * const s = disposeScope(card);\n * s.mount(card, renderCard); // mounts AND registers its disposer\n * s.effect(() => syncCard(card));\n * s.delegate(card, 'click', '.del', del);\n * s.add(() => observer.disconnect()); // any () => void disposer\n * // …when the card goes away:\n * disposeSubtree(feed); // runs every scope in feed (incl. feed)\n * feed.remove();\n *\n * Or install one observer and let removals auto-dispose:\n * observeRemovals(document.body);\n *\n * No module-level mutable state: scopes live in a `WeakMap` (GC-tied, keyed by\n * element), and `disposeSubtree` finds them by walking the subtree.\n */\nimport { delegate, type DelegateOptions } from './delegate.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A per-element teardown scope. Calling `disposeScope(el)` again returns the SAME scope. */\nexport interface Scope {\n /** Register any `() => void` disposer (a `mount`/`effect`/`delegate` return, a listener remover, …). Returns it. */\n add(dispose: () => void): () => void;\n /** `mount()` into `el` and register its disposer in one step. Returns the disposer. */\n mount(el: HTMLElement, render: () => MountResult): () => void;\n /** `effect(fn)` and register its disposer in one step. Returns the disposer. */\n effect(fn: () => void | (() => void)): () => void;\n /** `delegate(...)` and register its disposer in one step. Returns the disposer. */\n delegate<T extends Element = Element>(\n root: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n ): () => void;\n /** Run every registered disposer (best-effort — a throwing one won't strand the rest) and reset. Idempotent. */\n dispose(): void;\n}\n\ninterface ScopeState {\n scope: Scope;\n disposers: Array<() => void>;\n}\n\n// GC-tied cache keyed by element — const + WeakMap, so it is exempt from the\n// \"no module-level mutable state\" rule (like bindings.ts:insertedTextNodes).\nconst scopes = new WeakMap<Element, ScopeState>();\n\n/**\n * Get (or create) the teardown {@link Scope} for `el`. Repeated calls for the\n * same element return the same scope, so disparate code paths can register into\n * one place. After `dispose()`, a later `disposeScope(el)` starts fresh.\n */\nexport function disposeScope(el: Element): Scope {\n const existing = scopes.get(el);\n if (existing !== undefined) return existing.scope;\n\n const disposers: Array<() => void> = [];\n const scope: Scope = {\n add(dispose) {\n disposers.push(dispose);\n return dispose;\n },\n mount(target, render) {\n const dispose = mount(target, render);\n disposers.push(dispose);\n return dispose;\n },\n effect(fn) {\n const dispose = effect(fn);\n disposers.push(dispose);\n return dispose;\n },\n delegate(root, type, selector, handler, options) {\n const dispose = delegate(root, type, selector, handler, options);\n disposers.push(dispose);\n return dispose;\n },\n dispose() {\n scopes.delete(el);\n // splice() empties the array AND makes a second dispose() a no-op.\n for (const d of disposers.splice(0)) {\n try {\n d();\n } catch {\n /* best-effort: a throwing disposer must not strand the rest */\n }\n }\n },\n };\n scopes.set(el, { scope, disposers });\n return scope;\n}\n\n/**\n * Dispose every scope within `root` (including `root`'s own), then leave the DOM\n * to you. Call it right before removing a subtree. Finds scopes by walking the\n * subtree against the `WeakMap` — no marker attributes are added to your DOM.\n */\nexport function disposeSubtree(root: Element): void {\n // Static NodeList snapshot — safe to dispose (which deletes WeakMap entries)\n // while iterating. Root first, then descendants in document order.\n const own = scopes.get(root);\n if (own !== undefined) own.scope.dispose();\n for (const el of root.querySelectorAll('*')) {\n const state = scopes.get(el);\n if (state !== undefined) state.scope.dispose();\n }\n}\n\n/**\n * Install a `MutationObserver` on `root` that auto-disposes a node's scope when\n * that node (or an ancestor) is removed from the subtree. One observer covers\n * the whole tree. Returns a disconnect function. Note: `MutationObserver` fires\n * asynchronously, so disposal runs a microtask after the removal.\n */\nexport function observeRemovals(root: Element): () => void {\n const observer = new MutationObserver((records) => {\n for (const record of records) {\n for (const node of record.removedNodes) {\n if (node instanceof Element) disposeSubtree(node);\n }\n }\n });\n observer.observe(root, { childList: true, subtree: true });\n return () => observer.disconnect();\n}\n"]}
+8
-0

@@ -9,2 +9,10 @@ # Changelog

- **`trustedRaw(html)`** (main barrel) — the intention-revealing, lint-exempt escape hatch for injecting a **server-trusted dynamic** value (a CSRF token, a trusted `<script src>`, a server-issued id). Identical to `raw()` at runtime, but because it isn't named `raw`, the `kerfjs/no-raw-with-dynamic-arg` rule leaves it alone — replacing scattered `eslint-disable` comments with one explicit call. Not a sanitizer; only pass values you control.
- **`renderDocument(node, options?)`** (main barrel) — a tiny SSR helper that prepends the doctype to a rendered document, so server routes stop reinventing `"<!DOCTYPE html>" + page.toString()`. Takes a `SafeHtml` or string; optional `{ doctype }` (default `'html'`). Pure string work, no DOM dependency.
- **New `kerfjs/list` subpath** — `bindList(parent, source, options)`, a keyed list distinct from `each()`: each row is individually `mount()`ed, so a signal a row reads updates just that row (fine-grained, no full-list pass), and it can **virtualize** the viewport (`virtualize: { rowHeight }` renders only visible rows, padding keeps `scrollHeight` honest). `source` is a `signal<readonly T[]>` or an `arraySignal<T>` — a non-virtualized `arraySignal` source applies its structural patches **granularly** (O(patches)), everything else uses a keyed diff (transparent optimization). Reach for it for surgical per-row updates or long/windowed lists; `each()` stays the default for item-owned-state lists rendered to HTML strings. Optional and tree-shakeable.
- **New `kerfjs/async` subpath** — `resource<T>()` models async state (`{ status, data, error, progress }`) with the stale-response guard built in. You write the fetch (Node `fetch` for SSR, browser `fetch` client-side); `.run(fetcher)` drives `idle` → `running` → `completed`/`failed` and drops out-of-order responses (only the latest run resolves the state). It never rejects — a failure lands in `value.error` — keeps previous data across a re-run (stale-while-revalidate), and supports opt-in progress via a callback the fetcher receives. `value` is a tracking read. Signals only (no render core); tiny.
- **New `kerfjs/scope` subpath** — tie disposers to a DOM element's lifetime, so append-heavy UIs stop leaking detached-but-subscribed effects/listeners. `disposeScope(el)` returns a WeakMap-keyed, accumulating scope whose `add(disposer)` (plus convenience `mount` / `effect` / `delegate` wrappers that register their own disposer) collects teardown; `dispose()` runs it all best-effort and idempotently. `disposeSubtree(root)` sweeps a subtree before removal; `observeRemovals(root)` installs one `MutationObserver` that auto-disposes on removal. No module-level mutable state. Optional and tree-shakeable.
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement (that global is a no-op in Tauri webviews); `toast(content, options?)` is an auto-dismissing notification. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
- **New `kerfjs/actions` subpath** — the blessed delegated action-table helper. `action(value)` returns a `data-action` `AttrSpec` (a thin specialization of `attr()`); `delegateActions(root, eventType, table, options?)` wires a whole table of `data-action` handlers with one delegated listener (built on `delegate()`) and returns a disposer. Formalizes the most-reinvented idiom in real kerf apps — one `attr('data-action', …)` table as the single source of truth for both the JSX attribute and the delegate dispatch. Optional and tree-shakeable; adds nothing to the main barrel.
## [4.1.1] - 2026-08-14

@@ -11,0 +19,0 @@

+3
-104

@@ -1,107 +0,6 @@

import { bumpItemVersion } from './chunk-QIP723L4.js';
import { signal } from './chunk-3APBEVHF.js';
export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-MRYM3O3V.js';
import './chunk-QIP723L4.js';
import './chunk-3APBEVHF.js';
import './chunk-VVDJLWMP.js';
// src/array-signal.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
var ArraySignal = class {
_items;
_version;
_patches;
// Branded so `isArraySignal()` recognizes instances from any copy of this module.
[ARRAY_SIGNAL_BRAND] = true;
constructor(initial = []) {
this._items = [...initial];
this._version = signal(0);
this._patches = [];
}
/** Read-only snapshot. Reads inside an effect/computed register a dependency. */
get value() {
void this._version.value;
return this._items;
}
/**
* Replace the item at `index` with `fn(currentItem)`. Emits one `update`
* patch. Both styles work: returning a fresh object (idiomatic) invalidates
* the row by identity, and mutating `item` in place and returning it works
* too — a per-item content version (KF-418) makes the same-ref change visible
* to every consumer's row memo.
*/
update(index, fn) {
if (index < 0 || index >= this._items.length) {
throw new Error(
`arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`
);
}
const next = fn(this._items[index]);
this._items[index] = next;
this._patches.push({ type: "update", index, item: next });
bumpItemVersion(next);
this._version.value++;
}
/** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */
insert(index, item) {
if (index < 0 || index > this._items.length) {
throw new Error(
`arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`
);
}
this._items.splice(index, 0, item);
this._patches.push({ type: "insert", index, item });
this._version.value++;
}
/** Append `item` at the end. Sugar for `insert(items.length, item)`. */
push(item) {
this.insert(this._items.length, item);
}
/** Remove and return the item at `index`. Emits one `remove` patch. */
remove(index) {
if (index < 0 || index >= this._items.length) {
throw new Error(
`arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`
);
}
const [removed] = this._items.splice(index, 1);
this._patches.push({ type: "remove", index });
this._version.value++;
return removed;
}
/** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */
move(from, to) {
if (from === to) return;
if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {
throw new Error(
`arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`
);
}
const [item] = this._items.splice(from, 1);
this._items.splice(to, 0, item);
this._patches.push({ type: "move", from, to });
this._version.value++;
}
/** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */
replace(items) {
this._items = [...items];
this._patches.push({ type: "replace", items: this._items });
this._version.value++;
}
/**
* @internal Used by `each()` when binding this signal to a list. Returns
* the queue of granular patches issued since the previous call, then
* clears the queue. Best paired with a single binding — a second consumer
* in the same render gets an empty array (which forces the snapshot
* fall-back path, which is correct but slower).
*/
_consumePatches() {
const out = this._patches;
this._patches = [];
return out;
}
};
function arraySignal(initial = []) {
return new ArraySignal(initial);
}
export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal };
//# sourceMappingURL=array-signal.js.map
//# sourceMappingURL=array-signal.js.map

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"array-signal.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport { bumpItemVersion } from './item-version.js';\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognizes instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /**\n * Replace the item at `index` with `fn(currentItem)`. Emits one `update`\n * patch. Both styles work: returning a fresh object (idiomatic) invalidates\n * the row by identity, and mutating `item` in place and returning it works\n * too — a per-item content version (KF-418) makes the same-ref change visible\n * to every consumer's row memo.\n */\n update(index: number, fn: (item: T) => T): void {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n // KF-418: a same-ref update (fn mutates and returns the same object) is\n // invisible to the row memo, which is keyed on object identity. Bump the\n // item's content version so every consumer — this list, another list over\n // this signal, a second mount, a plain-array filter() view — re-renders it.\n // Non-object items (an arraySignal<number> used as a plain signal) are\n // skipped by bumpItemVersion — they can't be each() rows (KF-419).\n bumpItemVersion(next);\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (index < 0 || index > this._items.length) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (from === to) return;\n if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
{"version":3,"sources":[],"names":[],"mappings":"","file":"array-signal.js"}

@@ -1,4 +0,4 @@

import { _toSegment, assertEmittableAttrName, bindAttr, _renderAttrVerbatim, SafeHtml, bindMarkerAttr } from './chunk-JXAR5J54.js';
import { _toSegment, assertEmittableAttrName, bindAttr, _renderAttrVerbatim, SafeHtml, bindMarkerAttr } from './chunk-FSAQR6IU.js';
import { isSignal } from './chunk-3APBEVHF.js';
import { mergeChildSegments } from './chunk-GY4XV2UV.js';
import { isSignal } from './chunk-3APBEVHF.js';
import './chunk-VVDJLWMP.js';

@@ -5,0 +5,0 @@

@@ -0,4 +1,7 @@

export { A as AttrSpec, a as attr } from './attrSelector-Cmu2ZoGO.js';
export { D as DelegateOptions, d as delegate, a as delegateCapture } from './delegate-CL9VTZFb.js';
import { ArraySignal } from './array-signal.js';
import { SafeHtml } from './jsx-runtime.js';
export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
export { Fragment, isSafeHtml, raw, trustedRaw } from './jsx-runtime.js';
export { M as MountResult, m as mount } from './mount-Bo2qOx25.js';
import { Signal } from '@preact/signals-core';

@@ -37,172 +40,2 @@ export { ReadonlySignal, Signal, batch, computed } from '@preact/signals-core';

/**
* `attr(name, value)` — create a pre-computed attribute descriptor (static form).
* `attr(name)` — create a per-render factory for dynamic attribute values (dynamic form).
*
* **Static form** — best for fixed action names, filter keys, role values, etc.
* Escapes once at module-load time; produces a full {@link AttrSpec} with
* `.name`, `.value`, `.selector`, and `.attrs`.
*
* const ACTIONS = {
* toggle: attr('data-action', 'toggle'),
* remove: attr('data-action', 'remove'),
* } as const satisfies Record<string, AttrSpec<'data-action'>>;
*
* // In JSX — spread .attrs (rename-safe; no hardcoded attribute name):
* <button {...ACTIONS.toggle.attrs}>Toggle</button>
*
* // In delegate — use the pre-computed selector:
* delegate(root, 'click', ACTIONS.toggle.selector, handler);
*
* **Dynamic form** — best for per-row data like `data-id`, where the value
* changes per item but the attribute name is constant.
* The name is validated and pre-escaped at definition time; calling the
* returned factory is cheap (it just freezes a one-key object — the value is
* escaped later by the JSX attribute renderer when the result is spread).
*
* const ITEM = { id: attr('data-id') } as const;
*
* // In JSX — call the factory inline:
* <li {...ITEM.id(String(item.id))}>…</li>
*
* For ad-hoc compound selectors, concatenate `.selector` strings:
*
* delegate(root, 'click',
* ACTIONS.toggle.selector + attr('data-id', id).selector,
* handler);
*
* Escaping:
* - Attribute name: escaped as a CSS identifier via `cssEscapeIdent`, which is
* an SSR-safe (no `CSS.escape`) adaptation of the Mathias Bynens polyfill
* (https://github.com/mathiasbynens/CSS.escape, MIT licensed — see the
* Acknowledgements section of LICENSE). Handles
* control chars, leading digits, non-ASCII, and CSS metacharacters.
* - Attribute value: embedded in double quotes as a CSS string. Backslashes and
* double-quote characters are backslash-escaped; control characters are
* hex-escaped per CSS Syntax Level 3 §3.4.
*
* Throws on an empty attribute name (not a valid CSS identifier).
*/
/** Descriptor created by the static {@link attr} overload. */
interface AttrSpec<N extends string = string, V extends string = string> {
/** The raw attribute name passed to `attr()`. */
readonly name: N;
/** The raw attribute value passed to `attr()`. */
readonly value: V;
/** Pre-computed `[name="value"]` CSS selector string, safe to pass to `delegate()`. */
readonly selector: string;
/** Spreadable JSX object — `{ [name]: value }` — keeps the attribute name out of JSX literals. */
readonly attrs: {
readonly [K in N]: V;
};
}
/**
* Static overload — pre-computes the full descriptor at definition time.
* Returns an {@link AttrSpec} with `.name`, `.value`, `.selector`, and `.attrs`.
*/
declare function attr<N extends string, V extends string>(name: N, value: V): AttrSpec<N, V>;
/**
* Dynamic overload — pre-validates and pre-escapes the attribute name, returns a
* factory that accepts a per-render value and produces a frozen spreadable object.
* Use for per-row attributes like `data-id` where the value changes per item.
* The optional `V` generic constrains which values the factory accepts:
* `attr<'data-id', 'a'|'b'>('data-id')` → `(value: 'a'|'b') => { 'data-id': 'a'|'b' }`.
* Leaving both generics off infers `N` from the argument and defaults `V` to `string`.
*/
declare function attr<N extends string, V extends string = string>(name: N): (value: V) => {
readonly [K in N]: V;
};
/**
* Tiny event-delegation helpers. Replace per-element `addEventListener` calls
* (which don't survive morph re-renders for nodes the diff creates) with one
* listener at the morph-root that dispatches via `closest()`.
*
* Three-tier listener model:
*
* - Tier 1 (bubbling events) — use `delegate()`.
* click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
* drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
*
* `delegate()` also auto-promotes the well-known non-bubbling event
* types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,
* `mouseleave`) to the capture phase under the hood, so the call site
* looks identical for "interactive thing happens on a descendant"
* regardless of whether that event bubbles. Selector matching stays
* `closest()`-style — the same as for bubbling events — so a wrapper
* selector like `'.field-row'` still matches when the event fires on
* a descendant `<input>`.
*
* - Tier 2 (explicit capture) — use `delegateCapture()`.
* The escape hatch for cases the auto-promotion list doesn't cover
* (custom non-bubbling events) or when you want capture-phase
* interception. Selector matching is `closest()`-style by default —
* the same walk-up as `delegate()`, and it passes the matched ancestor
* (not the raw target) to the handler — so a click on any descendant of
* the selected element climbs to it. Pass `{ match: 'direct' }` to opt
* into strict `matches()`-style matching (fire only when the event lands
* on the exact element the selector identifies).
*
* - Tier 3 (per-element instances / library-owned subtrees) — mark the
* host element with `data-morph-skip` and manage the library's
* lifecycle directly. No delegation helper applies.
*/
/**
* How the selector is matched against the event's target:
*
* - `'closest'` (the default for both helpers) — walk UP from `event.target`
* via `closest(selector)`, firing for the nearest matching ancestor inside
* `rootEl`. This is the delegation behavior you almost always want: a click
* on an icon inside a button fires the button's handler.
* - `'direct'` — strict `matches()` match: fire only when `event.target`
* itself matches the selector, with no walk-up.
*/
interface DelegateOptions {
match?: 'closest' | 'direct';
}
/**
* Delegation that "just works" for both bubbling and the common non-bubbling
* events. Installs ONE listener on `rootEl`; for known non-bubblers (see
* `NON_BUBBLING` above) the listener is registered on the capture phase so
* it actually reaches the target, otherwise on the bubble phase. Either way,
* matching walks up from `event.target` via `closest(selector)` and fires
* `handler(event, matched)` if the match is inside `rootEl`.
*
* Pass `{ match: 'direct' }` to fire only when `event.target` itself matches
* the selector (no walk-up); the default is `'closest'`.
*
* The generic `T` narrows the second handler argument to the expected element
* type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`
* — so consumers can avoid casts. Defaults to `Element` for untyped calls.
*
* Returns a disposer that removes the listener.
*
* Usage (pseudo-code — see examples for live ones):
* delegate(rootEl, 'click', '[data-action="add"]', handlerFn);
* delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture
*/
declare function delegate<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
/**
* Capture-phase delegation — the escape hatch for custom non-bubbling events
* (ones `delegate()`'s auto-promotion list doesn't know about) and for
* capture-phase interception (run before any descendant's bubble-phase
* handler). Reaches descendants of `rootEl` that match `selector` regardless
* of how many times the diff has rebuilt them.
*
* Selector matching is `closest()`-style by default — the same walk-up as
* `delegate()`, and it passes the matched ancestor (not the raw target) to
* the handler — so a click on any descendant of the selected element climbs
* to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style
* matching (fire only when the event lands on the exact element the selector
* identifies, with no walk-up).
*
* The generic `T` narrows the second handler argument to the expected element
* type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.
*
* Usage (pseudo-code — see examples for live ones):
* delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);
* delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });
*/
declare function delegateCapture<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
/**
* `each(items, render, cacheKey?)` — keyed list iteration with per-item memoization.

@@ -342,54 +175,23 @@ *

/**
* `mount(rootEl, render)` — kerf's render primitive.
* `renderDocument(node)` — prepend the doctype to a rendered document.
*
* Wraps `effect()` so that whenever any signal read inside `render()`
* changes, we re-run `render()` and apply the minimum DOM mutations against
* the live tree. Element identity (and thus focus, selection, in-flight
* pointer interactions, and event listeners on preserved nodes) is preserved
* wherever the keyed/positional diff matches.
* Every kerf SSR app ends its routes with the identical concat
* `"<!DOCTYPE html>" + page.toString()`. This is that one line, blessed, so the
* doctype isn't reinvented (or forgotten) per route.
*
* Two phases per render:
* import { renderDocument } from 'kerfjs';
* return c.html(renderDocument(<Page />)); // "<!DOCTYPE html><html>…"
*
* - Static surrounds (everything outside `each()` lists): kerf's native
* `morph()` reconciler walks a freshly-built template against the live
* tree. Conventions: id/data-key matching, `data-morph-skip`, focus
* preservation.
*
* - List interiors (children of every `each()` parent): native keyed
* reconciler operates directly on the live parent's children. No
* re-parse, no morph walk for cache-hit rows. Cost is O(changes), not
* O(rows).
*
* Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,
* the user-visible win is that an `<input>` the user is typing into
* survives an unrelated re-render — its DOM node, focus state, and cursor
* position are not destroyed and recreated on each tick.
* `node` is a `SafeHtml` (from JSX / the `html` tagged template) or a raw string
* — both are stringified via `.toString()`. The optional `doctype` overrides the
* default `'html'`.
*/
/** What `mount()`'s render function may return; non-SafeHtml values coerce (nullish/boolean → render nothing). */
type MountResult = SafeHtml | string | number | boolean | null | undefined;
/**
* Bind `render()` to the children of `rootEl`. Re-runs whenever any signal
* read inside `render()` changes. Returns a disposer that tears down the
* effect; call it when the host element is removed from the DOM.
*
* Conventions:
*
* - Diff keys: `id` and `data-key` are matched across the morph by key
* rather than positionally, so list reorders move existing nodes instead
* of churning unrelated siblings.
* - `data-morph-skip`: any element with this attribute is left untouched
* inside on subsequent renders. Used for library-owned subtrees (xterm-
* style widgets, charts, third-party editors) where the library's own
* lifecycle manages the children.
* - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)
* keep their current value + selection range across morphs while focused.
* The user never sees their cursor jump mid-keystroke.
* - Focused `[contenteditable]` elements have their entire subtree
* skipped (same mechanism as `data-morph-skip`). The user's in-progress
* edit — typed content, caret position, multi-range selections, anything
* else they did to the DOM — survives verbatim. The next render after
* blur catches up.
*/
declare function mount(rootEl: HTMLElement, render: () => MountResult): () => void;
/** Options for {@link renderDocument}. */
interface RenderDocumentOptions {
/** The doctype name. Default `'html'` → `<!DOCTYPE html>`. */
doctype?: string;
}
/** Prepend `<!DOCTYPE …>` to a rendered `SafeHtml` (or string) document and return the full HTML string. */
declare function renderDocument(node: SafeHtml | string, options?: RenderDocumentOptions): string;

@@ -422,2 +224,2 @@ /**

export { type AttrSpec, type DelegateOptions, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, effect, morph, mount, signal, toElement };
export { type RenderDocumentOptions, SafeHtml, each, effect, morph, renderDocument, signal, toElement };

@@ -1,1516 +0,18 @@

import { itemVersion } from './chunk-QIP723L4.js';
import { parseRowTemplate, rowContractError, parseSingleRow, collectTemplateChildren } from './chunk-YHH7OUFA.js';
import { captureRowBindings, listSafeHtml, boundTextNodeOf, syncFormProp, newBindingContext, wireBindings, disposeRowBindings, isSafeHtml, wireRowBindings, _setBindingContext, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX, carryOrRewireRowBindings, granularListSafeHtml } from './chunk-JXAR5J54.js';
export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-JXAR5J54.js';
import { LIST_MARKER_PREFIX, flattenWithoutListItems, collectLists, flatten } from './chunk-GY4XV2UV.js';
export { defineStore, resetAllStores } from './chunk-SAYPJ6XR.js';
import { effect } from './chunk-3APBEVHF.js';
export { attr } from './chunk-U32TFTGZ.js';
export { delegate, delegateCapture } from './chunk-KEZTD6H4.js';
export { each, morph, mount } from './chunk-4MY2656S.js';
import './chunk-QIP723L4.js';
import './chunk-YHH7OUFA.js';
export { Fragment, SafeHtml, isSafeHtml, raw, trustedRaw } from './chunk-FSAQR6IU.js';
export { batch, computed, effect, signal } from './chunk-3APBEVHF.js';
import { devHooks } from './chunk-VVDJLWMP.js';
import './chunk-GY4XV2UV.js';
import './chunk-VVDJLWMP.js';
// src/attrSelector.ts
function cssEscapeIdent(value) {
if (value === "") {
throw new Error("attr: attribute name must not be empty");
}
const str = String(value);
let result = "";
for (let i = 0; i < str.length; i++) {
const cp = str.charCodeAt(i);
const ch = str.charAt(i);
if (cp === 0) {
result += "\uFFFD";
continue;
}
if (cp >= 1 && cp <= 31 || cp === 127) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (i === 0 && cp >= 48 && cp <= 57) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (i === 1 && cp >= 48 && cp <= 57 && str.charCodeAt(0) === 45) {
result += "\\" + cp.toString(16) + " ";
continue;
}
if (cp >= 128 || cp === 45 || // `-`
cp === 95 || // `_`
cp >= 48 && cp <= 57 || // 0-9
cp >= 65 && cp <= 90 || // A-Z
cp >= 97 && cp <= 122) {
result += ch;
continue;
}
result += "\\" + ch;
}
return result;
// src/renderDocument.ts
function renderDocument(node, options = {}) {
const { doctype = "html" } = options;
return `<!DOCTYPE ${doctype}>${node.toString()}`;
}
function escapeCSSString(value) {
let result = "";
for (let i = 0; i < value.length; i++) {
const cp = value.charCodeAt(i);
const ch = value.charAt(i);
if (cp === 0) {
result += "\uFFFD";
} else if (cp >= 1 && cp <= 31 || cp === 127) {
result += "\\" + cp.toString(16) + " ";
} else if (cp === 92) {
result += "\\\\";
} else if (cp === 34) {
result += '\\"';
} else {
result += ch;
}
}
return result;
}
function attr(name, value) {
const escapedName = cssEscapeIdent(name);
if (value !== void 0) {
const selector = `[${escapedName}="${escapeCSSString(value)}"]`;
return Object.freeze({
name,
value,
selector,
attrs: Object.freeze({ [name]: value })
});
}
return (v) => Object.freeze({ [name]: v });
}
// src/delegate.ts
var NON_BUBBLING = /* @__PURE__ */ new Set([
"focus",
"blur",
"scroll",
"load",
"error",
"mouseenter",
"mouseleave"
]);
function assertValidSelector(selector, fn) {
try {
document.createElement("div").matches(selector);
} catch {
throw new Error(
`${fn}: invalid selector "${selector}". Pass a valid CSS selector (e.g. '[data-action="add"]', '.btn', 'input').`
);
}
}
function makeListener(rootEl, selector, handler, match) {
return (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const matched = match === "direct" ? target.matches(selector) ? target : null : target.closest(selector);
if (matched !== null && rootEl.contains(matched)) {
handler(event, matched);
}
};
}
function delegate(rootEl, type, selector, handler, options) {
assertValidSelector(selector, "delegate");
devHooks.delegateInEffect?.("delegate");
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
const capture = NON_BUBBLING.has(type);
rootEl.addEventListener(type, listener, capture);
return () => {
rootEl.removeEventListener(type, listener, capture);
};
}
function delegateCapture(rootEl, type, selector, handler, options) {
assertValidSelector(selector, "delegateCapture");
devHooks.delegateInEffect?.("delegateCapture");
const listener = makeListener(rootEl, selector, handler, options?.match ?? "closest");
rootEl.addEventListener(type, listener, true);
return () => {
rootEl.removeEventListener(type, listener, true);
};
}
// src/list-render-state.ts
function deriveListRenderState(bindingCount) {
if (bindingCount === void 0) return "unbound";
return bindingCount === 0 ? "empty" : "bound";
}
function decideListPath(state, patches, snapshotLength, previousBindingCount) {
if (state === "unbound") return { path: "snapshot", reason: "first-render" };
if (state === "empty") return { path: "snapshot", reason: "empty-binding" };
if (patches.length === 0) return { path: "snapshot", reason: "no-patches" };
let netDelta = 0;
for (const p of patches) {
if (p.type === "insert") netDelta += 1;
else if (p.type === "remove") netDelta -= 1;
else if (p.type === "replace") return { path: "snapshot", reason: "replace" };
}
const count = previousBindingCount ?? 0;
if (count + netDelta !== snapshotLength) {
return { path: "snapshot", reason: "count-drift" };
}
return { path: "granular" };
}
// src/each.ts
var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
function isArraySignal(value) {
return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
}
var context = null;
var renderingRow = false;
function inRowScope(fn) {
const prev = renderingRow;
renderingRow = true;
try {
return fn();
} finally {
renderingRow = prev;
}
}
function _setRenderContext(c) {
context = c;
}
function _resetCallOrderListState(ctx) {
const isCallOrderId = (id) => !id.startsWith("k:");
for (const map of [ctx.caches, ctx.bindingCounts, ctx.bindingSources]) {
for (const id of Array.from(map.keys())) {
if (isCallOrderId(id)) map.delete(id);
}
}
}
function isEachOptions(v) {
return typeof v === "object" && v !== null;
}
var VALID_KEY = /^[A-Za-z0-9_.:/-]+$/;
function assertValidKey(key) {
if (typeof key !== "string" || !VALID_KEY.test(key) || key.includes("--")) {
throw new Error(
`each(): invalid list key ${JSON.stringify(key)}. A key must be a non-empty string of letters, digits, or _ . : / - (and may not contain "--"), because kerf writes it into the list's marker comment in the DOM. Use a short stable identifier, e.g. { key: 'results' }.`
);
}
}
function claimKey(ctx, key) {
assertValidKey(key);
if (renderingRow) {
throw new Error(
`each(): list key ${JSON.stringify(key)} was used by an each() inside a row render. A nested each() is not reconciled \u2014 the row is flattened to HTML, so the inner list never binds and would render as static markup. Render the inner collection with plain .map() (it re-renders with its row), or restructure to a flat list.`
);
}
if (ctx.keysThisRender.has(key)) {
throw new Error(
`each(): duplicate list key ${JSON.stringify(key)}. Every keyed each() in a mount must have its own key \u2014 two lists sharing one would share the same cache, binding and DOM anchor. Give each list a distinct key.`
);
}
ctx.keysThisRender.add(key);
return `k:${key}`;
}
function each(items, render, cacheKeyOrOptions) {
const useOptions = isEachOptions(cacheKeyOrOptions);
const cacheKey = useOptions ? cacheKeyOrOptions.cacheKey : cacheKeyOrOptions;
const listKey = useOptions ? cacheKeyOrOptions.key : void 0;
if (isArraySignal(items) && context !== null) {
return eachGranular(items, render, cacheKey, listKey);
}
const snapshotItems = isArraySignal(items) ? items.value : items;
return eachSnapshot(snapshotItems, render, cacheKey, listKey);
}
function eachSnapshot(items, render, cacheKey, listKey) {
let id;
if (context !== null) {
id = listKey !== void 0 ? claimKey(context, listKey) : String(context.counter++);
} else {
id = "orphan";
}
return eachSnapshotById(items, render, cacheKey, id);
}
function assertObjectItem(item, index) {
if (typeof item !== "object" || item === null) {
throw new Error(
`each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${index}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
);
}
}
function eachGranular(sig, render, cacheKey, listKey) {
const ctx = context;
const id = listKey !== void 0 ? claimKey(ctx, listKey) : String(ctx.counter++);
const previousBindingCount = ctx.bindingCounts.get(id);
const patches = sig._consumePatches();
const snapshot = sig.value;
const previousSource = ctx.bindingSources.get(id);
const sourceReused = ctx.bindingSources.has(id) && previousSource !== sig;
if (sourceReused && listKey === void 0) ctx.shiftCandidates.push(id);
const decision = sourceReused ? { path: "snapshot" } : decideListPath(
deriveListRenderState(previousBindingCount),
patches,
snapshot.length,
previousBindingCount
);
if (decision.path === "snapshot") {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
let staleIndexShift = false;
if (render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
const rendered = [];
for (let i = 0; i < previousBindingCount; i++) rendered.push(i);
for (const p of patches) {
if (p.type === "insert") rendered.splice(p.index, 0, p.index);
else if (p.type === "remove") rendered.splice(p.index, 1);
else if (p.type === "move") {
const [moved] = rendered.splice(p.from, 1);
rendered.splice(p.to, 0, moved);
}
}
for (let i = 0; i < rendered.length; i++) {
if (rendered[i] !== i) {
staleIndexShift = true;
break;
}
}
}
if (cacheKey !== void 0) {
const cache2 = ctx.caches.get(id);
for (let i = 0; i < snapshot.length; i++) {
const item = snapshot[i];
const k = cacheKey(item, i);
const cached = cache2.get(item);
if (cached !== void 0 && cached.cacheKey !== k) {
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
}
}
const renderRow = (item, index) => captureRowBindings(() => inRowScope(() => {
const out = render(item, index);
return isSafeHtml(out) ? out.toString() : out;
}));
const internalPatches = new Array(patches.length);
const cache = ctx.caches.get(id);
try {
for (let i = 0; i < patches.length; i++) {
const p = patches[i];
if (p.type === "insert" || p.type === "update") {
assertObjectItem(p.item, p.index);
const { html, bindings } = renderRow(p.item, p.index);
internalPatches[i] = {
type: p.type,
index: p.index,
item: p.item,
html,
bindings
};
cache?.set(p.item, {
cacheKey: cacheKey ? cacheKey(p.item, p.index) : void 0,
html,
bindings,
version: itemVersion(p.item),
index: p.index
});
} else {
internalPatches[i] = p;
}
}
} catch {
ctx.bindingCounts.delete(id);
return eachSnapshotById(snapshot, render, cacheKey, id, sig);
}
if (staleIndexShift) devHooks.staleIndex?.(id);
return granularListSafeHtml(id, [], internalPatches, sig);
}
function eachSnapshotById(items, render, cacheKey, id, source) {
let cache = null;
if (context !== null) {
let c = context.caches.get(id);
if (c === void 0) {
c = /* @__PURE__ */ new WeakMap();
context.caches.set(id, c);
}
cache = c;
}
const segItems = new Array(items.length);
const seen = /* @__PURE__ */ new Set();
for (let i = 0; i < items.length; i++) {
const item = items[i];
assertObjectItem(item, i);
if (seen.has(item)) {
throw new Error(
`each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
);
}
seen.add(item);
const k = cacheKey ? cacheKey(item, i) : void 0;
const version = itemVersion(item);
let html;
let bindings;
const cached = cache !== null ? cache.get(item) : void 0;
if (cached !== void 0 && cached.cacheKey === k && cached.version === version) {
html = cached.html;
bindings = cached.bindings;
if (cached.index !== i && render.length >= 2 && devHooks.staleIndexEnabled?.() === true) {
devHooks.staleIndex?.(id);
}
} else {
const captured = captureRowBindings(() => inRowScope(() => {
const out = render(item, i);
return isSafeHtml(out) ? out.toString() : out;
}));
html = captured.html;
bindings = captured.bindings;
if (cache !== null) cache.set(item, { cacheKey: k, html, bindings, version, index: i });
}
segItems[i] = { ref: item, cacheKey: k, html, bindings };
}
if (cacheKey !== void 0) {
devHooks.duplicateCacheKeys?.(id, segItems);
}
return listSafeHtml(id, segItems, source);
}
// src/list-reconcile-focus.ts
function captureFocus(liveParent) {
const active = document.activeElement;
if (active === null || active === document.body) return null;
if (!liveParent.contains(active)) return null;
const el = active;
let selStart = null;
let selEnd = null;
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
try {
selStart = el.selectionStart;
selEnd = el.selectionEnd;
} catch {
}
}
return { el, selStart, selEnd };
}
function restoreFocus(snap) {
if (document.activeElement === snap.el) return;
if (!snap.el.isConnected) return;
snap.el.focus();
if (snap.selStart !== null && snap.selEnd !== null) {
try {
snap.el.setSelectionRange(snap.selStart, snap.selEnd);
} catch {
}
}
}
// src/morph.ts
var ID_KEY_PREFIX = "id:";
var DATA_KEY_PREFIX = "data-key:";
var ELEMENT_NODE = 1;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function getNodeKey(node) {
if (node.nodeType !== ELEMENT_NODE) return void 0;
const el = node;
if (el.id !== "") return `${ID_KEY_PREFIX}${el.id}`;
if (el.dataset !== void 0 && el.dataset.key !== void 0) {
return `${DATA_KEY_PREFIX}${el.dataset.key}`;
}
return void 0;
}
var EMPTY_OWNED = /* @__PURE__ */ new Set();
function morph(liveRoot, template, ownedItems = EMPTY_OWNED) {
if (liveRoot == null) {
throw new Error(
'morph: liveRoot is null/undefined \u2014 pass the live element, e.g. morph(document.getElementById("app")!, template). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say Element.'
);
}
const templateEl = isElementNode(template) ? template : parseTemplate(liveRoot, template);
const focusSnap = captureFocus(liveRoot);
morphChildren(liveRoot, templateEl, ownedItems);
if (focusSnap !== null) restoreFocus(focusSnap);
}
function _morphElement(fromEl, toEl, ownedItems = EMPTY_OWNED) {
morphElement(fromEl, toEl, ownedItems);
}
function isElementNode(t) {
return typeof t === "object" && t !== null && t.nodeType === ELEMENT_NODE;
}
function parseTemplate(liveRoot, template) {
const el = liveRoot.cloneNode(false);
el.innerHTML = String(template);
return el;
}
function protectionTag(node) {
const { dataset } = node;
return (dataset.morphSkip !== void 0 ? "s" : "") + (dataset.morphSkipChildren !== void 0 ? "c" : "") + (dataset.morphPreserve !== void 0 ? "p" : "");
}
var MARKER_PREFIXES = [LIST_MARKER_PREFIX, TEXT_MARKER_PREFIX, ROW_TEXT_PREFIX];
function isMarker(node) {
if (node.nodeType !== COMMENT_NODE) return false;
const { data } = node;
return MARKER_PREFIXES.some((prefix) => data.startsWith(prefix));
}
function markersPairable(a, b) {
if (!isMarker(a) && !isMarker(b)) return true;
return a.data === b.data;
}
function skipOwned(node, ownedItems) {
while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
node = node.nextSibling;
}
return node;
}
function isListMarker(node) {
return node.nodeType === COMMENT_NODE && node.data.startsWith(LIST_MARKER_PREFIX);
}
function afterListRegion(marker, ownedItems) {
let last = marker;
for (let r = marker.nextSibling; r !== null; r = r.nextSibling) {
if (isListMarker(r)) break;
if (r.nodeType === ELEMENT_NODE && ownedItems.has(r)) last = r;
}
return last.nextSibling;
}
function morphChildren(fromParent, toParent, ownedItems) {
const keyed = /* @__PURE__ */ new Map();
for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
const k = getNodeKey(c);
if (k !== void 0) keyed.set(k, c);
}
let fromChild = skipOwned(fromParent.firstChild, ownedItems);
let toChild = toParent.firstChild;
while (toChild !== null) {
const toNext = toChild.nextSibling;
let matched = null;
const toKey = getNodeKey(toChild);
if (toKey !== void 0 && keyed.has(toKey)) {
matched = keyed.get(toKey);
keyed.delete(toKey);
if (matched !== fromChild) {
fromParent.insertBefore(matched, fromChild);
} else {
fromChild = skipOwned(fromChild.nextSibling, ownedItems);
}
}
if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && markersPairable(fromChild, toChild) && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0 && toKey === void 0 && protectionTag(fromChild) === protectionTag(toChild))) {
matched = fromChild;
fromChild = skipOwned(
isListMarker(matched) ? afterListRegion(matched, ownedItems) : fromChild.nextSibling,
ownedItems
);
if (matched.nodeType === COMMENT_NODE && fromChild !== null) {
const owned = boundTextNodeOf(matched);
if (owned !== null && fromChild === owned) {
fromChild = skipOwned(owned.nextSibling, ownedItems);
}
}
}
if (matched === null && toChild.nodeType === ELEMENT_NODE && fromChild !== null && toKey === void 0) {
const toTag = toChild.tagName;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== ELEMENT_NODE) continue;
const el = scan;
if (ownedItems.has(el)) continue;
if (el.tagName !== toTag || getNodeKey(el) !== void 0) continue;
if (protectionTag(el) !== protectionTag(toChild)) continue;
matched = el;
fromParent.insertBefore(el, fromChild);
break;
}
}
if (matched === null && fromChild !== null && toChild.nodeType === COMMENT_NODE && toChild.data.startsWith(LIST_MARKER_PREFIX)) {
const wantData = toChild.data;
for (let scan = fromChild.nextSibling; scan !== null; scan = scan.nextSibling) {
if (scan.nodeType !== COMMENT_NODE || scan.data !== wantData) continue;
const regionEnd = afterListRegion(scan, ownedItems);
const run = [];
for (let r = scan; r !== null && r !== regionEnd; r = r.nextSibling) {
run.push(r);
}
const focusSnap = captureFocus(fromParent);
for (const node of run) fromParent.insertBefore(node, fromChild);
if (focusSnap !== null) restoreFocus(focusSnap);
matched = scan;
break;
}
}
if (matched !== null) {
morphNode(matched, toChild, ownedItems);
} else {
const cloned = toChild.cloneNode(true);
fromParent.insertBefore(cloned, fromChild);
}
toChild = toNext;
}
while (fromChild !== null) {
const next = fromChild.nextSibling;
if (fromChild.nodeType === ELEMENT_NODE) {
const el = fromChild;
if (!ownedItems.has(el) && el.dataset.morphPreserve === void 0) {
fromParent.removeChild(fromChild);
}
} else {
fromParent.removeChild(fromChild);
}
fromChild = next;
}
}
function morphNode(fromNode, toNode, ownedItems) {
if (fromNode.nodeType === ELEMENT_NODE) {
morphElement(fromNode, toNode, ownedItems);
return;
}
if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
const fromText = fromNode;
const toText = toNode;
if (fromText.data !== toText.data) fromText.data = toText.data;
}
}
function morphElement(fromEl, toEl, ownedItems) {
if (fromEl.tagName !== toEl.tagName) {
const replacement = toEl.cloneNode(true);
fromEl.parentNode?.replaceChild(replacement, fromEl);
return;
}
if (fromEl.dataset.morphSkip !== void 0) return;
if (fromEl.isEqualNode(toEl)) return;
if (fromEl === document.activeElement) {
const ce = fromEl.getAttribute("contenteditable");
if (ce !== null && ce.toLowerCase() !== "false") return;
if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
}
morphAttributes(fromEl, toEl);
if (fromEl.dataset.morphSkipChildren !== void 0) return;
const syncTextareaValue = fromEl.tagName === "TEXTAREA" && fromEl !== document.activeElement && fromEl.textContent !== toEl.textContent;
morphChildren(fromEl, toEl, ownedItems);
if (syncTextareaValue) {
fromEl.value = toEl.textContent;
}
}
function isUserAgentOwnedAttr(tagName, name) {
return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
}
function morphAttributes(fromEl, toEl) {
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr2 = toAttrs[i];
const ns = attr2.namespaceURI;
const name = attr2.localName;
const value = attr2.value;
if (ns !== null) {
if (fromEl.getAttributeNS(ns, name) !== value) {
fromEl.setAttributeNS(ns, attr2.name, value);
}
} else if (fromEl.getAttribute(name) !== value) {
fromEl.setAttribute(name, value);
syncFormProp(fromEl, name, value, true);
}
}
const fromAttrs = fromEl.attributes;
const fromTag = fromEl.tagName;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr2 = fromAttrs[i];
const ns = attr2.namespaceURI;
const name = attr2.localName;
if (ns !== null) {
if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
} else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
fromEl.removeAttribute(name);
syncFormProp(fromEl, name, "", false);
}
}
}
function isTextInputOrTextarea(el) {
if (el.tagName === "TEXTAREA") return true;
if (el.tagName === "INPUT") {
const type = el.type;
return type === "text" || type === "search" || type === "url" || type === "email" || type === "tel" || type === "password" || type === "";
}
return false;
}
function preserveTextEntryState(fromEl, toEl) {
if (fromEl.tagName === "TEXTAREA" || fromEl.tagName === "INPUT") {
const fromInput = fromEl;
const toInput = toEl;
toInput.value = fromInput.value;
try {
toInput.setSelectionRange(fromInput.selectionStart, fromInput.selectionEnd);
} catch {
}
}
}
// src/list-binding.ts
function endAnchor(binding) {
if (binding.items.length > 0) {
return binding.items[binding.items.length - 1].node.nextSibling;
}
return binding.marker.nextSibling;
}
// src/list-reconcile-fast-paths.ts
var LT = 60;
var GT = 62;
var DQUOTE = 34;
var SQUOTE = 39;
var AMP = 38;
var EQ = 61;
var SLASH = 47;
var TEXT_NODE2 = 3;
var ELEMENT_NODE2 = 1;
function isWhitespace(cc) {
return cc === 32 || cc === 9 || cc === 10 || cc === 13;
}
function tryAttributeOnlyFastPath(liveNode, oldHtml, newHtml) {
const oldGt = oldHtml.indexOf(">");
const newGt = newHtml.indexOf(">");
if (oldGt === -1 || newGt === -1) return false;
if (oldHtml.length - oldGt !== newHtml.length - newGt) return false;
if (oldHtml.slice(oldGt) !== newHtml.slice(newGt)) return false;
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
const oldTag = parseOpeningTag(oldHtml, oldGt);
const newTag = parseOpeningTag(newHtml, newGt);
if (oldTag === null || newTag === null) return false;
if (oldTag.tagName !== newTag.tagName) return false;
for (const name of oldTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
for (const name of newTag.attrs.keys()) {
if (name.indexOf(":") !== -1) return false;
}
const liveTagUpper = liveNode.tagName;
for (const [name, rawValue] of newTag.attrs) {
const oldValue = oldTag.attrs.get(name);
if (oldValue === rawValue) continue;
const value = unescapeAttrValue(rawValue);
liveNode.setAttribute(name, value);
syncFormProp(liveNode, name, value, true);
}
for (const name of oldTag.attrs.keys()) {
if (newTag.attrs.has(name)) continue;
if (isUserAgentOwnedAttr2(liveTagUpper, name)) continue;
liveNode.removeAttribute(name);
syncFormProp(liveNode, name, "", false);
}
return true;
}
function tryTextContentFastPath(liveNode, oldHtml, newHtml) {
if (containsDataMorphSkip(oldHtml) || containsDataMorphSkip(newHtml)) return false;
let p = 0;
const minLen = Math.min(oldHtml.length, newHtml.length);
while (p < minLen && oldHtml.charCodeAt(p) === newHtml.charCodeAt(p)) p++;
let s = 0;
const maxS = minLen - p;
while (s < maxS && oldHtml.charCodeAt(oldHtml.length - 1 - s) === newHtml.charCodeAt(newHtml.length - 1 - s)) {
s++;
}
const oldWinEnd = oldHtml.length - s;
const newWinEnd = newHtml.length - s;
if (!isPureTextWindow(oldHtml, p, oldWinEnd)) return false;
if (!isPureTextWindow(newHtml, p, newWinEnd)) return false;
if (p === 0) return false;
const boundaryCc = oldHtml.charCodeAt(p - 1);
if (boundaryCc === LT || boundaryCc === DQUOTE || boundaryCc === SQUOTE || boundaryCc === EQ || boundaryCc === AMP) return false;
const textStart = lastIndexOfChar(oldHtml, GT, p - 1);
if (textStart === -1) return false;
const textEnd = oldHtml.indexOf("<", p);
if (textEnd === -1) return false;
if (textEnd < oldWinEnd) return false;
const newTextEnd = textEnd + (newHtml.length - oldHtml.length);
const oldText = oldHtml.slice(textStart + 1, textEnd);
const newText = newHtml.slice(textStart + 1, newTextEnd);
if (oldHtml.lastIndexOf("<!--kfb", textStart) !== -1) return false;
const textIdx = countTextNodesBefore(oldHtml, textStart + 1);
const targetNode = nthTextNodeDescendant(liveNode, textIdx);
if (targetNode === null) return false;
if (targetNode.nodeValue !== oldText) return false;
targetNode.nodeValue = newText;
const host = targetNode.parentNode;
if (host !== null && host.tagName === "TEXTAREA" && host !== document.activeElement) {
host.value = newText;
}
return true;
}
function containsDataMorphSkip(html) {
return html.indexOf("data-morph-skip") !== -1;
}
function isPureTextWindow(html, start, end) {
for (let i = start; i < end; i++) {
const cc = html.charCodeAt(i);
if (cc === LT || cc === GT || cc === DQUOTE || cc === SQUOTE || cc === AMP || cc === EQ) return false;
}
return true;
}
function lastIndexOfChar(html, target, beforeInclusive) {
for (let i = beforeInclusive; i >= 0; i--) {
if (html.charCodeAt(i) === target) return i;
}
return -1;
}
function countTextNodesBefore(html, beforePos) {
let count = 0;
let i = 0;
while (i < beforePos) {
if (html.charCodeAt(i) === LT) {
while (i < beforePos && html.charCodeAt(i) !== GT) i++;
i++;
} else {
const start = i;
while (i < beforePos && html.charCodeAt(i) !== LT) i++;
if (i > start) count++;
}
}
return count;
}
function nthTextNodeDescendant(root, n) {
let count = 0;
let result = null;
function walk(node) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (result !== null) return;
if (c.nodeType === TEXT_NODE2) {
if (count === n) {
result = c;
return;
}
count++;
} else if (c.nodeType === ELEMENT_NODE2) {
walk(c);
}
}
}
walk(root);
return result;
}
function parseOpeningTag(html, gtPos) {
if (html.charCodeAt(0) !== LT) return null;
let i = 1;
let end = gtPos;
if (i < end && html.charCodeAt(end - 1) === SLASH) end -= 1;
const nameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (isWhitespace(cc)) break;
i++;
}
const tagName = html.slice(nameStart, i);
if (tagName.length === 0) return null;
const attrs = /* @__PURE__ */ new Map();
while (i < end) {
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) break;
const aNameStart = i;
while (i < end) {
const cc = html.charCodeAt(i);
if (cc === EQ || isWhitespace(cc)) break;
i++;
}
const aName = html.slice(aNameStart, i);
if (aName.length === 0) return null;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i < end && html.charCodeAt(i) === EQ) {
i++;
while (i < end && isWhitespace(html.charCodeAt(i))) i++;
if (i >= end) return null;
const q = html.charCodeAt(i);
if (q !== DQUOTE && q !== SQUOTE) return null;
i++;
const vStart = i;
while (i < end && html.charCodeAt(i) !== q) i++;
if (i >= end) return null;
attrs.set(aName, html.slice(vStart, i));
i++;
} else {
attrs.set(aName, "");
}
}
return { tagName, attrs };
}
function unescapeAttrValue(s) {
if (s.indexOf("&") === -1) return s;
return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
function isUserAgentOwnedAttr2(tagNameUpper, name) {
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
}
// src/list-reconcile-granular.ts
function reconcileGranular(binding, patches) {
const { liveParent } = binding;
const items = binding.items;
const focusSnap = captureFocus(liveParent);
let i = 0;
while (i < patches.length) {
const patch = patches[i];
if (patch.type === "replace") {
i += 1;
continue;
}
if (patch.type === "update") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "update") {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleUpdate(liveParent, items, patch);
} else {
applyBulkUpdate(liveParent, items, patches, i, runEnd);
}
i = runEnd;
continue;
}
if (patch.type === "insert") {
let runEnd = i + 1;
while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
runEnd += 1;
}
const runLen = runEnd - i;
if (runLen === 1) {
applySingleInsert(liveParent, items, patch, endAnchor(binding));
} else {
applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
}
i = runEnd;
continue;
}
if (patch.type === "remove") {
const entry = items[patch.index];
disposeRowBindings(entry.bindingDisposers);
liveParent.removeChild(entry.node);
items.splice(patch.index, 1);
i += 1;
continue;
}
if (patch.type === "move") {
const moved = items[patch.from];
let anchorIdx = patch.to;
if (patch.from < patch.to) anchorIdx += 1;
const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
liveParent.insertBefore(moved.node, anchor);
items.splice(patch.from, 1);
items.splice(patch.to, 0, moved);
i += 1;
continue;
}
}
if (focusSnap !== null) restoreFocus(focusSnap);
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
}
function applySingleInsert(liveParent, items, patch, tailAnchor) {
const { html } = patch;
const newNode = parseSingleRow(html, patch.index, liveParent);
const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
liveParent.insertBefore(newNode, anchor);
items.splice(patch.index, 0, {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
// KF-294: wire the inserted row's fine-grained bindings to its new node.
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
});
}
function wireRowIfBound(node, bindings) {
return bindings !== void 0 && bindings.length > 0 ? wireRowBindings(node, bindings) : void 0;
}
function applySingleUpdate(liveParent, items, patch) {
const { html } = patch;
const oldEntry = items[patch.index];
if (html === oldEntry.html) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, html)) {
items[patch.index] = reuseBound(patch, html, oldEntry);
return;
}
const newNode = parseSingleRow(html, patch.index, liveParent);
applyParsedRowUpdate(liveParent, items, patch, html, newNode);
}
function applyParsedRowUpdate(liveParent, items, patch, html, newNode) {
const oldEntry = items[patch.index];
if (oldEntry.node.tagName === newNode.tagName) {
_morphElement(oldEntry.node, newNode);
items[patch.index] = reuseBound(patch, html, oldEntry);
} else {
disposeRowBindings(oldEntry.bindingDisposers);
liveParent.replaceChild(newNode, oldEntry.node);
items[patch.index] = {
ref: patch.item,
cacheKey: void 0,
html,
node: newNode,
bindings: patch.bindings,
bindingDisposers: wireRowIfBound(newNode, patch.bindings)
};
}
}
function reuseBound(patch, html, oldEntry) {
const kept = carryOrRewireRowBindings(
oldEntry.node,
oldEntry.bindings,
oldEntry.bindingDisposers,
patch.bindings
);
return {
ref: patch.item,
cacheKey: void 0,
html,
node: oldEntry.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
function applyBulkUpdate(liveParent, items, patches, start, end) {
const morphChanges = [];
for (let k = start; k < end; k++) {
const p = patches[k];
const oldEntry = items[p.index];
if (p.html === oldEntry.html) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
if (tryAttributeOnlyFastPath(oldEntry.node, oldEntry.html, p.html) || tryTextContentFastPath(oldEntry.node, oldEntry.html, p.html)) {
items[p.index] = reuseBound(p, p.html, oldEntry);
continue;
}
morphChanges.push({ patchIdx: k, html: p.html });
}
if (morphChanges.length === 0) return;
const { content, count } = parseRowTemplate(morphChanges.map((c) => c.html).join(""), liveParent);
if (count !== morphChanges.length) {
throw findOffendingChange(patches, morphChanges, liveParent);
}
const newNodes = collectTemplateChildren(content, morphChanges.length);
for (let k = 0; k < morphChanges.length; k++) {
const c = morphChanges[k];
const p = patches[c.patchIdx];
applyParsedRowUpdate(liveParent, items, p, c.html, newNodes[k]);
}
}
function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
const startIdx = patches[start].index;
const htmls = new Array(end - start);
for (let k = start; k < end; k++) {
htmls[k - start] = patches[k].html;
}
const { content, count } = parseRowTemplate(htmls.join(""), liveParent);
if (count !== htmls.length) {
throw findOffendingInsert(patches, start, htmls, liveParent);
}
const newNodes = collectTemplateChildren(content, end - start);
const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
liveParent.insertBefore(content, anchor);
const newEntries = new Array(end - start);
for (let k = 0; k < newEntries.length; k++) {
const p = patches[start + k];
newEntries[k] = {
ref: p.item,
cacheKey: void 0,
html: htmls[k],
node: newNodes[k],
bindings: p.bindings,
bindingDisposers: wireRowIfBound(newNodes[k], p.bindings)
// KF-294
};
}
items.splice(startIdx, 0, ...newEntries);
}
function findOffendingInsert(patches, start, htmls, liveParent) {
for (let i = 0; i < htmls.length; i++) {
if (parseRowTemplate(htmls[i], liveParent).count !== 1) {
return rowContractError(patches[start + i].index, htmls[i], liveParent);
}
}
return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
}
function findOffendingChange(patches, changes, liveParent) {
for (const c of changes) {
if (parseRowTemplate(c.html, liveParent).count !== 1) {
return rowContractError(patches[c.patchIdx].index, c.html, liveParent);
}
}
return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
}
// src/list-reconcile-inplace.ts
function tryInPlaceContentUpdate(binding, listSeg) {
const oldItems = binding.items;
const items = listSeg.items;
const n = items.length;
if (n === 0 || n !== oldItems.length) return false;
for (let i = 0; i < n; i++) {
if (items[i].ref !== oldItems[i].ref) return false;
}
const { liveParent } = binding;
const newRecord = new Array(n);
const focusSnap = captureFocus(liveParent);
for (let i = 0; i < n; i++) {
newRecord[i] = updateRowInPlace(liveParent, oldItems[i], items[i], i);
}
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
return true;
}
function updateRowInPlace(liveParent, old, ni, index) {
if (old.html === ni.html || tryAttributeOnlyFastPath(old.node, old.html, ni.html) || tryTextContentFastPath(old.node, old.html, ni.html)) {
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
const newNode = parseSingleRow(ni.html, index, liveParent);
if (old.node.tagName === newNode.tagName) {
_morphElement(old.node, newNode);
const kept = carryOrRewireRowBindings(old.node, old.bindings, old.bindingDisposers, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: old.node,
bindings: kept.bindings,
bindingDisposers: kept.bindingDisposers
};
}
disposeRowBindings(old.bindingDisposers);
liveParent.replaceChild(newNode, old.node);
const fresh = carryOrRewireRowBindings(newNode, void 0, void 0, ni.bindings);
return {
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: newNode,
bindings: fresh.bindings,
bindingDisposers: fresh.bindingDisposers
};
}
// src/list-reconcile-snapshot.ts
function reconcileSnapshot(binding, listSeg) {
if (tryInPlaceContentUpdate(binding, listSeg)) return;
const { liveParent } = binding;
const { newRecord, prevIdx, removedItems, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
const tailAnchor = endAnchor(binding);
buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent);
const focusSnap = captureFocus(liveParent);
removeOldNodes(liveParent, removedItems);
applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
if (focusSnap !== null) restoreFocus(focusSnap);
binding.items = newRecord;
if (newRecord.length > 0) {
devHooks.missingRowKey?.(newRecord[0].node, newRecord[0].html, binding);
}
}
function classifyItems(oldItems, listSeg) {
const oldByRef = /* @__PURE__ */ new Map();
for (let i = 0; i < oldItems.length; i++) {
oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
}
const newRecord = new Array(listSeg.items.length);
const prevIdx = new Array(listSeg.items.length);
const removedItems = [];
const freshIndices = [];
const freshHtmls = [];
for (let i = 0; i < listSeg.items.length; i++) {
const ni = listSeg.items[i];
const oi = oldByRef.get(ni.ref);
if (oi !== void 0) {
oldByRef.delete(ni.ref);
if (oi[0].html === ni.html) {
newRecord[i] = oi[0];
prevIdx[i] = oi[1];
continue;
}
removedItems.push(oi[0]);
}
newRecord[i] = {
// `node` placeholder is filled by `buildFreshNodes`; its parse-count
// check guarantees every fresh index gets a real element before use.
ref: ni.ref,
cacheKey: ni.cacheKey,
html: ni.html,
node: null,
bindings: ni.bindings
};
prevIdx[i] = -1;
freshIndices.push(i);
freshHtmls.push(ni.html);
}
for (const [, orphan] of oldByRef) removedItems.push(orphan[0]);
return { newRecord, prevIdx, removedItems, freshIndices, freshHtmls };
}
function buildFreshNodes(newRecord, freshIndices, freshHtmls, liveParent) {
if (freshHtmls.length === 0) return;
const { content, count } = parseRowTemplate(freshHtmls.join(""), liveParent);
if (count !== freshHtmls.length) {
throw findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent);
}
let node = content.firstElementChild;
for (const idx of freshIndices) {
const next = node.nextElementSibling;
const item = newRecord[idx];
item.node = node;
if (item.bindings !== void 0 && item.bindings.length > 0) {
item.bindingDisposers = wireRowBindings(item.node, item.bindings);
}
node = next;
}
}
function findOffendingRow(newRecord, freshIndices, freshHtmls, liveParent) {
for (let i = 0; i < freshHtmls.length; i++) {
if (parseRowTemplate(freshHtmls[i], liveParent).count !== 1) {
return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html, liveParent);
}
}
return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
}
function removeOldNodes(liveParent, removedItems) {
for (const item of removedItems) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement === liveParent) liveParent.removeChild(item.node);
}
}
function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
let nextSibling = tailAnchor;
for (let i = newRecord.length - 1; i >= 0; i--) {
const node = newRecord[i].node;
if (prevIdx[i] === -1 || !stable.has(i)) {
liveParent.insertBefore(node, nextSibling);
}
nextSibling = node;
}
}
function lis(arr) {
const tails = [];
const tailIdx = [];
const prev = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
if (v === -1) {
prev[i] = -1;
continue;
}
let lo = 0;
let hi = tails.length;
while (lo < hi) {
const mid = lo + hi >> 1;
if (tails[mid] < v) lo = mid + 1;
else hi = mid;
}
prev[i] = lo > 0 ? tailIdx[lo - 1] : -1;
tails[lo] = v;
tailIdx[lo] = i;
}
const out = /* @__PURE__ */ new Set();
let k = tailIdx.length > 0 ? tailIdx[tailIdx.length - 1] : -1;
while (k !== -1) {
out.add(k);
k = prev[k];
}
return out;
}
// src/list-reconcile.ts
function reconcileList(binding, listSeg) {
if (listSeg.patches !== void 0 && binding.items.length > 0) {
reconcileGranular(binding, listSeg.patches);
return;
}
reconcileSnapshot(binding, listSeg);
}
// src/mount.ts
var MOUNTED_MARKER = /* @__PURE__ */ Symbol.for("kerfjs.mounted");
var NESTED_MOUNT_MSG = "mount: rootEl is already inside (or contains) a mounted tree. kerf supports one mount per tree \u2014 compose with plain functions that return JSX instead of nesting mounts.";
function isMounted(el) {
return el[MOUNTED_MARKER] === true;
}
function setMounted(el, on) {
if (on) {
el[MOUNTED_MARKER] = true;
} else {
delete el[MOUNTED_MARKER];
}
}
function describeEl(el) {
const tag = el.tagName.toLowerCase();
const id = el.id ? `#${el.id}` : "";
return `<${tag}${id}>`;
}
function assertNotInsideMountedTree(rootEl) {
if (isMounted(rootEl)) {
throw new Error(
`mount: ${describeEl(rootEl)} is already mounted. Call the disposer returned by the first mount() before mounting again. kerf supports one mount per element \u2014 compose with plain functions that return JSX instead of nesting mounts.`
);
}
let ancestor = rootEl.parentElement;
while (ancestor !== null) {
if (isMounted(ancestor)) throw new Error(NESTED_MOUNT_MSG);
ancestor = ancestor.parentElement;
}
const stack = [];
for (let i = 0; i < rootEl.children.length; i++) stack.push(rootEl.children[i]);
while (stack.length > 0) {
const cur = stack.pop();
if (isMounted(cur)) throw new Error(NESTED_MOUNT_MSG);
for (let i = 0; i < cur.children.length; i++) stack.push(cur.children[i]);
}
}
function mount(rootEl, render) {
if (rootEl == null) {
throw new Error(
'mount: rootEl is null/undefined \u2014 pass the live element, e.g. mount(document.getElementById("app")!, render). A common cause is a typo in the id or selector that returns null at runtime even though the TypeScript types say HTMLElement.'
);
}
const owner = rootEl.ownerDocument;
if (owner !== document) {
if (owner.defaultView === null) document.adoptNode(rootEl);
}
assertNotInsideMountedTree(rootEl);
setMounted(rootEl, true);
const listenerWarnObserver = devHooks.listenerRebuild?.(rootEl) ?? null;
const bindings = /* @__PURE__ */ new Map();
const renderCtx = {
counter: 0,
caches: /* @__PURE__ */ new Map(),
bindingCounts: /* @__PURE__ */ new Map(),
bindingSources: /* @__PURE__ */ new Map(),
keysThisRender: /* @__PURE__ */ new Set(),
shiftCandidates: [],
warnedShiftIds: /* @__PURE__ */ new Set(),
rebuiltLists: /* @__PURE__ */ new Set()
};
const bindingCtx = newBindingContext();
let bindingDisposers = [];
let prevWiredBindings = [];
let isFirst = true;
let prevStaticHtml = "";
const valueOnlyWarnCtx = { warned: false };
const runRenderPass = () => {
renderCtx.counter = 0;
renderCtx.keysThisRender.clear();
renderCtx.shiftCandidates.length = 0;
bindingCtx.counter = 0;
bindingCtx.list = [];
_setRenderContext(renderCtx);
_setBindingContext(bindingCtx);
try {
return render();
} finally {
_setRenderContext(null);
_setBindingContext(null);
}
};
const disposeEffect = effect(() => {
let result = runRenderPass();
const countChanged = renderCtx.previousCallCount !== void 0 && renderCtx.previousCallCount !== renderCtx.counter;
if (countChanged) {
for (const id of renderCtx.shiftCandidates) {
if (renderCtx.warnedShiftIds.has(id)) continue;
renderCtx.warnedShiftIds.add(id);
devHooks.listIdShift?.(id);
}
_resetCallOrderListState(renderCtx);
result = runRenderPass();
}
let segment = resultToSegment(result);
if (isFirst) {
runFirstRender(rootEl, segment, bindings);
prevStaticHtml = flattenWithoutListItems(segment);
devHooks.parserRepair?.(prevStaticHtml);
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
isFirst = false;
} else {
let nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
if (anyRebuiltListIsGranular(segment, renderCtx.rebuiltLists)) {
for (const id of renderCtx.rebuiltLists) renderCtx.bindingCounts.delete(id);
result = runRenderPass();
segment = resultToSegment(result);
nextStaticHtml = runSubsequentRender(
rootEl,
segment,
bindings,
renderCtx,
prevStaticHtml,
valueOnlyWarnCtx
);
}
if (nextStaticHtml !== prevStaticHtml) {
bindingDisposers = wireBindings(rootEl, bindingCtx, bindingDisposers);
if (devHooks.staleBindingEnabled?.() === true) prevWiredBindings = bindingCtx.list;
} else {
devHooks.staleBinding?.(prevWiredBindings, bindingCtx.list);
}
prevStaticHtml = nextStaticHtml;
}
const expectedCounts = devHooks.listInvariantsEnabled?.() === true ? /* @__PURE__ */ new Map() : null;
for (const listSeg of collectLists(segment).values()) {
const binding = bindings.get(listSeg.id);
if (binding === void 0) {
throw new Error(
"mount: an each() list appeared in the render output but its marker never reached the live DOM. The most common cause is an each() introduced inside a data-morph-skip subtree on a re-render \u2014 the morph leaves that subtree untouched, so the list can never bind. Move the each() outside the skipped subtree, or remove data-morph-skip from its ancestor."
);
}
reconcileList(binding, listSeg);
renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
renderCtx.bindingSources.set(listSeg.id, listSeg.source);
expectedCounts?.set(
listSeg.id,
listSeg.patches !== void 0 && listSeg.source !== void 0 ? listSeg.source.value.length : listSeg.items.length
);
}
renderCtx.previousCallCount = renderCtx.counter;
devHooks.listInvariants?.(rootEl, bindings, expectedCounts ?? void 0);
});
return () => {
disposeEffect();
for (const d of bindingDisposers) d();
bindingDisposers = [];
for (const b of bindings.values()) {
for (const item of b.items) disposeRowBindings(item.bindingDisposers);
}
listenerWarnObserver?.disconnect();
setMounted(rootEl, false);
};
}
function runFirstRender(rootEl, segment, bindings) {
rootEl.innerHTML = flatten(segment, true);
bindListsFromMarkers(rootEl, segment, bindings, true);
}
function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml, valueOnlyWarnCtx) {
renderCtx.rebuiltLists.clear();
const currentStaticHtml = flattenWithoutListItems(segment);
if (currentStaticHtml === prevStaticHtml) {
return prevStaticHtml;
}
devHooks.valueOnlyRerender?.(prevStaticHtml, currentStaticHtml, valueOnlyWarnCtx);
cleanupOrphanBindings(segment, bindings, renderCtx);
const template = rootEl.cloneNode(false);
template.innerHTML = currentStaticHtml;
morph(rootEl, template, collectOwnedItems(bindings));
bindListsFromMarkers(rootEl, segment, bindings, false, renderCtx.rebuiltLists);
return currentStaticHtml;
}
function coerceRenderResult(result) {
if (result === null || result === void 0) return "";
if (result === false || result === true) return "";
return String(result);
}
function resultToSegment(result) {
return isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
}
function anyRebuiltListIsGranular(segment, rebuilt) {
if (rebuilt.size === 0) return false;
const lists = collectLists(segment);
for (const id of rebuilt) {
if (lists.get(id)?.patches !== void 0) return true;
}
return false;
}
function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems, rebuiltLists) {
const lists = collectLists(segment);
const found = [];
collectComments(rootEl, found);
for (const marker of found) {
if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
const id = marker.data.slice(LIST_MARKER_PREFIX.length);
const existing = bindings.get(id);
if (existing !== void 0) {
if (existing.marker === marker && rootEl.contains(existing.marker)) continue;
for (const item of existing.items) {
disposeRowBindings(item.bindingDisposers);
if (rootEl.contains(item.node)) {
item.node.parentElement?.removeChild(item.node);
}
}
bindings.delete(id);
rebuiltLists?.add(id);
devHooks.listRebind?.(id, marker.parentElement);
}
const listSeg = lists.get(id);
const liveParent = marker.parentElement;
const items = [];
if (inlinedItems) {
let next = marker.nextElementSibling;
for (let i = 0; i < listSeg.items.length && next !== null; i++) {
validateInlinedRowMatch(listSeg.items[i].html, i, next, liveParent);
const rowBindings = listSeg.items[i].bindings;
const bound = {
ref: listSeg.items[i].ref,
cacheKey: listSeg.items[i].cacheKey,
html: listSeg.items[i].html,
node: next,
bindings: rowBindings
};
if (rowBindings !== void 0 && rowBindings.length > 0) {
bound.bindingDisposers = wireRowBindings(next, rowBindings);
}
items.push(bound);
next = next.nextElementSibling;
}
}
const binding = { liveParent, items, marker };
if (items.length > 0) {
devHooks.missingRowKey?.(items[0].node, items[0].html, binding);
}
devHooks.eachInMorphSkip?.(id, liveParent, rootEl);
bindings.set(id, binding);
}
}
function validateInlinedRowMatch(expectedHtml, index, boundEl, liveParent) {
if (boundEl.outerHTML === expectedHtml) return;
const { content, count } = parseRowTemplate(expectedHtml, liveParent);
if (count !== 1) throw rowContractError(index, expectedHtml, liveParent);
const expectedTag = content.firstElementChild.tagName;
if (boundEl.tagName !== expectedTag) throw rowStructureError(index, boundEl.tagName, expectedTag);
}
function rowStructureError(index, gotTag, wantTag) {
const got = gotTag.toLowerCase();
const want = wantTag.toLowerCase();
return new Error(
`each(): row ${index} renders <${want}>, but the HTML parser wrapped the rows in <${got}> \u2014 so kerf cannot bind one row per element. This happens when an each() of <${want}> sits directly inside a table: the parser inserts <${got}> around the whole run. Put the each() inside an explicit <${got}> (e.g. <table><${got}>{each(...)}</${got}></table>) so the rows are the direct children kerf binds.`
);
}
function collectOwnedItems(bindings) {
const owned = /* @__PURE__ */ new Set();
for (const b of bindings.values()) {
for (const item of b.items) owned.add(item.node);
}
return owned;
}
function cleanupOrphanBindings(segment, bindings, renderCtx) {
const liveIds = collectLists(segment);
for (const [id, binding] of bindings) {
if (liveIds.has(id)) continue;
for (const item of binding.items) {
disposeRowBindings(item.bindingDisposers);
if (item.node.parentElement !== null) {
item.node.parentElement.removeChild(item.node);
}
}
if (binding.marker.parentElement !== null) {
binding.marker.parentElement.removeChild(binding.marker);
}
bindings.delete(id);
renderCtx.bindingCounts.delete(id);
renderCtx.bindingSources.delete(id);
renderCtx.caches.delete(id);
}
}
function collectComments(node, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) out.push(c);
else if (c.nodeType === Node.ELEMENT_NODE) collectComments(c, out);
}
}
// src/toElement.ts

@@ -1604,4 +106,4 @@ var SVG_NS = "http://www.w3.org/2000/svg";

export { attr, delegate, delegateCapture, each, morph, mount, toElement };
export { renderDocument, toElement };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

@@ -1166,2 +1166,14 @@ import { ReadonlySignal } from '@preact/signals-core';

/**
* `trustedRaw(html)` — identical to {@link raw} at runtime, but names your intent:
* "this dynamic value is server-trusted, inject it verbatim." The
* `kerfjs/no-raw-with-dynamic-arg` lint rule flags a `raw()` with a NON-literal
* argument (unsanitized user input is the common XSS mistake) but leaves
* `trustedRaw()` alone — so a CSRF token, a trusted `<script src>`, or a
* server-issued id can be injected without scattering `eslint-disable` comments.
*
* It is NOT a sanitizer — it bypasses escaping exactly like `raw()`. Only pass
* values you control (server output, config, hard-coded), never raw user input.
*/
declare function trustedRaw(html: string): SafeHtml;
/**
* Internal: build a `SafeHtml` representing a list segment. Used by

@@ -1240,2 +1252,2 @@ * `each()` so the JSX runtime is the sole owner of `SafeHtml` construction.

export { type AttrLike, type AttrValue, type DataAriaAttrs, Fragment, JSX, type KerfBaseAttrs, type KerfCustomElement, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw };
export { type AttrLike, type AttrValue, type DataAriaAttrs, Fragment, JSX, type KerfBaseAttrs, type KerfCustomElement, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw, trustedRaw };

@@ -1,6 +0,6 @@

export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-JXAR5J54.js';
export { Fragment, SafeHtml, assertEmittableAttrName as _assertEmittableAttrName, _renderAttrVerbatim, _toSegment, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw, trustedRaw } from './chunk-FSAQR6IU.js';
import './chunk-3APBEVHF.js';
import './chunk-GY4XV2UV.js';
import './chunk-3APBEVHF.js';
import './chunk-VVDJLWMP.js';
//# sourceMappingURL=jsx-runtime.js.map
//# sourceMappingURL=jsx-runtime.js.map
{
"name": "kerfjs",
"version": "4.1.1",
"version": "4.2.0-beta.1",
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",

@@ -67,2 +67,22 @@ "type": "module",

},
"./actions": {
"types": "./dist/actions.d.ts",
"import": "./dist/actions.js"
},
"./overlay": {
"types": "./dist/overlay.d.ts",
"import": "./dist/overlay.js"
},
"./scope": {
"types": "./dist/scope.d.ts",
"import": "./dist/scope.js"
},
"./async": {
"types": "./dist/async.d.ts",
"import": "./dist/async.js"
},
"./list": {
"types": "./dist/list.d.ts",
"import": "./dist/list.js"
},
"./ai/*": "./ai/*"

@@ -111,2 +131,3 @@ },

"release:beta": "bash scripts/release.sh --beta",
"release:beta:auto": "bash scripts/release-beta-auto.sh",
"commit:msg": "gitgist --staged --commit-message",

@@ -113,0 +134,0 @@ "prepublishOnly": "npm run build",

import { flatten, wrapWithTags, mergeChildSegments } from './chunk-GY4XV2UV.js';
import { effect, isSignal } from './chunk-3APBEVHF.js';
import { devHooks } from './chunk-VVDJLWMP.js';
// src/utils/syncFormProp.ts
function syncFormProp(el, name, value, present) {
const tag = el.tagName;
if (name === "checked") {
if (tag === "INPUT") el.checked = present;
} else if (name === "value") {
if (tag === "INPUT" && el !== document.activeElement) {
el.value = present ? value : "";
}
} else if (name === "selected") {
if (tag === "OPTION") el.selected = present;
}
}
// src/utils/urlScreen.ts
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "data"]);
var DANGEROUS_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript"]);
var INERT_JAVASCRIPT_URLS = /* @__PURE__ */ new Set([
"javascript:",
"javascript:;",
"javascript:void(0)",
"javascript:void(0);",
"javascript:void 0",
"javascript:void 0;"
]);
var CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
function normalizeUrl(value) {
return value.replace(CONTROL_CHARS, "").replace(/^\s+/, "");
}
function schemeOf(normalized) {
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(normalized);
return m ? m[1].toLowerCase() : null;
}
function isDangerousDataUrl(normalized) {
const media = /^data:([^;,]*)/.exec(normalized.toLowerCase())?.[1].trim() ?? "";
if (media === "" || media === "text/plain" || media === "text/css") return false;
if (media === "image/svg+xml") return true;
if (media.startsWith("image/")) return false;
if (media.startsWith("font/") || media.startsWith("application/font")) return false;
if (media.startsWith("audio/") || media.startsWith("video/")) return false;
return true;
}
function isDangerousUrlValue(name, value) {
if (!URL_ATTRS.has(name)) return false;
const normalized = normalizeUrl(value);
const scheme = schemeOf(normalized);
if (scheme === null) return false;
if (DANGEROUS_SCHEMES.has(scheme)) {
return !INERT_JAVASCRIPT_URLS.has(normalized.trimEnd().toLowerCase());
}
return scheme === "data" && isDangerousDataUrl(normalized);
}
function dangerousUrlWarning(name, value) {
return `dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and script-executing data: URLs (text/html, image/svg+xml, xml) in href/src/data/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`;
}
function reportDangerousUrl(context2, name, value) {
const message = `${context2}: ${dangerousUrlWarning(name, value)}`;
devHooks.urlScreenThrow?.(message);
console.warn(message);
}
// src/bindings.ts
var BIND_ATTR = "data-kfb";
var TEXT_MARKER_PREFIX = "kfb:";
var BIND_ATTR_ROW = "data-kfbrow";
var ROW_TEXT_PREFIX = "kfbr:";
var context = null;
var rowSink = null;
var rowCounter = 0;
var NO_DISPOSERS = Object.freeze([]);
function newBindingContext() {
return { counter: 0, list: [] };
}
function _setBindingContext(c) {
context = c;
}
function captureRowBindings(renderRow) {
const prevSink = rowSink;
const prevCounter = rowCounter;
rowSink = [];
rowCounter = 0;
try {
const html = renderRow();
return { html, bindings: rowSink };
} finally {
rowSink = prevSink;
rowCounter = prevCounter;
}
}
function bindAttr(attr, signal) {
if (rowSink !== null) {
const id = `a${rowCounter++}`;
rowSink.push({ kind: "attr", id, attr, signal });
return id;
}
if (context !== null) {
const id = `a${context.counter++}`;
context.list.push({ kind: "attr", id, attr, signal });
return id;
}
return null;
}
function bindMarkerAttr() {
return rowSink !== null ? BIND_ATTR_ROW : BIND_ATTR;
}
function bindText(signal) {
if (rowSink !== null) {
const id = `t${rowCounter++}`;
rowSink.push({ kind: "text", id, signal });
return `<!--${ROW_TEXT_PREFIX}${id}-->`;
}
if (context !== null) {
const id = `t${context.counter++}`;
context.list.push({ kind: "text", id, signal });
return `<!--${TEXT_MARKER_PREFIX}${id}-->`;
}
return null;
}
function wireBindings(rootEl, ctx, prevDisposers) {
for (const d of prevDisposers) d();
if (ctx.list.length === 0) return NO_DISPOSERS;
const disposers = [];
wireInto(rootEl, ctx.list, disposers);
return disposers;
}
function wireRowBindings(rowNode, bindings) {
const disposers = new Array(bindings.length);
const rootIds = rowNode.getAttribute(BIND_ATTR_ROW);
let rootIdSet = null;
let descIndex = null;
let textMarkers = null;
for (let i = 0; i < bindings.length; i++) {
const b = bindings[i];
if (b.kind === "attr") {
let onRoot = false;
if (rootIds !== null) {
if (rootIds === b.id) {
onRoot = true;
} else if (rootIds.indexOf(",") !== -1) {
rootIdSet ??= new Set(rootIds.split(","));
onRoot = rootIdSet.has(b.id);
}
}
let el;
if (onRoot) {
el = rowNode;
} else {
descIndex ??= indexAttrEls(rowNode, BIND_ATTR_ROW);
el = descIndex.get(b.id);
}
if (el === void 0) continue;
disposers[i] = attachAttrEffect(el, b.attr, b.signal);
} else {
if (textMarkers === null) {
textMarkers = /* @__PURE__ */ new Map();
collectComments(rowNode, ROW_TEXT_PREFIX, textMarkers);
}
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers[i] = attachTextEffect(marker, b.signal);
}
}
return disposers;
}
function disposeRowBindings(disposers) {
if (disposers === void 0) return;
for (const d of disposers) d();
}
function carryOrRewireRowBindings(node, oldBindings, oldDisposers, newBindings) {
const oldLen = oldBindings === void 0 ? 0 : oldBindings.length;
const newLen = newBindings === void 0 ? 0 : newBindings.length;
if (oldLen === newLen) {
let same = true;
for (let i = 0; i < newLen; i++) {
if (oldBindings[i].signal !== newBindings[i].signal) {
same = false;
break;
}
}
if (same) return { bindings: oldBindings, bindingDisposers: oldDisposers };
}
disposeRowBindings(oldDisposers);
return {
bindings: newBindings,
bindingDisposers: newLen > 0 ? wireRowBindings(node, newBindings) : void 0
};
}
function wireInto(scope, bindings, disposers) {
const attrEls = indexAttrEls(scope, BIND_ATTR);
const textMarkers = /* @__PURE__ */ new Map();
collectComments(scope, TEXT_MARKER_PREFIX, textMarkers);
for (const b of bindings) {
if (b.kind === "attr") {
const el = attrEls.get(b.id);
if (el === void 0) continue;
disposers.push(attachAttrEffect(el, b.attr, b.signal));
} else {
const marker = textMarkers.get(b.id);
if (marker === void 0) continue;
disposers.push(attachTextEffect(marker, b.signal));
}
}
}
function indexAttrEls(scope, attrName) {
const map = /* @__PURE__ */ new Map();
for (const el of scope.querySelectorAll(`[${attrName}]`)) {
for (const id of el.getAttribute(attrName).split(",")) map.set(id, el);
}
return map;
}
function attachAttrEffect(el, attr, signal) {
return effect(() => setBoundAttr(el, attr, signal.value));
}
var insertedTextNodes = /* @__PURE__ */ new WeakMap();
function boundTextNodeOf(marker) {
const t = insertedTextNodes.get(marker);
return t !== void 0 && marker.nextSibling === t ? t : null;
}
function attachTextEffect(marker, signal) {
let text = insertedTextNodes.get(marker);
if (text === void 0 || marker.nextSibling !== text) {
text = marker.ownerDocument.createTextNode("");
marker.parentNode.insertBefore(text, marker.nextSibling);
insertedTextNodes.set(marker, text);
}
const node = text;
return effect(() => {
node.data = coerceText(signal.value);
});
}
function setBoundAttr(el, name, value) {
if (value == null || value === false) {
el.removeAttribute(name);
syncFormProp(el, name, "", false);
return;
}
if (value === true) {
el.setAttribute(name, "");
syncFormProp(el, name, "", true);
return;
}
if (isSafeHtmlValue(value)) {
el.setAttribute(name, value.__html);
syncFormProp(el, name, value.__html, true);
return;
}
const str = String(value);
if (isDangerousUrlValue(name, str)) {
reportDangerousUrl("kerf binding", name, str);
el.removeAttribute(name);
return;
}
el.setAttribute(name, str);
syncFormProp(el, name, str, true);
}
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
function isSafeHtmlValue(v) {
return typeof v === "object" && v !== null && v[SAFE_HTML_BRAND] === true;
}
function coerceText(value) {
if (value == null || typeof value === "boolean") return "";
return String(value);
}
function collectComments(node, prefix, out) {
for (let c = node.firstChild; c !== null; c = c.nextSibling) {
if (c.nodeType === Node.COMMENT_NODE) {
const data = c.data;
if (data.startsWith(prefix)) out.set(data.slice(prefix.length), c);
} else if (c.nodeType === Node.ELEMENT_NODE) {
collectComments(c, prefix, out);
}
}
}
// src/utils/escapeHtml.ts
function escapeHtml(str) {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function escapeAttr(str) {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// src/utils/jsx-attr-aliases.ts
var ATTR_ALIASES = {
// HTML attributes
className: "class",
htmlFor: "for",
httpEquiv: "http-equiv",
acceptCharset: "accept-charset",
accessKey: "accesskey",
autoCapitalize: "autocapitalize",
autoComplete: "autocomplete",
autoFocus: "autofocus",
autoPlay: "autoplay",
colSpan: "colspan",
contentEditable: "contenteditable",
crossOrigin: "crossorigin",
dateTime: "datetime",
defaultChecked: "checked",
defaultSelected: "selected",
defaultValue: "value",
encType: "enctype",
formAction: "formaction",
formEncType: "formenctype",
formMethod: "formmethod",
formNoValidate: "formnovalidate",
formTarget: "formtarget",
hrefLang: "hreflang",
inputMode: "inputmode",
maxLength: "maxlength",
minLength: "minlength",
noModule: "nomodule",
noValidate: "novalidate",
readOnly: "readonly",
referrerPolicy: "referrerpolicy",
rowSpan: "rowspan",
spellCheck: "spellcheck",
srcDoc: "srcdoc",
srcLang: "srclang",
srcSet: "srcset",
tabIndex: "tabindex",
useMap: "usemap",
// SVG presentation attributes (camelCase → kebab-case)
strokeWidth: "stroke-width",
strokeLinecap: "stroke-linecap",
strokeLinejoin: "stroke-linejoin",
strokeDasharray: "stroke-dasharray",
strokeDashoffset: "stroke-dashoffset",
strokeMiterlimit: "stroke-miterlimit",
strokeOpacity: "stroke-opacity",
fillOpacity: "fill-opacity",
fillRule: "fill-rule",
clipPath: "clip-path",
clipRule: "clip-rule",
colorInterpolation: "color-interpolation",
colorInterpolationFilters: "color-interpolation-filters",
floodColor: "flood-color",
floodOpacity: "flood-opacity",
lightingColor: "lighting-color",
stopColor: "stop-color",
stopOpacity: "stop-opacity",
shapeRendering: "shape-rendering",
imageRendering: "image-rendering",
textRendering: "text-rendering",
pointerEvents: "pointer-events",
vectorEffect: "vector-effect",
paintOrder: "paint-order",
// SVG text/font attributes
fontFamily: "font-family",
fontSize: "font-size",
fontStyle: "font-style",
fontVariant: "font-variant",
fontWeight: "font-weight",
fontStretch: "font-stretch",
textAnchor: "text-anchor",
textDecoration: "text-decoration",
dominantBaseline: "dominant-baseline",
alignmentBaseline: "alignment-baseline",
baselineShift: "baseline-shift",
letterSpacing: "letter-spacing",
wordSpacing: "word-spacing",
writingMode: "writing-mode",
// SVG marker attributes
markerStart: "marker-start",
markerMid: "marker-mid",
markerEnd: "marker-end",
// SVG xlink (legacy but still used)
xlinkHref: "xlink:href",
xlinkShow: "xlink:show",
xlinkActuate: "xlink:actuate",
xlinkType: "xlink:type",
xlinkRole: "xlink:role",
xlinkTitle: "xlink:title",
xlinkArcrole: "xlink:arcrole",
xmlBase: "xml:base",
xmlLang: "xml:lang",
xmlSpace: "xml:space",
xmlnsXlink: "xmlns:xlink"
};
// src/jsx-runtime.ts
var SAFE_HTML_BRAND2 = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
var SafeHtml = class {
__html;
__segment;
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
[SAFE_HTML_BRAND2] = true;
constructor(input) {
if (typeof input === "string") {
this.__segment = { kind: "static", html: input };
this.__html = input;
} else {
this.__segment = input;
this.__html = flatten(input, false);
}
}
toString() {
return this.__html;
}
};
function isSafeHtml(value) {
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND2] === true;
}
function raw(html) {
return new SafeHtml(html);
}
function listSafeHtml(id, items, source) {
return new SafeHtml({ kind: "list", id, items, source });
}
function granularListSafeHtml(id, items, patches, source) {
return new SafeHtml({ kind: "list", id, items, patches, source });
}
var VOID_TAGS = /* @__PURE__ */ new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"source",
"track",
"wbr"
]);
function toSegment(child) {
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
if (isSignal(child)) {
const marker = bindText(child);
if (marker !== null) return { kind: "static", html: marker };
const v = child.value;
return { kind: "static", html: v == null || typeof v === "boolean" ? "" : escapeHtml(String(v)) };
}
if (isSafeHtml(child)) {
return child.__segment ?? { kind: "static", html: child.__html };
}
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
if (typeof child === "number") return { kind: "static", html: String(child) };
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
const maybeNode = child;
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
throw new Error(
"JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). Build the tree in one JSX expression and use querySelector after toElement() to get element refs."
);
}
throw new Error(
`JSX: unsupported child of type ${describeValue(child)}. Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), passing a function (call it first), or passing a Promise (await it before render).`
);
}
function describeValue(v) {
if (Array.isArray(v)) return "array";
if (typeof v === "object" && v !== null) {
const ctor = v.constructor?.name;
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
}
return typeof v;
}
var SAFE_ATTR_NAME = /^[A-Za-z_:][\w.:-]*$/;
function assertEmittableAttrName(key, name, isFn) {
if (/^on[a-z]/i.test(name)) {
if (isFn) {
throw new Error(
`JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX \u2192 HTML-string runtime. Use event delegation from the mount root instead:
delegate(rootEl, 'click', '[data-action="..."]', (evt, target) => { ... });
<button data-action="...">click</button>
See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
);
}
throw new Error(
`JSX: event-handler attribute ${JSON.stringify(key)} is not allowed \u2014 an \`on*\` attribute (whether a string emitted into HTML or a signal bound via setAttribute) installs a live inline handler, an XSS vector. kerf uses event delegation: delegate(rootEl, 'click', '[data-action="..."]', handler). See docs/5-event-delegation.md.`
);
}
if (!SAFE_ATTR_NAME.test(name)) {
throw new Error(
`JSX: invalid attribute name ${JSON.stringify(key)}. Attribute names must be a letter/underscore/colon followed by letters, digits, or "_.:-" (e.g. class, data-id, aria-label, xlink:href). This usually means an untrusted object was spread into JSX ({...obj}) with attacker-controlled keys \u2014 validate keys first.`
);
}
}
function renderAttr(key, value) {
return renderAttrNamed(key, ATTR_ALIASES[key] ?? key, value);
}
function renderAttrNamed(key, name, value) {
if (value == null || value === false) return "";
assertEmittableAttrName(key, name, typeof value === "function");
if (value === true) return ` ${name}`;
let strValue;
if (isSafeHtml(value)) {
strValue = value.__html;
} else if (typeof value === "number") {
strValue = String(value);
} else if (typeof value === "string") {
if (isDangerousUrlValue(name, value)) {
reportDangerousUrl("JSX", name, value);
return "";
}
strValue = escapeAttr(value);
} else {
throw new Error(
`JSX: unsupported value for attribute "${key}" \u2014 got ${describeValue(value)}. Attribute values must be string, number, boolean, null, undefined, or SafeHtml. Did you mean to read .value off a Signal, or stringify the object first?`
);
}
return ` ${name}="${strValue}"`;
}
function jsx(tag, props) {
if (typeof tag === "function") return tag(props);
const { children, ...attrs } = props;
let attrStr = "";
let bindIds = null;
for (const [k, v] of Object.entries(attrs)) {
if (isSignal(v)) {
const name = ATTR_ALIASES[k] ?? k;
assertEmittableAttrName(k, name, false);
const id = bindAttr(name, v);
if (id !== null) {
(bindIds ??= []).push(id);
continue;
}
attrStr += renderAttr(k, v.value);
continue;
}
attrStr += renderAttr(k, v);
}
if (bindIds !== null) attrStr += ` ${bindMarkerAttr()}="${bindIds.join(",")}"`;
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
}
function Fragment({ children }) {
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
}
function _toSegment(child) {
return toSegment(child);
}
function _renderAttrVerbatim(name, value) {
return renderAttrNamed(name, name, value);
}
export { Fragment, ROW_TEXT_PREFIX, SafeHtml, TEXT_MARKER_PREFIX, _renderAttrVerbatim, _setBindingContext, _toSegment, assertEmittableAttrName, bindAttr, bindMarkerAttr, boundTextNodeOf, captureRowBindings, carryOrRewireRowBindings, disposeRowBindings, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, newBindingContext, raw, syncFormProp, wireBindings, wireRowBindings };
//# sourceMappingURL=chunk-JXAR5J54.js.map
//# sourceMappingURL=chunk-JXAR5J54.js.map

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

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