kerfjs 4.1.1 → 4.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/actions.d.ts +72 -0
  3. package/dist/actions.js +26 -0
  4. package/dist/actions.js.map +1 -0
  5. package/dist/array-signal.js +3 -104
  6. package/dist/array-signal.js.map +1 -1
  7. package/dist/async.d.ts +59 -0
  8. package/dist/async.js +55 -0
  9. package/dist/async.js.map +1 -0
  10. package/dist/attrSelector-Cmu2ZoGO.d.ts +79 -0
  11. package/dist/chunk-4MY2656S.js +1395 -0
  12. package/dist/chunk-4MY2656S.js.map +1 -0
  13. package/dist/{chunk-JXAR5J54.js → chunk-FSAQR6IU.js} +7 -4
  14. package/dist/chunk-FSAQR6IU.js.map +1 -0
  15. package/dist/chunk-KEZTD6H4.js +54 -0
  16. package/dist/chunk-KEZTD6H4.js.map +1 -0
  17. package/dist/chunk-MRYM3O3V.js +106 -0
  18. package/dist/chunk-MRYM3O3V.js.map +1 -0
  19. package/dist/chunk-U32TFTGZ.js +74 -0
  20. package/dist/chunk-U32TFTGZ.js.map +1 -0
  21. package/dist/delegate-CL9VTZFb.d.ts +93 -0
  22. package/dist/html.js +2 -2
  23. package/dist/imperative.d.ts +34 -0
  24. package/dist/imperative.js +20 -0
  25. package/dist/imperative.js.map +1 -0
  26. package/dist/index.d.ts +21 -219
  27. package/dist/index.js +13 -1511
  28. package/dist/index.js.map +1 -1
  29. package/dist/jsx-runtime.d.ts +13 -1
  30. package/dist/jsx-runtime.js +2 -2
  31. package/dist/list.d.ts +39 -0
  32. package/dist/list.js +146 -0
  33. package/dist/list.js.map +1 -0
  34. package/dist/mount-Bo2qOx25.d.ts +57 -0
  35. package/dist/overlay.d.ts +204 -0
  36. package/dist/overlay.js +406 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/remount.d.ts +52 -0
  39. package/dist/remount.js +45 -0
  40. package/dist/remount.js.map +1 -0
  41. package/dist/scope.d.ts +67 -0
  42. package/dist/scope.js +71 -0
  43. package/dist/scope.js.map +1 -0
  44. package/dist/timing.d.ts +65 -0
  45. package/dist/timing.js +80 -0
  46. package/dist/timing.js.map +1 -0
  47. package/package.json +34 -1
  48. package/dist/chunk-JXAR5J54.js.map +0 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.
3
+ *
4
+ * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but
5
+ * nothing manages that widget's LIFECYCLE. You set it up imperatively after
6
+ * render and must remember to tear it down when the node is replaced/removed
7
+ * (dropping document-level listeners the widget added, etc.). `imperative` closes
8
+ * that seam: it's a `useEffect`-with-cleanup bound to one node.
9
+ *
10
+ * import { imperative } from 'kerfjs/imperative';
11
+ *
12
+ * imperative(canvasEl, (el) => {
13
+ * const chart = D3.mount(el);
14
+ * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)
15
+ * });
16
+ *
17
+ * `setup(node)` runs immediately and may return a teardown function. The teardown
18
+ * runs once — whichever comes first — when the node leaves the document (detected
19
+ * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any
20
+ * removal triggers it) or when the returned disposer is called. Re-creation is
21
+ * NOT handled here: a fresh node is a fresh `imperative()` call — pair it with
22
+ * `kerfjs/remount`, which replaces the node and re-runs your render (and thus
23
+ * this call) on the new one.
24
+ */
25
+ /** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */
26
+ type ImperativeSetup = (node: Element) => (() => void) | void;
27
+ /**
28
+ * Run `setup(node)` now, and its returned teardown once — when `node` leaves the
29
+ * document, or when the returned disposer is called, whichever is first. Returns
30
+ * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.
31
+ */
32
+ declare function imperative(node: Element, setup: ImperativeSetup): () => void;
33
+
34
+ export { type ImperativeSetup, imperative };
@@ -0,0 +1,20 @@
1
+ // src/imperative.ts
2
+ function imperative(node, setup) {
3
+ const teardown = setup(node);
4
+ let done = false;
5
+ const finish = () => {
6
+ if (done) return;
7
+ done = true;
8
+ observer.disconnect();
9
+ if (typeof teardown === "function") teardown();
10
+ };
11
+ const observer = new MutationObserver(() => {
12
+ if (!node.isConnected) finish();
13
+ });
14
+ observer.observe(node.getRootNode(), { childList: true, subtree: true });
15
+ return finish;
16
+ }
17
+
18
+ export { imperative };
19
+ //# sourceMappingURL=imperative.js.map
20
+ //# sourceMappingURL=imperative.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/imperative.ts"],"names":[],"mappings":";AAiCO,SAAS,UAAA,CAAW,MAAe,KAAA,EAAoC;AAC5E,EAAA,MAAM,QAAA,GAAW,MAAM,IAAI,CAAA;AAC3B,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,QAAA,EAAS;AAAA,EAC/C,CAAA;AAMA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAa,MAAA,EAAO;AAAA,EAChC,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,WAAA,EAAY,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAEvE,EAAA,OAAO,MAAA;AACT","file":"imperative.js","sourcesContent":["/**\n * `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `imperative` closes\n * that seam: it's a `useEffect`-with-cleanup bound to one node.\n *\n * import { imperative } from 'kerfjs/imperative';\n *\n * imperative(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately and may return a teardown function. The teardown\n * runs once — whichever comes first — when the node leaves the document (detected\n * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any\n * removal triggers it) or when the returned disposer is called. Re-creation is\n * NOT handled here: a fresh node is a fresh `imperative()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n */\n\n/** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */\nexport type ImperativeSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — when `node` leaves the\n * document, or when the returned disposer is called, whichever is first. Returns\n * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.\n */\nexport function imperative(node: Element, setup: ImperativeSetup): () => void {\n const teardown = setup(node);\n let done = false;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n // Observe the node's live tree (the document when connected) with subtree, so\n // an ANCESTOR removal — not just a direct one — is caught. Each mutation just\n // re-checks `node.isConnected`, which is true until the node (or an ancestor)\n // is removed, so a morph swap / remountOn replacement / manual removal all fire.\n const observer = new MutationObserver(() => {\n if (!node.isConnected) finish();\n });\n observer.observe(node.getRootNode(), { childList: true, subtree: true });\n\n return finish;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
+ export { A as AttrSpec, a as attr } from './attrSelector-Cmu2ZoGO.js';
2
+ export { D as DelegateOptions, d as delegate, a as delegateCapture } from './delegate-CL9VTZFb.js';
1
3
  import { ArraySignal } from './array-signal.js';
2
4
  import { SafeHtml } from './jsx-runtime.js';
3
- export { Fragment, isSafeHtml, raw } from './jsx-runtime.js';
5
+ export { Fragment, isSafeHtml, raw, trustedRaw } from './jsx-runtime.js';
6
+ export { M as MountResult, m as mount } from './mount-Bo2qOx25.js';
4
7
  import { Signal } from '@preact/signals-core';
5
8
  export { ReadonlySignal, Signal, batch, computed } from '@preact/signals-core';
6
9
  export { S as Store, d as defineStore, r as resetAllStores } from './testing-DNEY7wi3.js';
@@ -33,176 +36,6 @@ import './bindings-CYwoJpQb.js';
33
36
  declare function signal<T>(value?: T): Signal<T>;
34
37
  declare function effect(fn: () => void | (() => void)): () => void;
35
38
 
36
- /**
37
- * `attr(name, value)` — create a pre-computed attribute descriptor (static form).
38
- * `attr(name)` — create a per-render factory for dynamic attribute values (dynamic form).
39
- *
40
- * **Static form** — best for fixed action names, filter keys, role values, etc.
41
- * Escapes once at module-load time; produces a full {@link AttrSpec} with
42
- * `.name`, `.value`, `.selector`, and `.attrs`.
43
- *
44
- * const ACTIONS = {
45
- * toggle: attr('data-action', 'toggle'),
46
- * remove: attr('data-action', 'remove'),
47
- * } as const satisfies Record<string, AttrSpec<'data-action'>>;
48
- *
49
- * // In JSX — spread .attrs (rename-safe; no hardcoded attribute name):
50
- * <button {...ACTIONS.toggle.attrs}>Toggle</button>
51
- *
52
- * // In delegate — use the pre-computed selector:
53
- * delegate(root, 'click', ACTIONS.toggle.selector, handler);
54
- *
55
- * **Dynamic form** — best for per-row data like `data-id`, where the value
56
- * changes per item but the attribute name is constant.
57
- * The name is validated and pre-escaped at definition time; calling the
58
- * returned factory is cheap (it just freezes a one-key object — the value is
59
- * escaped later by the JSX attribute renderer when the result is spread).
60
- *
61
- * const ITEM = { id: attr('data-id') } as const;
62
- *
63
- * // In JSX — call the factory inline:
64
- * <li {...ITEM.id(String(item.id))}>…</li>
65
- *
66
- * For ad-hoc compound selectors, concatenate `.selector` strings:
67
- *
68
- * delegate(root, 'click',
69
- * ACTIONS.toggle.selector + attr('data-id', id).selector,
70
- * handler);
71
- *
72
- * Escaping:
73
- * - Attribute name: escaped as a CSS identifier via `cssEscapeIdent`, which is
74
- * an SSR-safe (no `CSS.escape`) adaptation of the Mathias Bynens polyfill
75
- * (https://github.com/mathiasbynens/CSS.escape, MIT licensed — see the
76
- * Acknowledgements section of LICENSE). Handles
77
- * control chars, leading digits, non-ASCII, and CSS metacharacters.
78
- * - Attribute value: embedded in double quotes as a CSS string. Backslashes and
79
- * double-quote characters are backslash-escaped; control characters are
80
- * hex-escaped per CSS Syntax Level 3 §3.4.
81
- *
82
- * Throws on an empty attribute name (not a valid CSS identifier).
83
- */
84
- /** Descriptor created by the static {@link attr} overload. */
85
- interface AttrSpec<N extends string = string, V extends string = string> {
86
- /** The raw attribute name passed to `attr()`. */
87
- readonly name: N;
88
- /** The raw attribute value passed to `attr()`. */
89
- readonly value: V;
90
- /** Pre-computed `[name="value"]` CSS selector string, safe to pass to `delegate()`. */
91
- readonly selector: string;
92
- /** Spreadable JSX object — `{ [name]: value }` — keeps the attribute name out of JSX literals. */
93
- readonly attrs: {
94
- readonly [K in N]: V;
95
- };
96
- }
97
- /**
98
- * Static overload — pre-computes the full descriptor at definition time.
99
- * Returns an {@link AttrSpec} with `.name`, `.value`, `.selector`, and `.attrs`.
100
- */
101
- declare function attr<N extends string, V extends string>(name: N, value: V): AttrSpec<N, V>;
102
- /**
103
- * Dynamic overload — pre-validates and pre-escapes the attribute name, returns a
104
- * factory that accepts a per-render value and produces a frozen spreadable object.
105
- * Use for per-row attributes like `data-id` where the value changes per item.
106
- * The optional `V` generic constrains which values the factory accepts:
107
- * `attr<'data-id', 'a'|'b'>('data-id')` → `(value: 'a'|'b') => { 'data-id': 'a'|'b' }`.
108
- * Leaving both generics off infers `N` from the argument and defaults `V` to `string`.
109
- */
110
- declare function attr<N extends string, V extends string = string>(name: N): (value: V) => {
111
- readonly [K in N]: V;
112
- };
113
-
114
- /**
115
- * Tiny event-delegation helpers. Replace per-element `addEventListener` calls
116
- * (which don't survive morph re-renders for nodes the diff creates) with one
117
- * listener at the morph-root that dispatches via `closest()`.
118
- *
119
- * Three-tier listener model:
120
- *
121
- * - Tier 1 (bubbling events) — use `delegate()`.
122
- * click, input, change, submit, mousedown/up, keydown/up, pointerdown/up/move,
123
- * drag*, drop, contextmenu, wheel, copy/paste/cut, focusin/focusout.
124
- *
125
- * `delegate()` also auto-promotes the well-known non-bubbling event
126
- * types (`focus`, `blur`, `scroll`, `load`, `error`, `mouseenter`,
127
- * `mouseleave`) to the capture phase under the hood, so the call site
128
- * looks identical for "interactive thing happens on a descendant"
129
- * regardless of whether that event bubbles. Selector matching stays
130
- * `closest()`-style — the same as for bubbling events — so a wrapper
131
- * selector like `'.field-row'` still matches when the event fires on
132
- * a descendant `<input>`.
133
- *
134
- * - Tier 2 (explicit capture) — use `delegateCapture()`.
135
- * The escape hatch for cases the auto-promotion list doesn't cover
136
- * (custom non-bubbling events) or when you want capture-phase
137
- * interception. Selector matching is `closest()`-style by default —
138
- * the same walk-up as `delegate()`, and it passes the matched ancestor
139
- * (not the raw target) to the handler — so a click on any descendant of
140
- * the selected element climbs to it. Pass `{ match: 'direct' }` to opt
141
- * into strict `matches()`-style matching (fire only when the event lands
142
- * on the exact element the selector identifies).
143
- *
144
- * - Tier 3 (per-element instances / library-owned subtrees) — mark the
145
- * host element with `data-morph-skip` and manage the library's
146
- * lifecycle directly. No delegation helper applies.
147
- */
148
- /**
149
- * How the selector is matched against the event's target:
150
- *
151
- * - `'closest'` (the default for both helpers) — walk UP from `event.target`
152
- * via `closest(selector)`, firing for the nearest matching ancestor inside
153
- * `rootEl`. This is the delegation behavior you almost always want: a click
154
- * on an icon inside a button fires the button's handler.
155
- * - `'direct'` — strict `matches()` match: fire only when `event.target`
156
- * itself matches the selector, with no walk-up.
157
- */
158
- interface DelegateOptions {
159
- match?: 'closest' | 'direct';
160
- }
161
- /**
162
- * Delegation that "just works" for both bubbling and the common non-bubbling
163
- * events. Installs ONE listener on `rootEl`; for known non-bubblers (see
164
- * `NON_BUBBLING` above) the listener is registered on the capture phase so
165
- * it actually reaches the target, otherwise on the bubble phase. Either way,
166
- * matching walks up from `event.target` via `closest(selector)` and fires
167
- * `handler(event, matched)` if the match is inside `rootEl`.
168
- *
169
- * Pass `{ match: 'direct' }` to fire only when `event.target` itself matches
170
- * the selector (no walk-up); the default is `'closest'`.
171
- *
172
- * The generic `T` narrows the second handler argument to the expected element
173
- * type — `delegate<HTMLButtonElement>(root, 'click', 'button', (e, btn) => btn.value)`
174
- * — so consumers can avoid casts. Defaults to `Element` for untyped calls.
175
- *
176
- * Returns a disposer that removes the listener.
177
- *
178
- * Usage (pseudo-code — see examples for live ones):
179
- * delegate(rootEl, 'click', '[data-action="add"]', handlerFn);
180
- * delegate(rootEl, 'focus', 'input', handlerFn); // auto-capture
181
- */
182
- declare function delegate<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
183
- /**
184
- * Capture-phase delegation — the escape hatch for custom non-bubbling events
185
- * (ones `delegate()`'s auto-promotion list doesn't know about) and for
186
- * capture-phase interception (run before any descendant's bubble-phase
187
- * handler). Reaches descendants of `rootEl` that match `selector` regardless
188
- * of how many times the diff has rebuilt them.
189
- *
190
- * Selector matching is `closest()`-style by default — the same walk-up as
191
- * `delegate()`, and it passes the matched ancestor (not the raw target) to
192
- * the handler — so a click on any descendant of the selected element climbs
193
- * to it. Pass `{ match: 'direct' }` to opt into strict `matches()`-style
194
- * matching (fire only when the event lands on the exact element the selector
195
- * identifies, with no walk-up).
196
- *
197
- * The generic `T` narrows the second handler argument to the expected element
198
- * type, mirroring `delegate<T>()`. Defaults to `Element` for untyped calls.
199
- *
200
- * Usage (pseudo-code — see examples for live ones):
201
- * delegateCapture(rootEl, 'focus', 'input, textarea', handlerFn);
202
- * delegateCapture(rootEl, 'click', '.exact', handlerFn, { match: 'direct' });
203
- */
204
- declare function delegateCapture<T extends Element = Element>(rootEl: HTMLElement, type: string, selector: string, handler: (event: Event, target: T) => void, options?: DelegateOptions): () => void;
205
-
206
39
  /**
207
40
  * `each(items, render, cacheKey?)` — keyed list iteration with per-item memoization.
208
41
  *
@@ -338,58 +171,27 @@ declare function each<T extends object>(items: readonly T[] | ArraySignal<T>, re
338
171
  declare function morph(liveRoot: Element, template: Element | SafeHtml | string, ownedItems?: ReadonlySet<Element>): void;
339
172
 
340
173
  /**
341
- * `mount(rootEl, render)` — kerf's render primitive.
342
- *
343
- * Wraps `effect()` so that whenever any signal read inside `render()`
344
- * changes, we re-run `render()` and apply the minimum DOM mutations against
345
- * the live tree. Element identity (and thus focus, selection, in-flight
346
- * pointer interactions, and event listeners on preserved nodes) is preserved
347
- * wherever the keyed/positional diff matches.
174
+ * `renderDocument(node)` — prepend the doctype to a rendered document.
348
175
  *
349
- * Two phases per render:
176
+ * Every kerf SSR app ends its routes with the identical concat
177
+ * `"<!DOCTYPE html>" + page.toString()`. This is that one line, blessed, so the
178
+ * doctype isn't reinvented (or forgotten) per route.
350
179
  *
351
- * - Static surrounds (everything outside `each()` lists): kerf's native
352
- * `morph()` reconciler walks a freshly-built template against the live
353
- * tree. Conventions: id/data-key matching, `data-morph-skip`, focus
354
- * preservation.
180
+ * import { renderDocument } from 'kerfjs';
181
+ * return c.html(renderDocument(<Page />)); // "<!DOCTYPE html><html>…"
355
182
  *
356
- * - List interiors (children of every `each()` parent): native keyed
357
- * reconciler operates directly on the live parent's children. No
358
- * re-parse, no morph walk for cache-hit rows. Cost is O(changes), not
359
- * O(rows).
360
- *
361
- * Compared to a `replaceChildren(...rows.map(toElement))` rebuild pattern,
362
- * the user-visible win is that an `<input>` the user is typing into
363
- * survives an unrelated re-render — its DOM node, focus state, and cursor
364
- * position are not destroyed and recreated on each tick.
183
+ * `node` is a `SafeHtml` (from JSX / the `html` tagged template) or a raw string
184
+ * — both are stringified via `.toString()`. The optional `doctype` overrides the
185
+ * default `'html'`.
365
186
  */
366
187
 
367
- /** What `mount()`'s render function may return; non-SafeHtml values coerce (nullish/boolean → render nothing). */
368
- type MountResult = SafeHtml | string | number | boolean | null | undefined;
369
- /**
370
- * Bind `render()` to the children of `rootEl`. Re-runs whenever any signal
371
- * read inside `render()` changes. Returns a disposer that tears down the
372
- * effect; call it when the host element is removed from the DOM.
373
- *
374
- * Conventions:
375
- *
376
- * - Diff keys: `id` and `data-key` are matched across the morph by key
377
- * rather than positionally, so list reorders move existing nodes instead
378
- * of churning unrelated siblings.
379
- * - `data-morph-skip`: any element with this attribute is left untouched
380
- * inside on subsequent renders. Used for library-owned subtrees (xterm-
381
- * style widgets, charts, third-party editors) where the library's own
382
- * lifecycle manages the children.
383
- * - Focused text-entry inputs (`<input>` of typing kinds, `<textarea>`)
384
- * keep their current value + selection range across morphs while focused.
385
- * The user never sees their cursor jump mid-keystroke.
386
- * - Focused `[contenteditable]` elements have their entire subtree
387
- * skipped (same mechanism as `data-morph-skip`). The user's in-progress
388
- * edit — typed content, caret position, multi-range selections, anything
389
- * else they did to the DOM — survives verbatim. The next render after
390
- * blur catches up.
391
- */
392
- declare function mount(rootEl: HTMLElement, render: () => MountResult): () => void;
188
+ /** Options for {@link renderDocument}. */
189
+ interface RenderDocumentOptions {
190
+ /** The doctype name. Default `'html'` → `<!DOCTYPE html>`. */
191
+ doctype?: string;
192
+ }
193
+ /** Prepend `<!DOCTYPE …>` to a rendered `SafeHtml` (or string) document and return the full HTML string. */
194
+ declare function renderDocument(node: SafeHtml | string, options?: RenderDocumentOptions): string;
393
195
 
394
196
  /**
395
197
  * `toElement(jsx)` — JSX → DOM, with SVG-aware namespace handling.
@@ -417,4 +219,4 @@ declare function mount(rootEl: HTMLElement, render: () => MountResult): () => vo
417
219
 
418
220
  declare function toElement(jsx: SafeHtml | string): Element | DocumentFragment;
419
221
 
420
- export { type AttrSpec, type DelegateOptions, type MountResult, SafeHtml, attr, delegate, delegateCapture, each, effect, morph, mount, signal, toElement };
222
+ export { type RenderDocumentOptions, SafeHtml, each, effect, morph, renderDocument, signal, toElement };