kerfjs 4.4.0 → 5.0.0-beta.3
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 +116 -0
- package/LICENSE +1 -1
- package/README.md +34 -35
- package/ai/cursorrules +46 -1
- package/ai/manifest.json +62 -5
- package/ai/skill.md +53 -2
- 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 +17 -10
- package/dist/async.js.map +1 -1
- package/dist/attach.d.ts +11 -8
- package/dist/attach.js +52 -4
- package/dist/attach.js.map +1 -1
- package/dist/chunk-GY4XV2UV.js +1 -1
- package/dist/{chunk-VVDJLWMP.js → chunk-HW7KSM2Y.js} +2 -2
- package/dist/chunk-HW7KSM2Y.js.map +1 -0
- package/dist/{chunk-KEZTD6H4.js → chunk-KPXIOG2C.js} +3 -3
- package/dist/{chunk-KEZTD6H4.js.map → chunk-KPXIOG2C.js.map} +1 -1
- package/dist/{chunk-SAYPJ6XR.js → chunk-KZJXHFIB.js} +10 -6
- package/dist/chunk-KZJXHFIB.js.map +1 -0
- package/dist/{chunk-MRYM3O3V.js → chunk-LVH3GC6B.js} +11 -8
- package/dist/chunk-LVH3GC6B.js.map +1 -0
- package/dist/chunk-QIP723L4.js +1 -1
- package/dist/{chunk-SRWQKB33.js → chunk-SVATPF5R.js} +97 -74
- package/dist/chunk-SVATPF5R.js.map +1 -0
- package/dist/{chunk-3APBEVHF.js → chunk-U6FK33SG.js} +3 -3
- package/dist/{chunk-3APBEVHF.js.map → chunk-U6FK33SG.js.map} +1 -1
- package/dist/{chunk-U32TFTGZ.js → chunk-UZJ6I4T6.js} +3 -3
- package/dist/chunk-UZJ6I4T6.js.map +1 -0
- package/dist/{chunk-YHH7OUFA.js → chunk-V46JKE44.js} +3 -3
- package/dist/chunk-V46JKE44.js.map +1 -0
- package/dist/{chunk-SUPUPSBE.js → chunk-ZDCJZCNO.js} +9 -8
- package/dist/chunk-ZDCJZCNO.js.map +1 -0
- package/dist/dev.d.ts +9 -6
- package/dist/dev.js +5 -5
- package/dist/dev.js.map +1 -1
- package/dist/html.d.ts +1 -1
- package/dist/html.js +5 -5
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +11 -11
- package/dist/jsx-runtime.js +5 -5
- package/dist/list.d.ts +1 -1
- package/dist/list.js +273 -211
- package/dist/list.js.map +1 -1
- package/dist/overlay.d.ts +190 -280
- package/dist/overlay.js +401 -361
- 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 +44 -22
- package/dist/router.js.map +1 -1
- package/dist/scope.d.ts +4 -3
- package/dist/scope.js +10 -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 +7 -4
- package/package.json +12 -10
- package/dist/chunk-MRYM3O3V.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.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/router.ts"],"names":["base"],"mappings":";;;;;;;AA0HA,SAAS,YAAA,CAAa,SAAiB,IAAA,EAA6C;AAClF,EAAA,IAAI,OAAA,KAAY,GAAA,EAAK,OAAO,EAAC;AAC7B,EAAA,MAAM,KAAK,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,OAAO,CAAA;AAC5C,EAAA,MAAM,KAAK,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,OAAO,CAAA;AACzC,EAAA,MAAM,SAAiC,EAAC;AACxC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,CAAG,QAAQ,CAAA,EAAA,EAAK;AAClC,IAAA,MAAM,GAAA,GAAM,GAAG,CAAC,CAAA;AAChB,IAAA,IAAI,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,EAAG;AAEvB,MAAA,MAAM,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,CAAC,CAAA;AACxB,MAAA,IAAI,IAAA,CAAK,MAAA,GAAS,CAAA,EAAG,MAAA,CAAO,IAAI,CAAA,GAAI,EAAA,CAAG,KAAA,CAAM,CAAC,CAAA,CAAE,GAAA,CAAI,kBAAkB,CAAA,CAAE,KAAK,GAAG,CAAA;AAChF,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAA,IAAK,EAAA,CAAG,MAAA,EAAQ,OAAO,IAAA;AAC3B,IAAA,IAAI,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,EAAG;AACvB,MAAA,MAAA,CAAO,GAAA,CAAI,MAAM,CAAC,CAAC,IAAI,kBAAA,CAAmB,EAAA,CAAG,CAAC,CAAC,CAAA;AAC/C,MAAA;AAAA,IACF;AACA,IAAA,IAAI,GAAA,KAAQ,EAAA,CAAG,CAAC,CAAA,EAAG,OAAO,IAAA;AAAA,EAC5B;AAEA,EAAA,OAAO,EAAA,CAAG,MAAA,KAAW,EAAA,CAAG,MAAA,GAAS,MAAA,GAAS,IAAA;AAC5C;AAQO,SAAS,aAAa,OAAA,EAAsC;AACjE,EAAA,MAAM,EAAE,QAAQ,IAAA,GAAO,SAAA,EAAW,OAAO,EAAA,EAAI,cAAA,GAAiB,MAAK,GAAI,OAAA;AAEvE,EAAA,MAAM,WAAW,IAAA,KAAS,GAAA,GAAM,KAAK,IAAA,CAAK,OAAA,CAAQ,OAAO,EAAE,CAAA;AAI3D,EAAA,IAAI,OAAA,GAAoE,IAAA;AAExE,EAAA,MAAM,eAAe,MAAkB;AACrC,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI,SAAS,MAAA,EAAQ;AAEnB,MAAA,MAAM,GAAA,GAAM,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,IAAK,GAAA;AACtC,MAAA,MAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC9B,MAAA,IAAA,GAAO,WAAW,EAAA,GAAK,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,GAAG,MAAM,CAAA;AAChD,MAAA,KAAA,GAAQ,IAAI,gBAAgB,MAAA,KAAW,EAAA,GAAK,KAAK,GAAA,CAAI,KAAA,CAAM,MAAA,GAAS,CAAC,CAAC,CAAA;AACtE,MAAA,IAAA,GAAO,EAAA;AAAA,IACT,CAAA,MAAO;AACL,MAAA,IAAA,GAAO,QAAA,CAAS,QAAA;AAChB,MAAA,IAAI,QAAA,CAAS,MAAA,GAAS,CAAA,IAAK,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,EAAG,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IAAK,GAAA;AAC5F,MAAA,KAAA,GAAQ,IAAI,eAAA,CAAgB,QAAA,CAAS,MAAM,CAAA;AAC3C,MAAA,IAAA,GAAO,QAAA,CAAS,IAAA;AAAA,IAClB;AACA,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,SAAU,GAAA,GAAM,IAAA;AACxC,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,EAAC,EAAG,OAAO,IAAA,EAAK;AAAA,EACzC,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAkC;AACjD,IAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACxB,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,GAAA,CAAI,IAAA,EAAM,MAAM,IAAI,CAAA;AAChD,MAAA,IAAI,WAAW,IAAA,EAAM;AACnB,QAAA,OAAA,GAAU,EAAE,KAAK,MAAA,EAAO;AACxB,QAAA,OAAO,EAAE,GAAG,KAAA,EAAO,MAAA,EAAO;AAAA,MAC5B;AAAA,IACF;AACA,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAmB,OAAA,CAAQ,YAAA,EAAc,CAAC,CAAA;AAIxD,EAAA,MAAM,OAAO,MAAY;AACvB,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,YAAA,EAAc,CAAA;AACnC,IAAA,IAAI,KAAK,IAAA,KAAS,KAAA,CAAM,MAAM,IAAA,IAAQ,IAAA,CAAK,MAAM,QAAA,EAAS,KAAM,KAAA,CAAM,KAAA,CAAM,MAAM,QAAA,EAAS,IACtF,KAAK,IAAA,KAAS,KAAA,CAAM,MAAM,IAAA,EAAM;AACnC,MAAA,KAAA,CAAM,KAAA,GAAQ,IAAA;AAAA,IAChB;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,QAAA,GAAW,CAAC,IAAA,EAAc,IAAA,GAAwB,EAAC,KAAY;AACnE,IAAA,MAAM,MAAM,IAAA,KAAS,MAAA,GACjB,GAAA,IAAO,IAAA,CAAK,WAAW,GAAG,CAAA,GAAI,IAAA,GAAO,GAAA,GAAM,QAC3C,QAAA,IAAY,IAAA,CAAK,WAAW,GAAG,CAAA,GAAI,OAAO,GAAA,GAAM,IAAA,CAAA;AAEpD,IAAA,OAAA,CAAQ,IAAA,CAAK,OAAA,KAAY,IAAA,GAAO,cAAA,GAAiB,WAAW,EAAE,IAAA,CAAK,KAAA,IAAS,IAAA,EAAM,EAAA,EAAI,GAAG,CAAA;AACzF,IAAA,KAAA,CAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,EAAc,CAAA;AAAA,EACtC,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,KACb,QAAA,CAAS,MAAM;AACb,IAAA,MAAM,CAAA,GAAI,MAAM,KAAA,CAAM,IAAA;AACtB,IAAA,IAAI,OAAA,KAAY,GAAA,EAAK,OAAO,CAAA,KAAM,GAAA;AAClC,IAAA,MAAMA,KAAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AACtC,IAAA,OAAO,CAAA,KAAMA,KAAAA,IAAQ,CAAA,CAAE,UAAA,CAAWA,QAAO,GAAG,CAAA;AAAA,EAC9C,CAAC,CAAA;AAEH,EAAA,MAAM,WAAA,GAAc,CAAC,OAAA,EAAiB,SAAA,KAA8C;AAClF,IAAA,MAAM,MAAA,GAAS,MAAM,OAAO,CAAA;AAC5B,IAAA,OAAO,QAAA,CAAS,MAAO,MAAA,CAAO,KAAA,GAAQ,YAAY,EAAG,CAAA;AAAA,EACvD,CAAA;AAEA,EAAA,MAAM,SAAS,MAAmB;AAChC,IAAA,KAAK,KAAA,CAAM,KAAA;AACX,IAAA,IAAI,OAAA,KAAY,MAAM,OAAO,IAAA;AAI7B,IAAA,OAAO,IAAI,KAAA,EAAO;AAAA,MAChB,oBAAA,EAAsB,EAAA;AAAA,MACtB,UAAA,EAAY,QAAQ,GAAA,CAAI,IAAA;AAAA,MACxB,UAAU,OAAA,CAAQ,GAAA,CAAI,UAAU,OAAA,CAAQ,MAAA,EAAQ,MAAM,KAAK;AAAA,KAC5D,CAAA;AAAA,EACH,CAAA;AAGA,EAAA,MAAM,WAA8B,EAAC;AACrC,EAAA,UAAA,CAAW,gBAAA,CAAiB,YAAY,IAAI,CAAA;AAC5C,EAAA,QAAA,CAAS,KAAK,MAAM,UAAA,CAAW,mBAAA,CAAoB,UAAA,EAAY,IAAI,CAAC,CAAA;AACpE,EAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,IAAA,UAAA,CAAW,gBAAA,CAAiB,cAAc,IAAI,CAAA;AAC9C,IAAA,QAAA,CAAS,KAAK,MAAM,UAAA,CAAW,mBAAA,CAAoB,YAAA,EAAc,IAAI,CAAC,CAAA;AAAA,EACxE;AAEA,EAAA,IAAI,cAAA,IAAkB,OAAO,QAAA,KAAa,WAAA,EAAa;AACrD,IAAA,MAAM,OAAA,GAAU,CAAC,KAAA,EAAc,MAAA,KAAoC;AACjE,MAAA,MAAM,CAAA,GAAI,KAAA;AAEV,MAAA,IAAI,CAAA,CAAE,gBAAA,IAAoB,CAAA,CAAE,MAAA,KAAW,CAAA,IAAK,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,MAAA,EAAQ;AAC9F,MAAA,IAAI,OAAO,YAAA,CAAa,UAAU,KAAK,MAAA,CAAO,YAAA,CAAa,oBAAoB,CAAA,EAAG;AAClF,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,YAAA,CAAa,QAAQ,CAAA;AAC3C,MAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,MAAA,KAAW,EAAA,IAAM,WAAW,OAAA,EAAS;AAC5D,MAAA,MAAM,GAAA,GAAM,MAAA,CAAO,YAAA,CAAa,KAAK,CAAA;AACrC,MAAA,IAAI,GAAA,KAAQ,IAAA,IAAQ,cAAA,CAAe,IAAA,CAAK,GAAG,CAAA,EAAG;AAC9C,MAAA,MAAM,MAAM,IAAI,GAAA,CAAI,MAAA,CAAO,IAAA,EAAM,SAAS,IAAI,CAAA;AAC9C,MAAA,IAAI,GAAA,CAAI,MAAA,KAAW,QAAA,CAAS,MAAA,EAAQ;AACpC,MAAA,IAAI,SAAS,SAAA,EAAW;AACtB,QAAA,IAAI,QAAA,CAAS,SAAS,CAAA,IAAK,CAAC,IAAI,QAAA,CAAS,UAAA,CAAW,QAAQ,CAAA,EAAG;AAC/D,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,QAAA,CAAS,GAAA,CAAI,SAAS,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,GAAI,GAAA,CAAI,MAAA,GAAS,GAAA,CAAI,IAAI,CAAA;AAAA,MACtE,CAAA,MAAO;AAEL,QAAA,IAAI,GAAA,CAAI,aAAa,QAAA,CAAS,QAAA,IAAY,CAAC,GAAA,CAAI,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA,EAAG;AACtE,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,QAAA,CAAS,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,MAC5B;AAAA,IACF,CAAA;AACA,IAAA,QAAA,CAAS,KAAK,QAAA,CAA4B,QAAA,CAAS,MAAM,OAAA,EAAS,SAAA,EAAW,OAAO,CAAC,CAAA;AAAA,EACvF;AAEA,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI,QAAA,EAAU;AACd,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,KAAA,MAAW,MAAA,IAAU,UAAU,MAAA,EAAO;AAAA,EACxC,CAAA;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,IAAA,EAAM,MAAM,QAAQ,IAAA,EAAK,EAAG,OAAA,EAAS,MAAM,QAAQ,OAAA,EAAQ,EAAG,KAAA,EAAO,WAAA,EAAa,QAAQ,OAAA,EAAQ;AAC9H","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 = (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"]}
|
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-SVATPF5R.js';
|
|
2
|
+
import { delegate } from './chunk-KPXIOG2C.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import { effect } from './chunk-U6FK33SG.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
// src/scope.ts
|
|
11
11
|
var scopes = /* @__PURE__ */ new WeakMap();
|
|
@@ -58,7 +58,7 @@ 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) disposeSubtree(node);
|
|
61
|
+
if (node instanceof Element && !root.contains(node)) disposeSubtree(node);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
});
|
|
@@ -67,5 +67,5 @@ function observeRemovals(root) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export { disposeScope, disposeSubtree, observeRemovals };
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
//# 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,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"]}
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { clearStoreRegistry } from './chunk-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export { clearStoreRegistry } from './chunk-KZJXHFIB.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-U6FK33SG.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,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;
|
|
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"]}
|
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
|
|
|
@@ -17,6 +17,7 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
17
17
|
- [`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
18
|
- [`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
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>`.
|
|
20
|
+
- [`@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
21
|
|
|
21
22
|
## Reference docs
|
|
22
23
|
|
|
@@ -29,8 +30,8 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
29
30
|
- [SVG handling](https://github.com/brianwestphal/kerf/blob/main/docs/7-svg.md): namespace propagation, `toElement`.
|
|
30
31
|
- [API reference](https://github.com/brianwestphal/kerf/blob/main/docs/8-api-reference.md): every export, every option.
|
|
31
32
|
- [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_*`
|
|
33
|
+
- [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.
|
|
34
|
+
- [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
35
|
- [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
36
|
- [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
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`.
|
|
@@ -38,8 +39,10 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
38
39
|
- [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
40
|
- [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
41
|
- [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
|
|
42
|
+
- [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
43
|
- [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.
|
|
44
|
+
- [`@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.
|
|
45
|
+
- [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
46
|
|
|
44
47
|
## Examples
|
|
45
48
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kerfjs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-beta.3",
|
|
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": [
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"LICENSE"
|
|
112
112
|
],
|
|
113
113
|
"scripts": {
|
|
114
|
-
"build": "tsup",
|
|
114
|
+
"build": "tsup && node scripts/clean-dist-output.mjs dist",
|
|
115
115
|
"dev": "tsup --watch",
|
|
116
116
|
"test": "vitest run --coverage",
|
|
117
117
|
"test:watch": "vitest",
|
|
@@ -123,13 +123,15 @@
|
|
|
123
123
|
"test:dist:examples": "npm run build && node node_modules/typescript7/bin/tsc -p site/src/examples/complete/tsconfig.json",
|
|
124
124
|
"test:dist:scaffold-typing": "npm run build && node node_modules/typescript7/bin/tsc -p tests/dist/scaffold-typing/tsconfig.json",
|
|
125
125
|
"test:browser": "npm run build && playwright test",
|
|
126
|
+
"test:ui": "npm --prefix ui test",
|
|
126
127
|
"bench:micro": "vitest bench --run --config vitest.config.bench.ts",
|
|
127
128
|
"bench:serve": "bash bench/site.sh",
|
|
128
129
|
"lint": "eslint src tests",
|
|
129
130
|
"typecheck": "node node_modules/typescript7/bin/tsc --noEmit",
|
|
130
|
-
"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/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",
|
|
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",
|
|
131
132
|
"check:docs:examples": "node scripts/check-docs-examples.mjs",
|
|
132
133
|
"check:full": "npm run check && npm run --silent check:audit && playwright test",
|
|
134
|
+
"check:ui": "npm --prefix ui run check",
|
|
133
135
|
"check:docs:test-inventory": "node scripts/check-doc-test-inventory.mjs",
|
|
134
136
|
"check:docs:api-coverage": "node scripts/check-doc-api-coverage.mjs",
|
|
135
137
|
"check:docs:site-tickets": "node scripts/check-doc-site-tickets.mjs",
|
|
@@ -139,6 +141,8 @@
|
|
|
139
141
|
"check:features": "node scripts/check-feature-coverage.mjs",
|
|
140
142
|
"check:ai-bundle-in-sync": "node scripts/check-ai-bundle.mjs",
|
|
141
143
|
"ai-bundle:sync": "node scripts/sync-ai-bundle.mjs",
|
|
144
|
+
"check:lockstep-versions": "node scripts/sync-lockstep-versions.mjs --check",
|
|
145
|
+
"sync:lockstep-versions": "node scripts/sync-lockstep-versions.mjs --write",
|
|
142
146
|
"clean": "rm -rf dist coverage node_modules/.cache",
|
|
143
147
|
"prepare": "husky",
|
|
144
148
|
"release": "bash scripts/release.sh",
|
|
@@ -162,30 +166,28 @@
|
|
|
162
166
|
},
|
|
163
167
|
"devDependencies": {
|
|
164
168
|
"@eslint/js": "^10.0.1",
|
|
165
|
-
"@playwright/test": "^1.
|
|
169
|
+
"@playwright/test": "^1.63.0",
|
|
166
170
|
"@types/jsdom": "^28.0.1",
|
|
167
171
|
"@types/node": "^22.10.0",
|
|
168
172
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
169
173
|
"@typescript-eslint/parser": "^8.65.0",
|
|
170
|
-
"@vitest/coverage-v8": "^4.1.
|
|
171
|
-
"domotion-svg": "^0.
|
|
174
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
175
|
+
"domotion-svg": "^0.28.2",
|
|
172
176
|
"eslint": "^10.8.0",
|
|
173
177
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
174
178
|
"gitgist": "^1.1.0",
|
|
175
|
-
"happy-dom": "^20.
|
|
179
|
+
"happy-dom": "^20.14.5",
|
|
176
180
|
"http-server": "^14.1.1",
|
|
177
181
|
"husky": "^9.1.7",
|
|
178
182
|
"jsdom": "^29.1.1",
|
|
179
183
|
"tsup": "^8.3.0",
|
|
180
184
|
"typescript": "^6.0.3",
|
|
181
185
|
"typescript7": "npm:typescript@^7.0.2",
|
|
182
|
-
"vitest": "^4.1.
|
|
186
|
+
"vitest": "^4.1.11"
|
|
183
187
|
},
|
|
184
188
|
"gitgist": {
|
|
185
189
|
"exclude": [
|
|
186
190
|
"site/public/demos/*.svg",
|
|
187
|
-
"site/src/content/docs/docs/*",
|
|
188
|
-
"site/src/content/docs/api.md",
|
|
189
191
|
"ai/*",
|
|
190
192
|
"site/public/llms.txt",
|
|
191
193
|
"bench/results.json",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/array-signal.ts"],"names":[],"mappings":"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","file":"chunk-MRYM3O3V.js","sourcesContent":["/**\n * `arraySignal(initial)` — granular collection signal.\n *\n * A keyed-list-friendly variant of `signal()` that emits typed patch events\n * for every mutation (update / insert / remove / move / replace). When such\n * a signal is bound to `each(...)` inside a `mount()`, the keyed list\n * reconciler applies just the patches against the live DOM — no per-item\n * iteration, no `classifyItems` Map build, no LIS pass over unchanged rows.\n *\n * const rows = arraySignal<Row>([]);\n *\n * rows.update(42, (r) => ({ ...r, label: 'changed' })); // 1 update event\n * rows.insert(0, { id: 'x', ... }); // 1 insert event\n * rows.remove(7); // 1 remove event\n * rows.move(3, 0); // 1 move event\n * rows.replace([...]); // falls back to snapshot reconcile\n *\n * Read-side semantics match a regular signal: `arraySig.value` is a\n * snapshot, and reads inside `effect()` / `computed()` register as\n * dependencies, so derived values keep working.\n */\n\nimport { bumpItemVersion } from './item-version.js';\nimport type { Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\n/** A single granular mutation event. */\nexport type ArrayPatch<T> =\n | { type: 'update'; index: number; item: T }\n | { type: 'insert'; index: number; item: T }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly T[] };\n\n/**\n * Cross-bundle brand for `ArraySignal` instances. `each()` and the\n * granular reconciler check for this brand instead of `instanceof\n * ArraySignal`, so the main `kerfjs` barrel can detect arraySignal\n * inputs without importing the class at runtime — the class lives\n * only in the `kerfjs/array-signal` subpath, so apps that don't need\n * granular collections shed ~1 KB.\n *\n * Same `Symbol.for(...)`-based pattern as `SafeHtml` (KF-14): cross-\n * bundle-safe, zero-cost runtime check.\n */\nexport const ARRAY_SIGNAL_BRAND = Symbol.for('kerfjs.ArraySignal');\n\nexport class ArraySignal<T> {\n private _items: T[];\n private _version: Signal<number>;\n private _patches: ArrayPatch<T>[];\n // Branded so `isArraySignal()` recognizes instances from any copy of this module.\n readonly [ARRAY_SIGNAL_BRAND] = true as const;\n\n constructor(initial: readonly T[] = []) {\n this._items = [...initial];\n this._version = signal(0);\n this._patches = [];\n }\n\n /** Read-only snapshot. Reads inside an effect/computed register a dependency. */\n get value(): readonly T[] {\n // Touch the version signal so signals-core treats reads as tracked.\n void this._version.value;\n return this._items;\n }\n\n /**\n * Replace the item at `index` with `fn(currentItem)`. Emits one `update`\n * patch. Both styles work: returning a fresh object (idiomatic) invalidates\n * the row by identity, and mutating `item` in place and returning it works\n * too — a per-item content version (KF-418) makes the same-ref change visible\n * to every consumer's row memo.\n */\n update(index: number, fn: (item: T) => T): void {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.update: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const next = fn(this._items[index]);\n this._items[index] = next;\n this._patches.push({ type: 'update', index, item: next });\n // KF-418: a same-ref update (fn mutates and returns the same object) is\n // invisible to the row memo, which is keyed on object identity. Bump the\n // item's content version so every consumer — this list, another list over\n // this signal, a second mount, a plain-array filter() view — re-renders it.\n // Non-object items (an arraySignal<number> used as a plain signal) are\n // skipped by bumpItemVersion — they can't be each() rows (KF-419).\n bumpItemVersion(next);\n this._version.value++;\n }\n\n /** Insert `item` at `index`. Existing items at index..N shift right. Emits one `insert` patch. */\n insert(index: number, item: T): void {\n if (index < 0 || index > this._items.length) {\n throw new Error(\n `arraySignal.insert: index ${index} out of bounds [0, ${this._items.length}].`,\n );\n }\n this._items.splice(index, 0, item);\n this._patches.push({ type: 'insert', index, item });\n this._version.value++;\n }\n\n /** Append `item` at the end. Sugar for `insert(items.length, item)`. */\n push(item: T): void {\n this.insert(this._items.length, item);\n }\n\n /** Remove and return the item at `index`. Emits one `remove` patch. */\n remove(index: number): T {\n if (index < 0 || index >= this._items.length) {\n throw new Error(\n `arraySignal.remove: index ${index} out of bounds [0, ${this._items.length}).`,\n );\n }\n const [removed] = this._items.splice(index, 1);\n this._patches.push({ type: 'remove', index });\n this._version.value++;\n return removed;\n }\n\n /** Move the item at `from` to position `to`. Emits one `move` patch (no-op when from === to). */\n move(from: number, to: number): void {\n if (from === to) return;\n if (from < 0 || from >= this._items.length || to < 0 || to >= this._items.length) {\n throw new Error(\n `arraySignal.move: indices out of bounds (from=${from}, to=${to}, length=${this._items.length}).`,\n );\n }\n const [item] = this._items.splice(from, 1);\n this._items.splice(to, 0, item);\n this._patches.push({ type: 'move', from, to });\n this._version.value++;\n }\n\n /** Replace every item. Emits one `replace` patch — the granular reconciler falls back to a full keyed diff for this case. */\n replace(items: readonly T[]): void {\n this._items = [...items];\n this._patches.push({ type: 'replace', items: this._items });\n this._version.value++;\n }\n\n /**\n * @internal Used by `each()` when binding this signal to a list. Returns\n * the queue of granular patches issued since the previous call, then\n * clears the queue. Best paired with a single binding — a second consumer\n * in the same render gets an empty array (which forces the snapshot\n * fall-back path, which is correct but slower).\n */\n _consumePatches(): ArrayPatch<T>[] {\n const out = this._patches;\n this._patches = [];\n return out;\n }\n}\n\n/** Construct an array signal seeded with `initial`. */\nexport function arraySignal<T>(initial: readonly T[] = []): ArraySignal<T> {\n return new ArraySignal(initial);\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store.ts"],"names":[],"mappings":";;;;AAsCA,IAAM,WAAyC,EAAC;AAEzC,SAAS,YACd,IAAA,EACyB;AACzB,EAAA,MAAM,QAAA,GAA2B,MAAA,CAAO,IAAA,CAAK,OAAA,EAAS,CAAA;AAKtD,EAAA,MAAM,UAAkC,QAAA,CAAS,SAAA,GAAY,EAAE,MAAA,EAAQ,OAAM,GAAI,IAAA;AAEjF,EAAA,MAAM,GAAA,GAAM,CAAC,IAAA,KAAuB;AAMlC,IAAA,MAAM,QAAQ,QAAA,CAAS,UAAA;AACvB,IAAA,MAAM,GAAA,GAAM,KAAA,GAAQ,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAClC,IAAA,IAAI,SAAS,QAAA,CAAS,SAAA,GAAY,QAAA,CAAS,KAAA,EAAO,KAAK,OAAO,CAAA;AAC9D,IAAA,QAAA,CAAS,KAAA,GAAQ,GAAA;AAAA,EACnB,CAAA;AAOA,EAAA,MAAM,MAAM,MAAwB;AAClC,IAAA,MAAM,IAAI,QAAA,CAAS,KAAA;AACnB,IAAA,MAAM,WAAW,QAAA,CAAS,aAAA;AAC1B,IAAA,IAAI,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,OAAO,MAAM,QAAA,EAAU;AACnD,MAAA,OAAO,SAAS,CAAoB,CAAA;AAAA,IACtC;AACA,IAAA,OAAO,CAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAErC,EAAA,MAAM,KAAA,GAAiC;AAAA,IACrC,KAAA,EAAO,QAAA;AAAA,IACP,OAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,QAAA,CAAS,KAAA,GAAQ,KAAK,OAAA,EAAQ;AAAA,IAChC;AAAA,GACF;AAEA,EAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,cAAA,GAAuB;AACrC,EAAA,KAAA,MAAW,CAAA,IAAK,QAAA,EAAU,CAAA,CAAE,KAAA,EAAM;AACpC;AAMO,SAAS,kBAAA,GAA2B;AACzC,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AACpB","file":"chunk-SAYPJ6XR.js","sourcesContent":["/**\n * `defineStore({ initial, actions })` — composable testable stores layered on\n * top of `reactive.ts`'s signals.\n *\n * Three rules:\n * 1. `state` is read-only. Consumers read via `state.value` or subscribe via\n * `effect()`. They cannot write directly.\n * 2. `actions` is the only mutation surface. All writes go through named\n * action functions. This is what makes stores testable — assert against\n * actions, not against arbitrary writes.\n * 3. `reset()` resets to `initial()`. Always defined; tests use it for\n * setup, lifecycle hooks (route change, sign-out, etc.) use it for\n * tear-down.\n *\n * A module-level registry tracks every store created via `defineStore()`;\n * `resetAllStores()` walks the registry and calls each `reset()`. Useful for\n * tests + project-switch / logout / route-reset scenarios where every piece\n * of client state should return to its initial shape.\n */\n\nimport { devHooks, type WarnOnceContext } from './dev-hooks.js';\nimport type { ReadonlySignal, Signal } from './reactive.js';\nimport { signal } from './reactive.js';\n\nexport interface Store<TState, TActions> {\n /** Read-only reactive view. Consumers read `state.value` or subscribe via `effect()`. */\n readonly state: ReadonlySignal<TState>;\n /** Named mutators — the only way to change state. */\n readonly actions: TActions;\n /** Reset state to `initial()`. Used by tests and lifecycle hooks. */\n reset(): void;\n}\n\ninterface DefineStoreSpec<TState, TActions> {\n initial: () => TState;\n actions: (set: (next: TState) => void, get: () => Readonly<TState>) => TActions;\n}\n\nconst REGISTRY: Array<{ reset: () => void }> = [];\n\nexport function defineStore<TState, TActions>(\n spec: DefineStoreSpec<TState, TActions>,\n): Store<TState, TActions> {\n const internal: Signal<TState> = signal(spec.initial());\n // KF-212: per-store one-shot dedup for the opt-in narrow-set warning.\n // Default off; consumers opt in via `KERF_DEV_WARN_NARROW_SET=1` in dev.\n // Allocated only when the diagnostics are installed — production never pays\n // for an object it will never read.\n const warnCtx: WarnOnceContext | null = devHooks.narrowSet ? { warned: false } : null;\n\n const set = (next: TState): void => {\n // With `kerfjs/dev` installed, `next` may carry proxies handed back by the\n // `get()` trap (e.g. `set({ ...get(), count: 1 })`). Unwrap them so the\n // internal signal only ever holds a plain object — the narrow-set warning\n // and every consumer read see raw state, never a Proxy. Production stores\n // the bare reference.\n const toRaw = devHooks.storeToRaw;\n const raw = toRaw ? toRaw(next) : next;\n if (warnCtx) devHooks.narrowSet?.(internal.value, raw, warnCtx);\n internal.value = raw;\n };\n // With the diagnostics installed, wrap the reference returned to actions in a\n // deep read-only Proxy so that `get().count = 42` / `get().nested.x = 1`\n // (documented Rule 8 violations) throw a `TypeError` instead of silently\n // landing on the underlying state without notifying subscribers. The live\n // state object is never frozen or mutated, so external references to it stay\n // writable. Production returns the bare reference — no proxy, no wrapping.\n const get = (): Readonly<TState> => {\n const v = internal.value;\n const readonly = devHooks.storeReadonly;\n if (readonly && v !== null && typeof v === 'object') {\n return readonly(v as TState & object);\n }\n return v;\n };\n\n const actions = spec.actions(set, get);\n\n const store: Store<TState, TActions> = {\n state: internal,\n actions,\n reset() {\n internal.value = spec.initial();\n },\n };\n\n REGISTRY.push(store);\n return store;\n}\n\n/**\n * Reset every store registered via `defineStore()` to its `initial()` value.\n * Used by tests and by application lifecycle hooks (project switch, logout,\n * route reset).\n */\nexport function resetAllStores(): void {\n for (const s of REGISTRY) s.reset();\n}\n\n/**\n * Test helper — clears the registry. Exposed via the `kerfjs/testing` subpath,\n * not the main `kerfjs` entry. Unit tests use it to isolate stores between cases.\n */\nexport function clearStoreRegistry(): void {\n REGISTRY.length = 0;\n}\n"]}
|