kerfjs 4.4.0 → 4.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +284 -104
- package/LICENSE +1 -1
- package/README.md +73 -54
- package/ai/cursorrules +130 -48
- package/ai/manifest.json +63 -5
- package/ai/skill.md +148 -65
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +4 -4
- package/dist/actions.js.map +1 -1
- package/dist/array-signal.js +5 -5
- package/dist/async.js +34 -24
- package/dist/async.js.map +1 -1
- package/dist/attach.d.ts +11 -8
- package/dist/attach.js +54 -4
- package/dist/attach.js.map +1 -1
- package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +4 -3
- package/dist/chunk-5WRGJZV6.js.map +1 -0
- package/dist/{chunk-GY4XV2UV.js → chunk-BDX3R4OM.js} +4 -3
- package/dist/chunk-BDX3R4OM.js.map +1 -0
- package/dist/{chunk-VVDJLWMP.js → chunk-CEQMZYLR.js} +2 -2
- package/dist/chunk-CEQMZYLR.js.map +1 -0
- package/dist/{chunk-KEZTD6H4.js → chunk-E5R5GNKE.js} +15 -5
- package/dist/chunk-E5R5GNKE.js.map +1 -0
- package/dist/{chunk-MRYM3O3V.js → chunk-ELXVRKY2.js} +12 -9
- package/dist/chunk-ELXVRKY2.js.map +1 -0
- package/dist/{chunk-U32TFTGZ.js → chunk-MK42GLPV.js} +3 -3
- package/dist/chunk-MK42GLPV.js.map +1 -0
- package/dist/{chunk-SUPUPSBE.js → chunk-QFUNWHKH.js} +42 -20
- package/dist/chunk-QFUNWHKH.js.map +1 -0
- package/dist/{chunk-SRWQKB33.js → chunk-V757BT6U.js} +206 -111
- package/dist/chunk-V757BT6U.js.map +1 -0
- package/dist/{chunk-YHH7OUFA.js → chunk-WKIPLNVO.js} +3 -3
- package/dist/chunk-WKIPLNVO.js.map +1 -0
- package/dist/{chunk-3APBEVHF.js → chunk-Y2FOYPBV.js} +3 -3
- package/dist/{chunk-3APBEVHF.js.map → chunk-Y2FOYPBV.js.map} +1 -1
- package/dist/{chunk-SAYPJ6XR.js → chunk-ZOIERTUW.js} +10 -6
- package/dist/chunk-ZOIERTUW.js.map +1 -0
- package/dist/dev.d.ts +9 -6
- package/dist/dev.js +66 -19
- package/dist/dev.js.map +1 -1
- package/dist/html.d.ts +1 -1
- package/dist/html.js +8 -7
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -19
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.js +5 -5
- package/dist/list.d.ts +1 -1
- package/dist/list.js +304 -213
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +190 -280
- package/dist/overlay.js +471 -364
- package/dist/overlay.js.map +1 -1
- package/dist/remount.d.ts +5 -3
- package/dist/remount.js +29 -10
- package/dist/remount.js.map +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/router.js +73 -28
- package/dist/router.js.map +1 -1
- package/dist/scope.d.ts +4 -3
- package/dist/scope.js +11 -10
- package/dist/scope.js.map +1 -1
- package/dist/testing.js +4 -4
- package/dist/timing.d.ts +3 -2
- package/dist/timing.js +5 -5
- package/dist/timing.js.map +1 -1
- package/llms.txt +13 -6
- package/package.json +35 -13
- package/setup/cli.mjs +90 -0
- package/setup/index.d.mts +37 -0
- package/setup/index.mjs +1347 -0
- package/setup/jsonc.mjs +201 -0
- package/setup/state.schema.json +56 -0
- package/dist/chunk-GY4XV2UV.js.map +0 -1
- package/dist/chunk-KEZTD6H4.js.map +0 -1
- package/dist/chunk-MRYM3O3V.js.map +0 -1
- package/dist/chunk-QIP723L4.js.map +0 -1
- package/dist/chunk-SAYPJ6XR.js.map +0 -1
- package/dist/chunk-SRWQKB33.js.map +0 -1
- package/dist/chunk-SUPUPSBE.js.map +0 -1
- package/dist/chunk-U32TFTGZ.js.map +0 -1
- package/dist/chunk-VVDJLWMP.js.map +0 -1
- package/dist/chunk-YHH7OUFA.js.map +0 -1
- /package/dist/{attrSelector-Cmu2ZoGO.d.ts → attr-Cmu2ZoGO.d.ts} +0 -0
package/dist/router.js
CHANGED
|
@@ -1,33 +1,58 @@
|
|
|
1
|
-
import { delegate } from './chunk-
|
|
2
|
-
import { jsx } from './chunk-
|
|
3
|
-
import { signal, computed } from './chunk-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { delegate } from './chunk-E5R5GNKE.js';
|
|
2
|
+
import { jsx } from './chunk-QFUNWHKH.js';
|
|
3
|
+
import { signal, computed } from './chunk-Y2FOYPBV.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
6
|
|
|
7
7
|
// src/router.ts
|
|
8
8
|
function matchPattern(pattern, path) {
|
|
9
9
|
if (pattern === "*") return {};
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const patternSegments = pattern.split("/").filter(Boolean);
|
|
11
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
12
12
|
const params = {};
|
|
13
|
-
for (let i = 0; i <
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
const name =
|
|
17
|
-
if (name.length > 0)
|
|
13
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
14
|
+
const patternSegment = patternSegments[i];
|
|
15
|
+
if (patternSegment.startsWith("*")) {
|
|
16
|
+
const name = patternSegment.slice(1);
|
|
17
|
+
if (name.length > 0) {
|
|
18
|
+
const decoded = [];
|
|
19
|
+
for (const part of pathSegments.slice(i)) {
|
|
20
|
+
const value = decodePathSegment(part);
|
|
21
|
+
if (value === null) return null;
|
|
22
|
+
decoded.push(value);
|
|
23
|
+
}
|
|
24
|
+
params[name] = decoded.join("/");
|
|
25
|
+
}
|
|
18
26
|
return params;
|
|
19
27
|
}
|
|
20
|
-
if (i >=
|
|
21
|
-
if (
|
|
22
|
-
|
|
28
|
+
if (i >= pathSegments.length) return null;
|
|
29
|
+
if (patternSegment.startsWith(":")) {
|
|
30
|
+
const value = decodePathSegment(pathSegments[i]);
|
|
31
|
+
if (value === null) return null;
|
|
32
|
+
params[patternSegment.slice(1)] = value;
|
|
23
33
|
continue;
|
|
24
34
|
}
|
|
25
|
-
if (
|
|
35
|
+
if (patternSegment !== pathSegments[i]) return null;
|
|
36
|
+
}
|
|
37
|
+
return pathSegments.length === patternSegments.length ? params : null;
|
|
38
|
+
}
|
|
39
|
+
function decodePathSegment(segment) {
|
|
40
|
+
try {
|
|
41
|
+
return decodeURIComponent(segment);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
26
44
|
}
|
|
27
|
-
|
|
45
|
+
}
|
|
46
|
+
function isWithinBase(path, base) {
|
|
47
|
+
return base.length === 0 || path === base || path.startsWith(base + "/");
|
|
28
48
|
}
|
|
29
49
|
function createRouter(options) {
|
|
30
|
-
const {
|
|
50
|
+
const {
|
|
51
|
+
routes,
|
|
52
|
+
mode = "history",
|
|
53
|
+
base = "",
|
|
54
|
+
interceptLinks = true
|
|
55
|
+
} = options;
|
|
31
56
|
const normBase = base === "/" ? "" : base.replace(/\/$/, "");
|
|
32
57
|
let matched = null;
|
|
33
58
|
const readLocation = () => {
|
|
@@ -42,7 +67,9 @@ function createRouter(options) {
|
|
|
42
67
|
hash = "";
|
|
43
68
|
} else {
|
|
44
69
|
path = location.pathname;
|
|
45
|
-
if (normBase.length > 0 &&
|
|
70
|
+
if (normBase.length > 0 && isWithinBase(path, normBase)) {
|
|
71
|
+
path = path.slice(normBase.length) || "/";
|
|
72
|
+
}
|
|
46
73
|
query = new URLSearchParams(location.search);
|
|
47
74
|
hash = location.hash;
|
|
48
75
|
}
|
|
@@ -69,7 +96,11 @@ function createRouter(options) {
|
|
|
69
96
|
};
|
|
70
97
|
const navigate = (path, opts = {}) => {
|
|
71
98
|
const url = mode === "hash" ? "#" + (path.startsWith("/") ? path : "/" + path) : normBase + (path.startsWith("/") ? path : "/" + path);
|
|
72
|
-
history[opts.replace === true ? "replaceState" : "pushState"](
|
|
99
|
+
history[opts.replace === true ? "replaceState" : "pushState"](
|
|
100
|
+
opts.state ?? null,
|
|
101
|
+
"",
|
|
102
|
+
url
|
|
103
|
+
);
|
|
73
104
|
route.value = resolve(readLocation());
|
|
74
105
|
};
|
|
75
106
|
const match = (pattern) => computed(() => {
|
|
@@ -100,9 +131,11 @@ function createRouter(options) {
|
|
|
100
131
|
}
|
|
101
132
|
if (interceptLinks && typeof document !== "undefined") {
|
|
102
133
|
const onClick = (event, anchor) => {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
|
|
134
|
+
const mouseEvent = event;
|
|
135
|
+
if (mouseEvent.defaultPrevented || mouseEvent.button !== 0 || mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.altKey)
|
|
136
|
+
return;
|
|
137
|
+
if (anchor.hasAttribute("download") || anchor.hasAttribute("data-router-ignore"))
|
|
138
|
+
return;
|
|
106
139
|
const target = anchor.getAttribute("target");
|
|
107
140
|
if (target !== null && target !== "" && target !== "_self") return;
|
|
108
141
|
const rel = anchor.getAttribute("rel");
|
|
@@ -110,16 +143,19 @@ function createRouter(options) {
|
|
|
110
143
|
const url = new URL(anchor.href, location.href);
|
|
111
144
|
if (url.origin !== location.origin) return;
|
|
112
145
|
if (mode === "history") {
|
|
113
|
-
if (
|
|
146
|
+
if (!isWithinBase(url.pathname, normBase)) return;
|
|
114
147
|
event.preventDefault();
|
|
115
148
|
navigate(url.pathname.slice(normBase.length) + url.search + url.hash);
|
|
116
149
|
} else {
|
|
117
|
-
if (url.pathname !== location.pathname || !url.hash.startsWith("#/"))
|
|
150
|
+
if (url.pathname !== location.pathname || !url.hash.startsWith("#/"))
|
|
151
|
+
return;
|
|
118
152
|
event.preventDefault();
|
|
119
153
|
navigate(url.hash.slice(1));
|
|
120
154
|
}
|
|
121
155
|
};
|
|
122
|
-
removers.push(
|
|
156
|
+
removers.push(
|
|
157
|
+
delegate(document.body, "click", "a[href]", onClick)
|
|
158
|
+
);
|
|
123
159
|
}
|
|
124
160
|
let disposed = false;
|
|
125
161
|
const dispose = () => {
|
|
@@ -127,9 +163,18 @@ function createRouter(options) {
|
|
|
127
163
|
disposed = true;
|
|
128
164
|
for (const remove of removers) remove();
|
|
129
165
|
};
|
|
130
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
route,
|
|
168
|
+
navigate,
|
|
169
|
+
back: () => history.back(),
|
|
170
|
+
forward: () => history.forward(),
|
|
171
|
+
match,
|
|
172
|
+
activeClass,
|
|
173
|
+
outlet,
|
|
174
|
+
dispose
|
|
175
|
+
};
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
export { createRouter };
|
|
134
|
-
|
|
179
|
+
|
|
135
180
|
//# sourceMappingURL=router.js.map
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/router.ts"],"names":["base"],"mappings":"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","file":"router.js","sourcesContent":["/**\n * `kerfjs/router` — the \"postcard router\": the smallest client-side router that\n * is still a router. The kerf *core* stays router-free (docs/1 \"Not a router\" is\n * about the runtime); this is an opt-in, tree-shakeable subpath, on the same\n * footing as `kerfjs/list` / `kerfjs/overlay` — it adds nothing to the main\n * barrel until you import it.\n *\n * import { createRouter } from 'kerfjs/router';\n *\n * const router = createRouter({\n * routes: [\n * { path: '/', component: () => <Home /> },\n * { path: '/users/:id', component: ({ id }) => <User id={id} /> },\n * { path: '*', component: () => <NotFound /> }, // catch-all\n * ],\n * });\n *\n * mount(app, () => (\n * <div>\n * <nav> … in-app links here are auto-intercepted … </nav>\n * {router.outlet()} // renders the matched route's component\n * </div>\n * ));\n *\n * The whole model is three moving parts kerf already has: a `signal` for the\n * current route, `delegate()` for link interception, and the keyed morph for the\n * outlet (a route change swaps the page wholesale; a param change updates in\n * place). Everything a full framework's router adds — nested layouts, data\n * loaders, lazy routes, guards, SSR matching — is deliberately OUT of scope;\n * compose those with kerf primitives (an `effect` on `router.route`, a `resource`\n * from `kerfjs/async`) when you need them.\n */\nimport { delegate } from './delegate.js';\nimport { jsx } from './jsx-runtime.js';\nimport { type MountResult } from './mount.js';\nimport { computed, type ReadonlySignal, signal } from './reactive.js';\n\n/** The reactive current-route snapshot (`router.route.value`). */\nexport interface RouteState {\n /** The matched pathname, base stripped (history mode) or the hash body (hash mode). Always starts with `/`. */\n path: string;\n /** Path parameters from the matched pattern — `/users/:id` on `/users/7` → `{ id: '7' }`. */\n params: Record<string, string>;\n /** The parsed query string (`?a=1` → `URLSearchParams`). Empty when there is none. */\n query: URLSearchParams;\n /** The raw location hash including `#` (history mode), or `''`. In hash mode the hash IS the route, so this is `''`. */\n hash: string;\n}\n\n/** A route's view: receives the matched `params` and the full `route` snapshot, returns kerf content. */\nexport type RouteComponent = (params: Record<string, string>, route: RouteState) => MountResult;\n\n/** One route in the table. `path` is a pattern: `/`, `/users/:id`, `/files/*rest`, or `*` (catch-all). */\nexport interface RouteDef {\n /** Pattern: static segments, `:param` captures, a trailing `*rest` wildcard, or `*` (matches anything — put last). */\n path: string;\n /** The view rendered in the outlet when this route matches. */\n component: RouteComponent;\n}\n\n/** Options for {@link navigate}. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. Default `false`. */\n replace?: boolean;\n /** Arbitrary state stored on the history entry (readable via `history.state`). */\n state?: unknown;\n}\n\n/** Options for {@link createRouter}. */\nexport interface RouterOptions {\n /** The route table, tried in order; the first match wins. Include a `path: '*'` entry last for a fallback. */\n routes: readonly RouteDef[];\n /**\n * `'history'` (default) uses the real pathname (`/users/7`) via the History\n * API; `'hash'` keeps the route after `#` (`#/users/7`) for static hosts with\n * no server rewrite.\n */\n mode?: 'history' | 'hash';\n /** History mode only: a base path every route sits under (`/app`), stripped from `route.path` and prepended on navigation. */\n base?: string;\n /**\n * Auto-intercept clicks on in-app `<a href>` links (same-origin, left-click, no\n * modifier keys / `target` / `download`) and route them instead of reloading.\n * Opt a single link out with `data-router-ignore` or `rel=\"external\"`. Default\n * `true`; set `false` to wire navigation entirely yourself.\n */\n interceptLinks?: boolean;\n}\n\n/** The handle {@link createRouter} returns. Holds no module-global state — it's a closure. */\nexport interface RouterHandle {\n /** The reactive current route. Read `.value` (tracked) in a render / `computed` / `effect`. */\n route: ReadonlySignal<RouteState>;\n /** Navigate to `path` (may include `?query` / `#hash`). Pushes history (or replaces, per options). */\n navigate: (path: string, options?: NavigateOptions) => void;\n /** History back — `history.back()`. */\n back: () => void;\n /** History forward — `history.forward()`. */\n forward: () => void;\n /**\n * A reactive \"is this path active?\" — true when the current path equals\n * `pattern` or is nested under it (`match('/users')` is true on `/users/7`).\n * `match('/')` is exact (only true on `/`). Bind it for active-nav styling.\n */\n match: (pattern: string) => ReadonlySignal<boolean>;\n /** Convenience: a bound class signal — `className` while {@link match}`(pattern)` is active, else `''`. */\n activeClass: (pattern: string, className: string) => ReadonlySignal<string>;\n /**\n * The routed view. Call it inside a `mount()` render: it renders the matched\n * route's component in a keyed wrapper, so a route change swaps the page\n * wholesale (fresh DOM) while a param change updates it in place.\n */\n outlet: () => MountResult;\n /** Tear down the popstate / link listeners. Idempotent. */\n dispose: () => void;\n}\n\n/**\n * Match `path` against a route `pattern`. Returns the captured params on a match,\n * or `null` on no match. `*` matches anything; a trailing `*name` captures the\n * remaining segments joined by `/`; `:name` captures one segment.\n */\nfunction matchPattern(pattern: string, path: string): Record<string, string> | null {\n if (pattern === '*') return {};\n const pp = pattern.split('/').filter(Boolean);\n const ps = path.split('/').filter(Boolean);\n const params: Record<string, string> = {};\n for (let i = 0; i < pp.length; i++) {\n const seg = pp[i];\n if (seg.startsWith('*')) {\n // Wildcard rest — consumes every remaining segment.\n const name = seg.slice(1);\n if (name.length > 0) params[name] = ps.slice(i).map(decodeURIComponent).join('/');\n return params;\n }\n if (i >= ps.length) return null;\n if (seg.startsWith(':')) {\n params[seg.slice(1)] = decodeURIComponent(ps[i]);\n continue;\n }\n if (seg !== ps[i]) return null;\n }\n // No wildcard matched, so the segment counts must be exactly equal.\n return ps.length === pp.length ? params : null;\n}\n\n/**\n * Create a router bound to the browser history. Reads the current location\n * immediately (so `route.value` is correct before first paint), installs a\n * `popstate` listener (+ `hashchange` in hash mode) and, unless disabled, a\n * single delegated link interceptor. See {@link RouterOptions} / {@link RouterHandle}.\n */\nexport function createRouter(options: RouterOptions): RouterHandle {\n const { routes, mode = 'history', base = '', interceptLinks = true } = options;\n // Normalize base to '' or '/foo' (no trailing slash), so `base + path` is clean.\n const normBase = base === '/' ? '' : base.replace(/\\/$/, '');\n\n // The current matched route def, kept in step with the signal so `outlet()`\n // doesn't have to re-match — it reads `route.value` only to subscribe.\n let matched: { def: RouteDef; params: Record<string, string> } | null = null;\n\n const readLocation = (): RouteState => {\n let path: string;\n let query: URLSearchParams;\n let hash: string;\n if (mode === 'hash') {\n // Everything after '#': '#/users/7?a=1' → path '/users/7', query 'a=1'.\n const raw = location.hash.slice(1) || '/';\n const qIndex = raw.indexOf('?');\n path = qIndex === -1 ? raw : raw.slice(0, qIndex);\n query = new URLSearchParams(qIndex === -1 ? '' : raw.slice(qIndex + 1));\n hash = '';\n } else {\n path = location.pathname;\n if (normBase.length > 0 && path.startsWith(normBase)) path = path.slice(normBase.length) || '/';\n query = new URLSearchParams(location.search);\n hash = location.hash;\n }\n if (!path.startsWith('/')) path = '/' + path;\n return { path, params: {}, query, hash };\n };\n\n const resolve = (state: RouteState): RouteState => {\n for (const def of routes) {\n const params = matchPattern(def.path, state.path);\n if (params !== null) {\n matched = { def, params };\n return { ...state, params };\n }\n }\n matched = null;\n return state;\n };\n\n const route = signal<RouteState>(resolve(readLocation()));\n\n // Re-read location → re-match → publish. Only writes when the path actually\n // changed, so a doubled popstate/hashchange is a harmless no-op.\n const sync = (): void => {\n const next = resolve(readLocation());\n if (next.path !== route.value.path || next.query.toString() !== route.value.query.toString()\n || next.hash !== route.value.hash) {\n route.value = next;\n }\n };\n\n const navigate = (path: string, opts: NavigateOptions = {}): void => {\n const url = mode === 'hash'\n ? '#' + (path.startsWith('/') ? path : '/' + path)\n : normBase + (path.startsWith('/') ? path : '/' + path);\n // pushState/replaceState do NOT fire popstate, so publish the new route ourselves.\n history[opts.replace === true ? 'replaceState' : 'pushState'](opts.state ?? null, '', url);\n route.value = resolve(readLocation());\n };\n\n const match = (pattern: string): ReadonlySignal<boolean> =>\n computed(() => {\n const p = route.value.path;\n if (pattern === '/') return p === '/';\n const base = pattern.replace(/\\/$/, '');\n return p === base || p.startsWith(base + '/');\n });\n\n const activeClass = (pattern: string, className: string): ReadonlySignal<string> => {\n const active = match(pattern);\n return computed(() => (active.value ? className : ''));\n };\n\n const outlet = (): MountResult => {\n void route.value; // tracked read — subscribes the enclosing mount to navigation\n if (matched === null) return null;\n // Keyed by the route PATTERN: a different pattern → the morph replaces the\n // wrapper wholesale (fresh DOM for the new page); the same pattern (only the\n // params changed) → the morph reconciles the children in place.\n return jsx('div', {\n 'data-router-outlet': '',\n 'data-key': matched.def.path,\n children: matched.def.component(matched.params, route.value),\n });\n };\n\n // --- Listeners ---------------------------------------------------------\n const removers: Array<() => void> = [];\n globalThis.addEventListener('popstate', sync);\n removers.push(() => globalThis.removeEventListener('popstate', sync));\n if (mode === 'hash') {\n globalThis.addEventListener('hashchange', sync);\n removers.push(() => globalThis.removeEventListener('hashchange', sync));\n }\n\n if (interceptLinks && typeof document !== 'undefined') {\n const onClick = (event: Event, anchor: HTMLAnchorElement): void => {\n const e = event as MouseEvent;\n // Let the browser handle anything that isn't a plain left-click navigation.\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n if (anchor.hasAttribute('download') || anchor.hasAttribute('data-router-ignore')) return;\n const target = anchor.getAttribute('target');\n if (target !== null && target !== '' && target !== '_self') return;\n const rel = anchor.getAttribute('rel');\n if (rel !== null && /\\bexternal\\b/.test(rel)) return;\n const url = new URL(anchor.href, location.href);\n if (url.origin !== location.origin) return;\n if (mode === 'history') {\n if (normBase.length > 0 && !url.pathname.startsWith(normBase)) return; // outside the app's base\n event.preventDefault();\n navigate(url.pathname.slice(normBase.length) + url.search + url.hash);\n } else {\n // Hash mode: only intercept in-app hash links (`#/...`), leave others alone.\n if (url.pathname !== location.pathname || !url.hash.startsWith('#/')) return;\n event.preventDefault();\n navigate(url.hash.slice(1));\n }\n };\n removers.push(delegate<HTMLAnchorElement>(document.body, 'click', 'a[href]', onClick));\n }\n\n let disposed = false;\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n for (const remove of removers) remove();\n };\n\n return { route, navigate, back: () => history.back(), forward: () => history.forward(), match, activeClass, outlet, dispose };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/router.ts"],"names":["base"],"mappings":"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","file":"router.js","sourcesContent":["/**\n * `kerfjs/router` — the \"postcard router\": the smallest client-side router that\n * is still a router. The kerf *core* stays router-free (docs/1 \"Not a router\" is\n * about the runtime); this is an opt-in, tree-shakeable subpath, on the same\n * footing as `kerfjs/list` / `kerfjs/overlay` — it adds nothing to the main\n * barrel until you import it.\n *\n * import { createRouter } from 'kerfjs/router';\n *\n * const router = createRouter({\n * routes: [\n * { path: '/', component: () => <Home /> },\n * { path: '/users/:id', component: ({ id }) => <User id={id} /> },\n * { path: '*', component: () => <NotFound /> }, // catch-all\n * ],\n * });\n *\n * mount(app, () => (\n * <div>\n * <nav> … in-app links here are auto-intercepted … </nav>\n * {router.outlet()} // renders the matched route's component\n * </div>\n * ));\n *\n * The whole model is three moving parts kerf already has: a `signal` for the\n * current route, `delegate()` for link interception, and the keyed morph for the\n * outlet (a route change swaps the page wholesale; a param change updates in\n * place). Everything a full framework's router adds — nested layouts, data\n * loaders, lazy routes, guards, SSR matching — is deliberately OUT of scope;\n * compose those with kerf primitives (an `effect` on `router.route`, a `resource`\n * from `kerfjs/async`) when you need them.\n */\nimport { delegate } from './delegate.js';\nimport { jsx } from './jsx-runtime.js';\nimport { type MountResult } from './mount.js';\nimport { computed, type ReadonlySignal, signal } from './reactive.js';\n\n/** The reactive current-route snapshot (`router.route.value`). */\nexport interface RouteState {\n /** The matched pathname, base stripped (history mode) or the hash body (hash mode). Always starts with `/`. */\n path: string;\n /** Path parameters from the matched pattern — `/users/:id` on `/users/7` → `{ id: '7' }`. */\n params: Record<string, string>;\n /** The parsed query string (`?a=1` → `URLSearchParams`). Empty when there is none. */\n query: URLSearchParams;\n /** The raw location hash including `#` (history mode), or `''`. In hash mode the hash IS the route, so this is `''`. */\n hash: string;\n}\n\n/** A route's view: receives the matched `params` and the full `route` snapshot, returns kerf content. */\nexport type RouteComponent = (\n params: Record<string, string>,\n route: RouteState,\n) => MountResult;\n\n/** One route in the table. `path` is a pattern: `/`, `/users/:id`, `/files/*rest`, or `*` (catch-all). */\nexport interface RouteDef {\n /** Pattern: static segments, `:param` captures, a trailing `*rest` wildcard, or `*` (matches anything — put last). */\n path: string;\n /** The view rendered in the outlet when this route matches. */\n component: RouteComponent;\n}\n\n/** Options for {@link navigate}. */\nexport interface NavigateOptions {\n /** Replace the current history entry instead of pushing a new one. Default `false`. */\n replace?: boolean;\n /** Arbitrary state stored on the history entry (readable via `history.state`). */\n state?: unknown;\n}\n\n/** Options for {@link createRouter}. */\nexport interface RouterOptions {\n /** The route table, tried in order; the first match wins. Include a `path: '*'` entry last for a fallback. */\n routes: readonly RouteDef[];\n /**\n * `'history'` (default) uses the real pathname (`/users/7`) via the History\n * API; `'hash'` keeps the route after `#` (`#/users/7`) for static hosts with\n * no server rewrite.\n */\n mode?: 'history' | 'hash';\n /** History mode only: a base path every route sits under (`/app`), matched at a segment boundary, stripped from `route.path`, and prepended on navigation. */\n base?: string;\n /**\n * Auto-intercept clicks on in-app `<a href>` links (same-origin, left-click, no\n * modifier keys / `target` / `download`) and route them instead of reloading.\n * Opt a single link out with `data-router-ignore` or `rel=\"external\"`. Default\n * `true`; set `false` to wire navigation entirely yourself.\n */\n interceptLinks?: boolean;\n}\n\n/** The handle {@link createRouter} returns. Holds no module-global state — it's a closure. */\nexport interface RouterHandle {\n /** The reactive current route. Read `.value` (tracked) in a render / `computed` / `effect`. */\n route: ReadonlySignal<RouteState>;\n /** Navigate to `path` (may include `?query` / `#hash`). Pushes history (or replaces, per options). */\n navigate: (path: string, options?: NavigateOptions) => void;\n /** History back — `history.back()`. */\n back: () => void;\n /** History forward — `history.forward()`. */\n forward: () => void;\n /**\n * A reactive \"is this path active?\" — true when the current path equals\n * `pattern` or is nested under it (`match('/users')` is true on `/users/7`).\n * `match('/')` is exact (only true on `/`). Bind it for active-nav styling.\n */\n match: (pattern: string) => ReadonlySignal<boolean>;\n /** Convenience: a bound class signal — `className` while {@link match}`(pattern)` is active, else `''`. */\n activeClass: (pattern: string, className: string) => ReadonlySignal<string>;\n /**\n * The routed view. Call it inside a `mount()` render: it renders the matched\n * route's component in a keyed wrapper, so a route change swaps the page\n * wholesale (fresh DOM) while a param change updates it in place.\n */\n outlet: () => MountResult;\n /** Tear down the popstate / link listeners. Idempotent. */\n dispose: () => void;\n}\n\n/**\n * Match `path` against a route `pattern`. Returns the captured params on a match,\n * or `null` on no match. `*` matches anything; a trailing `*name` captures the\n * remaining segments joined by `/`; `:name` captures one segment.\n */\nfunction matchPattern(\n pattern: string,\n path: string,\n): Record<string, string> | null {\n if (pattern === '*') return {};\n const patternSegments = pattern.split('/').filter(Boolean);\n const pathSegments = path.split('/').filter(Boolean);\n const params: Record<string, string> = {};\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSegment = patternSegments[i];\n if (patternSegment.startsWith('*')) {\n // Wildcard rest — consumes every remaining segment.\n const name = patternSegment.slice(1);\n if (name.length > 0) {\n const decoded: string[] = [];\n for (const part of pathSegments.slice(i)) {\n const value = decodePathSegment(part);\n if (value === null) return null;\n decoded.push(value);\n }\n params[name] = decoded.join('/');\n }\n return params;\n }\n if (i >= pathSegments.length) return null;\n if (patternSegment.startsWith(':')) {\n const value = decodePathSegment(pathSegments[i]);\n if (value === null) return null;\n params[patternSegment.slice(1)] = value;\n continue;\n }\n if (patternSegment !== pathSegments[i]) return null;\n }\n // No wildcard matched, so the segment counts must be exactly equal.\n return pathSegments.length === patternSegments.length ? params : null;\n}\n\n/** Decode a route parameter, treating malformed percent escapes as no match. */\nfunction decodePathSegment(segment: string): string | null {\n try {\n return decodeURIComponent(segment);\n } catch {\n return null;\n }\n}\n\n/** Whether `path` is exactly `base` or starts with it at a segment boundary. */\nfunction isWithinBase(path: string, base: string): boolean {\n return base.length === 0 || path === base || path.startsWith(base + '/');\n}\n\n/**\n * Create a router bound to the browser history. Reads the current location\n * immediately (so `route.value` is correct before first paint), installs a\n * `popstate` listener (+ `hashchange` in hash mode) and, unless disabled, a\n * single delegated link interceptor. See {@link RouterOptions} / {@link RouterHandle}.\n */\nexport function createRouter(options: RouterOptions): RouterHandle {\n const {\n routes,\n mode = 'history',\n base = '',\n interceptLinks = true,\n } = options;\n // Normalize base to '' or '/foo' (no trailing slash), so `base + path` is clean.\n const normBase = base === '/' ? '' : base.replace(/\\/$/, '');\n\n // The current matched route def, kept in step with the signal so `outlet()`\n // doesn't have to re-match — it reads `route.value` only to subscribe.\n let matched: { def: RouteDef; params: Record<string, string> } | null = null;\n\n const readLocation = (): RouteState => {\n let path: string;\n let query: URLSearchParams;\n let hash: string;\n if (mode === 'hash') {\n // Everything after '#': '#/users/7?a=1' → path '/users/7', query 'a=1'.\n const raw = location.hash.slice(1) || '/';\n const qIndex = raw.indexOf('?');\n path = qIndex === -1 ? raw : raw.slice(0, qIndex);\n query = new URLSearchParams(qIndex === -1 ? '' : raw.slice(qIndex + 1));\n hash = '';\n } else {\n path = location.pathname;\n if (normBase.length > 0 && isWithinBase(path, normBase)) {\n path = path.slice(normBase.length) || '/';\n }\n query = new URLSearchParams(location.search);\n hash = location.hash;\n }\n if (!path.startsWith('/')) path = '/' + path;\n return { path, params: {}, query, hash };\n };\n\n const resolve = (state: RouteState): RouteState => {\n for (const def of routes) {\n const params = matchPattern(def.path, state.path);\n if (params !== null) {\n matched = { def, params };\n return { ...state, params };\n }\n }\n matched = null;\n return state;\n };\n\n const route = signal<RouteState>(resolve(readLocation()));\n\n // Re-read location → re-match → publish. Only writes when the path actually\n // changed, so a doubled popstate/hashchange is a harmless no-op.\n const sync = (): void => {\n const next = resolve(readLocation());\n if (\n next.path !== route.value.path ||\n next.query.toString() !== route.value.query.toString() ||\n next.hash !== route.value.hash\n ) {\n route.value = next;\n }\n };\n\n const navigate = (path: string, opts: NavigateOptions = {}): void => {\n const url =\n mode === 'hash'\n ? '#' + (path.startsWith('/') ? path : '/' + path)\n : normBase + (path.startsWith('/') ? path : '/' + path);\n // pushState/replaceState do NOT fire popstate, so publish the new route ourselves.\n history[opts.replace === true ? 'replaceState' : 'pushState'](\n opts.state ?? null,\n '',\n url,\n );\n route.value = resolve(readLocation());\n };\n\n const match = (pattern: string): ReadonlySignal<boolean> =>\n computed(() => {\n const p = route.value.path;\n if (pattern === '/') return p === '/';\n const base = pattern.replace(/\\/$/, '');\n return p === base || p.startsWith(base + '/');\n });\n\n const activeClass = (\n pattern: string,\n className: string,\n ): ReadonlySignal<string> => {\n const active = match(pattern);\n return computed(() => (active.value ? className : ''));\n };\n\n const outlet = (): MountResult => {\n void route.value; // tracked read — subscribes the enclosing mount to navigation\n if (matched === null) return null;\n // Keyed by the route PATTERN: a different pattern → the morph replaces the\n // wrapper wholesale (fresh DOM for the new page); the same pattern (only the\n // params changed) → the morph reconciles the children in place.\n return jsx('div', {\n 'data-router-outlet': '',\n 'data-key': matched.def.path,\n children: matched.def.component(matched.params, route.value),\n });\n };\n\n // --- Listeners ---------------------------------------------------------\n const removers: Array<() => void> = [];\n globalThis.addEventListener('popstate', sync);\n removers.push(() => globalThis.removeEventListener('popstate', sync));\n if (mode === 'hash') {\n globalThis.addEventListener('hashchange', sync);\n removers.push(() => globalThis.removeEventListener('hashchange', sync));\n }\n\n if (interceptLinks && typeof document !== 'undefined') {\n const onClick = (event: Event, anchor: HTMLAnchorElement): void => {\n const mouseEvent = event as MouseEvent;\n // Let the browser handle anything that isn't a plain left-click navigation.\n if (\n mouseEvent.defaultPrevented ||\n mouseEvent.button !== 0 ||\n mouseEvent.metaKey ||\n mouseEvent.ctrlKey ||\n mouseEvent.shiftKey ||\n mouseEvent.altKey\n )\n return;\n if (\n anchor.hasAttribute('download') ||\n anchor.hasAttribute('data-router-ignore')\n )\n return;\n const target = anchor.getAttribute('target');\n if (target !== null && target !== '' && target !== '_self') return;\n const rel = anchor.getAttribute('rel');\n if (rel !== null && /\\bexternal\\b/.test(rel)) return;\n const url = new URL(anchor.href, location.href);\n if (url.origin !== location.origin) return;\n if (mode === 'history') {\n if (!isWithinBase(url.pathname, normBase)) return; // outside the app's base\n event.preventDefault();\n navigate(url.pathname.slice(normBase.length) + url.search + url.hash);\n } else {\n // Hash mode: only intercept in-app hash links (`#/...`), leave others alone.\n if (url.pathname !== location.pathname || !url.hash.startsWith('#/'))\n return;\n event.preventDefault();\n navigate(url.hash.slice(1));\n }\n };\n removers.push(\n delegate<HTMLAnchorElement>(document.body, 'click', 'a[href]', onClick),\n );\n }\n\n let disposed = false;\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n for (const remove of removers) remove();\n };\n\n return {\n route,\n navigate,\n back: () => history.back(),\n forward: () => history.forward(),\n match,\n activeClass,\n outlet,\n dispose,\n };\n}\n"]}
|
package/dist/scope.d.ts
CHANGED
|
@@ -58,9 +58,10 @@ declare function disposeScope(el: Element): Scope;
|
|
|
58
58
|
declare function disposeSubtree(root: Element): void;
|
|
59
59
|
/**
|
|
60
60
|
* Install a `MutationObserver` on `root` that auto-disposes a node's scope when
|
|
61
|
-
* that node (or an ancestor) is removed from the subtree.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* that node (or an ancestor) is permanently removed from the subtree. A node
|
|
62
|
+
* moved or reordered within `root` remains live. One observer covers the whole
|
|
63
|
+
* tree. Returns a disconnect function. Note: `MutationObserver` fires
|
|
64
|
+
* asynchronously, so final containment and disposal run after the mutation.
|
|
64
65
|
*/
|
|
65
66
|
declare function observeRemovals(root: Element): () => void;
|
|
66
67
|
|
package/dist/scope.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mount } from './chunk-
|
|
2
|
-
import { delegate } from './chunk-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { effect } from './chunk-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { mount } from './chunk-V757BT6U.js';
|
|
2
|
+
import { delegate } from './chunk-E5R5GNKE.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import { effect } from './chunk-Y2FOYPBV.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
// src/scope.ts
|
|
11
11
|
var scopes = /* @__PURE__ */ new WeakMap();
|
|
@@ -58,7 +58,8 @@ function observeRemovals(root) {
|
|
|
58
58
|
const observer = new MutationObserver((records) => {
|
|
59
59
|
for (const record of records) {
|
|
60
60
|
for (const node of record.removedNodes) {
|
|
61
|
-
if (node instanceof Element
|
|
61
|
+
if (node instanceof Element && !root.contains(node))
|
|
62
|
+
disposeSubtree(node);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
});
|
|
@@ -67,5 +68,5 @@ function observeRemovals(root) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export { disposeScope, disposeSubtree, observeRemovals };
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
//# sourceMappingURL=scope.js.map
|
package/dist/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"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;
|
|
1
|
+
{"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;AASO,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,IAAW,CAAC,IAAA,CAAK,SAAS,IAAI,CAAA;AAChD,UAAA,cAAA,CAAe,IAAI,CAAA;AAAA,MACvB;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 permanently removed from the subtree. A node\n * moved or reordered within `root` remains live. One observer covers the whole\n * tree. Returns a disconnect function. Note: `MutationObserver` fires\n * asynchronously, so final containment and disposal run after the mutation.\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 && !root.contains(node))\n disposeSubtree(node);\n }\n }\n });\n observer.observe(root, { childList: true, subtree: true });\n return () => observer.disconnect();\n}\n"]}
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { clearStoreRegistry } from './chunk-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export { clearStoreRegistry } from './chunk-ZOIERTUW.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
5
|
//# sourceMappingURL=testing.js.map
|
package/dist/timing.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ interface Debounced<A extends unknown[]> {
|
|
|
33
33
|
/** A throttled function: call it like the original, plus `cancel()` / `flush()`. */
|
|
34
34
|
interface Throttled<A extends unknown[]> {
|
|
35
35
|
(...args: A): void;
|
|
36
|
-
/** Drop any pending trailing call and reset the rate window
|
|
36
|
+
/** Drop any pending trailing call and reset the rate window, including from inside `fn`. */
|
|
37
37
|
cancel(): void;
|
|
38
38
|
/** Invoke the pending trailing call now (if any). */
|
|
39
39
|
flush(): void;
|
|
@@ -48,7 +48,8 @@ declare function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: num
|
|
|
48
48
|
* Leading-plus-trailing throttle: `fn` runs immediately on the first call, then
|
|
49
49
|
* at most once per `ms`. Calls during a cooldown collapse to a single trailing
|
|
50
50
|
* call at the window's end (with the latest arguments). `cancel()` drops a
|
|
51
|
-
* pending trailing call and resets the window
|
|
51
|
+
* pending trailing call and resets the window, even when called by a leading
|
|
52
|
+
* or trailing callback; `flush()` runs a pending trailing call now.
|
|
52
53
|
*/
|
|
53
54
|
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, ms: number): Throttled<A>;
|
|
54
55
|
/**
|
package/dist/timing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { signal, effect } from './chunk-
|
|
2
|
-
|
|
1
|
+
import { signal, effect } from './chunk-Y2FOYPBV.js';
|
|
2
|
+
|
|
3
3
|
|
|
4
4
|
// src/timing.ts
|
|
5
5
|
function debounce(fn, ms) {
|
|
@@ -41,15 +41,15 @@ function throttle(fn, ms) {
|
|
|
41
41
|
timer = setTimeout(() => {
|
|
42
42
|
timer = void 0;
|
|
43
43
|
if (trailingArgs !== void 0) {
|
|
44
|
-
runTrailing();
|
|
45
44
|
startCooldown();
|
|
45
|
+
runTrailing();
|
|
46
46
|
}
|
|
47
47
|
}, ms);
|
|
48
48
|
};
|
|
49
49
|
const throttled = ((...args) => {
|
|
50
50
|
if (timer === void 0) {
|
|
51
|
-
fn(...args);
|
|
52
51
|
startCooldown();
|
|
52
|
+
fn(...args);
|
|
53
53
|
} else {
|
|
54
54
|
trailingArgs = args;
|
|
55
55
|
}
|
|
@@ -76,5 +76,5 @@ function debouncedSignal(source, ms) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export { debounce, debouncedSignal, throttle };
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
//# sourceMappingURL=timing.js.map
|
package/dist/timing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/timing.ts"],"names":[],"mappings":";;;;AA8CO,SAAS,QAAA,
|
|
1
|
+
{"version":3,"sources":["../src/timing.ts"],"names":[],"mappings":";;;;AA8CO,SAAS,QAAA,CACd,IACA,EAAA,EACc;AACd,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,MAAM,IAAA,GAAO,QAAA;AACb,IAAA,QAAA,GAAW,MAAA;AACX,IAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,SAAA,IAAa,IAAI,IAAA,KAAkB;AACvC,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,UAAA,CAAW,QAAQ,EAAE,CAAA;AAAA,EAC/B,CAAA,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,QAAA,GAAW,MAAA;AAAA,EACb,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAC5B,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,MAAA,EAAO;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,SAAA;AACT;AASO,SAAS,QAAA,CACd,IACA,EAAA,EACc;AACd,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,YAAA;AAEJ,EAAA,MAAM,cAAc,MAAY;AAC9B,IAAA,MAAM,IAAA,GAAO,YAAA;AACb,IAAA,YAAA,GAAe,MAAA;AACf,IAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,gBAAgB,MAAY;AAChC,IAAA,KAAA,GAAQ,WAAW,MAAM;AACvB,MAAA,KAAA,GAAQ,MAAA;AACR,MAAA,IAAI,iBAAiB,MAAA,EAAW;AAI9B,QAAA,aAAA,EAAc;AACd,QAAA,WAAA,EAAY;AAAA,MACd;AAAA,IACF,GAAG,EAAE,CAAA;AAAA,EACP,CAAA;AAEA,EAAA,MAAM,SAAA,IAAa,IAAI,IAAA,KAAkB;AACvC,IAAA,IAAI,UAAU,MAAA,EAAW;AAGvB,MAAA,aAAA,EAAc;AACd,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,IACZ,CAAA,MAAO;AACL,MAAA,YAAA,GAAe,IAAA;AAAA,IACjB;AAAA,EACF,CAAA,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,YAAA,GAAe,MAAA;AAAA,EACjB,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAC5B,IAAA,IAAI,YAAA,KAAiB,QAAW,WAAA,EAAY;AAAA,EAC9C,CAAA;AAEA,EAAA,OAAO,SAAA;AACT;AAWO,SAAS,eAAA,CACd,QACA,EAAA,EACmB;AACnB,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA;AAC/B,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,CAAC,KAAA,KAAa;AACnC,IAAA,GAAA,CAAI,KAAA,GAAQ,KAAA;AAAA,EACd,GAAG,EAAE,CAAA;AACL,EAAA,MAAA,CAAO,MAAM;AACX,IAAA,KAAA,CAAM,OAAO,KAAK,CAAA;AAAA,EACpB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT","file":"timing.js","sourcesContent":["/**\n * `kerfjs/timing` — the small timing primitives every app hand-rolls.\n *\n * kerf already replaced most imperative bookkeeping — `delegate` for listeners,\n * `mount`/`effect` for render, `defineStore` for state — but debouncing and\n * throttling still get written by hand as `let timer; clearTimeout(timer);\n * timer = setTimeout(fn, ms)`. This subpath blesses that with disposer-shaped\n * ergonomics (`.cancel()` / `.flush()`), plus `debouncedSignal` so a trailing\n * value composes inside the reactive graph instead of beside it.\n *\n * import { debounce, throttle, debouncedSignal } from 'kerfjs/timing';\n *\n * const save = debounce(() => persist(state), 300);\n * input.addEventListener('input', save); // save.cancel() on teardown\n *\n * const query = signal('');\n * const debouncedQuery = debouncedSignal(query, 250); // trails query by 250ms\n *\n * Tree-shakeable and tiny — `debounce`/`throttle` are dependency-free; only\n * `debouncedSignal` pulls in signals (no render core).\n */\nimport { effect, type ReadonlySignal, signal } from './reactive.js';\n\n/** A debounced function: call it like the original, plus `cancel()` / `flush()`. */\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop any pending trailing call without invoking it. */\n cancel(): void;\n /** Invoke the pending trailing call now (if any) and clear the timer. */\n flush(): void;\n}\n\n/** A throttled function: call it like the original, plus `cancel()` / `flush()`. */\nexport interface Throttled<A extends unknown[]> {\n (...args: A): void;\n /** Drop any pending trailing call and reset the rate window, including from inside `fn`. */\n cancel(): void;\n /** Invoke the pending trailing call now (if any). */\n flush(): void;\n}\n\n/**\n * Trailing-edge debounce: `fn` runs `ms` after calls STOP, with the most recent\n * arguments. Every call within the quiet window resets the timer. `cancel()`\n * drops a pending call; `flush()` runs it immediately.\n */\nexport function debounce<A extends unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let lastArgs: A | undefined;\n\n const invoke = (): void => {\n timer = undefined;\n const args = lastArgs as A;\n lastArgs = undefined;\n fn(...args);\n };\n\n const debounced = ((...args: A): void => {\n lastArgs = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(invoke, ms);\n }) as Debounced<A>;\n\n debounced.cancel = (): void => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n lastArgs = undefined;\n };\n\n debounced.flush = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n invoke();\n }\n };\n\n return debounced;\n}\n\n/**\n * Leading-plus-trailing throttle: `fn` runs immediately on the first call, then\n * at most once per `ms`. Calls during a cooldown collapse to a single trailing\n * call at the window's end (with the latest arguments). `cancel()` drops a\n * pending trailing call and resets the window, even when called by a leading\n * or trailing callback; `flush()` runs a pending trailing call now.\n */\nexport function throttle<A extends unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n): Throttled<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let trailingArgs: A | undefined;\n\n const runTrailing = (): void => {\n const args = trailingArgs as A;\n trailingArgs = undefined;\n fn(...args);\n };\n\n const startCooldown = (): void => {\n timer = setTimeout(() => {\n timer = undefined;\n if (trailingArgs !== undefined) {\n // Install the next window before invoking user code. A trailing\n // callback can therefore cancel that window, and reentrant calls stay\n // throttled unless the callback explicitly resets it first.\n startCooldown();\n runTrailing();\n }\n }, ms);\n };\n\n const throttled = ((...args: A): void => {\n if (timer === undefined) {\n // Make the pending window visible before invoking user code so\n // `cancel()` from inside a leading callback remains effective.\n startCooldown();\n fn(...args); // leading edge\n } else {\n trailingArgs = args; // collapse into one trailing call\n }\n }) as Throttled<A>;\n\n throttled.cancel = (): void => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n trailingArgs = undefined;\n };\n\n throttled.flush = (): void => {\n if (trailingArgs !== undefined) runTrailing();\n };\n\n return throttled;\n}\n\n/**\n * A read-only signal that trails `source` by `ms` (trailing-edge). Writes to\n * `source` reschedule; the derived value updates once writes go quiet, so it\n * composes with `computed()`/`effect()`/`mount()` like any signal.\n *\n * Holds a live subscription to `source` for its lifetime (like a module-scope\n * `effect`) — intended for app-lifetime signals, not throwaway ones. For a\n * disposable variant, drive your own `effect` with {@link debounce}.\n */\nexport function debouncedSignal<T>(\n source: ReadonlySignal<T>,\n ms: number,\n): ReadonlySignal<T> {\n const out = signal(source.value);\n const write = debounce((value: T) => {\n out.value = value;\n }, ms);\n effect(() => {\n write(source.value); // tracks source; reschedules on every change\n });\n return out;\n}\n"]}
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A tiny (~12 KB minified + gzipped including its one runtime dependency `@preact/signals-core`; ~13 KB with `arraySignal`) reactive UI framework — fine-grained signals + DOM morphing + JSX. No virtual DOM, no compiler. Apply the smallest possible cut to update your DOM.
|
|
4
4
|
|
|
5
|
-
kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children
|
|
5
|
+
kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "list"/"mixed" segments where `each(...)` was used) and reconciles it against the live tree with a custom segment-aware morph. Static surrounds go through a general-purpose tree-morph; list contents go through a keyed reconciler that operates directly on live children. Snapshot reconciliation scans O(rows) but limits rendering and DOM mutations to cache misses and structural changes; eligible `arraySignal` patch delivery runs in O(patches). Reactivity is provided by [@preact/signals-core](https://github.com/preactjs/signals). It pairs well with server-rendered HTML, embedded widgets, and any UI where preserving focus / selection across re-renders matters. Public API is one import: `signal`, `computed`, `effect`, `batch`, `defineStore`, `resetAllStores`, `mount`, `morph`, `each`, `attr`, `delegate`, `delegateCapture`, `toElement`, `renderDocument`, `SafeHtml`, `isSafeHtml`, `raw`, `Fragment`. (Two more subpaths: `kerfjs/testing` exposes `clearStoreRegistry` for unit-test isolation; `kerfjs/jsx-runtime` exposes the typed JSX building blocks for declaration-merging custom-element types.) An optional subpath at `kerfjs/array-signal` adds `arraySignal()` — a granular keyed-list signal whose patch events let `each()` reconcile in O(patches) instead of O(N). An optional subpath at `kerfjs/dev` installs the development diagnostics — kerf does NOT infer dev mode, so you import it behind your own build's dev flag (`if (import.meta.env.DEV) await import('kerfjs/dev');`); omitting it is production and sheds ~4.7 KB min+gzip. Another optional subpath at `kerfjs/html` adds the `html` tagged template — JSX-identical runtime semantics with no JSX transform, so CDN/importmap projects can author kerf UIs with literally no build step. A family of optional, tree-shakeable **companion-utility** subpaths cover patterns real apps hand-roll: `kerfjs/list` (`bindList` — a keyed list with per-row fine-grained mounts and fixed / declared / measured-height viewport virtualization, plus `observeRowHeights`), `kerfjs/router` (`createRouter` — the opt-in "postcard router": route matching + `navigate` + `<a>` link interception + a keyed outlet, core stays router-free), `kerfjs/overlay` (`overlay` / `confirm` / `prompt` / `form` / `choice` / `popover` / `tooltip` / `toast` + `positionAnchored` / `autoReposition`; `native: true` top-layer backing via `<dialog>` / the Popover API applies to the seven overlay/dialog/popover/tooltip surfaces), `kerfjs/scope` (`disposeScope` / `disposeSubtree` / `observeRemovals`), `kerfjs/async` (`resource` async-state with a stale-response guard + SWR cache), `kerfjs/timing` (`debounce` / `throttle` / `debouncedSignal`), `kerfjs/remount` (`remountOn` — key-driven wholesale subtree replacement), `kerfjs/attach` (`attach` — bind a non-kerf widget's lifecycle to one node), and `kerfjs/actions` (`action` / `delegateActions` — the delegated `data-action` table idiom).
|
|
6
6
|
|
|
7
7
|
## For humans new to the codebase
|
|
8
8
|
|
|
@@ -10,13 +10,18 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
10
10
|
|
|
11
11
|
## For AI assistants
|
|
12
12
|
|
|
13
|
+
- One-step project setup: run `npx kerfjs setup` for a safe dry-run, then
|
|
14
|
+
`npx kerfjs setup --write --yes`; see
|
|
15
|
+
[AI-first setup](https://github.com/brianwestphal/kerf/blob/main/docs/24-ai-first-setup.md).
|
|
16
|
+
|
|
13
17
|
- [AI usage guide](https://github.com/brianwestphal/kerf/blob/main/docs/ai/usage-guide.md): when to recommend kerf, the full public API, the four core patterns, hard rules, and a common-errors table.
|
|
14
18
|
- [Code summary](https://github.com/brianwestphal/kerf/blob/main/docs/ai/code-summary.md): directory tree + reverse index of every public export.
|
|
15
19
|
- [Requirements summary](https://github.com/brianwestphal/kerf/blob/main/docs/ai/requirements-summary.md): synthesized view of the numbered design docs.
|
|
16
20
|
- [`kerf.cursorrules`](https://github.com/brianwestphal/kerf/blob/main/kerf.cursorrules): drop-in [Cursor](https://cursor.sh) rules for projects that import `kerfjs`. Copy into your project as `.cursorrules` — or use the bundled mirror at `node_modules/kerfjs/ai/cursorrules` once you've `npm install`ed kerfjs.
|
|
17
21
|
- [`kerf.claude-skill.md`](https://github.com/brianwestphal/kerf/blob/main/kerf.claude-skill.md): drop-in [Claude Code](https://claude.com/claude-code) skill. Copy into `~/.claude/skills/kerf-app/SKILL.md` (or your project's `.claude/skills/kerf-app/SKILL.md`) — or use the bundled mirror at `node_modules/kerfjs/ai/skill.md` once you've `npm install`ed kerfjs.
|
|
18
22
|
- [`eslint-plugin-kerfjs`](https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/README.md): companion ESLint plugin enforcing the hard rules at edit time — eight rules: `no-inline-jsx-event-handlers`, `require-data-key-in-each`, `no-nested-mount`, `prefer-module-jsx-augmentation` (error) plus `require-delegate-disposer`, `prefer-attr-selector`, `no-raw-with-dynamic-arg`, `ai-assistant-configs` (warn). AST-only, no `parserServices` dependency. Install with `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to your eslint config. Recommended when authoring kerf code with an AI assistant — eslint feedback surfaces in the IDE before `tsc` or runtime warns ever run.
|
|
19
|
-
- [`create-kerf-component`](https://github.com/brianwestphal/kerf/blob/main/create-kerf-component/README.md): companion initializer that scaffolds a publishable kerf component package with the hard packaging rules
|
|
23
|
+
- [`create-kerf-component`](https://github.com/brianwestphal/kerf/blob/main/create-kerf-component/README.md): companion initializer that scaffolds a publishable kerf component package with the hard packaging rules, an example component, and explicit `kerf.components.json` source metadata plus deterministic `component-catalog-v2.json` generation/checking. Run `npm create kerf-component@latest <dir>`.
|
|
24
|
+
- [`@kerfjs/ui` AI guide](https://github.com/brianwestphal/kerf/blob/main/ui/ai/skill.md): component-selection, styling, accessibility, and event-wiring contract for the optional first-party UI package.
|
|
20
25
|
|
|
21
26
|
## Reference docs
|
|
22
27
|
|
|
@@ -29,17 +34,19 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
29
34
|
- [SVG handling](https://github.com/brianwestphal/kerf/blob/main/docs/7-svg.md): namespace propagation, `toElement`.
|
|
30
35
|
- [API reference](https://github.com/brianwestphal/kerf/blob/main/docs/8-api-reference.md): every export, every option.
|
|
31
36
|
- [Live demo](https://github.com/brianwestphal/kerf/blob/main/docs/9-live-demo.md): the GitHub Pages deploy of `examples/reactivity-demo`.
|
|
32
|
-
- [Migrating](https://github.com/brianwestphal/kerf/blob/main/docs/10-migrating.md): the `/kerf/migrating/` comparison hub —
|
|
33
|
-
- [Dev-mode warnings](https://github.com/brianwestphal/kerf/blob/main/docs/11-dev-warnings.md): the opt-in `KERF_DEV_WARN_*`
|
|
37
|
+
- [Migrating](https://github.com/brianwestphal/kerf/blob/main/docs/10-migrating.md): the 15-page `/kerf/migrating/` comparison hub — index, incremental adoption, and side-by-side todo-list translations for React, Preact, Vue, Svelte, Solid, Angular, Lit, Alpine, htmx, jQuery, Redux, Astro, and vanjs.
|
|
38
|
+
- [Dev-mode warnings](https://github.com/brianwestphal/kerf/blob/main/docs/11-dev-warnings.md): the opt-in, switch-gated `KERF_DEV_WARN_*` family (environment variables in Node/CI or `enableWarnings()` in browser builds), plus the always-on dev guards and `KERF_DEV_INVARIANTS` structural checks; diagnostics are installed through `kerfjs/dev`, never inferred from the environment.
|
|
34
39
|
- [AI-assistant configs](https://github.com/brianwestphal/kerf/blob/main/docs/12-ai-assistant-configs.md): how the drop-in Claude Code skill + Cursor rules ship inside the `kerfjs` npm package at `ai/skill.md` / `ai/cursorrules` / `ai/manifest.json`, the version + marker contract for customization preservation, and the `kerfjs/ai-assistant-configs` ESLint rule that surfaces drift on every lint pass.
|
|
35
40
|
- [Component packages](https://github.com/brianwestphal/kerf/blob/main/docs/13-component-packages.md): building and publishing reusable kerf components as npm packages — the no-instance component model, per-instance state via factories, event/cleanup patterns, and `kerfjs`-as-peer-dependency packaging modeled on `eslint-plugin-kerfjs`. Scaffold one with `npm create kerf-component@latest <dir>` (the `create-kerf-component` initializer).
|
|
36
|
-
- [Feature coverage](https://github.com/brianwestphal/kerf/blob/main/docs/14-feature-coverage.md): the per-behavior coverage axis orthogonal to line coverage — an index mapping each behavior (especially list-reconciler
|
|
41
|
+
- [Feature coverage](https://github.com/brianwestphal/kerf/blob/main/docs/14-feature-coverage.md): the per-behavior coverage axis orthogonal to line coverage — an index mapping each behavior (especially list-reconciler _state transitions_) to its guarding test, enforced by `npm run check:features`.
|
|
37
42
|
- [No-build example](https://github.com/brianwestphal/kerf/blob/main/docs/15-no-build-example.md): the served-as-source `live-poll` example app — plain JS + importmap + the `html` tagged template, zero tooling — and the vendor-copy contract that ships it.
|
|
38
43
|
- [List identity](https://github.com/brianwestphal/kerf/blob/main/docs/16-list-identity.md): why an `each()` list's call-order identity is not stable, what the source guard fixes and what it doesn't, the five constraints any scheme must survive, and the explicit-key recommendation now shipped as `each(items, render, { key })`.
|
|
39
44
|
- [List virtualization](https://github.com/brianwestphal/kerf/blob/main/docs/17-list-virtualization.md): `bindList`'s virtualization — the `window` (default) height models (fixed `number`, app-declared `(item, index) => number`, and measured `{ estimate }` + `setHeight`) with kerf owning the cumulative-offset math and scroll anchoring while the app owns measurement (the `observeRowHeights` helper), the `minRows` render-all threshold and container/resize ergonomics, and the `content-visibility` mode that keeps every row in the DOM (full find-in-page / a11y) while the browser skips off-screen layout, plus the findability/a11y tradeoff of the default `window` mode.
|
|
40
45
|
- [State-preserving moves](https://github.com/brianwestphal/kerf/blob/main/docs/18-state-preserving-moves.md): connected-row reorders (every `each()` / `bindList` / `morph` move site) use `Node.prototype.moveBefore()` where the engine supports it — an atomic move that keeps focus, selection, `<iframe>` state, playing media, and running CSS animations across the reorder — falling back to `insertBefore()` otherwise. Transparent internal `moveNode` helper; no API change.
|
|
41
|
-
- [Native overlay backing](https://github.com/brianwestphal/kerf/blob/main/docs/19-native-overlay-backing.md): opt-in `native: true` on
|
|
46
|
+
- [Native overlay backing](https://github.com/brianwestphal/kerf/blob/main/docs/19-native-overlay-backing.md): opt-in `native: true` on the seven overlay/dialog/popover/tooltip surfaces hosts the overlay in the browser top layer — a `<dialog>.showModal()` for modal surfaces, the Popover API for non-modal — feature-detected, falling back to today's plain `<div>` where unsupported. Toasts and standalone positioning helpers do not create native top-layer hosts.
|
|
42
47
|
- [Router](https://github.com/brianwestphal/kerf/blob/main/docs/20-router.md): the opt-in, tree-shakeable `kerfjs/router` subpath — the "postcard router". `createRouter({ routes, mode?, base?, interceptLinks? })` → a reactive `route` signal, `navigate`/`back`/`forward`, `match`/`activeClass` active-link helpers, a keyed `outlet()` (a route change swaps the page wholesale, a same-route param change morphs in place), and `dispose()`. Route matching (`:param` / `*rest` / `*`), `delegate()`-based `<a href>` interception, history + hash modes, optional base. The core stays router-free (docs/1's "Not a router" is about the runtime); deliberately excludes nested layouts / loaders / lazy routes / guards / SSR.
|
|
48
|
+
- [`@kerfjs/ui`](https://github.com/brianwestphal/kerf/blob/main/docs/21-ui-package.md): the first-party component package, semantic CSS tokens, accessibility and keyboard contracts, explicit Web Awesome registration, UX catalog, AI surfaces, and lockstep release model.
|
|
49
|
+
- [UI CSS authoring](https://github.com/brianwestphal/kerf/blob/main/docs/22-ui-css-authoring.md): pixel-first `remify(<px>)` source syntax, fixed 16px conversion baseline, compiled `rem` package output, and Vite HMR behavior.
|
|
43
50
|
|
|
44
51
|
## Examples
|
|
45
52
|
|