kerfjs 5.0.0-beta.4 → 5.0.0-beta.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +345 -116
  2. package/README.md +43 -23
  3. package/ai/cursorrules +87 -48
  4. package/ai/manifest.json +10 -7
  5. package/ai/skill.md +101 -67
  6. package/dist/actions.js +2 -2
  7. package/dist/actions.js.map +1 -1
  8. package/dist/array-signal.js +1 -1
  9. package/dist/async.js +22 -19
  10. package/dist/async.js.map +1 -1
  11. package/dist/attach.js +3 -1
  12. package/dist/attach.js.map +1 -1
  13. package/dist/{chunk-QIP723L4.js → chunk-5WRGJZV6.js} +3 -2
  14. package/dist/chunk-5WRGJZV6.js.map +1 -0
  15. package/dist/{chunk-GY4XV2UV.js → chunk-BDX3R4OM.js} +3 -2
  16. package/dist/chunk-BDX3R4OM.js.map +1 -0
  17. package/dist/{chunk-HW7KSM2Y.js → chunk-CEQMZYLR.js} +1 -1
  18. package/dist/{chunk-HW7KSM2Y.js.map → chunk-CEQMZYLR.js.map} +1 -1
  19. package/dist/{chunk-SVATPF5R.js → chunk-DLY444MN.js} +121 -49
  20. package/dist/chunk-DLY444MN.js.map +1 -0
  21. package/dist/{chunk-KPXIOG2C.js → chunk-E5R5GNKE.js} +14 -4
  22. package/dist/chunk-E5R5GNKE.js.map +1 -0
  23. package/dist/{chunk-LVH3GC6B.js → chunk-ELXVRKY2.js} +3 -3
  24. package/dist/chunk-ELXVRKY2.js.map +1 -0
  25. package/dist/{chunk-ZDCJZCNO.js → chunk-JRKE6XEK.js} +37 -16
  26. package/dist/chunk-JRKE6XEK.js.map +1 -0
  27. package/dist/{chunk-UZJ6I4T6.js → chunk-MK42GLPV.js} +1 -1
  28. package/dist/chunk-MK42GLPV.js.map +1 -0
  29. package/dist/{chunk-V46JKE44.js → chunk-WKIPLNVO.js} +1 -1
  30. package/dist/chunk-WKIPLNVO.js.map +1 -0
  31. package/dist/{chunk-U6FK33SG.js → chunk-Y2FOYPBV.js} +2 -2
  32. package/dist/{chunk-U6FK33SG.js.map → chunk-Y2FOYPBV.js.map} +1 -1
  33. package/dist/{chunk-KZJXHFIB.js → chunk-ZOIERTUW.js} +3 -3
  34. package/dist/chunk-ZOIERTUW.js.map +1 -0
  35. package/dist/dev.js +64 -17
  36. package/dist/dev.js.map +1 -1
  37. package/dist/html.js +5 -4
  38. package/dist/html.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +23 -14
  41. package/dist/index.js.map +1 -1
  42. package/dist/jsx-runtime.d.ts +12 -5
  43. package/dist/jsx-runtime.js +1 -1
  44. package/dist/list.js +43 -14
  45. package/dist/list.js.map +1 -1
  46. package/dist/overlay.js +99 -32
  47. package/dist/overlay.js.map +1 -1
  48. package/dist/remount.js +2 -2
  49. package/dist/router.js +33 -10
  50. package/dist/router.js.map +1 -1
  51. package/dist/scope.js +5 -4
  52. package/dist/scope.js.map +1 -1
  53. package/dist/testing.js +1 -1
  54. package/dist/timing.js +1 -1
  55. package/dist/timing.js.map +1 -1
  56. package/llms.txt +6 -2
  57. package/package.json +32 -7
  58. package/setup/cli.mjs +90 -0
  59. package/setup/index.d.mts +37 -0
  60. package/setup/index.mjs +1347 -0
  61. package/setup/jsonc.mjs +201 -0
  62. package/setup/state.schema.json +56 -0
  63. package/dist/chunk-GY4XV2UV.js.map +0 -1
  64. package/dist/chunk-KPXIOG2C.js.map +0 -1
  65. package/dist/chunk-KZJXHFIB.js.map +0 -1
  66. package/dist/chunk-LVH3GC6B.js.map +0 -1
  67. package/dist/chunk-QIP723L4.js.map +0 -1
  68. package/dist/chunk-SVATPF5R.js.map +0 -1
  69. package/dist/chunk-UZJ6I4T6.js.map +0 -1
  70. package/dist/chunk-V46JKE44.js.map +0 -1
  71. package/dist/chunk-ZDCJZCNO.js.map +0 -1
package/dist/remount.js CHANGED
@@ -1,8 +1,8 @@
1
- import { mount } from './chunk-SVATPF5R.js';
1
+ import { mount } from './chunk-DLY444MN.js';
2
2
 
3
3
 
4
4
 
5
- import { effect } from './chunk-U6FK33SG.js';
5
+ import { effect } from './chunk-Y2FOYPBV.js';
6
6
 
7
7
 
8
8
 
package/dist/router.js CHANGED
@@ -1,6 +1,6 @@
1
- import { delegate } from './chunk-KPXIOG2C.js';
2
- import { jsx } from './chunk-ZDCJZCNO.js';
3
- import { signal, computed } from './chunk-U6FK33SG.js';
1
+ import { delegate } from './chunk-E5R5GNKE.js';
2
+ import { jsx } from './chunk-JRKE6XEK.js';
3
+ import { signal, computed } from './chunk-Y2FOYPBV.js';
4
4
 
5
5
 
6
6
 
@@ -47,7 +47,12 @@ function isWithinBase(path, base) {
47
47
  return base.length === 0 || path === base || path.startsWith(base + "/");
48
48
  }
49
49
  function createRouter(options) {
50
- const { routes, mode = "history", base = "", interceptLinks = true } = options;
50
+ const {
51
+ routes,
52
+ mode = "history",
53
+ base = "",
54
+ interceptLinks = true
55
+ } = options;
51
56
  const normBase = base === "/" ? "" : base.replace(/\/$/, "");
52
57
  let matched = null;
53
58
  const readLocation = () => {
@@ -91,7 +96,11 @@ function createRouter(options) {
91
96
  };
92
97
  const navigate = (path, opts = {}) => {
93
98
  const url = mode === "hash" ? "#" + (path.startsWith("/") ? path : "/" + path) : normBase + (path.startsWith("/") ? path : "/" + path);
94
- history[opts.replace === true ? "replaceState" : "pushState"](opts.state ?? null, "", url);
99
+ history[opts.replace === true ? "replaceState" : "pushState"](
100
+ opts.state ?? null,
101
+ "",
102
+ url
103
+ );
95
104
  route.value = resolve(readLocation());
96
105
  };
97
106
  const match = (pattern) => computed(() => {
@@ -123,8 +132,10 @@ function createRouter(options) {
123
132
  if (interceptLinks && typeof document !== "undefined") {
124
133
  const onClick = (event, anchor) => {
125
134
  const mouseEvent = event;
126
- if (mouseEvent.defaultPrevented || mouseEvent.button !== 0 || mouseEvent.metaKey || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.altKey) return;
127
- if (anchor.hasAttribute("download") || anchor.hasAttribute("data-router-ignore")) return;
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;
128
139
  const target = anchor.getAttribute("target");
129
140
  if (target !== null && target !== "" && target !== "_self") return;
130
141
  const rel = anchor.getAttribute("rel");
@@ -136,12 +147,15 @@ function createRouter(options) {
136
147
  event.preventDefault();
137
148
  navigate(url.pathname.slice(normBase.length) + url.search + url.hash);
138
149
  } else {
139
- if (url.pathname !== location.pathname || !url.hash.startsWith("#/")) return;
150
+ if (url.pathname !== location.pathname || !url.hash.startsWith("#/"))
151
+ return;
140
152
  event.preventDefault();
141
153
  navigate(url.hash.slice(1));
142
154
  }
143
155
  };
144
- removers.push(delegate(document.body, "click", "a[href]", onClick));
156
+ removers.push(
157
+ delegate(document.body, "click", "a[href]", onClick)
158
+ );
145
159
  }
146
160
  let disposed = false;
147
161
  const dispose = () => {
@@ -149,7 +163,16 @@ function createRouter(options) {
149
163
  disposed = true;
150
164
  for (const remove of removers) remove();
151
165
  };
152
- return { route, navigate, back: () => history.back(), forward: () => history.forward(), match, activeClass, outlet, dispose };
166
+ return {
167
+ route,
168
+ navigate,
169
+ back: () => history.back(),
170
+ forward: () => history.forward(),
171
+ match,
172
+ activeClass,
173
+ outlet,
174
+ dispose
175
+ };
153
176
  }
154
177
 
155
178
  export { createRouter };
@@ -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`), 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(pattern: string, path: string): 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 { 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 && 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 (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 mouseEvent = event as MouseEvent;\n // Let the browser handle anything that isn't a plain left-click navigation.\n if (mouseEvent.defaultPrevented || mouseEvent.button !== 0 || mouseEvent.metaKey\n || mouseEvent.ctrlKey || mouseEvent.shiftKey || mouseEvent.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 (!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('#/')) 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.js CHANGED
@@ -1,9 +1,9 @@
1
- import { mount } from './chunk-SVATPF5R.js';
2
- import { delegate } from './chunk-KPXIOG2C.js';
1
+ import { mount } from './chunk-DLY444MN.js';
2
+ import { delegate } from './chunk-E5R5GNKE.js';
3
3
 
4
4
 
5
5
 
6
- import { effect } from './chunk-U6FK33SG.js';
6
+ import { effect } from './chunk-Y2FOYPBV.js';
7
7
 
8
8
 
9
9
 
@@ -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 && !root.contains(node)) disposeSubtree(node);
61
+ if (node instanceof Element && !root.contains(node))
62
+ disposeSubtree(node);
62
63
  }
63
64
  }
64
65
  });
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;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,WAAW,CAAC,IAAA,CAAK,SAAS,IAAI,CAAA,iBAAkB,IAAI,CAAA;AAAA,MAC1E;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)) disposeSubtree(node);\n }\n }\n });\n observer.observe(root, { childList: true, subtree: true });\n return () => observer.disconnect();\n}\n"]}
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,4 +1,4 @@
1
- export { clearStoreRegistry } from './chunk-KZJXHFIB.js';
1
+ export { clearStoreRegistry } from './chunk-ZOIERTUW.js';
2
2
 
3
3
 
4
4
 
package/dist/timing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { signal, effect } from './chunk-U6FK33SG.js';
1
+ import { signal, effect } from './chunk-Y2FOYPBV.js';
2
2
 
3
3
 
4
4
  // src/timing.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/timing.ts"],"names":[],"mappings":";;;;AA8CO,SAAS,QAAA,CAA8B,IAA0B,EAAA,EAA0B;AAChG,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,CAA8B,IAA0B,EAAA,EAA0B;AAChG,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,CAAmB,QAA2B,EAAA,EAA+B;AAC3F,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[]>(fn: (...args: A) => void, ms: number): 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[]>(fn: (...args: A) => void, ms: number): 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>(source: ReadonlySignal<T>, ms: number): 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"]}
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
@@ -10,13 +10,17 @@ 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 already wired (kerfjs as a peer dependency + `external` in the build, ESM + `.d.ts`, `jsxImportSource: "kerfjs"`, subpath exports) plus an example component (per-instance state via a factory, a `wire(root)` delegation disposer). Run `npm create kerf-component@latest <dir>`.
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>`.
20
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.
21
25
 
22
26
  ## Reference docs
@@ -34,7 +38,7 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
34
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.
35
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.
36
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).
37
- - [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`.
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`.
38
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.
39
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 })`.
40
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kerfjs",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.41",
4
4
  "description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -35,6 +35,9 @@
35
35
  },
36
36
  "main": "./dist/index.js",
37
37
  "types": "./dist/index.d.ts",
38
+ "bin": {
39
+ "kerfjs": "./setup/cli.mjs"
40
+ },
38
41
  "exports": {
39
42
  ".": {
40
43
  "types": "./dist/index.d.ts",
@@ -100,11 +103,17 @@
100
103
  "types": "./dist/attach.d.ts",
101
104
  "import": "./dist/attach.js"
102
105
  },
103
- "./ai/*": "./ai/*"
106
+ "./ai/*": "./ai/*",
107
+ "./setup": {
108
+ "types": "./setup/index.d.mts",
109
+ "import": "./setup/index.mjs"
110
+ },
111
+ "./setup/state.schema.json": "./setup/state.schema.json"
104
112
  },
105
113
  "files": [
106
114
  "dist",
107
115
  "ai",
116
+ "setup",
108
117
  "llms.txt",
109
118
  "README.md",
110
119
  "CHANGELOG.md",
@@ -120,15 +129,20 @@
120
129
  "test:dist": "npm run build && vitest run --config vitest.config.dist.ts",
121
130
  "test:dist:full": "npm run build && vitest run --config vitest.config.dist-full.ts",
122
131
  "test:dist:jsx-typing": "npm run build && node node_modules/typescript7/bin/tsc -p tests/dist/jsx-typing/tsconfig.json",
132
+ "test:packed:jsx-typing": "npm run build && node scripts/check-packed-jsx-typing.mjs",
123
133
  "test:dist:examples": "npm run build && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json",
124
134
  "test:dist:scaffold-typing": "npm run build && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json",
125
135
  "test:browser": "npm run build && playwright test",
126
136
  "test:ui": "npm --prefix ui test",
127
137
  "bench:micro": "vitest bench --run --config vitest.config.bench.ts",
128
138
  "bench:serve": "bash bench/site.sh",
129
- "lint": "eslint src tests",
139
+ "format": "prettier --write . --ignore-unknown",
140
+ "format:check": "prettier --check . --ignore-unknown",
141
+ "lint:eslint": "eslint src tests",
142
+ "lint": "npm run lint:eslint && npm run format:check",
130
143
  "typecheck": "node node_modules/typescript7/bin/tsc --noEmit",
131
- "check": "npm run lint && npm run typecheck && node scripts/check-doc-test-inventory.mjs && node scripts/check-doc-api-coverage.mjs && node scripts/check-doc-site-tickets.mjs && node scripts/check-cdn-versions.mjs && node scripts/check-dev-warn-docs.mjs && node scripts/check-skill-freshness.mjs && node scripts/check-design-rule-5.mjs && node scripts/check-feature-coverage.mjs && node scripts/sync-lockstep-versions.mjs --check && node scripts/check-ai-bundle.mjs && npm test && npm run build && node scripts/check-bundle-size.mjs && node scripts/check-doc-api-signatures.mjs && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts && node node_modules/typescript7/bin/tsc -p tests/dist/jsx-typing/tsconfig.json && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json && node scripts/check-docs-examples.mjs",
144
+ "check": "node scripts/check-guidance-integrity.mjs -- npm run check:core",
145
+ "check:core": "npm run lint && npm run typecheck && node scripts/check-doc-test-inventory.mjs && node scripts/check-doc-api-coverage.mjs && node scripts/check-doc-site-tickets.mjs && node scripts/check-cdn-versions.mjs && node scripts/check-dev-warn-docs.mjs && node scripts/check-skill-freshness.mjs && node scripts/check-design-rule-5.mjs && node scripts/check-feature-coverage.mjs && node scripts/sync-lockstep-versions.mjs --check && node scripts/sync-scaffold-catalog-schema.mjs --check && node scripts/check-ai-bundle.mjs && npm test && npm run build && node scripts/check-bundle-size.mjs && node scripts/check-doc-api-signatures.mjs && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts && node node_modules/typescript7/bin/tsc -p tests/dist/jsx-typing/tsconfig.json && node scripts/check-packed-jsx-typing.mjs && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json && node scripts/check-docs-examples.mjs",
132
146
  "check:docs:examples": "node scripts/check-docs-examples.mjs",
133
147
  "check:full": "npm run check && npm run --silent check:audit && playwright test",
134
148
  "check:ui": "npm --prefix ui run check",
@@ -143,6 +157,8 @@
143
157
  "ai-bundle:sync": "node scripts/sync-ai-bundle.mjs",
144
158
  "check:lockstep-versions": "node scripts/sync-lockstep-versions.mjs --check",
145
159
  "sync:lockstep-versions": "node scripts/sync-lockstep-versions.mjs --write",
160
+ "check:scaffold-catalog-schema": "node scripts/sync-scaffold-catalog-schema.mjs --check",
161
+ "sync:scaffold-catalog-schema": "node scripts/sync-scaffold-catalog-schema.mjs --write",
146
162
  "clean": "rm -rf dist coverage node_modules/.cache",
147
163
  "prepare": "husky",
148
164
  "release": "bash scripts/release.sh",
@@ -159,10 +175,13 @@
159
175
  "check:skills": "node scripts/check-skill-freshness.mjs",
160
176
  "check:design-rule-5": "node scripts/check-design-rule-5.mjs",
161
177
  "check:docs:dev-warns": "node scripts/check-dev-warn-docs.mjs",
162
- "check:audit": "npm audit --omit=dev --audit-level=high"
178
+ "check:audit": "npm audit --omit=dev --audit-level=high",
179
+ "ticket:timing": "node scripts/ticket-timing.mjs"
163
180
  },
164
181
  "dependencies": {
165
- "@preact/signals-core": "^1.14.1"
182
+ "@preact/signals-core": "^1.14.1",
183
+ "minimatch": "^9.0.9",
184
+ "yaml": "^2.9.1"
166
185
  },
167
186
  "devDependencies": {
168
187
  "@eslint/js": "^10.0.1",
@@ -172,7 +191,7 @@
172
191
  "@typescript-eslint/eslint-plugin": "^8.65.0",
173
192
  "@typescript-eslint/parser": "^8.65.0",
174
193
  "@vitest/coverage-v8": "^4.1.11",
175
- "domotion-svg": "^0.28.2",
194
+ "domotion-svg": "^0.30.1",
176
195
  "eslint": "^10.8.0",
177
196
  "eslint-plugin-simple-import-sort": "^12.1.1",
178
197
  "gitgist": "^1.1.0",
@@ -180,11 +199,17 @@
180
199
  "http-server": "^14.1.1",
181
200
  "husky": "^9.1.7",
182
201
  "jsdom": "^29.1.1",
202
+ "prettier": "^3.9.8",
203
+ "prettier-plugin-astro": "^1.0.1",
204
+ "prettier-plugin-sh": "^0.19.0",
183
205
  "tsup": "^8.3.0",
184
206
  "typescript": "^6.0.3",
185
207
  "typescript7": "npm:typescript@^7.0.2",
186
208
  "vitest": "^4.1.11"
187
209
  },
210
+ "overrides": {
211
+ "esbuild": "0.28.2"
212
+ },
188
213
  "gitgist": {
189
214
  "exclude": [
190
215
  "site/public/demos/*.svg",
package/setup/cli.mjs ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ import console from 'node:console';
4
+ import process from 'node:process';
5
+
6
+ import { applyKerfSetup, formatSetupPlan, planKerfSetup } from './index.mjs';
7
+
8
+ const args = process.argv.slice(2);
9
+ const command = args[0] && !args[0].startsWith('-') ? args.shift() : 'setup';
10
+ const take = (name) => {
11
+ const index = args.indexOf(name);
12
+ if (index < 0) return undefined;
13
+ const value = args[index + 1];
14
+ if (!value || value.startsWith('--'))
15
+ throw new Error(`${name} requires a value.`);
16
+ args.splice(index, 2);
17
+ return value;
18
+ };
19
+ const takeAll = (name) => {
20
+ const values = [];
21
+ while (args.includes(name)) values.push(take(name));
22
+ return values;
23
+ };
24
+
25
+ try {
26
+ if (args.includes('--help') || args.includes('-h')) {
27
+ console.log(`Usage: kerfjs setup [options]
28
+
29
+ Options:
30
+ --root <path> Workspace root (default: current directory)
31
+ --package <name> Select a workspace package by name or relative path
32
+ --core | --ui Choose a mode when it cannot be detected
33
+ --resolve <id>=<choice>
34
+ Resolve one conflict with keep or kerf (repeatable)
35
+ --write Apply the displayed plan (default is dry-run)
36
+ --yes Required with --write in non-interactive use
37
+ --offline Forbid registry access during dependency installation
38
+ --no-install Write configuration without running the package manager`);
39
+ process.exit(0);
40
+ }
41
+ if (command !== 'setup') throw new Error(`Unknown command ${command}.`);
42
+ const core = args.includes('--core');
43
+ const ui = args.includes('--ui');
44
+ if (core && ui) throw new Error('--core and --ui are mutually exclusive.');
45
+ const write = args.includes('--write');
46
+ const yes = args.includes('--yes');
47
+ const offline = args.includes('--offline');
48
+ const noInstall = args.includes('--no-install');
49
+ const root = take('--root');
50
+ const packageSelector = take('--package');
51
+ const resolutions = {};
52
+ for (const entry of takeAll('--resolve')) {
53
+ const index = entry.lastIndexOf('=');
54
+ if (index < 1) throw new Error('--resolve requires <id>=keep|kerf.');
55
+ const id = entry.slice(0, index);
56
+ const choice = entry.slice(index + 1);
57
+ if (id in resolutions && resolutions[id] !== choice)
58
+ throw new Error(`Conflicting resolutions for ${id}.`);
59
+ resolutions[id] = choice;
60
+ }
61
+ const known = new Set([
62
+ '--core',
63
+ '--ui',
64
+ '--write',
65
+ '--yes',
66
+ '--offline',
67
+ '--no-install',
68
+ ]);
69
+ const unknown = args.filter((arg) => !known.has(arg));
70
+ if (unknown.length)
71
+ throw new Error(`Unknown arguments: ${unknown.join(' ')}`);
72
+ const plan = await planKerfSetup({
73
+ root,
74
+ package: packageSelector,
75
+ mode: ui ? 'ui' : core ? 'core' : undefined,
76
+ resolutions,
77
+ });
78
+ console.log(formatSetupPlan(plan));
79
+ if (plan.conflicts.length) {
80
+ process.exitCode = 2;
81
+ } else if (write) {
82
+ if (!yes)
83
+ throw new Error('--write requires --yes after reviewing the plan.');
84
+ await applyKerfSetup(plan, { install: !noInstall, offline });
85
+ console.log(`Applied ${plan.actions.length} change(s).`);
86
+ }
87
+ } catch (error) {
88
+ console.error(`kerfjs setup: ${error.message}`);
89
+ process.exitCode = 2;
90
+ }
@@ -0,0 +1,37 @@
1
+ export interface SetupConflict {
2
+ id: string;
3
+ message: string;
4
+ }
5
+ export interface SetupAction {
6
+ path: string;
7
+ absolutePath: string;
8
+ before: string | null;
9
+ after: string;
10
+ kind: string;
11
+ }
12
+ export interface KerfSetupPlan {
13
+ schemaVersion: 1;
14
+ root: string;
15
+ packageRoot: string;
16
+ packageName: string;
17
+ packageManager: 'npm' | 'pnpm' | 'yarn';
18
+ packageManagerVersion?: string;
19
+ packageManagerVariant?: 'classic' | 'berry';
20
+ mode: 'core' | 'ui';
21
+ setupVersion: string;
22
+ actions: SetupAction[];
23
+ conflicts: SetupConflict[];
24
+ generatedPaths: string[];
25
+ }
26
+ export function planKerfSetup(options?: {
27
+ root?: string;
28
+ package?: string;
29
+ mode?: 'core' | 'ui';
30
+ version?: string;
31
+ resolutions?: Record<string, 'keep' | 'kerf'>;
32
+ }): Promise<KerfSetupPlan>;
33
+ export function formatSetupPlan(plan: KerfSetupPlan): string;
34
+ export function applyKerfSetup(
35
+ plan: KerfSetupPlan,
36
+ options?: { install?: boolean; offline?: boolean; runner?: Function },
37
+ ): Promise<{ changed: string[] }>;