kerfjs 4.4.1 → 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 +99 -0
- package/LICENSE +1 -1
- package/README.md +3 -3
- 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 -8
- 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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,105 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- Fixed the complete kanban example's mobile layout by stacking its columns and header controls without horizontal overflow while preserving the wider board layout.
|
|
10
|
+
- Fixed the complete dashboard example's mobile layout so its status header, chart, and table fit the viewport without horizontal overflow.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- 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).
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- 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.
|
|
26
|
+
- 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.
|
|
27
|
+
- 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.
|
|
28
|
+
- Strengthened the `bindList` feature index with exact existing guards for same-key rebuilds, granular patch sequences, and teardown semantics.
|
|
29
|
+
- Extracted pure granular-list index-shift and cache-key-drift stages from the `each()` transition coordinator for direct testing.
|
|
30
|
+
- Normalized internal helper module filenames to the repository's kebab-case or primary-export convention and updated all live imports and documentation paths.
|
|
31
|
+
- Made list-binding rationale comments self-contained and named the bounded template/URL diagnostic excerpt limits.
|
|
32
|
+
- Split `mount()`'s render effect into named synchronous static-render and list-reconcile/commit phases without changing lifecycle order.
|
|
33
|
+
- Replaced terse router and overlay-position internals with descriptive route-segment, event, rectangle, and viewport names.
|
|
34
|
+
- Preserved edited stale AI-assistant configs as forks by validating their versioned canonical section against shipped historical hashes before offering an autofix.
|
|
35
|
+
- Required every complete example app to have its own feature-coverage row mapped to a smoke test inside that app's Playwright suite.
|
|
36
|
+
- Made router parameter decoding fail closed to no-match on malformed percent escapes instead of throwing `URIError`, including named and wildcard captures.
|
|
37
|
+
- Pointed `@kerfjs/ui` package metadata at the existing published component-packages documentation route and added a source-backed metadata regression test.
|
|
38
|
+
- Fixed the complete router example's broken documentation links and taught the docs-example gate to validate published routes and heading fragments.
|
|
39
|
+
- 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.
|
|
40
|
+
- Extended the feature-completeness gate to inventory router-subpath exports and deduplicate values re-exported across public surfaces.
|
|
41
|
+
- Kept `kerfjs/ai-assistant-configs` filesystem checks read-only during plain ESLint runs while retaining explicit `eslint --fix` installation and stale-file updates.
|
|
42
|
+
- Required history-router bases to match an exact path or path-segment boundary, so `/app` no longer strips or intercepts `/apple`.
|
|
43
|
+
- Added the missing MIT `LICENSE` to generated `create-kerf-component` packages, including package-name token replacement and publish-contents coverage.
|
|
44
|
+
- Made dangerous-URL attribute-name matching ASCII-case-insensitive so mixed-case spellings cannot bypass static or bound screening.
|
|
45
|
+
- Normalized synchronous `resource().run()` fetcher throws into the same stale-guarded failed state and resolving promise used for asynchronous rejections.
|
|
46
|
+
- Escaped plain-string `toast()` content as text while preserving trusted `SafeHtml` and render-function markup, closing a stored-markup injection path.
|
|
47
|
+
- 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.
|
|
48
|
+
- 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.
|
|
49
|
+
- 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.
|
|
50
|
+
- Gave every `ValueTableRow` 8px of root-scaled top and bottom padding while preserving its semantic inline inset and icon-aware separator alignment.
|
|
51
|
+
- 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.
|
|
52
|
+
- Removed the unsolicited command-palette catalog demo and production recipe while retaining the earlier, independent application-local adapter example for missing-concept guidance.
|
|
53
|
+
- 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.
|
|
54
|
+
- 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.
|
|
55
|
+
- 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.
|
|
56
|
+
- 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.
|
|
57
|
+
- 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.
|
|
58
|
+
- 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.
|
|
59
|
+
- 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.
|
|
60
|
+
- 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.
|
|
61
|
+
- Aligned the UX catalog's header-composition dialog with the shared 8px inline layout gutter while preserving its 16px vertical separation.
|
|
62
|
+
- 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.
|
|
63
|
+
- 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.
|
|
64
|
+
- 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.
|
|
65
|
+
- 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.
|
|
66
|
+
- Fixed keyboard deletion of a controlled `TokenSearchField` chip so `wireTokenSearchFields` restores focus and the text-relative caret after the application rerenders the editor.
|
|
67
|
+
- 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.
|
|
68
|
+
- 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.
|
|
69
|
+
- 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.
|
|
70
|
+
- 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.
|
|
71
|
+
- 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.
|
|
72
|
+
- 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.
|
|
73
|
+
- 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.
|
|
74
|
+
- 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.
|
|
75
|
+
- 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.
|
|
76
|
+
- 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.
|
|
77
|
+
- 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.
|
|
78
|
+
- 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.
|
|
79
|
+
- 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.
|
|
80
|
+
- 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.
|
|
81
|
+
- 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.
|
|
82
|
+
- 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.
|
|
83
|
+
- 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.
|
|
84
|
+
- 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.
|
|
85
|
+
- 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.
|
|
86
|
+
- 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.
|
|
87
|
+
- 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.
|
|
88
|
+
- 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.
|
|
89
|
+
- 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.
|
|
90
|
+
- 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.
|
|
91
|
+
- 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.
|
|
92
|
+
- Unified Accordion, Details, Breadcrumb, and Kerf Select chevron scale through an overridable shared disclosure-icon token.
|
|
93
|
+
- 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.
|
|
94
|
+
- Expanded the Web Awesome Animation UX specimen into an interactive settings panel with preset, easing, timing, playback, transport, lifecycle, and reduced-motion behavior.
|
|
95
|
+
- 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.
|
|
96
|
+
- Made the Web Awesome intersection, mutation, and resize observer UX specimens interactive, with deliberate triggers, visible live-event results, and browser coverage.
|
|
97
|
+
- 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.
|
|
98
|
+
- 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.
|
|
99
|
+
- 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.
|
|
100
|
+
- Updated `@kerfjs/ui` to develop and test against Lucide 1.43.0 and Web Awesome 3.12.0, with matching peer baselines.
|
|
101
|
+
- 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.
|
|
102
|
+
- 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.
|
|
103
|
+
- 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.
|
|
104
|
+
- 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.
|
|
105
|
+
- 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.
|
|
106
|
+
- 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.
|
|
107
|
+
|
|
9
108
|
## [4.4.1] - 2026-08-26
|
|
10
109
|
|
|
11
110
|
|
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/
|
|
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.
|
|
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 +
|
|
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
|
|
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.
|
|
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": "
|
|
2
|
+
"kerfjsVersion": "5.0.0-beta.3",
|
|
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.
|
|
10
|
-
"sha256": "
|
|
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.
|
|
18
|
-
"sha256": "
|
|
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.
|
|
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
package/dist/actions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { attr } from './chunk-
|
|
2
|
-
import { delegate } from './chunk-
|
|
3
|
-
|
|
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
|
-
|
|
25
|
+
|
|
26
26
|
//# sourceMappingURL=actions.js.map
|
package/dist/actions.js.map
CHANGED
|
@@ -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 './
|
|
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"]}
|
package/dist/array-signal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { ARRAY_SIGNAL_BRAND, ArraySignal, arraySignal } from './chunk-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"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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 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"]}
|
package/dist/attach.d.ts
CHANGED
|
@@ -19,11 +19,13 @@
|
|
|
19
19
|
* to wait for (this is NOT React's `useEffect`: no dependency array, no re-run,
|
|
20
20
|
* no render-phase or hook-order scoping; it is closer to a Web Component's
|
|
21
21
|
* `connectedCallback`/`disconnectedCallback` pair, Svelte's
|
|
22
|
-
* `onMount(() => () => cleanup)`, or Solid's `onCleanup`).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* `onMount(() => () => cleanup)`, or Solid's `onCleanup`). A node may start
|
|
23
|
+
* disconnected: setup still runs now, and teardown waits until it has first
|
|
24
|
+
* connected and subsequently leaves the document. The returned teardown runs
|
|
25
|
+
* once — whichever comes first — on that removal (detected by a
|
|
26
|
+
* `MutationObserver`, so a morph swap, a `remountOn` replacement, or any removal
|
|
27
|
+
* triggers it) or when the returned disposer is called. Re-creation is NOT
|
|
28
|
+
* handled here: a fresh node is a fresh `attach()` call — pair it with
|
|
27
29
|
* `kerfjs/remount`, which replaces the node and re-runs your render (and thus
|
|
28
30
|
* this call) on the new one.
|
|
29
31
|
*
|
|
@@ -36,9 +38,10 @@
|
|
|
36
38
|
/** The setup callback for {@link attach}: run against `node`, optionally return a teardown. */
|
|
37
39
|
type AttachSetup = (node: Element) => (() => void) | void;
|
|
38
40
|
/**
|
|
39
|
-
* Run `setup(node)` now, and its returned teardown once —
|
|
40
|
-
* document, or when the returned disposer is
|
|
41
|
-
*
|
|
41
|
+
* Run `setup(node)` now, and its returned teardown once — after `node` has been
|
|
42
|
+
* connected and then leaves the document, or when the returned disposer is
|
|
43
|
+
* called, whichever is first. Returns an idempotent disposer so a `mount()` /
|
|
44
|
+
* `Scope` can drive teardown explicitly.
|
|
42
45
|
*/
|
|
43
46
|
declare function attach(node: Element, setup: AttachSetup): () => void;
|
|
44
47
|
|