kerfjs 5.0.0-beta.90 → 5.0.0-beta.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +3 -3
- package/ai/cursorrules +1 -1
- package/ai/manifest.json +5 -5
- package/ai/skill.md +1 -1
- package/dist/async.js +3 -3
- package/dist/async.js.map +1 -1
- package/dist/{chunk-WKIPLNVO.js → chunk-6LXUC4ZY.js} +29 -2
- package/dist/chunk-6LXUC4ZY.js.map +1 -0
- package/dist/{chunk-T33U7SRR.js → chunk-E53LC47B.js} +296 -20
- package/dist/chunk-E53LC47B.js.map +1 -0
- package/dist/{chunk-UGNPDLNI.js → chunk-VUAZFDLB.js} +48 -9
- package/dist/chunk-VUAZFDLB.js.map +1 -0
- package/dist/dev.js +1 -1
- package/dist/html.js +1 -1
- package/dist/index.js +2 -2
- package/dist/jsx-runtime.js +1 -1
- package/dist/list.js +1 -1
- package/dist/overlay.js +2 -2
- package/dist/remount.js +1 -1
- package/dist/router.js +1 -1
- package/dist/scope.js +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/dist/chunk-T33U7SRR.js.map +0 -1
- package/dist/chunk-UGNPDLNI.js.map +0 -1
- package/dist/chunk-WKIPLNVO.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,12 +8,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
8
8
|
|
|
9
9
|
### Fixed
|
|
10
10
|
|
|
11
|
+
- `resource().run()` can be called inside an `effect()`. `run()` read the resource's own state through a tracking read, so an effect that re-ran a resource from an input signal subscribed to that state, wrote it, and threw `Cycle detected`. `run()` and its progress and failure paths now read the state untracked; `resource.value` stays a tracking read (`KF-H5PNHK`).
|
|
12
|
+
|
|
13
|
+
- Re-renders of a large list-free page now scale with the change instead of the page. When a `mount()` render has no `each()` list and no fine-grained bindings, kerf compares the previous and next static HTML, finds the innermost element around the change whose markup parses on its own, and re-parses and morphs only that element's changed children; anything it cannot prove identical to a full diff still takes the full diff. A one-row selection on a 300-row, ~1.4 MB page drops from about 23 ms to 13.5 ms per render in Chromium. One documented behavior narrows: an attribute set imperatively on an element whose markup did not change now survives a render that changed something else, as it already did on a no-op render. The core grows by about 2 KB min+gzip (~13 KB to ~15 KB) (`KF-HA8R27`).
|
|
14
|
+
|
|
15
|
+
- The morph no longer strips the defaults a custom element reflects onto its host. Web Awesome reflects `variant`, `appearance`, `size`, `attention`, and similar defaults onto `wa-button`, `wa-tag`, `wa-badge`, `wa-textarea`, and others; a template that omitted them lost them on every re-render that reached the element, and Lit put them straight back with a full update (one row selection on a 300-row page flipped 1,200 attributes). On custom elements the morph now removes an attribute the template omits only when the previous template rendered it; a template that stops rendering an attribute still removes it, and plain elements are unchanged. An attribute set imperatively on a custom element now survives re-renders (`KF-K8S4RX`). Elements kerf creates (a mount's first render, a morph insertion, a new `each()` row) start with their template's record, taken before they connect, so even the first re-render leaves their reflected defaults alone; only an element kerf did not create uses the ordinary rule on its first attribute sync (`KF-G7N4PK`).
|
|
16
|
+
|
|
17
|
+
- `KUI-L017` (exceptional spacing shorthand, such as `<List gap="s" />`) now has one severity everywhere: a review finding. `eslint-plugin-kerfjs` reported it from the `error`-level `ui-css-values` rule, so a deliberate off-scale choice failed lint while `kerf-ui-analyze` queued it for review, and `kerf-ui-doctor` counted every occurrence twice (error plus review) with a `KUI-D003` conflict warning. The finding moves to a new `ui-exceptional-shorthands` rule, `warn` in both `recommended-ui` and `strict-ui`; `ui-css-values` keeps `KUI-L013`–`KUI-L016` at `error`. The doctor reads an ESLint warning for an analyzer review code as that review finding, and the analyzer's `KUI-L014`–`KUI-L017` messages now match ESLint's word for word, so the doctor merges each CSS-value finding into one diagnostic. A new `@kerfjs/ui` contract test fails if any ESLint UI rule's preset severity disagrees with the analyzer's classification of the codes it reports (`KF-P3945Y`).
|
|
18
|
+
|
|
19
|
+
- `mount()` no longer walks the whole live tree for `each()` list markers after a re-render that changed the page when the render has no `each()` lists and no list is bound. A large list-free page (one `mount()` with plain `.map()`) paid a full DOM traversal per render for markers kerf never emitted (`KF-VBQJAX`).
|
|
20
|
+
|
|
21
|
+
- Faster JSX attribute serialization on large pages: the runtime remembers, per attribute name, that the name passed the `on*` / malformed-name check and whether it is URL-bearing, instead of re-running those regexes for every attribute of every render, and the URL screen returns early for values without a `:` (no scheme to screen). Rejected names still throw their exact message on every render, and every URL value is still screened. Building a 300-row, ~1.4 MB list-free page drops from about 13.7 ms to 10.8 ms of CPU (`KF-VBQJAX`).
|
|
22
|
+
|
|
23
|
+
- `wireScrollDividers` no longer forces a synchronous layout after a render. A content mutation used to re-measure the scrollers in the next animation frame, and a structural one at once in the mutation callback, both before the browser had laid out the new tree. Both now re-measure once the frame has laid out and before it paints: the wiring toggles an inert, zero-size probe outside the app tree, and its `ResizeObserver` reports the probe after layout. Without `ResizeObserver` the previous timing remains. In a 300-row work area, `measure` self time drops from about 408 ms to under 1 ms over 16 segment switches and 8 row selections; the layout itself now runs once, in the browser's frame (`KF-4XYQ95`).
|
|
24
|
+
|
|
25
|
+
- `wireWorkbench` no longer forces a document-wide style recalculation after ordinary renders and presses. It caches each responsive panel's computed overlay state until a Workbench resize, a root font-size change, or a presentation attribute change on that panel, and its observer re-syncs only for a new Workbench, an added or removed panel, or an attribute change on a panel rather than any `data-collapsed` or `data-presentation` change below the root. In a 300-row work area with Web Awesome menus, the overlay check's forced style time drops from about 150 ms to 0 over 16 segment switches and 8 row selections (`KF-CRRD70`).
|
|
26
|
+
|
|
27
|
+
- Faster `PopupMenu` and `LucideIcon` re-renders in large lists. `LucideIcon` returns the same `SafeHtml` for the same icon and props instead of rebuilding its `<svg>` every render (up to 64 prop variants per icon), and builds its child markup as a Fragment instead of wrapping joined strings in `raw()` (`KF-XW15D6`). `PopupMenu` hashes icon and details markup of 64 or more characters as a memoized digest of the whole string instead of reading every character every render. A 300-row list with a nine-entry menu per row drops from about 12.7 ms to 2.1 ms per render when the icons are rebuilt each render, and from 3.9 ms to 1.3 ms when they are prebuilt. A menu with such a part now has a different `data-key` value than before; the key is still stable across renders, and every other byte of the rendered HTML is unchanged (`KF-V2ZBHT`).
|
|
28
|
+
|
|
29
|
+
- `PopupMenu` hashes its content key with a streaming `charCodeAt` loop instead of joining every field (including icon markup) into one string and iterating its code points, and caches each menu's trigger and entry markup in a bounded least-recently-used cache keyed by that hash and an independent check hash. An unchanged menu now costs one hash pass instead of rendering every entry again for the morph to skip. Rendered HTML, including the `data-key` digest, is byte-identical (`KF-GVCMC7`).
|
|
30
|
+
|
|
31
|
+
- Faster attribute serialization for large static values: `LucideIcon` caches each icon node's rendered child markup by node identity, and the JSX runtime's HTML/attribute escaping skips its replace passes when a value has nothing to escape. A 138-icon render pass drops from about 0.80 ms to 0.45 ms (`KF-JPP0Z5`). `jsx()` also iterates props without the rest-spread and `Object.entries` allocations, and the URL screen checks lowercase attribute names without allocating a lowercased copy, about 25% faster for an attribute-heavy list row (`KF-YHNV84`).
|
|
32
|
+
|
|
33
|
+
- `wireTokenSearchFields` collapses an empty field blurred by a pointer press in the click's own task, after the click reaches the window, instead of a separate task, so the click's render and the collapse share one style recalculation; a click that stops propagating still falls back to the next task (`KF-AS24QP`).
|
|
34
|
+
|
|
35
|
+
- `wireScrollDividers` no longer re-collects pairings and re-measures every scroller for mutations that cannot change a pairing, such as a row's selection class or a list morph inside scrolled content; it still refreshes when structural elements or target ids are added, removed, re-classed, or renamed (`KF-50PA0Y`). Those content mutations instead re-measure the known scrollers once in the next animation frame, so an absolutely positioned descendant that extends the scroll area still updates its dividers (`KF-E29N3W`).
|
|
36
|
+
|
|
11
37
|
- The UX Catalog filter keeps its placeholder hidden when a controlled state change rerenders the sidebar while filter text remains (`KF-4EA9R6`).
|
|
12
38
|
|
|
13
39
|
- Inspector TabBars use the shared rounded rectangle radius on the strip and inset tabs while ordinary segmented TabBars retain their pill geometry (`KF-K3RDN1`).
|
|
14
40
|
|
|
15
41
|
### Documentation
|
|
16
42
|
|
|
43
|
+
- New recipe in the `kerfjs/async` API reference: move heavy app compute (fuzzy search, CSV or markdown parsing, aggregation) into a Worker by having a `resource()` fetcher post a message to the worker and resolve with its reply. It covers request/reply ids, dropping superseded results, worker errors, transferring `ArrayBuffer`s, and keeping the render on the main thread (`KF-04KHT9`).
|
|
44
|
+
|
|
17
45
|
- Clarify for AI consumers that sunken/lowered token names resolve to a translucent tint, and explain intentional nested layers versus duplicate surface paint (`KF-9CM6D8`).
|
|
18
46
|
|
|
19
47
|
### Removed
|
|
@@ -52,6 +80,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
52
80
|
|
|
53
81
|
### Changed
|
|
54
82
|
|
|
83
|
+
- HTML and attribute escaping in the JSX runtime now runs one pass over a value instead of one `replace` pass per special character, about 2x faster on values that need escaping. Output is unchanged (`KF-3SHRTR`).
|
|
84
|
+
|
|
55
85
|
- Rename the internal TabNavigator implementation folder, files, and focused test files from `tab-scaffold` to `tab-navigator`; public imports and rendered DOM hooks stay stable.
|
|
56
86
|
|
|
57
87
|
- `deepInset` now adds the same 8px to the top and bottom of Pane and plain TabNavigator scroll content, including scroll padding; Pane header and footer still own their safe-area edges.
|
package/README.md
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://www.npmjs.com/package/kerfjs"><img src="https://img.shields.io/npm/v/kerfjs.svg" alt="npm version" /></a>
|
|
15
|
-
<img src="https://img.shields.io/badge/min%2Bgzip-~
|
|
15
|
+
<img src="https://img.shields.io/badge/min%2Bgzip-~15%20KB-brightgreen.svg" alt="~15 KB minified and gzipped" />
|
|
16
16
|
<img src="https://img.shields.io/npm/l/kerfjs.svg" alt="MIT license" />
|
|
17
17
|
<img src="https://img.shields.io/badge/types-included-3178c6.svg" alt="TypeScript types included" />
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
> ~
|
|
22
|
+
> ~15 KB. No virtual DOM. No compiler. No magic.
|
|
23
23
|
> Reactive UI that touches only the bytes that changed.
|
|
24
24
|
|
|
25
25
|
```ts
|
|
@@ -76,7 +76,7 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
|
|
|
76
76
|
|
|
77
77
|
## Why Kerf
|
|
78
78
|
|
|
79
|
-
1. **~
|
|
79
|
+
1. **~15 KB in the browser.** ~15 KB minified + gzipped including `@preact/signals-core` (~15 KB with `arraySignal`). The optional setup CLI has its own npm dependencies; they stay out of the browser bundle. No virtual DOM, no scheduler, no concurrent-mode machinery. On the official [krausest benchmark](https://krausest.github.io/js-framework-benchmark/current.html) kerf sits in the same cluster as Vue, Lit, and vanjs; Solid's compiler leads the update-path benchmarks, which kerf doesn't try to match by design — no compiler.
|
|
80
80
|
|
|
81
81
|
2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it _is_ the DOM.
|
|
82
82
|
|
package/ai/cursorrules
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
# These rules condense `docs/ai/usage-guide.md` into the form Cursor parses.
|
|
12
12
|
|
|
13
|
-
You are writing a UI in kerf — a ~
|
|
13
|
+
You are writing a UI in kerf — a ~15 KB reactive framework (still ~15 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler.
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
package/ai/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kerfjsVersion": "5.0.0-beta.
|
|
2
|
+
"kerfjsVersion": "5.0.0-beta.92",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"name": "skill",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"bundle": "ai/skill.md",
|
|
8
8
|
"dest": ".claude/skills/kerf-app/SKILL.md",
|
|
9
9
|
"version": "1.25.0",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "bc2622cb12b382118e44aa50d1afb058252e0caf85b7c0ced4433bc0ca76f7c1",
|
|
11
11
|
"history": {
|
|
12
12
|
"1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
|
|
13
13
|
"1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"1.23.0": "28b82b4d31b98814c8fbbe1cb0bcf716192deb494c020b9af41a30830e6a0007",
|
|
41
41
|
"1.23.1": "e21d7db1097ad95c11af9248ad17d514716bebc5646becf57d19a7598d6e382c",
|
|
42
42
|
"1.24.0": "f74616ec20120fa97ec2ff7e92971f5721ee8dcb0b5b7da2dd2604bb87002b69",
|
|
43
|
-
"1.25.0": "
|
|
43
|
+
"1.25.0": "bc2622cb12b382118e44aa50d1afb058252e0caf85b7c0ced4433bc0ca76f7c1"
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"bundle": "ai/cursorrules",
|
|
50
50
|
"dest": ".cursorrules",
|
|
51
51
|
"version": "1.23.0",
|
|
52
|
-
"sha256": "
|
|
52
|
+
"sha256": "dad6f677c85bf01760da6694afb7ead1439f0e1f0298ba8dac65de44ba031848",
|
|
53
53
|
"history": {
|
|
54
54
|
"1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
|
|
55
55
|
"1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"1.21.0": "a1b663b44650b1a04f89c60b26b002322fc6789650291477e14e13329c3ce25f",
|
|
81
81
|
"1.21.1": "9b3eb825a7bec05bb6f0138f77b45fadf896b83020178c34c5a2f531c38f4dee",
|
|
82
82
|
"1.22.0": "3515c663e908b2d6dc9a2a9cf42f2a5bf9a5b3a10c79d43e30cc8427e2ce209d",
|
|
83
|
-
"1.23.0": "
|
|
83
|
+
"1.23.0": "dad6f677c85bf01760da6694afb7ead1439f0e1f0298ba8dac65de44ba031848"
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
]
|
package/ai/skill.md
CHANGED
|
@@ -10,7 +10,7 @@ kerf-skill-version: 1.25.0
|
|
|
10
10
|
> project's `.claude/skills/kerf-app/SKILL.md`) so Claude Code activates
|
|
11
11
|
> it whenever you work on a kerf app.
|
|
12
12
|
|
|
13
|
-
kerf is a ~
|
|
13
|
+
kerf is a ~15 KB reactive UI framework (still ~15 KB with `arraySignal`): signals + DOM morphing + JSX → HTML strings. No virtual DOM, no compiler, no scheduler. The whole public surface fits in 17 exports.
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
package/dist/async.js
CHANGED
|
@@ -32,7 +32,7 @@ function resource(options = {}) {
|
|
|
32
32
|
const fetcher = maybeFetcher ?? inputOrFetcher;
|
|
33
33
|
const input = maybeFetcher === void 0 ? void 0 : inputOrFetcher;
|
|
34
34
|
const key = cacheKey !== void 0 && maybeFetcher !== void 0 ? cacheKey(input) : void 0;
|
|
35
|
-
const runningData = cacheKey !== void 0 ? key !== void 0 ? cache.get(key) : void 0 : state.
|
|
35
|
+
const runningData = cacheKey !== void 0 ? key !== void 0 ? cache.get(key) : void 0 : state.peek().data;
|
|
36
36
|
const gen = ++generation;
|
|
37
37
|
state.value = {
|
|
38
38
|
status: "running",
|
|
@@ -44,13 +44,13 @@ function resource(options = {}) {
|
|
|
44
44
|
};
|
|
45
45
|
const report = (completed, total) => {
|
|
46
46
|
if (gen === generation) {
|
|
47
|
-
state.value = { ...state.
|
|
47
|
+
state.value = { ...state.peek(), progress: { completed, total } };
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
const fail = (error) => {
|
|
51
51
|
if (gen === generation) {
|
|
52
52
|
state.value = {
|
|
53
|
-
...state.
|
|
53
|
+
...state.peek(),
|
|
54
54
|
status: "failed",
|
|
55
55
|
error,
|
|
56
56
|
progress: void 0,
|
package/dist/async.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":"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","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 *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\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, I = void> {\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 * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\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> = (\n report: (completed: number, total: number) => void,\n) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\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 /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\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, I = void>(\n options: ResourceOptions<T, I> = {},\n): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined\n ? lastData !== next\n : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as\n I | undefined;\n const key =\n cacheKey !== undefined && maybeFetcher !== undefined\n ? cacheKey(input as I)\n : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData =\n cacheKey !== undefined\n ? key !== undefined\n ? cache.get(key)\n : undefined\n : state.value.data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\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 const fail = (error: unknown): undefined => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = {\n ...state.value,\n status: 'failed',\n error,\n progress: undefined,\n input,\n };\n }\n return undefined;\n };\n\n let pending: Promise<T>;\n try {\n pending = fetcher(report);\n } catch (error: unknown) {\n return Promise.resolve(fail(error));\n }\n\n return pending.then((data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n }, fail);\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":";;;;AA0IO,SAAS,QAAA,CACd,OAAA,GAAiC,EAAC,EAClB;AAChB,EAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAO,GAAI,OAAA;AAC7B,EAAA,MAAM,EAAA,GAA8B,UAAU,MAAA,CAAO,EAAA;AACrD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAe;AAGjC,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,IAAI,QAAA;AACJ,EAAA,MAAM,UAAU,CAAC,IAAA;AAAA;AAAA,IAEf,QAAA,KAAa,UAAa,IAAA,KAAS,MAAA,GAC/B,aAAa,IAAA,GACb,CAAC,EAAA,CAAG,QAAA,EAAU,IAAI;AAAA,GAAA;AACxB,EAAA,MAAM,MAAA,GAAS,CAAC,IAAA,KAAgC;AAC9C,IAAA,IAAI,OAAA,CAAQ,IAAI,CAAA,EAAG;AACjB,MAAA,QAAA,EAAA;AACA,MAAA,QAAA,GAAW,IAAA;AAAA,IACb;AACA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,QAAQ,MAAA,CAA4B;AAAA,IACxC,MAAA,EAAQ,MAAA;AAAA,IACR,IAAA,EAAM,MAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,QAAA,EAAU,MAAA;AAAA,IACV,KAAA,EAAO,MAAA;AAAA,IACP,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,EAAA,SAAS,GAAA,CACP,gBACA,YAAA,EACwB;AAIxB,IAAA,MAAM,UAAW,YAAA,IAAgB,cAAA;AACjC,IAAA,MAAM,KAAA,GAAS,YAAA,KAAiB,MAAA,GAAY,MAAA,GAAY,cAAA;AAExD,IAAA,MAAM,MACJ,QAAA,KAAa,MAAA,IAAa,iBAAiB,MAAA,GACvC,QAAA,CAAS,KAAU,CAAA,GACnB,MAAA;AAIN,IAAA,MAAM,WAAA,GACJ,QAAA,KAAa,MAAA,GACT,GAAA,KAAQ,MAAA,GACN,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,GACb,MAAA,GACF,KAAA,CAAM,IAAA,EAAK,CAAE,IAAA;AAEnB,IAAA,MAAM,MAAM,EAAE,UAAA;AACd,IAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,MACZ,MAAA,EAAQ,SAAA;AAAA,MACR,IAAA,EAAM,WAAA;AAAA,MACN,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,MAAA;AAAA,MACV,KAAA;AAAA,MACA,QAAA,EAAU,OAAO,WAAW;AAAA,KAC9B;AAEA,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,IAAA,IAAQ,QAAA,EAAU,EAAE,SAAA,EAAW,KAAA,EAAM,EAAE;AAAA,MAClE;AAAA,IACF,CAAA;AAEA,IAAA,MAAM,IAAA,GAAO,CAAC,KAAA,KAA8B;AAC1C,MAAA,IAAI,QAAQ,UAAA,EAAY;AAEtB,QAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,UACZ,GAAG,MAAM,IAAA,EAAK;AAAA,UACd,MAAA,EAAQ,QAAA;AAAA,UACR,KAAA;AAAA,UACA,QAAA,EAAU,MAAA;AAAA,UACV;AAAA,SACF;AAAA,MACF;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAEA,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAU,QAAQ,MAAM,CAAA;AAAA,IAC1B,SAAS,KAAA,EAAgB;AACvB,MAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IACpC;AAEA,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,CAAC,IAAA,KAAS;AAC5B,MAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,QAAA,IAAI,GAAA,KAAQ,MAAA,EAAW,KAAA,CAAM,GAAA,CAAI,KAAK,IAAI,CAAA;AAC1C,QAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,UACZ,MAAA,EAAQ,WAAA;AAAA,UACR,IAAA;AAAA,UACA,KAAA,EAAO,MAAA;AAAA,UACP,QAAA,EAAU,MAAA;AAAA,UACV,KAAA;AAAA,UACA,QAAA,EAAU,OAAO,IAAI;AAAA,SACvB;AAAA,MACF;AACA,MAAA,OAAO,IAAA;AAAA,IACT,GAAG,IAAI,CAAA;AAAA,EACT;AAEA,EAAA,SAAS,KAAA,GAAc;AACrB,IAAA,UAAA,EAAA;AACA,IAAA,KAAA,CAAM,KAAA,EAAM;AACZ,IAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,MACZ,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,MAAA;AAAA,MACN,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,MAAA;AAAA,MACV,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,OAAO,MAAS;AAAA,KAC5B;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,KAAA,CAAM,KAAA;AAAA,IACf,CAAA;AAAA,IACA,GAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAA,EAAQ,CAAC,CAAA,KAAM,KAAA,CAAM,IAAI,CAAC,CAAA;AAAA,IAC1B,YAAY,MAAM,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAAA,IACzC,UAAA,EAAY,CAAC,CAAA,KAAM;AACjB,MAAA,IAAI,CAAA,KAAM,MAAA,EAAW,KAAA,CAAM,KAAA,EAAM;AAAA,WAC5B,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,IACrB;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 *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\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, I = void> {\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 * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\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> = (\n report: (completed: number, total: number) => void,\n) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\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 /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\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, I = void>(\n options: ResourceOptions<T, I> = {},\n): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined\n ? lastData !== next\n : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as\n I | undefined;\n const key =\n cacheKey !== undefined && maybeFetcher !== undefined\n ? cacheKey(input as I)\n : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData =\n cacheKey !== undefined\n ? key !== undefined\n ? cache.get(key)\n : undefined\n : state.peek().data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.peek(), progress: { completed, total } };\n }\n };\n\n const fail = (error: unknown): undefined => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = {\n ...state.peek(),\n status: 'failed',\n error,\n progress: undefined,\n input,\n };\n }\n return undefined;\n };\n\n let pending: Promise<T>;\n try {\n pending = fetcher(report);\n } catch (error: unknown) {\n return Promise.resolve(fail(error));\n }\n\n return pending.then((data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n }, fail);\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
// src/utils/isUserAgentOwnedAttr.ts
|
|
2
|
+
function isUserAgentOwnedAttr(tagNameUpper, name) {
|
|
3
|
+
if (tagNameUpper.includes("-")) {
|
|
4
|
+
return name === "open" || name === "role" || name.startsWith("aria-");
|
|
5
|
+
}
|
|
6
|
+
return name === "open" && (tagNameUpper === "DETAILS" || tagNameUpper === "DIALOG");
|
|
7
|
+
}
|
|
8
|
+
var RENDERED_ATTRS = /* @__PURE__ */ Symbol();
|
|
9
|
+
var CUSTOM_TAG = /<[a-z][^\s/>-]*-/i;
|
|
10
|
+
function hasCustomTag(html) {
|
|
11
|
+
return CUSTOM_TAG.test(html);
|
|
12
|
+
}
|
|
13
|
+
function seedRenderedAttrs(from, to = from) {
|
|
14
|
+
if (from.nodeType === 1) seedOne(from, to);
|
|
15
|
+
const a = from.querySelectorAll("*");
|
|
16
|
+
const b = to.querySelectorAll("*");
|
|
17
|
+
for (let i = 0; i < a.length; i++) seedOne(a[i], b[i]);
|
|
18
|
+
}
|
|
19
|
+
function seedOne(from, to) {
|
|
20
|
+
const tag = from.localName;
|
|
21
|
+
if (tag.includes("-") && to?.localName === tag) {
|
|
22
|
+
to[RENDERED_ATTRS] = from.getAttributeNames();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
// src/utils/row-contract.ts
|
|
2
27
|
var ROW_HTML_SNIPPET_MAX = 120;
|
|
3
28
|
function truncateRowHtml(html) {
|
|
@@ -23,12 +48,14 @@ function parseRowTemplate(html, parent) {
|
|
|
23
48
|
const wrapper = foreignWrapper(parent);
|
|
24
49
|
if (wrapper === null) {
|
|
25
50
|
tpl.innerHTML = html;
|
|
51
|
+
if (hasCustomTag(html)) seedRenderedAttrs(tpl.content);
|
|
26
52
|
return { content: tpl.content, count: tpl.content.children.length };
|
|
27
53
|
}
|
|
28
54
|
tpl.innerHTML = `<${wrapper}>${html}</${wrapper}>`;
|
|
29
55
|
const host = tpl.content.firstElementChild;
|
|
30
56
|
const content = document.createDocumentFragment();
|
|
31
57
|
while (host.firstChild !== null) content.appendChild(host.firstChild);
|
|
58
|
+
if (hasCustomTag(html)) seedRenderedAttrs(content);
|
|
32
59
|
return { content, count: content.children.length };
|
|
33
60
|
}
|
|
34
61
|
function parseSingleRow(html, index, parent) {
|
|
@@ -53,6 +80,6 @@ function rowContractError(index, html, parent) {
|
|
|
53
80
|
);
|
|
54
81
|
}
|
|
55
82
|
|
|
56
|
-
export { collectTemplateChildren, parseRowTemplate, parseSingleRow, rowContractError, truncateRowHtml };
|
|
83
|
+
export { RENDERED_ATTRS, collectTemplateChildren, hasCustomTag, isUserAgentOwnedAttr, parseRowTemplate, parseSingleRow, rowContractError, seedRenderedAttrs, truncateRowHtml };
|
|
57
84
|
|
|
58
|
-
//# sourceMappingURL=chunk-
|
|
85
|
+
//# sourceMappingURL=chunk-6LXUC4ZY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/isUserAgentOwnedAttr.ts","../src/utils/row-contract.ts"],"names":[],"mappings":"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","file":"chunk-6LXUC4ZY.js","sourcesContent":["/**\n * Attributes whose live value is owned by the element itself, not the\n * template: the reconcilers never REMOVE them just because the template omits\n * them (setting one from the template still applies). Shared by the morph and\n * the keyed-list fast path so both routes honor one rule.\n *\n * `open` qualifies on:\n * - `<details>` / `<dialog>` — the user agent toggles it on summary click and\n * on `show()` / `showModal()`.\n * - custom elements (any hyphenated tag) — Web Awesome's `wa-select`,\n * `wa-dropdown`, `wa-details`, `wa-dialog`, `wa-drawer`, `wa-popover`, …\n * reflect their live open state to `open`, so stripping it on an unrelated\n * re-render closed an open popup under the user.\n *\n * `role` and every `aria-*` qualify on custom elements (any hyphenated tag).\n * Web components conventionally own their host's ARIA — Web Awesome's\n * `wa-option` sets `role=\"option\"` / `aria-selected` / `aria-disabled` from its\n * live state, `wa-divider` sets `role=\"separator\"` / `aria-orientation`, and\n * Shoelace, FAST, and Spectrum do the same in `connectedCallback` / `updated`\n * — so a template that never mentions them must not strip them. Plain\n * elements keep the ordinary rule: a `role` / `aria-*` the template omits is\n * removed. `tabindex` is deliberately NOT included (too often template-driven).\n *\n * Trade-off (same as `<details open>`): a template cannot REMOVE one of these\n * from a custom element by omitting it — render an explicit value instead\n * (`aria-expanded=\"false\"`), bind it to a signal (a binding set to\n * `null` / `false` removes it; bindings are explicit intent and do not consult\n * this rule), or remove it imperatively.\n */\nexport function isUserAgentOwnedAttr(\n tagNameUpper: string,\n name: string,\n): boolean {\n if (tagNameUpper.includes('-')) {\n return name === 'open' || name === 'role' || name.startsWith('aria-');\n }\n return (\n name === 'open' && (tagNameUpper === 'DETAILS' || tagNameUpper === 'DIALOG')\n );\n}\n\n/**\n * Expando on a custom element (any hyphenated tag): the attribute names the\n * template rendered onto it the last time a reconciler synced its attributes.\n * Custom elements reflect their own defaults onto the host (Web Awesome's\n * `wa-button` sets `variant=\"neutral\"`, `wa-tag` sets `size=\"m\"`, …), so the\n * reconcilers only REMOVE an attribute the template omits when the previous\n * template rendered it. Removing a reflected default made the element put it\n * straight back, so every unrelated re-render forced an update on every such\n * host. Elements the reconcilers create are seeded with their template's\n * names before they connect (`seedRenderedAttrs`); an element with no record\n * (one kerf did not create, or one the keyed-list fast path last wrote) gets\n * the ordinary rule on its next sync. Stored on the element, so it lives\n * exactly as long as the element.\n */\nexport const RENDERED_ATTRS: unique symbol = Symbol();\n\n/** A live element that may carry the {@link RENDERED_ATTRS} record. */\nexport type RenderedAttrsHost = Element & {\n [RENDERED_ATTRS]?: readonly string[];\n};\n\n/** Markup that opens a custom element (a hyphenated tag name). */\nconst CUSTOM_TAG = /<[a-z][^\\s/>-]*-/i;\n\n/** Whether `html` may contain a custom element worth seeding. */\nexport function hasCustomTag(html: string): boolean {\n return CUSTOM_TAG.test(html);\n}\n\n/**\n * Seed the {@link RENDERED_ATTRS} record on every custom element under\n * `from` — and on `from` itself when it is an element — so a freshly created\n * element's first sync already knows what its template rendered and leaves\n * reflected defaults alone. Must run before the nodes connect (and so before\n * the element can reflect anything): the names come from the template.\n * `to` is the live copy when the reconciler inserts a clone of `from`; the\n * two subtrees are walked in parallel and an element whose tag does not line\n * up is left unseeded (the ordinary rule then applies once).\n */\nexport function seedRenderedAttrs(\n from: Element | DocumentFragment,\n to: Element | DocumentFragment = from,\n): void {\n if (from.nodeType === 1) seedOne(from as Element, to as Element);\n const a = from.querySelectorAll('*');\n const b = to.querySelectorAll('*');\n for (let i = 0; i < a.length; i++) seedOne(a[i], b[i]);\n}\n\nfunction seedOne(from: Element, to: Element | undefined): void {\n const tag = from.localName;\n if (tag.includes('-') && to?.localName === tag) {\n (to as RenderedAttrsHost)[RENDERED_ATTRS] = from.getAttributeNames();\n }\n}\n","/**\n * Shared \"exactly one top-level element per row\" contract helpers (KF-103).\n *\n * The keyed-list reconciler in `list-reconcile.ts` and the first-render\n * binding path in `mount.ts` both enforce the same contract on `each()`\n * row HTML. Centralizing the constants and helpers here keeps the three\n * call sites that build error snippets and the eight call sites that\n * parse a row's HTML into a template element from drifting (KF-111 +\n * KF-115).\n */\n\nimport { hasCustomTag, seedRenderedAttrs } from './isUserAgentOwnedAttr.js';\n\n/**\n * Truncation limit applied to row HTML when including it in a contract-\n * violation error message. Keeps the error readable when a row produced\n * megabytes of HTML by accident.\n */\nexport const ROW_HTML_SNIPPET_MAX = 120;\n\n/**\n * Truncate a row's HTML for inclusion in an error message. Returns the\n * raw string if it's short enough; otherwise the first\n * `ROW_HTML_SNIPPET_MAX` characters with a trailing ellipsis.\n */\nexport function truncateRowHtml(html: string): string {\n return html.length > ROW_HTML_SNIPPET_MAX\n ? html.slice(0, ROW_HTML_SNIPPET_MAX) + '…'\n : html;\n}\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\nconst MATHML_NS = 'http://www.w3.org/1998/Math/MathML';\n\n/**\n * HTML integration points: foreign-content elements under which the HTML parser\n * UNCONDITIONALLY re-enters HTML content, so an `each()` row parented there is\n * ordinary HTML and must NOT be wrapped. SVG has three (`foreignObject`, `desc`,\n * `title`); MathML has the five text integration points (`mi`, `mo`, `mn`, `ms`,\n * `mtext`). localNames are case-preserving for both namespaces, so `foreignObject`\n * matches as authored.\n *\n * `annotation-xml` is deliberately absent: it is a MathML HTML integration point\n * only CONDITIONALLY (with `encoding=\"text/html\"`/`application/xhtml+xml`),\n * otherwise its children are MathML. That conditional rule for a rare element is\n * out of scope — a MathML parent not in the text set always wraps in `<math>`.\n */\nconst SVG_HTML_INTEGRATION = new Set(['foreignObject', 'desc', 'title']);\nconst MATHML_TEXT_INTEGRATION = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']);\n\n/**\n * The foreign-content wrapper tag a row destined for `parent` must be parsed\n * inside — `'svg'` (KF-389) or `'math'` (KF-417) — or `null` for the ordinary\n * HTML path.\n *\n * A bare `<template>` parses in the HTML namespace, so an `each()` row like\n * `<circle/>` or `<mn/>` comes back an `HTMLUnknownElement` and never paints as\n * SVG/MathML. First render escapes this because rows are inlined into the mount\n * root's `innerHTML`, where the surrounding `<svg>`/`<math>` puts the parser in\n * foreign-content mode; every LATER parse (granular insert, snapshot rebuild,\n * structural update) goes through the bare template and produces dead nodes.\n * `parseRowTemplate` re-enters foreign content by wrapping the row HTML in this\n * tag, so those parses namespace the rows the same way the first render did.\n *\n * A parent that is itself an HTML integration point (see the sets above) takes\n * the ordinary HTML path — wrapping HTML rows in `<svg>`/`<math>` there would\n * either crash (a breakout tag pops the wrapper, emptying it) or mis-namespace\n * the row (KF-420).\n */\nfunction foreignWrapper(\n parent: Element | null | undefined,\n): 'svg' | 'math' | null {\n if (parent == null) return null;\n const { namespaceURI, localName } = parent;\n if (namespaceURI === SVG_NS) {\n return SVG_HTML_INTEGRATION.has(localName) ? null : 'svg';\n }\n if (namespaceURI === MATHML_NS) {\n return MATHML_TEXT_INTEGRATION.has(localName) ? null : 'math';\n }\n return null;\n}\n\n/**\n * Parse a row's HTML and report how many top-level elements it produced. The\n * reconciler uses the count to detect contract violations (count !== 1) on\n * both single-row and bulk-row paths.\n *\n * `parent` is the list's live parent when the caller knows it; it selects the\n * parse namespace (KF-389). Callers that only need the element COUNT for an\n * error message may omit it — the count is namespace-independent.\n *\n * Returns a `DocumentFragment` either way, so callers insert it identically.\n */\nexport function parseRowTemplate(\n html: string,\n parent?: Element | null,\n): { content: DocumentFragment; count: number } {\n const tpl = document.createElement('template');\n const wrapper = foreignWrapper(parent);\n if (wrapper === null) {\n tpl.innerHTML = html;\n if (hasCustomTag(html)) seedRenderedAttrs(tpl.content);\n return { content: tpl.content, count: tpl.content.children.length };\n }\n // Re-enter foreign content the same way the first render does: wrap in an\n // <svg>/<math> so the HTML parser namespaces the rows, then lift them out.\n // Using the parser (rather than DOMParser/XML) keeps row markup as forgiving\n // here as it is on first render — the two paths must accept the same input.\n tpl.innerHTML = `<${wrapper}>${html}</${wrapper}>`;\n const host = tpl.content.firstElementChild as Element;\n const content = document.createDocumentFragment();\n while (host.firstChild !== null) content.appendChild(host.firstChild);\n if (hasCustomTag(html)) seedRenderedAttrs(content);\n return { content, count: content.children.length };\n}\n\n/**\n * Parse one row's HTML to its single top-level element, enforcing the\n * \"exactly one top-level element per row\" contract with a row-precise error.\n * Shared by the granular reconciler's single-row paths and the snapshot\n * in-place morph path.\n */\nexport function parseSingleRow(\n html: string,\n index: number,\n parent?: Element | null,\n): Element {\n const { content, count } = parseRowTemplate(html, parent);\n if (count !== 1) throw rowContractError(index, html, parent);\n return content.firstElementChild as Element;\n}\n\n/**\n * Capture the first `n` element children of a parsed row fragment into an\n * array BEFORE a fragment insert empties it. Callers have already verified the\n * parse count equals `n`, so the walk never sees a null.\n */\nexport function collectTemplateChildren(\n content: DocumentFragment,\n n: number,\n): Element[] {\n const nodes = new Array<Element>(n);\n let child = content.firstElementChild;\n for (let k = 0; k < n; k++) {\n nodes[k] = child as Element;\n child = (child as Element).nextElementSibling;\n }\n return nodes;\n}\n\n/**\n * Build a precise contract-violation `Error` for the row at `index` whose\n * render produced the given `html`. The thrown message mentions the row\n * index, the actual element count, and the (truncated) HTML so the author\n * can locate and fix the offending row quickly.\n *\n * `parent` selects the parse namespace so the reported count matches what the\n * reconciler actually saw (KF-420): re-parsing without it could count a row\n * differently under a foreign-content parent and print a self-contradictory\n * \"produced 1 top-level elements; exactly one is required\".\n */\nexport function rowContractError(\n index: number,\n html: string,\n parent?: Element | null,\n): Error {\n const { count } = parseRowTemplate(html, parent);\n const reason =\n count === 0\n ? 'produced no top-level element'\n : `produced ${count} top-level elements; exactly one is required`;\n return new Error(\n `each(): row render at index ${index} ${reason}. ` +\n \"Each item's render must return exactly one element — \" +\n 'wrap multiple roots in a single parent (e.g. <li>...</li>). ' +\n `Got HTML: ${JSON.stringify(truncateRowHtml(html))}`,\n );\n}\n"]}
|