kerfjs 4.4.1 → 5.0.0-beta.4

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 (73) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/LICENSE +1 -1
  3. package/README.md +3 -3
  4. package/ai/cursorrules +46 -1
  5. package/ai/manifest.json +62 -5
  6. package/ai/skill.md +53 -2
  7. package/dist/actions.d.ts +1 -1
  8. package/dist/actions.js +4 -4
  9. package/dist/actions.js.map +1 -1
  10. package/dist/array-signal.js +5 -5
  11. package/dist/async.js +17 -10
  12. package/dist/async.js.map +1 -1
  13. package/dist/attach.d.ts +11 -8
  14. package/dist/attach.js +52 -4
  15. package/dist/attach.js.map +1 -1
  16. package/dist/chunk-GY4XV2UV.js +1 -1
  17. package/dist/{chunk-VVDJLWMP.js → chunk-HW7KSM2Y.js} +2 -2
  18. package/dist/chunk-HW7KSM2Y.js.map +1 -0
  19. package/dist/{chunk-KEZTD6H4.js → chunk-KPXIOG2C.js} +3 -3
  20. package/dist/{chunk-KEZTD6H4.js.map → chunk-KPXIOG2C.js.map} +1 -1
  21. package/dist/{chunk-SAYPJ6XR.js → chunk-KZJXHFIB.js} +10 -6
  22. package/dist/chunk-KZJXHFIB.js.map +1 -0
  23. package/dist/{chunk-MRYM3O3V.js → chunk-LVH3GC6B.js} +11 -8
  24. package/dist/chunk-LVH3GC6B.js.map +1 -0
  25. package/dist/chunk-QIP723L4.js +1 -1
  26. package/dist/{chunk-SRWQKB33.js → chunk-SVATPF5R.js} +97 -74
  27. package/dist/chunk-SVATPF5R.js.map +1 -0
  28. package/dist/{chunk-3APBEVHF.js → chunk-U6FK33SG.js} +3 -3
  29. package/dist/{chunk-3APBEVHF.js.map → chunk-U6FK33SG.js.map} +1 -1
  30. package/dist/{chunk-U32TFTGZ.js → chunk-UZJ6I4T6.js} +3 -3
  31. package/dist/chunk-UZJ6I4T6.js.map +1 -0
  32. package/dist/{chunk-YHH7OUFA.js → chunk-V46JKE44.js} +3 -3
  33. package/dist/chunk-V46JKE44.js.map +1 -0
  34. package/dist/{chunk-SUPUPSBE.js → chunk-ZDCJZCNO.js} +9 -8
  35. package/dist/chunk-ZDCJZCNO.js.map +1 -0
  36. package/dist/dev.d.ts +9 -6
  37. package/dist/dev.js +5 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/html.d.ts +1 -1
  40. package/dist/html.js +5 -5
  41. package/dist/html.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +11 -11
  44. package/dist/jsx-runtime.js +5 -5
  45. package/dist/list.d.ts +1 -1
  46. package/dist/list.js +273 -211
  47. package/dist/list.js.map +1 -1
  48. package/dist/overlay.d.ts +190 -280
  49. package/dist/overlay.js +401 -361
  50. package/dist/overlay.js.map +1 -1
  51. package/dist/remount.d.ts +5 -3
  52. package/dist/remount.js +29 -10
  53. package/dist/remount.js.map +1 -1
  54. package/dist/router.d.ts +1 -1
  55. package/dist/router.js +44 -22
  56. package/dist/router.js.map +1 -1
  57. package/dist/scope.d.ts +4 -3
  58. package/dist/scope.js +10 -10
  59. package/dist/scope.js.map +1 -1
  60. package/dist/testing.js +4 -4
  61. package/dist/timing.d.ts +3 -2
  62. package/dist/timing.js +5 -5
  63. package/dist/timing.js.map +1 -1
  64. package/llms.txt +7 -4
  65. package/package.json +12 -8
  66. package/dist/chunk-MRYM3O3V.js.map +0 -1
  67. package/dist/chunk-SAYPJ6XR.js.map +0 -1
  68. package/dist/chunk-SRWQKB33.js.map +0 -1
  69. package/dist/chunk-SUPUPSBE.js.map +0 -1
  70. package/dist/chunk-U32TFTGZ.js.map +0 -1
  71. package/dist/chunk-VVDJLWMP.js.map +0 -1
  72. package/dist/chunk-YHH7OUFA.js.map +0 -1
  73. /package/dist/{attrSelector-Cmu2ZoGO.d.ts → attr-Cmu2ZoGO.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,117 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - Made the UX catalog's theme action follow the operating system's initial light/dark appearance, label itself with the appearance it will switch to, and explicitly override either direction when activated.
10
+ - Corrected the UX catalog's project-details dialog gutters so its header uses less side inset, its selected title uses the full content gutter, its value table fills the available detail width between the usual margins, and its record actions avoid a doubled left inset.
11
+ - Fixed the UI catalog's Kerf logo in the Vite development server and kept emitted assets working when the demo is hosted below a preview or proxy path.
12
+ - Aligned the composer-form recipe's labels, hints, character count, and textarea value to a consistent 8px field gutter at wide and narrow widths.
13
+ - Prevented UX catalog resource links from overlapping the related-component selector at phone widths.
14
+ - Replaced the UX catalog's generic K badge with the Kerf logo and DialogHeader-like title/subtitle alignment; collapsing the catalog now removes its empty rail and moves the restore action to the main toolbar's leading edge, with first-class guidance for corresponding inline-start and inline-end pane controls.
15
+ - Corrected the composer-form recipe hierarchy and layout: `DialogHeader` now supplies the form's referenced title and summary, while fields and actions align to the shared 8px control gutter instead of a doubled content-item inset.
16
+ - Aligned the UX catalog's StateBanner tone labels with each banner's leading icon by deriving the shared 8px margin, 1px border, and 8px content inset in demo-only specimen chrome.
17
+ - Let the UX catalog's resizable-panel specimen fill its available preview height and moved its committed-width readout into the shared status footer.
18
+ - Reworked the UI catalog into collapsible pane chrome with toolbar headers, a full-stage checkerboard preview, compact resource footer, hidden-by-default floating recipe notes, and edge-aligned `PageHeader` actions.
19
+ - Aligned multiline `MenuItem` and `MenuActionRow` leading icons with the first text line instead of centering them against the full wrapped label.
20
+ - Fixed packages generated by `create-kerf-component` failing declaration builds under TypeScript 6 when tsup injects its deprecated `baseUrl` option.
21
+ - Fixed the complete kanban example's mobile layout by stacking its columns and header controls without horizontal overflow while preserving the wider board layout.
22
+ - Fixed the complete dashboard example's mobile layout so its status header, chart, and table fit the viewport without horizontal overflow.
23
+ - Decomposed `bindList` into focused keyed-row and virtualization controllers while keeping `list.ts` as the public orchestration entry; snapshot and granular updates now share one item-replacement contract.
24
+ - Corrected the canonical, published, and AI-facing dev-warning documentation to reflect the hook-only production boundary: kerf never gates diagnostics on `NODE_ENV`; omitting `kerfjs/dev` makes them unreachable, while installed opt-in warnings check only their own switch.
25
+ - Added the missing always-on row-key warning to the published diagnostics guide and made the documentation gate require every canonical diagnostic section on the public site.
26
+ - Restored the site-wide Playwright visual QA command with dynamic discovery of every built HTML surface (including `404.html`), desktop/tablet/mobile full-page captures, root- and URL-aware route filtering, collision-safe evidence names, and automated page-health checks for overflow, settled images, and collapsed content.
27
+ - Kept `observeRemovals()` scopes alive when their nodes move between parents or reorder within the observed root; automatic disposal now waits for permanent removal from that root.
28
+ - Made `remountOn()` disposal idempotent and ownership-safe: the first call tears down its mounted subtree and stops the key watcher, while repeated calls leave any later external content in the parent untouched.
29
+ - Made `throttle().cancel()` effective from inside both leading and trailing callbacks: the callback can now reset the active rate window immediately instead of having a new cooldown installed after it returns.
30
+ - Removed the overlay barrel/helper cycle by extracting the lifecycle core and splitting `confirm`, `prompt`, `form`, and `choice` into focused internal modules, without changing the public `kerfjs/overlay` API.
31
+ - Fixed `attach()` for nodes prepared before insertion: setup still runs immediately, teardown waits through the initially-disconnected phase, then fires once after the node has connected and is later removed (including direct insertion into an already-connected shadow root, ancestor removal, same-batch light-DOM insertion/removal, and removal through a shadow host).
32
+ - Made `bindList` reject duplicate keys before changing the DOM, with errors that name the key and both indices; rejected `arraySignal` batches are drained and the next valid state snapshot-recovers before granular updates resume.
33
+ - Made `bindList` recover after a granular row render throws: the failing call still reports the error, while the next source update reconciles the authoritative snapshot before granular patches resume, including when an earlier patch in the same batch already changed the DOM.
34
+ - Fixed stores created before `kerfjs/dev` installation never emitting the opt-in narrow-set warning. Store actions now resolve the warning hook at `set()` call time and allocate per-store dedup state only when diagnostics are present.
35
+ - Added a deterministic `bindList` transition matrix covering empty/refill recovery, granular-to-snapshot-to-granular sequences, and mixed batched structural changes with node-identity assertions.
36
+ - Made concurrent fallback overlays arbitrate dismissal from the top down, so one Escape, backdrop, or outside click closes only the active modal or non-modal surface.
37
+ - Made `prompt()` and `form()` reject malformed bring-your-own dialog markup immediately: missing required input markers now close the incomplete overlay and throw an error naming the exact marker (and form field) instead of failing later on interaction.
38
+ - Covered `each()` count-drift recovery through the public API: after a granular insert drains its patches and row rendering fails, a subsequent update now has a regression test proving the snapshot path repairs the DOM.
39
+ - Fixed tooltips hiding when just one of their pointer/focus triggers left; pointer and focus presence are now tracked independently, so either interaction keeps the tooltip open until both end.
40
+ - Strengthened the `bindList` feature index with exact existing guards for same-key rebuilds, granular patch sequences, and teardown semantics.
41
+ - Extracted pure granular-list index-shift and cache-key-drift stages from the `each()` transition coordinator for direct testing.
42
+ - Normalized internal helper module filenames to the repository's kebab-case or primary-export convention and updated all live imports and documentation paths.
43
+ - Made list-binding rationale comments self-contained and named the bounded template/URL diagnostic excerpt limits.
44
+ - Split `mount()`'s render effect into named synchronous static-render and list-reconcile/commit phases without changing lifecycle order.
45
+ - Replaced terse router and overlay-position internals with descriptive route-segment, event, rectangle, and viewport names.
46
+ - Preserved edited stale AI-assistant configs as forks by validating their versioned canonical section against shipped historical hashes before offering an autofix.
47
+ - Required every complete example app to have its own feature-coverage row mapped to a smoke test inside that app's Playwright suite.
48
+ - Made router parameter decoding fail closed to no-match on malformed percent escapes instead of throwing `URIError`, including named and wildcard captures.
49
+ - Pointed `@kerfjs/ui` package metadata at the existing published component-packages documentation route and added a source-backed metadata regression test.
50
+ - Fixed the complete router example's broken documentation links and taught the docs-example gate to validate published routes and heading fragments.
51
+ - Added full-pipeline coverage for missing-row-key diagnostics across initial, snapshot, granular, and in-place list reconciliation, including per-binding deduplication and production silence.
52
+ - Extended the feature-completeness gate to inventory router-subpath exports and deduplicate values re-exported across public surfaces.
53
+ - Kept `kerfjs/ai-assistant-configs` filesystem checks read-only during plain ESLint runs while retaining explicit `eslint --fix` installation and stale-file updates.
54
+ - Required history-router bases to match an exact path or path-segment boundary, so `/app` no longer strips or intercepts `/apple`.
55
+ - Added the missing MIT `LICENSE` to generated `create-kerf-component` packages, including package-name token replacement and publish-contents coverage.
56
+ - Made dangerous-URL attribute-name matching ASCII-case-insensitive so mixed-case spellings cannot bypass static or bound screening.
57
+ - Normalized synchronous `resource().run()` fetcher throws into the same stale-guarded failed state and resolving promise used for asynchronous rejections.
58
+ - Escaped plain-string `toast()` content as text while preserving trusted `SafeHtml` and render-function markup, closing a stored-markup injection path.
59
+ - Rejected non-finite and fractional indices in every indexed `arraySignal` mutator before changing source state or emitting a patch; even equal `move()` indices are now validated before the no-op path.
60
+ - Made `DisclosureArrow` animate configurable directions over the shortest rotation path, including the left-to-up demo's natural 90-degree clockwise turn; closed-to-open 180-degree ties use counterclockwise rotation.
61
+ - Reworked `DialogHeader` as a true top toolbar: its 24px icon in a 34px circle and first title line align with automatically grouped actions, while an optional subtitle remains below; added localized action-group labels and compatibility for existing pre-grouped actions.
62
+ - Gave every `ValueTableRow` 8px of root-scaled top and bottom padding while preserving its semantic inline inset and icon-aware separator alignment.
63
+ - Fixed the composer-form recipe's Reset action so upgraded Web Awesome input and textarea controls clear their live displayed values together with the controlled Kerf state, while retaining the `Draft reset` announcement.
64
+ - Removed the unsolicited command-palette catalog demo and production recipe while retaining the earlier, independent application-local adapter example for missing-concept guidance.
65
+ - Kept the `ResizableRegion` UX specimen readable by moving committed-width status below the controlled pane at narrow widths, preserving its 200% root-scaled split, and retaining local scroll access to the handle at its maximum size.
66
+ - Reworked the composer-form recipe into one coherent visible form surface with exactly three transparent 8/1/8 content sections, 24px major rhythm, and a conditional `StateBanner` as its only nested semantic surface.
67
+ - Made `MenuHeader` fill its available inline width, align separate actions at the logical end in both LTR and RTL, and use an overridable 18px visible action glyph without shrinking the 44px target.
68
+ - Added first-class `MenuHeader` count semantics with a required localized `countLabel`, zero-safe neutral pill presentation, accessible heading/disclosure naming, legacy non-count badge exclusivity, and runtime filtering for invalid counts and protected presence flags.
69
+ - Made `MenuHeader` toggle mode supply the production 18px `DisclosureArrow` by default while retaining custom `actionIcon` replacement, removed competing raw SVG sizing/rotation, and made the menu-composition Tools disclosure control real content instead of showing a false Projects chevron.
70
+ - Repaired the `DisclosureArrow` UX specimen by replacing its ambiguous sorting glyph with a right-facing arrow, putting both examples in independently controlled native buttons with stable names and `aria-expanded`, and demonstrating custom directions through delegated pointer and keyboard activation across narrow and 200%-zoom layouts.
71
+ - Set `DisclosureArrow` to an 18px root-scaled default, removed the catalog-only 24px enlargement, retained `--kui-disclosure-arrow-size` for scoped consumer overrides, and kept Kerf `Select` on its independent `.5` Web Awesome expand-glyph scale.
72
+ - Added a site-scoped CI dependency audit that includes the complete static-site build tree and fails on high or critical advisories while keeping low/moderate development-tool findings informational.
73
+ - Aligned the UX catalog's header-composition dialog with the shared 8px inline layout gutter while preserving its 16px vertical separation.
74
+ - Added visible `View demo source` and `Read guidance` affordances to every UI catalog detail, plus `View component source` for first-party components, backed by deterministic repository-relative paths for the main renderer, individual recipe files, Web Awesome specimens, and canonical browser-import implementations. Web Awesome entries explicitly label local documentation as Kerf integration guidance; all links use deploy-safe GitHub URLs and a responsive accessible layout.
75
+ - Added runtime-safe `AppTab.rootAttributes` for domain `data-*` metadata plus decorative `AppTab.closeIcon` and `ResizableRegion.handleIcon` slots, preserving existing tab and resize wiring ownership while blocking case-folded component/action/drag/drop collisions.
76
+ - Added `MenuActionRow`, a full-width navigation row with sibling primary and trailing native-button actions, independent names and disabled states, controlled selection semantics, safe metadata/popover extension slots, and responsive 44px targets. `MenuItem.trailing` is now explicitly documented as dormant content.
77
+ - Added safe typed MenuItem/MenuHeader extension slots for application `data-*` metadata and native popover trigger relationships while protecting component-owned action, selection, disclosure, accessible-name, disabled, icon, and native-button semantics.
78
+ - Fixed keyboard deletion of a controlled `TokenSearchField` chip so `wireTokenSearchFields` restores focus and the text-relative caret after the application rerenders the editor.
79
+ - Aligned `@kerfjs/ui` CSS guidance and AI regression scoring with the catalog's exact public anatomy: documented public-class composition selectors are supported, private tag/id/attribute/unlisted-class descendants remain rejected, and historical measured runs replay under their recorded stricter oracle.
80
+ - Added typed `ValueTableRow` composition with a first-class optional icon hook. Value-table separators now align 8px from both edges for iconless rows and 40px from the left plus 8px from the right for rows with a 24px leading icon.
81
+ - Added first-class collapsible `TokenSearchField` support: an empty closed field becomes one iconic search action, controlled activation animates to the complete searchbox, and text or tokens keep it expanded after blur. It works standalone or when composed inside `ToolbarControlGroup`; the UX demo demonstrates that common toolbar composition at wide and narrow sizes.
82
+ - Restored the public documentation site to its Astro + Starlight implementation while the Kerf UI redesign is reconsidered, and removed the in-progress `@kerfjs/ui` page and promotional links from public navigation.
83
+ - Fixed `TokenSearchField` first-line alignment so the leading icon, editable text, clear action, and trailing content share the field's vertical center while remaining pinned when text wraps.
84
+ - Replaced sidebar-specific and wrapper-inset UI geometry with one shared pane/content model across sidebars, main areas, inspectors, and dialogs. Unpadded panes now compose optional toolbars, one scrolling content stack, and optional footers; content children own consistent 8px margin, 1px transparent-or-visible border, 8px padding, 12px or 22px radii, and 24px major separation. Toolbar groups retain 44px geometry even with transparent chrome, and `MenuHeader` now supports a badge while separating dormant title content from its action.
85
+ - Added pixel-first `remify(<px>)` authoring for `@kerfjs/ui` styles. The build converts values exactly against a 16px baseline into ordinary `rem`, package CSS exports now deliver compiled `dist/styles/` files, and the UX catalog applies the same transform during Vite development so CSS edits remain hot-reloaded.
86
+ - Added a side-effect-free `@kerfjs/ui/webawesome` TypeScript declaration subpath for all 70 catalog-supported Web Awesome elements, synchronized against the installed custom-elements manifest and covered by a downstream package-export compile fixture.
87
+ - Added a deterministic internal `@kerfjs/ui` AI-regression foundation with seven task-shaped prompts, frozen versioned context conditions, AST-aware structural scoring, generated public declaration signatures, equivalent supported import scoring in suite v2, and an opt-in non-executing TypeScript compile-evidence sidecar with exact response/config/package/declaration hashes. Adversarial fixtures catch component substitution, duplicated primitives, and spacing/layout ownership regressions without invoking a live model in CI.
88
+ - Added seven lazy, production-backed `@kerfjs/ui` composition recipes for application shells, sidebars, workspace headers, master-detail dialogs, composer forms, list-state lifecycles, and mixed-control toolbars, with stable catalog routes, canonical AI metadata, drift gates, responsive/keyboard/state coverage, and a copyable stable-root adapter that wires delegated actions, form/dialog events, resize commits, and idempotent disposal outside the catalog.
89
+ - Added a shipped, schema-described `@kerfjs/ui` machine-readable component catalog with deterministic typed UX projection and drift gates for public exports, package/CSS/registration paths, relationships, Web Awesome manifest coverage, AI guidance, routes, CSS hooks, and documentation links.
90
+ - Added a canonical `@kerfjs/ui/layout.css` vocabulary for responsive page, pane, section, control, metadata, surface, dialog, and scroll ownership, with semantic variables, compact/narrow behavior, consumer and AI guidance, normalized catalog compositions, and wide/intermediate/narrow/200%-zoom geometry coverage.
91
+ - Added a need-first `@kerfjs/ui` component-selection matrix and automated drift gate so AI tools choose among reuse, composition, thin application adapters, custom semantics, and overlapping Web Awesome components without inferring contracts from implementation CSS. A typed application-local command-palette example now shows how a missing package concept still uses canonical layout, one inset owner, and related-control grouping while keeping semantics honest and proposing recurring behavior upstream.
92
+ - Fixed `@kerfjs/ui` `Select` option icons disappearing after a Kerf rerender. Icon slots now have stable per-select/per-choice keys and preserve Web Awesome-owned slot state, while custom selected content is keyed by the controlled value so it updates cleanly.
93
+ - Added a shared `@kerfjs/ui` sidebar spacing composition with distinct 10px interaction and 20px content rails, 24px icon slots plus 10px gaps, and 44px row/header/toolbar targets. Menu headers, icon-bearing and iconless rows, bordered surfaces, and `.kui-sidebar-toolbar` now share public geometry tokens, consumer/AI guidance, responsive and RTL specimens, and browser assertions.
94
+ - Added a reusable controlled `TokenSearchField` to `@kerfjs/ui`, extracting Hot Sheet 2's free-text plus ordered atomic filter-chip field with accessible edit/remove/clear actions, DOM read and caret helpers, tree-shakeable styles, and full catalog coverage.
95
+ - Reorganized the largest catch-all unit suites into behavior-focused files while preserving all 1,554 existing tests, assertion counts, transition matrices, coverage, and feature-index mappings.
96
+ - Expanded the `@kerfjs/ui` pull-request browser gate from Chromium-only coverage to the full Chromium, Firefox, and WebKit suite, with Playwright binary caching keyed to the UI lockfile.
97
+ - Added a strict npm install-script policy for the site: only the reviewed `esbuild` and `sharp` binary installers run, the linked root package's Husky-only `prepare` is denied, and a preinstall checker detects lockfile installer drift.
98
+ - Updated the root and UI browser-test harnesses for Node 26: Playwright 1.63 removes the deprecated ESM loader registration, and normalized color variables keep web-server and worker output free of `NO_COLOR`/`FORCE_COLOR` conflict warnings.
99
+ - Cleaned generated root and `@kerfjs/ui` ESM shims so downstream bundlers no longer report ignored bare chunk imports, and deduplicated source-map directives without weakening package tree shaking.
100
+ - Added explicit Web Awesome component-selection guidance and aligned UX descriptions: prefer Kerf Select, SegmentedControl, TabBar, LucideIcon, and ResizableRegion for common app patterns; consider Popup for custom anchoring; reserve specialized ecosystem alternatives for concrete requirements. ResizableRegion now includes Hot Sheet 2's overridable 1px separator plus hover/focus grip.
101
+ - Made Web Awesome Tooltip and Popover arrowless by default in the optional Kerf theme, with public per-theme, scoped, and per-instance overrides plus focused UX and browser coverage.
102
+ - Documented Web Awesome Markdown's trusted-input-only security boundary, client-only rendering, and shared mutable Marked configuration, with an explicitly labeled UX specimen and a regression assertion for the warning.
103
+ - Clarified that Web Awesome Badge and Tag are the tree-shakeable generic primitives: status badges use the pill treatment, tags remain rounded rectangles, and domain adapters stay application-owned when they add mappings or mutation behavior.
104
+ - Unified Accordion, Details, Breadcrumb, and Kerf Select chevron scale through an overridable shared disclosure-icon token.
105
+ - Matched Web Awesome Carousel navigation to Kerf's compact disclosure geometry, using overridable 16px arrows and 7px visible page dots with accessible macOS-sized hit targets.
106
+ - Expanded the Web Awesome Animation UX specimen into an interactive settings panel with preset, easing, timing, playback, transport, lifecycle, and reduced-motion behavior.
107
+ - Fixed the Web Awesome Toast UX specimen to use the component's programmatic stack API, with visible browser coverage and an explicit note about Hot Sheet 2's current custom toast implementation.
108
+ - Made the Web Awesome intersection, mutation, and resize observer UX specimens interactive, with deliberate triggers, visible live-event results, and browser coverage.
109
+ - Updated the UI package's Vitest toolchain to 4.1.11 and resolved the remaining development-only npm audit advisories without changing its runtime dependency surface.
110
+ - Added a first-class controlled `SegmentedControl` to `@kerfjs/ui`, with Hot Sheet 2-aligned toolbar, rounded-rectangle, pill, equal-width, small, and disabled presentations; native pressed-button semantics; scoped palette overrides; tree-shakeable component CSS; and a complete UX-catalog route.
111
+ - Aligned `@kerfjs/ui` defaults with Hot Sheet 2, added overridable semantic state palettes, completed ToolbarControlGroup and StateBanner catalog variants, simplified related-component navigation, corrected MenuHeader action alignment, and added a controlled horizontally scrolling TabBar with pointer and keyboard reordering plus proximity-based edge autoscroll.
112
+ - Updated `@kerfjs/ui` to develop and test against Lucide 1.43.0 and Web Awesome 3.12.0, with matching peer baselines.
113
+ - Added an opt-in `@kerfjs/ui/webawesome.css` theme for Web Awesome's free components. It carries the Hot Sheet 2-aligned semantic palette, form and panel geometry, focus, tooltip, radius, and shadow choices without registering component JavaScript; the UX catalog now lists all 70 free Web Awesome 3.12 components under a collapsible ecosystem section, gives each a focused themed route and dependency links, and retains the aggregate light, dark, narrow, and scoped-override gallery.
114
+ - Fixed trapped overlays losing sequential focus in WebKit after the second control. Implicitly focusable descendants now receive `tabindex="0"` inside the trap, while authored tabindex values (including `-1`) remain intact.
115
+ - Fixed Firefox contenteditable carets jumping to the list container during keyed `moveBefore()` reorders. `each()` and `bindList` now restore exact contenteditable Selection boundary nodes and offsets after every move pass, even when the browser keeps `activeElement` unchanged.
116
+ - Repaired lockstep version metadata for the ESLint plugin and component scaffold, and added a release-time sync plus repository gate so package manifests, locks, plugin metadata, and published examples cannot drift again.
117
+ - Added the lockstep `@kerfjs/ui` package: accessible toolbar, menu, tab, header, resize, select, and feedback primitives; semantic CSS tokens and browser component subpaths that automatically include only reachable component styles; CSS-free root/SSR entries and an optional complete stylesheet; opt-in Web Awesome registration; AI-oriented docs; a production-backed UX catalog; bundle/unit/three-engine browser gates; and a least-privilege release workflow.
118
+ - The virtual-list demo animation opens on the full app again (header, the "N in the DOM" badge, and the list) before the scroll; the list now scrolls inside its own box while the header stays put (previously the whole frame was a full-bleed list strip), holds at row 0 before gliding instead of starting a few rows down, and the loop stays in phase on every pass. Captured with domotion-svg 0.28.2.
119
+
9
120
  ## [4.4.1] - 2026-08-26
10
121
 
11
122
 
package/LICENSE CHANGED
@@ -54,7 +54,7 @@ Steele-Idem, which is also distributed under the MIT License:
54
54
 
55
55
  ---
56
56
 
57
- `src/attrSelector.ts`'s `cssEscapeIdent` adapts the CSS identifier escaping of
57
+ `src/attr.ts`'s `cssEscapeIdent` adapts the CSS identifier escaping of
58
58
  the [CSS.escape polyfill](https://github.com/mathiasbynens/CSS.escape) by
59
59
  Mathias Bynens, which is also distributed under the MIT License:
60
60
 
package/README.md CHANGED
@@ -63,13 +63,13 @@ Write plain `.tsx` and build with your existing esbuild / Vite / tsup — no ext
63
63
 
64
64
  2. **No virtual DOM, no compiler.** JSX → HTML strings → native diff. DevTools shows the real DOM because it *is* the DOM.
65
65
 
66
- 3. **Values bind, structure re-renders.** Hand a signal *itself* into a JSX hole — `class={selectedId}` — and kerf binds that one node: on change, only that attribute updates, with no render re-run and no list reconcile. A selection flip on a 10,000-row table touches exactly one class. ([more →](#fine-grained-updates-bind-a-signal-into-a-hole))
66
+ 3. **Values bind, structure re-renders.** Hand a signal *itself* into a JSX hole — `class={selectedId}` — and kerf binds that one node: on change, only that attribute updates, with no render re-run and no list reconcile. Moving selection between rows in a 10,000-row table touches at most the old and new row classes. ([more →](#fine-grained-updates-bind-a-signal-into-a-hole))
67
67
 
68
68
  4. **Focus, selection, and listeners survive re-renders — even mid-list.** The reconciler morphs instead of rebuilding, so caret position, IME composition, scroll, and delegated listeners survive every update; keyed rows are patched in place rather than recreated.
69
69
 
70
70
  5. **Safe by default.** Text and attributes are HTML-escaped automatically, URL attributes are scheme-screened (`javascript:` dropped), and inline `on*` handlers are rejected outright — so untrusted data stays inert. `raw()` is the explicit, auditable opt-out.
71
71
 
72
- **Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a ~18-export API with no hooks, lifecycle, or per-instance state · **nine** tree-shakeable companion subpaths (`router`, `list`, `overlay`, `async`, …) that stay out of the core until imported · an [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) + opt-in dev warnings + a `create-kerf-component` scaffold · plain TS/JSX/ESM that drops into esbuild / Vite / tsup — or **no** build at all via the `html` tagged template.
72
+ **Plus, nothing you don't ask for:** JSX typed against the HTML standard (not React's props) · a ~18-export API with no hooks, lifecycle, or per-instance state · **nine** tree-shakeable companion subpaths (`router`, `list`, `overlay`, `async`, …) that stay out of the core until imported · an [ESLint plugin](https://brianwestphal.github.io/kerf/docs/eslint-plugin/) + opt-in dev warnings + `create-kerf-component` scaffold · plain TS/JSX/ESM that drops into esbuild / Vite / tsup — or **no** build at all via the `html` tagged template.
73
73
  ## When to use Kerf
74
74
 
75
75
  - **Hybrid desktop apps (Tauri / Electron)** — small bundle, predictable diff, debuggable runtime; ideal for the embedded webview.
@@ -158,7 +158,7 @@ mount(root, () => (
158
158
  status.value = 'saving'; // updates the class + the text node directly — no re-render
159
159
  ```
160
160
 
161
- The headline use is external state driving one spot: a `selectedId` flipping a single row's class inside a 10,000-row `each()` list touches exactly that one node, no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
161
+ The headline use is external state driving a hot spot: a `selectedId` moving between rows inside a 10,000-row `each()` list updates at most the old and new row class attributes, with no reconcile. Works in static content and inside `each()` rows (a row's binding is torn down with the row); outside a `mount()` (SSR / `SafeHtml.toString()`) a bound signal just snapshots its current value.
162
162
 
163
163
  This is kerf's guiding idiom — *values bind, structure re-renders*: pass the signal itself wherever a hole is just a value, and read `.value` in the render function only where the JSX structure depends on it. A render that reads no `.value` runs exactly once; from then on every update is a direct write to the node it concerns. See [`docs/2-reactivity.md`](./docs/2-reactivity.md) §2.9.
164
164
 
package/ai/cursorrules CHANGED
@@ -1,4 +1,4 @@
1
- <!-- kerf-skill-version: 1.14.2 -->
1
+ <!-- kerf-skill-version: 1.20.0 -->
2
2
  # kerf.cursorrules — rules for building apps with kerf
3
3
  #
4
4
  # Drop this file into your project as `.cursorrules` (Cursor will pick it
@@ -17,6 +17,51 @@ You are writing a UI in kerf — a ~12 KB reactive framework (~13 KB with `array
17
17
  - **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
18
18
  - Recommended: also install `eslint-plugin-kerfjs` (`npm install --save-dev eslint-plugin-kerfjs`) and add `kerfjs.configs.recommended` to the project's eslint config. It enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time so violations surface as IDE squiggles before any code runs.
19
19
 
20
+ ## Optional first-party UI
21
+
22
+ Install `@kerfjs/ui` for shared toolbars, menu rows, controlled tab bars, headers/value tables,
23
+ resizable regions, selects, banners, empty states, and loading indicators.
24
+ Import visual components from explicit subpaths so a CSS-aware browser bundler
25
+ includes only their reachable styles. The root barrel and
26
+ `@kerfjs/ui/unstyled` are CSS-free; pair the barrel with `styles.css` only when
27
+ the complete layer is intentional. Reuse these primitives before creating
28
+ parallel local ones; keep domain state, commands, routing, and tab/menu policy
29
+ in the app.
30
+
31
+ Use MenuItem/MenuActionRow/MenuHeader `rootAttributes` only for application
32
+ `data-*` metadata. MenuActionRow `trailingActionAttributes` and MenuHeader
33
+ `triggerAttributes` additionally support native popover target/action and
34
+ `aria-controls`/`aria-haspopup`. Do not use these slots to override action,
35
+ item identity, selection, disclosure, naming, disabled, icon, or role
36
+ semantics; one `role="menuitem"` does not make a complete menu widget.
37
+ `MenuItem.trailing` is dormant. Use `MenuActionRow` for sibling primary and
38
+ trailing native buttons, and keep its `label`, `icon`, and
39
+ `trailingActionIcon` SafeHtml slots free of controls.
40
+
41
+ Wire emitted `data-action` hooks at the mount root and retain disposers.
42
+ `wireResizableRegions()` and `wireTabBars()` are eventful helpers and return
43
+ disposers. Compose `AppTab` inside controlled `TabBar`; `wireTabBars()` supplies
44
+ horizontal edge autoscroll while dragging. Apply reorder reports
45
+ with `reorderTabs()`, and keep order, selection, close policy, routing, panels,
46
+ and persistence in the host. Icons/spinners are decorative unless
47
+ labeled, and assertive banners are only for urgent interruption.
48
+
49
+ The opinionated semantic ramps match Hot Sheet 2 and Web Awesome. Override
50
+ `--kui-color-*` globally or component properties such as
51
+ `--kui-state-banner-background` at a tone or instance boundary; do not replace
52
+ private descendant selectors.
53
+
54
+ For Web Awesome's broader free component set, import the CSS-only
55
+ `@kerfjs/ui/webawesome.css` theme once, then import only each Web Awesome
56
+ component module the app renders. The theme registers no component JavaScript
57
+ and remains overridable through later or scoped `--wa-*` values.
58
+
59
+ `Select` is pure markup until the app explicitly imports
60
+ `@kerfjs/ui/select/register`. Do that once in the application entry that uses
61
+ Select; never hide registration inside a component. Web Awesome is an optional
62
+ peer and stays out of bundles that use neither Select nor the Web Awesome theme. More detail ships at
63
+ `node_modules/@kerfjs/ui/ai/skill.md`.
64
+
20
65
  ## Public API — one import path
21
66
 
22
67
  ```ts
package/ai/manifest.json CHANGED
@@ -1,21 +1,78 @@
1
1
  {
2
- "kerfjsVersion": "4.4.1",
2
+ "kerfjsVersion": "5.0.0-beta.4",
3
3
  "files": [
4
4
  {
5
5
  "name": "skill",
6
6
  "source": "kerf.claude-skill.md",
7
7
  "bundle": "ai/skill.md",
8
8
  "dest": ".claude/skills/kerf-app/SKILL.md",
9
- "version": "1.14.2",
10
- "sha256": "3a9e4dd8d4ecfcf963e8d16f98ea98800a3c243f11bbb829134d8f7792a8d06d"
9
+ "version": "1.21.0",
10
+ "sha256": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5",
11
+ "history": {
12
+ "1.0.0": "bef982cb743fd4c73e666ee5cf774112e57c93ebb13b24bbacbf08577db0930a",
13
+ "1.1.0": "f2aadb21e33dd389028951ae1ecbd9f8ba68648b3c5beba43fb4dfa2ab29f195",
14
+ "1.1.1": "f163d33ce1007dd24551503398160881377eedad1fb4f6d37d9d9b8c8100f143",
15
+ "1.2.0": "76550ec18da8e063123bfabc575d9ad3a43583d41a498c6e4b4be03c17dbd188",
16
+ "1.2.1": "290e57bfc9b4af0630c586770e61063742d5f5fac3d75406f5acbc2e5322440f",
17
+ "1.3.0": "789cfd9bbe0fa8a01b779595912b7fef569b3c021be412d35f453476e576bac6",
18
+ "1.4.0": "2c42d476174847cfca1aa941c1026195b6a1786074fa9c2bd0b5a287639543b7",
19
+ "1.5.0": "c4fae8806907e79507ac095a2e6fed1761ddf61e5c9bfa5ec28719897ea56613",
20
+ "1.6.0": "797b48ef0736b9113df9e300ab47109c83c5cdd582f110d0cde7f388a37f4a26",
21
+ "1.7.0": "033c6e538d3a48f36c52a1ba154c6c7782f6c7cca9399adee0fbd519cfaf5919",
22
+ "1.8.0": "76b8a88bc7aaf9d50e6f3eee7b57a976b3279127f5d4970599aef1f839a06655",
23
+ "1.8.1": "32b8239399d94b014927563c3ff4251df97c36dc31478d832dba5a4309c221fb",
24
+ "1.9.0": "3225206e0339a8f6e2f9c728b76cf22728983f5810d614eab6224f16c8f9c6d4",
25
+ "1.10.0": "4b8e73a44fba060cb2fc4c01ed87f3853ac13906ee759e982c5c140025dfd493",
26
+ "1.11.0": "4d7f6099cd47f76a9e85cfe953886eaeb650eeb11a540cbac45104362b32eda5",
27
+ "1.12.0": "7acf72fd1b2781054520cc5af66cd73ee2f44b43c643e981f1f7a92ead60f747",
28
+ "1.13.0": "6f9a7d00ff6ad4799c51b52da613dfc5c11f09abaf9b6a3b9ca960f05f30b927",
29
+ "1.14.0": "15786c4e2b5f5d51dcd17d973f29aef8348faaa1410e40d86259be599fb459c9",
30
+ "1.14.1": "15573973dabf6d53f67e51696c276732fb1d17a59bb843300e88200389305b1f",
31
+ "1.14.2": "3a9e4dd8d4ecfcf963e8d16f98ea98800a3c243f11bbb829134d8f7792a8d06d",
32
+ "1.15.0": "72db3f328ce4457bbecd31bfab6ee69b2d423d9bee7db8e2a01e7aa3ae369af9",
33
+ "1.16.0": "0fa5d8ef354059fe7e20843dcd0c6f38234f3018d47856503bf47ddff4541231",
34
+ "1.17.0": "e0e1f370a97bc94aa33ed7723ebcf0059a9c7613f1083e3b77f0c62aecc108a7",
35
+ "1.18.0": "d2d092b1e4d4d71012f9ec36921b3f5c1421f0a8b2ced50351d56daa9e35f79b",
36
+ "1.19.0": "bbbe87446cc4e1be250d27152adfd385431f4498cde320534f65f10f853ab7f9",
37
+ "1.20.0": "aa88ccdbf969624bbc6cefadba76ada1f090fae436597a7bab564e24c467287f",
38
+ "1.21.0": "265b747eeb9c3d445c60d66a1109eec42705075b7cd154d107bd79d543c1e7e5"
39
+ }
11
40
  },
12
41
  {
13
42
  "name": "cursorrules",
14
43
  "source": "kerf.cursorrules",
15
44
  "bundle": "ai/cursorrules",
16
45
  "dest": ".cursorrules",
17
- "version": "1.14.2",
18
- "sha256": "09d9f0290fe84d3244e1655c8284c8431b47cdbe220f7161f38bc96fc8ef8ba3"
46
+ "version": "1.20.0",
47
+ "sha256": "c4dac40630fc456c6c7e6ba9d1b75215bc76bd76e6f31ba36d3bc9ab867d46e0",
48
+ "history": {
49
+ "1.0.0": "98f7faff2b7585ef9cc511add42ea5d9bf0cbae8307b066af090e9281136d2ab",
50
+ "1.1.0": "a2ec8c221dc694c98b81009cd2d99f9159a45fda94585de2f54471b227099eda",
51
+ "1.1.1": "f28b963171b7c2cbc36823d7220080730cda85a41f105b90e81790c1188e6186",
52
+ "1.2.0": "dcf2fc03f5dfca3b9bfea8cfefc65ac8ce31e1c601c613e91af69b6195c5e267",
53
+ "1.2.1": "2fcd5e9e0fd889febdab19bcc7e415c342b10ae81e937852b8280014b8d374e0",
54
+ "1.3.0": "2524a42cc3b488f8cc3cea736cf5b8d3e57d868c85260342ec50905243aa65f1",
55
+ "1.4.0": "f2c0fb328d487cc4a64ccd0b671c3385f1a25074c8c5e502c507ee799cd65e80",
56
+ "1.5.0": "2c012cbffa2fd9f96a3b8cdca48b55e584530ee6e5be73c922d3bb49470b6c31",
57
+ "1.6.0": "9370876e58ac634936ca321bf5313bb339659cb7c7c1b38a9b10f2bf4b03836d",
58
+ "1.7.0": "8ce556caf8fa95f783f5423bf882a735fc3ce6b7eb83e823aaa0417b3272af15",
59
+ "1.8.0": "f91402df009941b4198ea659498f458d2433843d47a3b524b0ca5e2d3d2fc949",
60
+ "1.8.1": "eed385b8edf3b96e88d8f00ea45ee4146004a74d3f0e495b870a27c137d0ec44",
61
+ "1.9.0": "c3242e1aa8c5ca51fca3e2e3c3df8e55dffb5d6c7b5bc8d677949d48ff5563cc",
62
+ "1.10.0": "7adefcc5ab66fcc3e5ae10625d7c762e86ac02ca297648aa58b0b201b35b04f8",
63
+ "1.11.0": "506c3338ec1c2268912f5eb9530a6af04ad937061281b5ea84529fc8b10486ff",
64
+ "1.12.0": "48ff3d0f483a9fcd7f57ca20c17c0aadfce0e4a0b39d9e96e964ef9260f782d3",
65
+ "1.13.0": "3a6bf9b1c74e0be00053faddc2af3c979ba5bb26c11db93bfcb8b1ecbe66284d",
66
+ "1.14.0": "b83de9cff13789cdebf572f2e98d3d61fdd4f31c7eb237a0a4999b6a1dfe4d9b",
67
+ "1.14.1": "9d2cc9da69e974a56d79393ad3b3a9d76c068fbd46b392621790a82fc0772d00",
68
+ "1.14.2": "09d9f0290fe84d3244e1655c8284c8431b47cdbe220f7161f38bc96fc8ef8ba3",
69
+ "1.15.0": "eece0b3075ceeeec5f6a59393a82c105d28ad1a34374412fbf6a4e0c23d7e8b9",
70
+ "1.16.0": "49794d543ab1f9112f9f6a1dcdc7aad4347b8965e1b4003bb7884086a02411c3",
71
+ "1.17.0": "6ad51fb0eec468f584346d1d7b4d1c2eca2cb52b04891b38ba44e93b57eb9e1c",
72
+ "1.18.0": "de3eea1bc181da629694d19a0817d252a868e4704437fbdd9e923856b8a0890a",
73
+ "1.19.0": "c5b91e449f6feccaa36a04cb7f8550d3e414eb2795ea6c5dccdf6071206b3261",
74
+ "1.20.0": "c4dac40630fc456c6c7e6ba9d1b75215bc76bd76e6f31ba36d3bc9ab867d46e0"
75
+ }
19
76
  }
20
77
  ]
21
78
  }
package/ai/skill.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: kerf-app
3
- description: Build UIs in the kerf reactive framework (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
- kerf-skill-version: 1.14.2
3
+ description: Build UIs in the kerf reactive framework and its @kerfjs/ui component package (https://github.com/brianwestphal/kerf). Use this skill whenever the user is writing or modifying code that imports `kerfjs` or `@kerfjs/ui`, asks to add a feature to a kerf app, or asks "how do I do X in kerf?". Use it proactively the moment you spot a kerf import in the file you're editing.
4
+ kerf-skill-version: 1.21.0
5
5
  ---
6
6
 
7
7
  # Building apps with kerf
@@ -22,6 +22,57 @@ kerf is a ~12 KB reactive UI framework (~13 KB with `arraySignal`): signals + DO
22
22
  - **A component package must NEVER import `kerfjs/dev`.** The hooks are process-global, so installing them is the consuming app's decision — a library that does it forces the diagnostics (and the chunk) on every consumer. Put the import in your demo page or test harness instead.
23
23
  - Recommended companion: `npm install --save-dev eslint-plugin-kerfjs` and add `kerfjs.configs.recommended` to the project's eslint config. Enforces five of the hard rules below (no inline JSX event handlers, require `data-key` in `each()`, capture `delegate()` disposers, no nested `mount()`, prefer module JSX augmentation) at edit time — useful as a self-correction signal when authoring kerf code.
24
24
 
25
+ ## Optional first-party UI
26
+
27
+ Install `@kerfjs/ui` when the app needs shared toolbars, menu rows, controlled tab bars,
28
+ headers/value tables, resizable regions, selects, banners, empty states, or
29
+ loading indicators. Import visual components from explicit subpaths so a
30
+ CSS-aware browser bundler includes only their reachable styles. The root barrel
31
+ and `@kerfjs/ui/unstyled` are CSS-free; pair the barrel with `styles.css` only
32
+ when the complete layer is intentional. Prefer a component before inventing a
33
+ parallel local primitive, but keep domain state, routing, commands, and tab/menu
34
+ policy in the app.
35
+
36
+ Use MenuItem/MenuActionRow/MenuHeader/AppTab `rootAttributes` only for application
37
+ `data-*` metadata. MenuActionRow `trailingActionAttributes` and MenuHeader
38
+ `triggerAttributes` additionally support native popover target/action and
39
+ `aria-controls`/`aria-haspopup`. Do not use these slots to override action,
40
+ item identity, selection, disclosure, naming, disabled, icon, or role
41
+ semantics; one `role="menuitem"` does not make a complete menu widget.
42
+ `MenuItem.trailing` is dormant. Use `MenuActionRow` for sibling primary and
43
+ trailing native buttons, and keep its `label`, `icon`, and
44
+ `trailingActionIcon` SafeHtml slots free of controls.
45
+ AppTab protects its component/action/identity/selection and transient drag/drop
46
+ attributes at runtime. Keep `AppTab.closeIcon` and
47
+ `ResizableRegion.handleIcon` decorative and free of controls; the existing
48
+ named controls and wiring retain interaction and disposal ownership.
49
+
50
+ Components emit stable `data-action` hooks; wire them at the mount root and
51
+ retain every disposer. `wireResizableRegions()` and `wireTabBars()` are the
52
+ explicit behavioral helpers and return disposers. Compose `AppTab` inside a
53
+ controlled `TabBar`; `wireTabBars()` supplies horizontal edge autoscroll during
54
+ dragging. Apply reorder reports with `reorderTabs()`, and keep order,
55
+ selection, close policy, routing, panels, and persistence in the host.
56
+ Icons and spinners are decorative unless labeled; use assertive banners only
57
+ for urgent interruption.
58
+
59
+ The opinionated semantic ramps match Hot Sheet 2 and Web Awesome. Override
60
+ `--kui-color-*` globally or component properties such as
61
+ `--kui-state-banner-background` at a tone or instance boundary; do not replace
62
+ private descendant selectors.
63
+
64
+ For Web Awesome's broader free component set, import the CSS-only
65
+ `@kerfjs/ui/webawesome.css` theme once, then import only each Web Awesome
66
+ component module the app renders. The theme registers no component JavaScript
67
+ and remains overridable through later or scoped `--wa-*` values.
68
+
69
+ `Select` renders pure Web Awesome markup. Import
70
+ `@kerfjs/ui/select/register` once in an application entry that uses it; never
71
+ hide that registration inside another component. Web Awesome is an optional
72
+ peer and must remain absent from bundles that use neither Select nor the Web
73
+ Awesome theme. The package's
74
+ full AI contract is at `node_modules/@kerfjs/ui/ai/skill.md`.
75
+
25
76
  ## Public API — one import path
26
77
 
27
78
  ```ts
package/dist/actions.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as AttrSpec } from './attrSelector-Cmu2ZoGO.js';
1
+ import { A as AttrSpec } from './attr-Cmu2ZoGO.js';
2
2
  import { D as DelegateOptions } from './delegate-CL9VTZFb.js';
3
3
 
4
4
  /**
package/dist/actions.js CHANGED
@@ -1,6 +1,6 @@
1
- import { attr } from './chunk-U32TFTGZ.js';
2
- import { delegate } from './chunk-KEZTD6H4.js';
3
- import './chunk-VVDJLWMP.js';
1
+ import { attr } from './chunk-UZJ6I4T6.js';
2
+ import { delegate } from './chunk-KPXIOG2C.js';
3
+
4
4
 
5
5
  // src/actions.ts
6
6
  var DEFAULT_ACTION_ATTR = "data-action";
@@ -22,5 +22,5 @@ function delegateActions(root, eventType, table, options) {
22
22
  }
23
23
 
24
24
  export { action, delegateActions };
25
- //# sourceMappingURL=actions.js.map
25
+
26
26
  //# sourceMappingURL=actions.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAerB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAW,CAAA;AACzD,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attrSelector.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (event: Event, el: E) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const handler = table[el.getAttribute(attrName) as string];\n if (handler !== undefined) handler(event, el);\n },\n options,\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/actions.ts"],"names":[],"mappings":";;;;;AAmCA,IAAM,mBAAA,GAAsB,aAAA;AAerB,SAAS,OAAyB,KAAA,EAAsC;AAC7E,EAAA,OAAO,IAAA,CAAK,qBAAqB,KAAK,CAAA;AACxC;AAwBO,SAAS,eAAA,CACd,IAAA,EACA,SAAA,EACA,KAAA,EACA,OAAA,EACY;AACZ,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,IAAQ,mBAAA;AAClC,EAAA,OAAO,QAAA;AAAA,IACL,IAAA;AAAA,IACA,SAAA;AAAA,IACA,IAAI,QAAQ,CAAA,CAAA,CAAA;AAAA,IACZ,CAAC,OAAO,EAAA,KAAO;AAEb,MAAA,MAAM,OAAA,GAAU,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,CAAW,CAAA;AACzD,MAAA,IAAI,OAAA,KAAY,MAAA,EAAW,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAAA,IAC9C,CAAA;AAAA,IACA;AAAA,GACF;AACF","file":"actions.js","sourcesContent":["/**\n * `kerfjs/actions` — the delegated action-table helper.\n *\n * The most-reinvented idiom across real kerf apps: one table of `data-action`\n * attribute specs used as the single source of truth for BOTH the JSX attribute\n * and the delegate selector, plus a hand-rolled `switch (dataset.action)`\n * dispatcher. This subpath blesses it as two thin helpers over the existing\n * `attr()` + `delegate()` — it does NOT replace them.\n *\n * import { action, delegateActions } from 'kerfjs/actions';\n *\n * const A = {\n * select: action('select-file'),\n * remove: action('remove-file'),\n * };\n *\n * // JSX — spread the attr (rename-safe; no hardcoded attribute name):\n * // <button {...A.select.attrs} data-id={id}>…</button>\n *\n * // Wire the whole table with ONE delegated listener; returns a disposer:\n * const dispose = delegateActions(root, 'click', {\n * [A.select.value]: (_e, el) => selectFile(el.getAttribute('data-id')),\n * [A.remove.value]: (_e, el) => removeFile(el.getAttribute('data-id')),\n * });\n *\n * Contract: `delegateActions` returns a `() => void` disposer and holds no\n * per-instance state — the same shape as `delegate()`, which it builds on (so\n * it inherits the single-listener dispatch and the capture auto-promotion for\n * well-known non-bubbling event types). One event type per call, mirroring\n * `delegate()`; collect the disposers for a root that needs several.\n */\nimport { attr, type AttrSpec } from './attr.js';\nimport { delegate, type DelegateOptions } from './delegate.js';\n\n/** The attribute an action table keys on by default. */\nconst DEFAULT_ACTION_ATTR = 'data-action';\n\n/**\n * A handler in a {@link delegateActions} table. Receives the DOM event and the\n * matched element (walk-up `closest()` match by default) — the same shape as a\n * `delegate()` handler.\n */\nexport type ActionHandler<E extends Element = Element> = (event: Event, el: E) => void;\n\n/**\n * `action(value)` — an {@link AttrSpec} on `data-action`. A thin specialization\n * of `attr('data-action', value)`: spread its `.attrs` in JSX and use its\n * `.value` as the handler-table key, so the action name lives in exactly one\n * place and can't drift between the markup and the dispatcher.\n */\nexport function action<V extends string>(value: V): AttrSpec<'data-action', V> {\n return attr(DEFAULT_ACTION_ATTR, value);\n}\n\n/** Options for {@link delegateActions}. Extends {@link DelegateOptions}. */\nexport interface DelegateActionsOptions extends DelegateOptions {\n /**\n * The attribute the table keys on. Default `'data-action'`. Override it only\n * if you also author the specs with `attr(yourName, …)` instead of `action()`.\n */\n attr?: string;\n}\n\n/**\n * Wire a whole table of action handlers with ONE delegated listener.\n *\n * On `eventType`, the nearest element carrying the action attribute (walk-up\n * `closest()` by default; pass `{ match: 'direct' }` for an exact-element match)\n * is looked up in `table` by its attribute value, and the matching handler\n * runs. An element whose action is absent from the table is ignored — the same\n * behavior as a `switch (dataset.action)` with no matching `case`.\n *\n * Returns a `() => void` disposer. One event type per call (the smallest\n * surface, mirroring `delegate()`); collect the disposers when a root needs\n * several event types.\n */\nexport function delegateActions<E extends Element = Element>(\n root: HTMLElement,\n eventType: string,\n table: Readonly<Record<string, ActionHandler<E>>>,\n options?: DelegateActionsOptions,\n): () => void {\n const attrName = options?.attr ?? DEFAULT_ACTION_ATTR;\n return delegate<E>(\n root,\n eventType,\n `[${attrName}]`,\n (event, el) => {\n // `el` matched `[${attrName}]`, so the attribute is always present.\n const handler = table[el.getAttribute(attrName) as string];\n if (handler !== undefined) handler(event, el);\n },\n options,\n );\n}\n"]}
@@ -1,6 +1,6 @@
1
- export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-MRYM3O3V.js';
2
- import './chunk-QIP723L4.js';
3
- import './chunk-3APBEVHF.js';
4
- import './chunk-VVDJLWMP.js';
5
- //# sourceMappingURL=array-signal.js.map
1
+ export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-LVH3GC6B.js';
2
+
3
+
4
+
5
+
6
6
  //# sourceMappingURL=array-signal.js.map
package/dist/async.js CHANGED
@@ -1,5 +1,5 @@
1
- import { signal } from './chunk-3APBEVHF.js';
2
- import './chunk-VVDJLWMP.js';
1
+ import { signal } from './chunk-U6FK33SG.js';
2
+
3
3
 
4
4
  // src/async.ts
5
5
  function resource(options = {}) {
@@ -47,7 +47,19 @@ function resource(options = {}) {
47
47
  state.value = { ...state.value, progress: { completed, total } };
48
48
  }
49
49
  };
50
- return fetcher(report).then(
50
+ const fail = (error) => {
51
+ if (gen === generation) {
52
+ state.value = { ...state.value, status: "failed", error, progress: void 0, input };
53
+ }
54
+ return void 0;
55
+ };
56
+ let pending;
57
+ try {
58
+ pending = fetcher(report);
59
+ } catch (error) {
60
+ return Promise.resolve(fail(error));
61
+ }
62
+ return pending.then(
51
63
  (data) => {
52
64
  if (gen === generation) {
53
65
  if (key !== void 0) cache.set(key, data);
@@ -62,12 +74,7 @@ function resource(options = {}) {
62
74
  }
63
75
  return data;
64
76
  },
65
- (error) => {
66
- if (gen === generation) {
67
- state.value = { ...state.value, status: "failed", error, progress: void 0, input };
68
- }
69
- return void 0;
70
- }
77
+ fail
71
78
  );
72
79
  }
73
80
  function reset() {
@@ -98,5 +105,5 @@ function resource(options = {}) {
98
105
  }
99
106
 
100
107
  export { resource };
101
- //# sourceMappingURL=async.js.map
108
+
102
109
  //# sourceMappingURL=async.js.map
package/dist/async.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/async.ts"],"names":[],"mappings":"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","file":"async.js","sourcesContent":["/**\n * `kerfjs/async` — model async state, with the stale-response guard built in.\n *\n * Every real kerf app reproduces the same shape — `{ status, data, error }` —\n * for loading/error UI, each paired with a hand-rolled generation counter so a\n * slow response can't overwrite a newer one. This subpath blesses exactly that,\n * and no more: you still write the fetch (Node `fetch` for SSR, browser `fetch`\n * client-side), and `.run()` owns the status transitions plus the stale guard.\n *\n * import { resource } from 'kerfjs/async';\n *\n * const users = resource<User[]>();\n * users.run(() => fetch('/api/users').then((r) => r.json()));\n * // render off users.value.status: 'idle' | 'running' | 'completed' | 'failed'\n *\n * Only the LATEST run may resolve the state, so out-of-order responses are\n * dropped automatically. Optional progress: declare the `report` parameter on\n * your fetcher and call it (e.g. from an upload's progress events).\n *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\n */\nimport { signal } from './reactive.js';\n\n/** The lifecycle status of a {@link Resource}. */\nexport type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';\n\n/** Optional progress for a long-running fetch (uploads, chunked work). */\nexport interface ResourceProgress {\n completed: number;\n total: number;\n}\n\n/** The reactive state a {@link Resource} exposes. */\nexport interface ResourceState<T, I = void> {\n status: ResourceStatus;\n /** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */\n data: T | undefined;\n /** The rejection from the most recent failed run. */\n error: unknown;\n /** Latest reported progress while running, or `undefined`. */\n progress: ResourceProgress | undefined;\n /**\n * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\n}\n\n/**\n * The fetcher passed to {@link Resource.run}. You own the transport. It receives\n * a `report(completed, total)` callback for optional progress — ignore it if you\n * don't need progress (a plain `() => Promise<T>` is assignable here).\n */\nexport type ResourceFetcher<T> = (report: (completed: number, total: number) => void) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\n /**\n * Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding\n * against stale responses (only the latest run resolves the state). Never\n * rejects — a failure lands in `value.error`; resolves with the data (or\n * `undefined` on failure) for callers who want to await it.\n */\n run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\n}\n\n/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */\nexport function resource<T, I = void>(options: ResourceOptions<T, I> = {}): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined ? lastData !== next : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as I | undefined;\n const key = cacheKey !== undefined && maybeFetcher !== undefined ? cacheKey(input as I) : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData = cacheKey !== undefined\n ? (key !== undefined ? cache.get(key) : undefined)\n : state.value.data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.value, progress: { completed, total } };\n }\n };\n\n return fetcher(report).then(\n (data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n },\n (error: unknown) => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = { ...state.value, status: 'failed', error, progress: undefined, input };\n }\n return undefined;\n },\n );\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/async.ts"],"names":[],"mappings":";;;;AAwIO,SAAS,QAAA,CAAsB,OAAA,GAAiC,EAAC,EAAmB;AACzF,EAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAO,GAAI,OAAA;AAC7B,EAAA,MAAM,EAAA,GAA8B,UAAU,MAAA,CAAO,EAAA;AACrD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAAe;AAGjC,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,IAAI,QAAA;AACJ,EAAA,MAAM,UAAU,CAAC,IAAA;AAAA;AAAA,IAEf,QAAA,KAAa,UAAa,IAAA,KAAS,MAAA,GAAY,aAAa,IAAA,GAAO,CAAC,EAAA,CAAG,QAAA,EAAU,IAAI;AAAA,GAAA;AACvF,EAAA,MAAM,MAAA,GAAS,CAAC,IAAA,KAAgC;AAC9C,IAAA,IAAI,OAAA,CAAQ,IAAI,CAAA,EAAG;AACjB,MAAA,QAAA,EAAA;AACA,MAAA,QAAA,GAAW,IAAA;AAAA,IACb;AACA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,QAAQ,MAAA,CAA4B;AAAA,IACxC,MAAA,EAAQ,MAAA;AAAA,IACR,IAAA,EAAM,MAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,QAAA,EAAU,MAAA;AAAA,IACV,KAAA,EAAO,MAAA;AAAA,IACP,QAAA,EAAU;AAAA,GACX,CAAA;AAED,EAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,EAAA,SAAS,GAAA,CACP,gBACA,YAAA,EACwB;AAIxB,IAAA,MAAM,UAAW,YAAA,IAAgB,cAAA;AACjC,IAAA,MAAM,KAAA,GAAS,YAAA,KAAiB,MAAA,GAAY,MAAA,GAAY,cAAA;AACxD,IAAA,MAAM,MAAM,QAAA,KAAa,MAAA,IAAa,iBAAiB,MAAA,GAAY,QAAA,CAAS,KAAU,CAAA,GAAI,MAAA;AAI1F,IAAA,MAAM,WAAA,GAAc,QAAA,KAAa,MAAA,GAC5B,GAAA,KAAQ,MAAA,GAAY,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA,GAAI,MAAA,GACtC,KAAA,CAAM,KAAA,CAAM,IAAA;AAEhB,IAAA,MAAM,MAAM,EAAE,UAAA;AACd,IAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,MACZ,MAAA,EAAQ,SAAA;AAAA,MACR,IAAA,EAAM,WAAA;AAAA,MACN,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,MAAA;AAAA,MACV,KAAA;AAAA,MACA,QAAA,EAAU,OAAO,WAAW;AAAA,KAC9B;AAEA,IAAA,MAAM,MAAA,GAAS,CAAC,SAAA,EAAmB,KAAA,KAAwB;AACzD,MAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,QAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,OAAO,QAAA,EAAU,EAAE,SAAA,EAAW,KAAA,EAAM,EAAE;AAAA,MACjE;AAAA,IACF,CAAA;AAEA,IAAA,MAAM,IAAA,GAAO,CAAC,KAAA,KAA8B;AAC1C,MAAA,IAAI,QAAQ,UAAA,EAAY;AAEtB,QAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,KAAA,EAAO,QAAQ,QAAA,EAAU,KAAA,EAAO,QAAA,EAAU,MAAA,EAAW,KAAA,EAAM;AAAA,MACtF;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAEA,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAU,QAAQ,MAAM,CAAA;AAAA,IAC1B,SAAS,KAAA,EAAgB;AACvB,MAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IACpC;AAEA,IAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,MACb,CAAC,IAAA,KAAS;AACR,QAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,UAAA,IAAI,GAAA,KAAQ,MAAA,EAAW,KAAA,CAAM,GAAA,CAAI,KAAK,IAAI,CAAA;AAC1C,UAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,YACZ,MAAA,EAAQ,WAAA;AAAA,YACR,IAAA;AAAA,YACA,KAAA,EAAO,MAAA;AAAA,YACP,QAAA,EAAU,MAAA;AAAA,YACV,KAAA;AAAA,YACA,QAAA,EAAU,OAAO,IAAI;AAAA,WACvB;AAAA,QACF;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAEA,EAAA,SAAS,KAAA,GAAc;AACrB,IAAA,UAAA,EAAA;AACA,IAAA,KAAA,CAAM,KAAA,EAAM;AACZ,IAAA,KAAA,CAAM,KAAA,GAAQ;AAAA,MACZ,MAAA,EAAQ,MAAA;AAAA,MACR,IAAA,EAAM,MAAA;AAAA,MACN,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,MAAA;AAAA,MACV,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,OAAO,MAAS;AAAA,KAC5B;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,KAAA,CAAM,KAAA;AAAA,IACf,CAAA;AAAA,IACA,GAAA;AAAA,IACA,KAAA;AAAA,IACA,MAAA,EAAQ,CAAC,CAAA,KAAM,KAAA,CAAM,IAAI,CAAC,CAAA;AAAA,IAC1B,YAAY,MAAM,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAAA,IACzC,UAAA,EAAY,CAAC,CAAA,KAAM;AACjB,MAAA,IAAI,CAAA,KAAM,MAAA,EAAW,KAAA,CAAM,KAAA,EAAM;AAAA,WAC5B,KAAA,CAAM,OAAO,CAAC,CAAA;AAAA,IACrB;AAAA,GACF;AACF","file":"async.js","sourcesContent":["/**\n * `kerfjs/async` — model async state, with the stale-response guard built in.\n *\n * Every real kerf app reproduces the same shape — `{ status, data, error }` —\n * for loading/error UI, each paired with a hand-rolled generation counter so a\n * slow response can't overwrite a newer one. This subpath blesses exactly that,\n * and no more: you still write the fetch (Node `fetch` for SSR, browser `fetch`\n * client-side), and `.run()` owns the status transitions plus the stale guard.\n *\n * import { resource } from 'kerfjs/async';\n *\n * const users = resource<User[]>();\n * users.run(() => fetch('/api/users').then((r) => r.json()));\n * // render off users.value.status: 'idle' | 'running' | 'completed' | 'failed'\n *\n * Only the LATEST run may resolve the state, so out-of-order responses are\n * dropped automatically. Optional progress: declare the `report` parameter on\n * your fetcher and call it (e.g. from an upload's progress events).\n *\n * Pass an input — `run(input, fetcher)` — to carry which request a run is for\n * through to `value.input` (set for `running`/`completed`/`failed`), so a\n * failure handler can recover the id/params of the run that failed:\n *\n * const diff = resource<Diff, { fileId: string }>();\n * diff.run({ fileId }, (report) => fetchDiff(fileId, report));\n * // on failure: diff.value.status === 'failed' && diff.value.input.fileId\n *\n * For a real SWR-with-cache section, pass `cacheKey` to keep the last value PER\n * input key (switching back to a loaded key paints its cached slice instantly\n * while it revalidates), and read `value.revision` — a counter that bumps only\n * when `data` actually CHANGES (by `equals`, default `Object.is`) — to skip a\n * redundant paint when a poll tick returns identical data:\n *\n * const win = resource<Slice, string>({ cacheKey: (w) => w, equals: sameSlice });\n * win.run(w, () => fetchSlice(w)); // instant cached paint for a revisited w\n */\nimport { signal } from './reactive.js';\n\n/** The lifecycle status of a {@link Resource}. */\nexport type ResourceStatus = 'idle' | 'running' | 'completed' | 'failed';\n\n/** Optional progress for a long-running fetch (uploads, chunked work). */\nexport interface ResourceProgress {\n completed: number;\n total: number;\n}\n\n/** The reactive state a {@link Resource} exposes. */\nexport interface ResourceState<T, I = void> {\n status: ResourceStatus;\n /** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */\n data: T | undefined;\n /** The rejection from the most recent failed run. */\n error: unknown;\n /** Latest reported progress while running, or `undefined`. */\n progress: ResourceProgress | undefined;\n /**\n * The input of the LATEST run — the value passed to {@link Resource.run} as\n * `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same\n * stale-guard rule as the rest of the state), so an effect can branch on\n * `status === 'failed'` and still know which request failed. `undefined` in\n * `idle`, and for the no-input `run(fetcher)` form.\n */\n input: I | undefined;\n /**\n * A monotonic counter that increments only when `data` actually CHANGES (by\n * the resource's `equals`, default `Object.is`). Compare it against the value\n * you last painted to skip a redundant re-render — e.g. a 30s poll returning\n * identical data leaves `revision` untouched, so you can bail before wiping\n * scroll / sort / hover state. Starts at `0`.\n */\n revision: number;\n}\n\n/** Construction options for {@link resource}. */\nexport interface ResourceOptions<T, I = void> {\n /**\n * Derive a cache key from a run's `input`. When set, the resource keeps the\n * last successful value PER key: starting a run for a key that was loaded\n * before paints its cached slice immediately (still `running`) while the fetch\n * revalidates in the background; a never-loaded key starts with no `data`.\n * Without `cacheKey`, a run keeps the previous run's `data` (single-slot\n * stale-while-revalidate), as before.\n */\n cacheKey?: (input: I) => string;\n /**\n * Equality used to decide whether `data` changed (drives `value.revision`).\n * Default `Object.is`. Pass a structural comparison to dedup a poll that\n * returns a fresh-but-equal object.\n */\n equals?: (a: T, b: T) => boolean;\n}\n\n/**\n * The fetcher passed to {@link Resource.run}. You own the transport. It receives\n * a `report(completed, total)` callback for optional progress — ignore it if you\n * don't need progress (a plain `() => Promise<T>` is assignable here).\n */\nexport type ResourceFetcher<T> = (report: (completed: number, total: number) => void) => Promise<T>;\n\n/**\n * An async-state container. Its `value` is a tracking read; drive UI off\n * `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)\n * to carry a typed `run(input, fetcher)` input through to `value.input`.\n */\nexport interface Resource<T, I = void> {\n /** Tracking read of the current {@link ResourceState}. */\n readonly value: ResourceState<T, I>;\n /**\n * Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding\n * against stale responses (only the latest run resolves the state). Never\n * rejects — a failure lands in `value.error`; resolves with the data (or\n * `undefined` on failure) for callers who want to await it.\n */\n run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /**\n * Run `fetcher` for a given `input`, exposing it as `value.input` for the\n * `running`/`completed`/`failed` states of THIS run — so a failure handler can\n * recover which request failed. Same stale guard: only the latest run resolves.\n */\n run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;\n /** Reset to `idle` (clearing data/error/progress/input, and the per-key cache) and invalidate any in-flight run. */\n reset(): void;\n /**\n * Read-only: the cached value for a `cacheKey` key, or `undefined` if that key\n * isn't cached (or no `cacheKey` was given). Lets a consumer ask \"is this slice\n * cached?\" without running it (which would mutate state).\n */\n cached(key: string): T | undefined;\n /** Read-only: the keys currently in the per-input cache (`.length` is the cache size). */\n cachedKeys(): string[];\n /** Evict the per-input cache — one `key`, or the whole cache when called with no argument. Does NOT change `value`. */\n clearCache(key?: string): void;\n}\n\n/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */\nexport function resource<T, I = void>(options: ResourceOptions<T, I> = {}): Resource<T, I> {\n const { cacheKey, equals } = options;\n const eq: (a: T, b: T) => boolean = equals ?? Object.is;\n const cache = new Map<string, T>(); // per-key SWR cache (GC-tied to the resource)\n\n // Revision tracking: `revision` bumps only when `data` changes (by `eq`).\n let revision = 0;\n let lastData: T | undefined;\n const changed = (next: T | undefined): boolean =>\n // undefined transitions are handled by reference; two defined values by `eq`.\n lastData === undefined || next === undefined ? lastData !== next : !eq(lastData, next);\n const commit = (next: T | undefined): number => {\n if (changed(next)) {\n revision++;\n lastData = next;\n }\n return revision;\n };\n\n const state = signal<ResourceState<T, I>>({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: 0,\n });\n // Per-resource run counter (closure-local, not module state) — the stale guard.\n let generation = 0;\n\n function run(\n inputOrFetcher: I | ResourceFetcher<T>,\n maybeFetcher?: ResourceFetcher<T>,\n ): Promise<T | undefined> {\n // Two-arg form is (input, fetcher); one-arg form is (fetcher) with no input.\n // A fetcher is always a function, so `maybeFetcher === undefined` uniquely\n // identifies the one-arg call — even when the input value is itself undefined.\n const fetcher = (maybeFetcher ?? inputOrFetcher) as ResourceFetcher<T>;\n const input = (maybeFetcher === undefined ? undefined : inputOrFetcher) as I | undefined;\n const key = cacheKey !== undefined && maybeFetcher !== undefined ? cacheKey(input as I) : undefined;\n\n // What `data` shows while running: the cached slice for this key (per-key\n // SWR), or the previous run's data (single-slot SWR) when no cacheKey.\n const runningData = cacheKey !== undefined\n ? (key !== undefined ? cache.get(key) : undefined)\n : state.value.data;\n\n const gen = ++generation;\n state.value = {\n status: 'running',\n data: runningData,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(runningData),\n };\n\n const report = (completed: number, total: number): void => {\n if (gen === generation) {\n state.value = { ...state.value, progress: { completed, total } };\n }\n };\n\n const fail = (error: unknown): undefined => {\n if (gen === generation) {\n // Keep `data` (and thus `revision`) on failure — stale-while-error.\n state.value = { ...state.value, status: 'failed', error, progress: undefined, input };\n }\n return undefined;\n };\n\n let pending: Promise<T>;\n try {\n pending = fetcher(report);\n } catch (error: unknown) {\n return Promise.resolve(fail(error));\n }\n\n return pending.then(\n (data) => {\n if (gen === generation) {\n if (key !== undefined) cache.set(key, data);\n state.value = {\n status: 'completed',\n data,\n error: undefined,\n progress: undefined,\n input,\n revision: commit(data),\n };\n }\n return data;\n },\n fail,\n );\n }\n\n function reset(): void {\n generation++; // invalidate any in-flight run\n cache.clear();\n state.value = {\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\n revision: commit(undefined),\n };\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n cached: (k) => cache.get(k),\n cachedKeys: () => Array.from(cache.keys()),\n clearCache: (k) => {\n if (k === undefined) cache.clear();\n else cache.delete(k);\n },\n };\n}\n"]}