kerfjs 4.2.0-beta.1 → 4.2.0-beta.2
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 +5 -2
- package/dist/async.d.ts +24 -6
- package/dist/async.js +8 -5
- package/dist/async.js.map +1 -1
- package/dist/imperative.d.ts +34 -0
- package/dist/imperative.js +20 -0
- package/dist/imperative.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/overlay.d.ts +108 -1
- package/dist/overlay.js +213 -2
- package/dist/overlay.js.map +1 -1
- package/dist/remount.d.ts +52 -0
- package/dist/remount.js +45 -0
- package/dist/remount.js.map +1 -0
- package/dist/scope.js +1 -1
- package/dist/timing.d.ts +65 -0
- package/dist/timing.js +80 -0
- package/dist/timing.js.map +1 -0
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,9 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
9
9
|
- **`trustedRaw(html)`** (main barrel) — the intention-revealing, lint-exempt escape hatch for injecting a **server-trusted dynamic** value (a CSRF token, a trusted `<script src>`, a server-issued id). Identical to `raw()` at runtime, but because it isn't named `raw`, the `kerfjs/no-raw-with-dynamic-arg` rule leaves it alone — replacing scattered `eslint-disable` comments with one explicit call. Not a sanitizer; only pass values you control.
|
|
10
10
|
- **`renderDocument(node, options?)`** (main barrel) — a tiny SSR helper that prepends the doctype to a rendered document, so server routes stop reinventing `"<!DOCTYPE html>" + page.toString()`. Takes a `SafeHtml` or string; optional `{ doctype }` (default `'html'`). Pure string work, no DOM dependency.
|
|
11
11
|
- **New `kerfjs/list` subpath** — `bindList(parent, source, options)`, a keyed list distinct from `each()`: each row is individually `mount()`ed, so a signal a row reads updates just that row (fine-grained, no full-list pass), and it can **virtualize** the viewport (`virtualize: { rowHeight }` renders only visible rows, padding keeps `scrollHeight` honest). `source` is a `signal<readonly T[]>` or an `arraySignal<T>` — a non-virtualized `arraySignal` source applies its structural patches **granularly** (O(patches)), everything else uses a keyed diff (transparent optimization). Reach for it for surgical per-row updates or long/windowed lists; `each()` stays the default for item-owned-state lists rendered to HTML strings. Optional and tree-shakeable.
|
|
12
|
-
- **New `kerfjs/async` subpath** — `resource<T>()` models async state (`{ status, data, error, progress }`) with the stale-response guard built in. You write the fetch (Node `fetch` for SSR, browser `fetch` client-side); `.run(fetcher)` drives `idle` → `running` → `completed`/`failed` and drops out-of-order responses (only the latest run resolves the state). It never rejects — a failure lands in `value.error` — keeps previous data across a re-run (stale-while-revalidate), and supports opt-in progress via a callback the fetcher receives. `value` is a tracking read. Signals only (no render core); tiny.
|
|
12
|
+
- **New `kerfjs/async` subpath** — `resource<T, I = void>()` models async state (`{ status, data, error, progress, input }`) with the stale-response guard built in. You write the fetch (Node `fetch` for SSR, browser `fetch` client-side); `.run(fetcher)` drives `idle` → `running` → `completed`/`failed` and drops out-of-order responses (only the latest run resolves the state). It never rejects — a failure lands in `value.error` — keeps previous data across a re-run (stale-while-revalidate), and supports opt-in progress via a callback the fetcher receives. The `.run(input, fetcher)` form threads the run's `input` to `value.input` for `running`/`completed`/`failed` (latest-wins under the stale guard), so a failure handler can recover **which request failed** (e.g. an inline error keyed by `value.input.fileId`) without reintroducing module-scope bookkeeping. `value` is a tracking read. Signals only (no render core); tiny.
|
|
13
|
+
- **New `kerfjs/imperative` subpath** — `imperative(node, setup)`, a `useEffect`-with-cleanup bound to a single DOM node, purpose-built for the `data-morph-skip` escape hatch. `setup(node)` runs immediately and may return a teardown; the teardown runs once when the node leaves the document (detected by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any removal triggers it) or when the returned idempotent disposer is called. Turns the "library owns this subtree" convention into a supported seam with real lifecycle guarantees. Re-creation is handled by pairing with `kerfjs/remount`. DOM only (no signals, no render core) — the smallest subpath. Optional and tree-shakeable.
|
|
14
|
+
- **New `kerfjs/remount` subpath** — `remountOn(parent, key, render)`, the opposite of kerf's morph-by-default: **replace** a subtree wholesale when `key` changes instead of morphing it. Names the hand-rolled `data-key={`gen-${n}`}` + `data-morph-skip` counter trick, for library-owned subtrees (a highlighted diff, a chart, an editor) that must tear down and re-initialize on fresh DOM. `key` is a signal or a thunk `() => K`; an unchanged key (including a thunk whose inputs moved but whose value stayed equal) leaves the subtree alone, so per-row reactivity inside `render` still updates in place. An optional `onMount(root)` callback runs after each (re)mount with the live subtree — the place to bind an imperative widget (`kerfjs/imperative`) to the fresh DOM; returning `imperative`'s disposer from it makes teardown synchronous. `remountOn` owns `parent`'s children and returns a disposer. Optional and tree-shakeable.
|
|
15
|
+
- **New `kerfjs/timing` subpath** — the `let timer; clearTimeout(timer); timer = setTimeout(…)` pattern every app hand-rolls, blessed with disposer-shaped ergonomics. `debounce(fn, ms)` is trailing-edge (runs once `ms` after calls stop, latest args); `throttle(fn, ms)` is leading-plus-trailing (fires immediately, then at most once per `ms`, collapsing a burst to one trailing call). Both return a callable with `cancel()` / `flush()`. `debouncedSignal(source, ms)` is a read-only signal that trails `source` by `ms` so it composes inside the reactive graph (`computed`/`effect`/`mount`) instead of beside it. `debounce`/`throttle` are dependency-free; `debouncedSignal` pulls in signals only (no render core). Optional and tree-shakeable; tiny.
|
|
13
16
|
- **New `kerfjs/scope` subpath** — tie disposers to a DOM element's lifetime, so append-heavy UIs stop leaking detached-but-subscribed effects/listeners. `disposeScope(el)` returns a WeakMap-keyed, accumulating scope whose `add(disposer)` (plus convenience `mount` / `effect` / `delegate` wrappers that register their own disposer) collects teardown; `dispose()` runs it all best-effort and idempotently. `disposeSubtree(root)` sweeps a subtree before removal; `observeRemovals(root)` installs one `MutationObserver` that auto-disposes on removal. No module-level mutable state. Optional and tree-shakeable.
|
|
14
|
-
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement (
|
|
17
|
+
- **New `kerfjs/overlay` subpath** — the blessed modal/overlay + dismiss manager that every real kerf app hand-rolls. `overlay(content, options?)` appends a wrapper, `mount()`s content inside it (owning the disposal), wires dismissals (Escape / backdrop / outside-click, with `outsideIgnore`), a focus trap (`role="dialog"` / `aria-modal`, Tab wrap-around, restore-focus-on-close), and returns `{ el, close(result?), result }`. `confirm(message, options?)` is a promise-based `window.confirm` replacement, and `prompt(message, options?)` → `Promise<string | null>` its `window.prompt` counterpart (both globals are no-ops in Tauri webviews); `form(fields, options?)` → `Promise<Record<string, string> | null>` collects a two-or-three-field dialog. `prompt`/`form` submit on Enter and take an inline `validate`; all auto-escape their content. `popover(anchor, content, options?)` → `OverlayHandle` is a non-modal **anchored** overlay: it positions the content relative to `anchor` (below by default, flipping above on viewport overflow, clamped horizontally), defaults to dismiss-on-outside with the anchor exempt, and repositions on scroll / resize. `toast(content, options?)` is an auto-dismissing notification. Structural only — kerf ships no CSS. Optional and tree-shakeable; shares the core with the main barrel via code-splitting.
|
|
15
18
|
- **New `kerfjs/actions` subpath** — the blessed delegated action-table helper. `action(value)` returns a `data-action` `AttrSpec` (a thin specialization of `attr()`); `delegateActions(root, eventType, table, options?)` wires a whole table of `data-action` handlers with one delegated listener (built on `delegate()`) and returns a disposer. Formalizes the most-reinvented idiom in real kerf apps — one `attr('data-action', …)` table as the single source of truth for both the JSX attribute and the delegate dispatch. Optional and tree-shakeable; adds nothing to the main barrel.
|
|
16
19
|
|
|
17
20
|
## [4.1.1] - 2026-08-14
|
package/dist/async.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface ResourceProgress {
|
|
|
6
6
|
total: number;
|
|
7
7
|
}
|
|
8
8
|
/** The reactive state a {@link Resource} exposes. */
|
|
9
|
-
interface ResourceState<T> {
|
|
9
|
+
interface ResourceState<T, I = void> {
|
|
10
10
|
status: ResourceStatus;
|
|
11
11
|
/** The last successful value. Kept across a re-run (stale-while-revalidate) and on failure. */
|
|
12
12
|
data: T | undefined;
|
|
@@ -14,6 +14,14 @@ interface ResourceState<T> {
|
|
|
14
14
|
error: unknown;
|
|
15
15
|
/** Latest reported progress while running, or `undefined`. */
|
|
16
16
|
progress: ResourceProgress | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The input of the LATEST run — the value passed to {@link Resource.run} as
|
|
19
|
+
* `run(input, fetcher)`. Set for `running`, `completed`, AND `failed` (same
|
|
20
|
+
* stale-guard rule as the rest of the state), so an effect can branch on
|
|
21
|
+
* `status === 'failed'` and still know which request failed. `undefined` in
|
|
22
|
+
* `idle`, and for the no-input `run(fetcher)` form.
|
|
23
|
+
*/
|
|
24
|
+
input: I | undefined;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* The fetcher passed to {@link Resource.run}. You own the transport. It receives
|
|
@@ -21,10 +29,14 @@ interface ResourceState<T> {
|
|
|
21
29
|
* don't need progress (a plain `() => Promise<T>` is assignable here).
|
|
22
30
|
*/
|
|
23
31
|
type ResourceFetcher<T> = (report: (completed: number, total: number) => void) => Promise<T>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
32
|
+
/**
|
|
33
|
+
* An async-state container. Its `value` is a tracking read; drive UI off
|
|
34
|
+
* `value.status`. `I` is the run-input type — parametrize it (`resource<T, I>()`)
|
|
35
|
+
* to carry a typed `run(input, fetcher)` input through to `value.input`.
|
|
36
|
+
*/
|
|
37
|
+
interface Resource<T, I = void> {
|
|
26
38
|
/** Tracking read of the current {@link ResourceState}. */
|
|
27
|
-
readonly value: ResourceState<T>;
|
|
39
|
+
readonly value: ResourceState<T, I>;
|
|
28
40
|
/**
|
|
29
41
|
* Run `fetcher`, driving `idle`/`running` → `completed`/`failed` and guarding
|
|
30
42
|
* against stale responses (only the latest run resolves the state). Never
|
|
@@ -32,10 +44,16 @@ interface Resource<T> {
|
|
|
32
44
|
* `undefined` on failure) for callers who want to await it.
|
|
33
45
|
*/
|
|
34
46
|
run(fetcher: ResourceFetcher<T>): Promise<T | undefined>;
|
|
35
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Run `fetcher` for a given `input`, exposing it as `value.input` for the
|
|
49
|
+
* `running`/`completed`/`failed` states of THIS run — so a failure handler can
|
|
50
|
+
* recover which request failed. Same stale guard: only the latest run resolves.
|
|
51
|
+
*/
|
|
52
|
+
run(input: I, fetcher: ResourceFetcher<T>): Promise<T | undefined>;
|
|
53
|
+
/** Reset to `idle` (clearing data/error/progress/input) and invalidate any in-flight run. */
|
|
36
54
|
reset(): void;
|
|
37
55
|
}
|
|
38
56
|
/** Create an async-state {@link Resource}. No per-instance framework state — it's a closure over a signal. */
|
|
39
|
-
declare function resource<T>(): Resource<T>;
|
|
57
|
+
declare function resource<T, I = void>(): Resource<T, I>;
|
|
40
58
|
|
|
41
59
|
export { type Resource, type ResourceFetcher, type ResourceProgress, type ResourceState, type ResourceStatus, resource };
|
package/dist/async.js
CHANGED
|
@@ -6,14 +6,17 @@ var IDLE = () => ({
|
|
|
6
6
|
status: "idle",
|
|
7
7
|
data: void 0,
|
|
8
8
|
error: void 0,
|
|
9
|
-
progress: void 0
|
|
9
|
+
progress: void 0,
|
|
10
|
+
input: void 0
|
|
10
11
|
});
|
|
11
12
|
function resource() {
|
|
12
13
|
const state = signal(IDLE());
|
|
13
14
|
let generation = 0;
|
|
14
|
-
function run(
|
|
15
|
+
function run(inputOrFetcher, maybeFetcher) {
|
|
16
|
+
const fetcher = maybeFetcher ?? inputOrFetcher;
|
|
17
|
+
const input = maybeFetcher === void 0 ? void 0 : inputOrFetcher;
|
|
15
18
|
const gen = ++generation;
|
|
16
|
-
state.value = { ...state.value, status: "running", error: void 0, progress: void 0 };
|
|
19
|
+
state.value = { ...state.value, status: "running", error: void 0, progress: void 0, input };
|
|
17
20
|
const report = (completed, total) => {
|
|
18
21
|
if (gen === generation) {
|
|
19
22
|
state.value = { ...state.value, progress: { completed, total } };
|
|
@@ -22,13 +25,13 @@ function resource() {
|
|
|
22
25
|
return fetcher(report).then(
|
|
23
26
|
(data) => {
|
|
24
27
|
if (gen === generation) {
|
|
25
|
-
state.value = { status: "completed", data, error: void 0, progress: void 0 };
|
|
28
|
+
state.value = { status: "completed", data, error: void 0, progress: void 0, input };
|
|
26
29
|
}
|
|
27
30
|
return data;
|
|
28
31
|
},
|
|
29
32
|
(error) => {
|
|
30
33
|
if (gen === generation) {
|
|
31
|
-
state.value = { ...state.value, status: "failed", error, progress: void 0 };
|
|
34
|
+
state.value = { ...state.value, status: "failed", error, progress: void 0, input };
|
|
32
35
|
}
|
|
33
36
|
return void 0;
|
|
34
37
|
}
|
package/dist/async.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../src/async.ts"],"names":[],"mappings":";;;;AAyFA,IAAM,OAAO,OAAkC;AAAA,EAC7C,MAAA,EAAQ,MAAA;AAAA,EACR,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,MAAA;AAAA,EACP,QAAA,EAAU,MAAA;AAAA,EACV,KAAA,EAAO;AACT,CAAA,CAAA;AAGO,SAAS,QAAA,GAAwC;AACtD,EAAA,MAAM,KAAA,GAAQ,MAAA,CAA4B,IAAA,EAAY,CAAA;AAEtD,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;AAExD,IAAA,MAAM,MAAM,EAAE,UAAA;AACd,IAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,KAAA,EAAO,MAAA,EAAQ,SAAA,EAAW,KAAA,EAAO,MAAA,EAAW,QAAA,EAAU,MAAA,EAAW,KAAA,EAAM;AAEhG,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,OAAO,OAAA,CAAQ,MAAM,CAAA,CAAE,IAAA;AAAA,MACrB,CAAC,IAAA,KAAS;AACR,QAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,UAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,MAAA,EAAQ,WAAA,EAAa,MAAM,KAAA,EAAO,MAAA,EAAW,QAAA,EAAU,MAAA,EAAW,KAAA,EAAM;AAAA,QAC1F;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAA;AAAA,MACA,CAAC,KAAA,KAAmB;AAClB,QAAA,IAAI,QAAQ,UAAA,EAAY;AACtB,UAAA,KAAA,CAAM,KAAA,GAAQ,EAAE,GAAG,KAAA,CAAM,KAAA,EAAO,QAAQ,QAAA,EAAU,KAAA,EAAO,QAAA,EAAU,MAAA,EAAW,KAAA,EAAM;AAAA,QACtF;AACA,QAAA,OAAO,MAAA;AAAA,MACT;AAAA,KACF;AAAA,EACF;AAEA,EAAA,SAAS,KAAA,GAAc;AACrB,IAAA,UAAA,EAAA;AACA,IAAA,KAAA,CAAM,QAAQ,IAAA,EAAW;AAAA,EAC3B;AAEA,EAAA,OAAO;AAAA,IACL,IAAI,KAAA,GAAQ;AACV,MAAA,OAAO,KAAA,CAAM,KAAA;AAAA,IACf,CAAA;AAAA,IACA,GAAA;AAAA,IACA;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 */\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\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 invalidate any in-flight run. */\n reset(): void;\n}\n\nconst IDLE = <T, I>(): ResourceState<T, I> => ({\n status: 'idle',\n data: undefined,\n error: undefined,\n progress: undefined,\n input: undefined,\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>(): Resource<T, I> {\n const state = signal<ResourceState<T, I>>(IDLE<T, I>());\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\n const gen = ++generation;\n state.value = { ...state.value, status: 'running', error: undefined, progress: undefined, input };\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 state.value = { status: 'completed', data, error: undefined, progress: undefined, input };\n }\n return data;\n },\n (error: unknown) => {\n if (gen === generation) {\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 state.value = IDLE<T, I>();\n }\n\n return {\n get value() {\n return state.value;\n },\n run,\n reset,\n };\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.
|
|
3
|
+
*
|
|
4
|
+
* `data-morph-skip` lets a library own a subtree so kerf won't touch it — but
|
|
5
|
+
* nothing manages that widget's LIFECYCLE. You set it up imperatively after
|
|
6
|
+
* render and must remember to tear it down when the node is replaced/removed
|
|
7
|
+
* (dropping document-level listeners the widget added, etc.). `imperative` closes
|
|
8
|
+
* that seam: it's a `useEffect`-with-cleanup bound to one node.
|
|
9
|
+
*
|
|
10
|
+
* import { imperative } from 'kerfjs/imperative';
|
|
11
|
+
*
|
|
12
|
+
* imperative(canvasEl, (el) => {
|
|
13
|
+
* const chart = D3.mount(el);
|
|
14
|
+
* return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* `setup(node)` runs immediately and may return a teardown function. The teardown
|
|
18
|
+
* runs once — whichever comes first — when the node leaves the document (detected
|
|
19
|
+
* by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any
|
|
20
|
+
* removal triggers it) or when the returned disposer is called. Re-creation is
|
|
21
|
+
* NOT handled here: a fresh node is a fresh `imperative()` call — pair it with
|
|
22
|
+
* `kerfjs/remount`, which replaces the node and re-runs your render (and thus
|
|
23
|
+
* this call) on the new one.
|
|
24
|
+
*/
|
|
25
|
+
/** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */
|
|
26
|
+
type ImperativeSetup = (node: Element) => (() => void) | void;
|
|
27
|
+
/**
|
|
28
|
+
* Run `setup(node)` now, and its returned teardown once — when `node` leaves the
|
|
29
|
+
* document, or when the returned disposer is called, whichever is first. Returns
|
|
30
|
+
* a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.
|
|
31
|
+
*/
|
|
32
|
+
declare function imperative(node: Element, setup: ImperativeSetup): () => void;
|
|
33
|
+
|
|
34
|
+
export { type ImperativeSetup, imperative };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/imperative.ts
|
|
2
|
+
function imperative(node, setup) {
|
|
3
|
+
const teardown = setup(node);
|
|
4
|
+
let done = false;
|
|
5
|
+
const finish = () => {
|
|
6
|
+
if (done) return;
|
|
7
|
+
done = true;
|
|
8
|
+
observer.disconnect();
|
|
9
|
+
if (typeof teardown === "function") teardown();
|
|
10
|
+
};
|
|
11
|
+
const observer = new MutationObserver(() => {
|
|
12
|
+
if (!node.isConnected) finish();
|
|
13
|
+
});
|
|
14
|
+
observer.observe(node.getRootNode(), { childList: true, subtree: true });
|
|
15
|
+
return finish;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { imperative };
|
|
19
|
+
//# sourceMappingURL=imperative.js.map
|
|
20
|
+
//# sourceMappingURL=imperative.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/imperative.ts"],"names":[],"mappings":";AAiCO,SAAS,UAAA,CAAW,MAAe,KAAA,EAAoC;AAC5E,EAAA,MAAM,QAAA,GAAW,MAAM,IAAI,CAAA;AAC3B,EAAA,IAAI,IAAA,GAAO,KAAA;AAEX,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,QAAA,EAAS;AAAA,EAC/C,CAAA;AAMA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,EAAa,MAAA,EAAO;AAAA,EAChC,CAAC,CAAA;AACD,EAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,WAAA,EAAY,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAEvE,EAAA,OAAO,MAAA;AACT","file":"imperative.js","sourcesContent":["/**\n * `kerfjs/imperative` — bind a non-kerf widget's lifecycle to a single DOM node.\n *\n * `data-morph-skip` lets a library own a subtree so kerf won't touch it — but\n * nothing manages that widget's LIFECYCLE. You set it up imperatively after\n * render and must remember to tear it down when the node is replaced/removed\n * (dropping document-level listeners the widget added, etc.). `imperative` closes\n * that seam: it's a `useEffect`-with-cleanup bound to one node.\n *\n * import { imperative } from 'kerfjs/imperative';\n *\n * imperative(canvasEl, (el) => {\n * const chart = D3.mount(el);\n * return () => chart.destroy(); // runs when el leaves the DOM (or on dispose)\n * });\n *\n * `setup(node)` runs immediately and may return a teardown function. The teardown\n * runs once — whichever comes first — when the node leaves the document (detected\n * by a `MutationObserver`, so a morph swap, a `remountOn` replacement, or any\n * removal triggers it) or when the returned disposer is called. Re-creation is\n * NOT handled here: a fresh node is a fresh `imperative()` call — pair it with\n * `kerfjs/remount`, which replaces the node and re-runs your render (and thus\n * this call) on the new one.\n */\n\n/** The setup callback for {@link imperative}: run against `node`, optionally return a teardown. */\nexport type ImperativeSetup = (node: Element) => (() => void) | void;\n\n/**\n * Run `setup(node)` now, and its returned teardown once — when `node` leaves the\n * document, or when the returned disposer is called, whichever is first. Returns\n * a disposer (idempotent) so a `mount()` / `Scope` can drive teardown explicitly.\n */\nexport function imperative(node: Element, setup: ImperativeSetup): () => void {\n const teardown = setup(node);\n let done = false;\n\n const finish = (): void => {\n if (done) return;\n done = true;\n observer.disconnect();\n if (typeof teardown === 'function') teardown();\n };\n\n // Observe the node's live tree (the document when connected) with subtree, so\n // an ANCESTOR removal — not just a direct one — is caught. Each mutation just\n // re-checks `node.isConnected`, which is true until the node (or an ancestor)\n // is removed, so a morph swap / remountOn replacement / manual removal all fire.\n const observer = new MutationObserver(() => {\n if (!node.isConnected) finish();\n });\n observer.observe(node.getRootNode(), { childList: true, subtree: true });\n\n return finish;\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
export { each, morph, mount } from './chunk-4MY2656S.js';
|
|
1
2
|
export { defineStore, resetAllStores } from './chunk-SAYPJ6XR.js';
|
|
2
3
|
export { attr } from './chunk-U32TFTGZ.js';
|
|
3
4
|
export { delegate, delegateCapture } from './chunk-KEZTD6H4.js';
|
|
4
|
-
export { each, morph, mount } from './chunk-4MY2656S.js';
|
|
5
5
|
import './chunk-QIP723L4.js';
|
|
6
6
|
import './chunk-YHH7OUFA.js';
|
|
7
7
|
export { Fragment, SafeHtml, isSafeHtml, raw, trustedRaw } from './chunk-FSAQR6IU.js';
|
package/dist/overlay.d.ts
CHANGED
|
@@ -75,6 +75,113 @@ interface ConfirmOptions {
|
|
|
75
75
|
* auto-escaped (rendered through the JSX runtime).
|
|
76
76
|
*/
|
|
77
77
|
declare function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Validate a single field's value. Return a non-empty error string to BLOCK
|
|
80
|
+
* submission (shown inline next to the field); return `undefined`/`null`/`''` to
|
|
81
|
+
* allow it.
|
|
82
|
+
*/
|
|
83
|
+
type FieldValidator = (value: string) => string | null | undefined | void;
|
|
84
|
+
/** Options for {@link prompt}. */
|
|
85
|
+
interface PromptOptions {
|
|
86
|
+
/** Where to append the overlay. Default `document.body`. */
|
|
87
|
+
container?: Element;
|
|
88
|
+
/** Wrapper class. Default `'kerf-overlay'`. */
|
|
89
|
+
className?: string;
|
|
90
|
+
/** Optional heading above the message. */
|
|
91
|
+
title?: string;
|
|
92
|
+
/** Pre-filled input value. Default `''`. */
|
|
93
|
+
defaultValue?: string;
|
|
94
|
+
/** Input placeholder. */
|
|
95
|
+
placeholder?: string;
|
|
96
|
+
/** `type` attribute of the input (`'text'`, `'email'`, `'password'`, …). Default `'text'`. */
|
|
97
|
+
inputType?: string;
|
|
98
|
+
/** Confirm button label. Default `'OK'`. */
|
|
99
|
+
okText?: string;
|
|
100
|
+
/** Cancel button label. Default `'Cancel'`. */
|
|
101
|
+
cancelText?: string;
|
|
102
|
+
/** Block OK while this returns an error string; the message shows inline. */
|
|
103
|
+
validate?: FieldValidator;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A promise-based `window.prompt` replacement (that global is a no-op in Tauri
|
|
107
|
+
* webviews). Renders a one-field dialog and resolves the entered **string** on OK
|
|
108
|
+
* (an empty string is a valid result) or `null` on Cancel / dismissal. Enter in
|
|
109
|
+
* the input submits. `message`, the default value, and labels are auto-escaped
|
|
110
|
+
* (rendered through the JSX runtime). Optional `validate` blocks OK inline.
|
|
111
|
+
*/
|
|
112
|
+
declare function prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
113
|
+
/** A single field in a {@link form}. */
|
|
114
|
+
interface FormField {
|
|
115
|
+
/** Field name — the key in the resolved record (and the input's `name`). */
|
|
116
|
+
name: string;
|
|
117
|
+
/** Label shown above the input. Defaults to `name`. */
|
|
118
|
+
label?: string;
|
|
119
|
+
/** Pre-filled value. Default `''`. */
|
|
120
|
+
defaultValue?: string;
|
|
121
|
+
/** Input placeholder. */
|
|
122
|
+
placeholder?: string;
|
|
123
|
+
/** `type` attribute of the input. Default `'text'`. */
|
|
124
|
+
type?: string;
|
|
125
|
+
/** Block OK while this returns an error string; the message shows inline for this field. */
|
|
126
|
+
validate?: FieldValidator;
|
|
127
|
+
}
|
|
128
|
+
/** Options for {@link form}. */
|
|
129
|
+
interface FormOptions {
|
|
130
|
+
/** Where to append the overlay. Default `document.body`. */
|
|
131
|
+
container?: Element;
|
|
132
|
+
/** Wrapper class. Default `'kerf-overlay'`. */
|
|
133
|
+
className?: string;
|
|
134
|
+
/** Optional heading above the fields. */
|
|
135
|
+
title?: string;
|
|
136
|
+
/** Confirm button label. Default `'OK'`. */
|
|
137
|
+
okText?: string;
|
|
138
|
+
/** Cancel button label. Default `'Cancel'`. */
|
|
139
|
+
cancelText?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A promise-based multi-field dialog — the two-or-three-input sibling of
|
|
143
|
+
* {@link prompt}. Renders one labeled input per {@link FormField} and resolves a
|
|
144
|
+
* `Record<name, value>` on OK (after every field's `validate` passes) or `null`
|
|
145
|
+
* on Cancel / dismissal. Enter in any field submits. All labels, defaults, and
|
|
146
|
+
* the title are auto-escaped through the JSX runtime.
|
|
147
|
+
*/
|
|
148
|
+
declare function form(fields: readonly FormField[], options?: FormOptions): Promise<Record<string, string> | null>;
|
|
149
|
+
/** Vertical placement of a {@link popover} relative to its anchor. */
|
|
150
|
+
type PopoverPlacement = 'bottom' | 'top';
|
|
151
|
+
/** Options for {@link popover}. */
|
|
152
|
+
interface PopoverOptions {
|
|
153
|
+
/** Where to append the popover wrapper. Default `document.body`. */
|
|
154
|
+
container?: Element;
|
|
155
|
+
/** Class on the wrapper. Default `'kerf-popover'`. */
|
|
156
|
+
className?: string;
|
|
157
|
+
/** Preferred side of the anchor. Flips to the other side if it would overflow the viewport. Default `'bottom'`. */
|
|
158
|
+
placement?: PopoverPlacement;
|
|
159
|
+
/** Horizontal edge to line up with the anchor: `'start'` (left edges) or `'end'` (right edges). Default `'start'`. */
|
|
160
|
+
align?: 'start' | 'end';
|
|
161
|
+
/** Gap in px between the anchor and the popover. Default `4`. */
|
|
162
|
+
gap?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Which user actions dismiss the popover. Default `['outside']` (a click
|
|
165
|
+
* outside the popover, the anchor exempt). Pass `false` to close only via `close()`.
|
|
166
|
+
*/
|
|
167
|
+
dismiss?: DismissTrigger | DismissTrigger[] | false;
|
|
168
|
+
/** Focus behavior on open. Default `false` (non-modal — leave focus alone). */
|
|
169
|
+
initialFocus?: string | boolean;
|
|
170
|
+
/** Extra elements (besides the anchor) whose clicks do NOT count as outside. */
|
|
171
|
+
outsideIgnore?: Element | readonly Element[];
|
|
172
|
+
/** Called on any user-initiated dismissal. */
|
|
173
|
+
onDismiss?: () => void;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Anchored, non-modal overlay: positions `content` relative to `anchor` (below by
|
|
177
|
+
* default, flipping above if it would overflow, and clamped horizontally to the
|
|
178
|
+
* viewport) and repositions on scroll / resize while open. A thin wrapper over
|
|
179
|
+
* {@link overlay} with non-modal defaults — `trap: false`, `dismiss: ['outside']`,
|
|
180
|
+
* and the anchor added to `outsideIgnore` so the trigger click doesn't self-close.
|
|
181
|
+
* Returns the same {@link OverlayHandle}; `close()` also drops the reposition
|
|
182
|
+
* listeners. `position: fixed` is set inline (you style everything else).
|
|
183
|
+
*/
|
|
184
|
+
declare function popover(anchor: Element, content: OverlayContent, options?: PopoverOptions): OverlayHandle;
|
|
78
185
|
/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */
|
|
79
186
|
type ToastContent = string | SafeHtml | (() => MountResult);
|
|
80
187
|
/** Options for {@link toast}. */
|
|
@@ -94,4 +201,4 @@ interface ToastOptions {
|
|
|
94
201
|
*/
|
|
95
202
|
declare function toast(content: ToastContent, options?: ToastOptions): () => void;
|
|
96
203
|
|
|
97
|
-
export { type ConfirmOptions, type DismissTrigger, type OverlayContent, type OverlayHandle, type OverlayOptions, type ToastContent, type ToastOptions, confirm, overlay, toast };
|
|
204
|
+
export { type ConfirmOptions, type DismissTrigger, type FieldValidator, type FormField, type FormOptions, type OverlayContent, type OverlayHandle, type OverlayOptions, type PopoverOptions, type PopoverPlacement, type PromptOptions, type ToastContent, type ToastOptions, confirm, form, overlay, popover, prompt, toast };
|
package/dist/overlay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { delegate } from './chunk-KEZTD6H4.js';
|
|
2
1
|
import { mount } from './chunk-4MY2656S.js';
|
|
2
|
+
import { delegate } from './chunk-KEZTD6H4.js';
|
|
3
3
|
import './chunk-QIP723L4.js';
|
|
4
4
|
import './chunk-YHH7OUFA.js';
|
|
5
5
|
import { jsx } from './chunk-FSAQR6IU.js';
|
|
@@ -158,6 +158,217 @@ function confirm(message, options = {}) {
|
|
|
158
158
|
});
|
|
159
159
|
return handle.result.then((value) => value === true);
|
|
160
160
|
}
|
|
161
|
+
function prompt(message, options = {}) {
|
|
162
|
+
const {
|
|
163
|
+
container,
|
|
164
|
+
className = "kerf-overlay",
|
|
165
|
+
title,
|
|
166
|
+
defaultValue = "",
|
|
167
|
+
placeholder,
|
|
168
|
+
inputType = "text",
|
|
169
|
+
okText = "OK",
|
|
170
|
+
cancelText = "Cancel",
|
|
171
|
+
validate
|
|
172
|
+
} = options;
|
|
173
|
+
const body = jsx("div", {
|
|
174
|
+
class: "kerf-prompt",
|
|
175
|
+
children: [
|
|
176
|
+
title !== void 0 ? jsx("h2", { class: "kerf-prompt__title", children: title }) : "",
|
|
177
|
+
jsx("label", { class: "kerf-prompt__message", children: message }),
|
|
178
|
+
jsx("input", {
|
|
179
|
+
class: "kerf-prompt__input",
|
|
180
|
+
type: inputType,
|
|
181
|
+
value: defaultValue,
|
|
182
|
+
...placeholder !== void 0 ? { placeholder } : {},
|
|
183
|
+
"data-prompt-input": ""
|
|
184
|
+
}),
|
|
185
|
+
jsx("p", { class: "kerf-prompt__error", "data-prompt-error": "", children: "" }),
|
|
186
|
+
jsx("div", {
|
|
187
|
+
class: "kerf-prompt__actions",
|
|
188
|
+
children: [
|
|
189
|
+
jsx("button", { type: "button", "data-prompt": "cancel", children: cancelText }),
|
|
190
|
+
jsx("button", {
|
|
191
|
+
type: "button",
|
|
192
|
+
"data-prompt": "ok",
|
|
193
|
+
class: "kerf-prompt__ok",
|
|
194
|
+
children: okText
|
|
195
|
+
})
|
|
196
|
+
]
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
const handle = overlay(body, {
|
|
201
|
+
container,
|
|
202
|
+
className,
|
|
203
|
+
dismiss: ["escape", "backdrop"],
|
|
204
|
+
initialFocus: ".kerf-prompt__input",
|
|
205
|
+
trap: true
|
|
206
|
+
});
|
|
207
|
+
const input = handle.el.querySelector("[data-prompt-input]");
|
|
208
|
+
const errorEl = handle.el.querySelector("[data-prompt-error]");
|
|
209
|
+
errorEl.hidden = true;
|
|
210
|
+
function attemptOk() {
|
|
211
|
+
const value = input.value;
|
|
212
|
+
const error = validate?.(value);
|
|
213
|
+
if (typeof error === "string" && error.length > 0) {
|
|
214
|
+
errorEl.textContent = error;
|
|
215
|
+
errorEl.hidden = false;
|
|
216
|
+
input.focus();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
handle.close(value);
|
|
220
|
+
}
|
|
221
|
+
delegate(handle.el, "click", "[data-prompt]", (_event, el) => {
|
|
222
|
+
if (el.getAttribute("data-prompt") === "ok") attemptOk();
|
|
223
|
+
else handle.close(null);
|
|
224
|
+
});
|
|
225
|
+
handle.el.addEventListener("keydown", (event) => {
|
|
226
|
+
if (event.key === "Enter" && event.target === input) {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
attemptOk();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return handle.result.then((value) => typeof value === "string" ? value : null);
|
|
232
|
+
}
|
|
233
|
+
function form(fields, options = {}) {
|
|
234
|
+
const { container, className = "kerf-overlay", title, okText = "OK", cancelText = "Cancel" } = options;
|
|
235
|
+
const body = jsx("div", {
|
|
236
|
+
class: "kerf-form",
|
|
237
|
+
children: [
|
|
238
|
+
title !== void 0 ? jsx("h2", { class: "kerf-form__title", children: title }) : "",
|
|
239
|
+
...fields.map(
|
|
240
|
+
(field) => jsx("div", {
|
|
241
|
+
class: "kerf-form__field",
|
|
242
|
+
children: [
|
|
243
|
+
jsx("label", { class: "kerf-form__label", children: field.label ?? field.name }),
|
|
244
|
+
jsx("input", {
|
|
245
|
+
class: "kerf-form__input",
|
|
246
|
+
type: field.type ?? "text",
|
|
247
|
+
name: field.name,
|
|
248
|
+
value: field.defaultValue ?? "",
|
|
249
|
+
...field.placeholder !== void 0 ? { placeholder: field.placeholder } : {},
|
|
250
|
+
"data-field": field.name
|
|
251
|
+
}),
|
|
252
|
+
jsx("p", {
|
|
253
|
+
class: "kerf-form__error",
|
|
254
|
+
"data-field-error": field.name,
|
|
255
|
+
children: ""
|
|
256
|
+
})
|
|
257
|
+
]
|
|
258
|
+
})
|
|
259
|
+
),
|
|
260
|
+
jsx("div", {
|
|
261
|
+
class: "kerf-form__actions",
|
|
262
|
+
children: [
|
|
263
|
+
jsx("button", { type: "button", "data-form": "cancel", children: cancelText }),
|
|
264
|
+
jsx("button", {
|
|
265
|
+
type: "button",
|
|
266
|
+
"data-form": "ok",
|
|
267
|
+
class: "kerf-form__ok",
|
|
268
|
+
children: okText
|
|
269
|
+
})
|
|
270
|
+
]
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
});
|
|
274
|
+
const handle = overlay(body, {
|
|
275
|
+
container,
|
|
276
|
+
className,
|
|
277
|
+
dismiss: ["escape", "backdrop"],
|
|
278
|
+
initialFocus: ".kerf-form__input",
|
|
279
|
+
trap: true
|
|
280
|
+
});
|
|
281
|
+
const byAttr = (attr, name) => Array.from(handle.el.querySelectorAll(`[${attr}]`)).find(
|
|
282
|
+
(el) => el.getAttribute(attr) === name
|
|
283
|
+
);
|
|
284
|
+
for (const field of fields) {
|
|
285
|
+
byAttr("data-field-error", field.name).hidden = true;
|
|
286
|
+
}
|
|
287
|
+
function attemptOk() {
|
|
288
|
+
const record = {};
|
|
289
|
+
let firstInvalid = null;
|
|
290
|
+
for (const field of fields) {
|
|
291
|
+
const el = byAttr("data-field", field.name);
|
|
292
|
+
const value = el.value;
|
|
293
|
+
record[field.name] = value;
|
|
294
|
+
const error = field.validate?.(value);
|
|
295
|
+
const errorEl = byAttr("data-field-error", field.name);
|
|
296
|
+
if (typeof error === "string" && error.length > 0) {
|
|
297
|
+
errorEl.textContent = error;
|
|
298
|
+
errorEl.hidden = false;
|
|
299
|
+
if (firstInvalid === null) firstInvalid = el;
|
|
300
|
+
} else {
|
|
301
|
+
errorEl.hidden = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (firstInvalid !== null) {
|
|
305
|
+
firstInvalid.focus();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
handle.close(record);
|
|
309
|
+
}
|
|
310
|
+
delegate(handle.el, "click", "[data-form]", (_event, el) => {
|
|
311
|
+
if (el.getAttribute("data-form") === "ok") attemptOk();
|
|
312
|
+
else handle.close(null);
|
|
313
|
+
});
|
|
314
|
+
handle.el.addEventListener("keydown", (event) => {
|
|
315
|
+
if (event.key === "Enter" && event.target?.matches("[data-field]")) {
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
attemptOk();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
return handle.result.then(
|
|
321
|
+
(value) => value !== null && typeof value === "object" ? value : null
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
function popover(anchor, content, options = {}) {
|
|
325
|
+
const {
|
|
326
|
+
container,
|
|
327
|
+
className = "kerf-popover",
|
|
328
|
+
placement = "bottom",
|
|
329
|
+
align = "start",
|
|
330
|
+
gap = 4,
|
|
331
|
+
dismiss = ["outside"],
|
|
332
|
+
initialFocus = false,
|
|
333
|
+
outsideIgnore,
|
|
334
|
+
onDismiss
|
|
335
|
+
} = options;
|
|
336
|
+
const extraIgnore = outsideIgnore === void 0 ? [] : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];
|
|
337
|
+
const handle = overlay(content, {
|
|
338
|
+
container,
|
|
339
|
+
className,
|
|
340
|
+
dismiss,
|
|
341
|
+
trap: false,
|
|
342
|
+
initialFocus,
|
|
343
|
+
onDismiss,
|
|
344
|
+
outsideIgnore: [anchor, ...extraIgnore]
|
|
345
|
+
});
|
|
346
|
+
handle.el.style.position = "fixed";
|
|
347
|
+
handle.el.style.margin = "0";
|
|
348
|
+
const reposition = () => {
|
|
349
|
+
const a = anchor.getBoundingClientRect();
|
|
350
|
+
const p = handle.el.getBoundingClientRect();
|
|
351
|
+
const vw = window.innerWidth;
|
|
352
|
+
const vh = window.innerHeight;
|
|
353
|
+
const belowTop = a.bottom + gap;
|
|
354
|
+
const aboveTop = a.top - gap - p.height;
|
|
355
|
+
let below = placement !== "top";
|
|
356
|
+
if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;
|
|
357
|
+
else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;
|
|
358
|
+
let left = align === "end" ? a.right - p.width : a.left;
|
|
359
|
+
left = Math.max(0, Math.min(left, vw - p.width));
|
|
360
|
+
handle.el.style.left = `${left}px`;
|
|
361
|
+
handle.el.style.top = `${below ? belowTop : aboveTop}px`;
|
|
362
|
+
};
|
|
363
|
+
reposition();
|
|
364
|
+
window.addEventListener("scroll", reposition, true);
|
|
365
|
+
window.addEventListener("resize", reposition);
|
|
366
|
+
void handle.result.then(() => {
|
|
367
|
+
window.removeEventListener("scroll", reposition, true);
|
|
368
|
+
window.removeEventListener("resize", reposition);
|
|
369
|
+
});
|
|
370
|
+
return handle;
|
|
371
|
+
}
|
|
161
372
|
function toastRegion(container) {
|
|
162
373
|
if (container !== void 0) return container;
|
|
163
374
|
const existing = document.querySelector(".kerf-toasts");
|
|
@@ -190,6 +401,6 @@ function toast(content, options = {}) {
|
|
|
190
401
|
return dismiss;
|
|
191
402
|
}
|
|
192
403
|
|
|
193
|
-
export { confirm, overlay, toast };
|
|
404
|
+
export { confirm, form, overlay, popover, prompt, toast };
|
|
194
405
|
//# sourceMappingURL=overlay.js.map
|
|
195
406
|
//# sourceMappingURL=overlay.js.map
|
package/dist/overlay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/overlay.ts"],"names":[],"mappings":"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","file":"overlay.js","sourcesContent":["/**\n * `kerfjs/overlay` — the modal / overlay + dismiss manager.\n *\n * Every real kerf app hand-rolls this: `toElement → body.appendChild → mount →\n * wire dismissal → remove`, plus the fiddly parts (Escape, backdrop / outside\n * click, focus trap, restoring focus on close). `window.confirm` is a no-op in\n * Tauri WKWebViews, so a hand-built overlay is mandatory there. This subpath\n * blesses the pattern as three functions over `mount()` — `overlay()`, and the\n * `confirm()` / `toast()` conveniences built on it. No per-instance framework\n * state: each call owns its DOM + listeners in a closure and returns a handle.\n *\n * import { overlay, confirm, toast } from 'kerfjs/overlay';\n *\n * const ok = await confirm('Delete this file?', { danger: true });\n * toast('Saved');\n * const dialog = overlay(<Settings />, { dismiss: ['escape', 'backdrop'] });\n * // …later: dialog.close(); or await dialog.result;\n *\n * Structural only — kerf ships no CSS. The wrapper gets your `className`; style\n * the backdrop / centering / animation yourself.\n */\nimport { delegate } from './delegate.js';\nimport { jsx, type SafeHtml } from './jsx-runtime.js';\nimport { mount, type MountResult } from './mount.js';\n\n/** A user-initiated dismissal trigger. */\nexport type DismissTrigger = 'escape' | 'backdrop' | 'outside';\n\n/** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */\nexport type OverlayContent = SafeHtml | (() => MountResult);\n\n/** Options for {@link overlay}. */\nexport interface OverlayOptions {\n /** Where to append the overlay wrapper. Default `document.body`. */\n container?: Element;\n /** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */\n className?: string;\n /**\n * Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.\n * `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`\n * = a click anywhere outside the wrapper (for anchored popovers). `false`\n * disables user dismissal (close it programmatically).\n */\n dismiss?: DismissTrigger | DismissTrigger[] | false;\n /**\n * Where focus lands on open: a selector, `true` (first focusable element, or\n * the wrapper if none), or `false` (leave focus alone). Default `true`.\n */\n initialFocus?: string | boolean;\n /**\n * Trap Tab / Shift+Tab within the overlay while open and mark it\n * `role=\"dialog\"` / `aria-modal=\"true\"`. Default `true`. Set `false` for a\n * non-modal popover.\n */\n trap?: boolean;\n /** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */\n role?: string;\n /** Called on any user-initiated dismissal (before `close()` runs). */\n onDismiss?: () => void;\n /** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */\n outsideIgnore?: Element | readonly Element[];\n}\n\n/** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */\nexport interface OverlayHandle {\n /** The wrapper element (mounted into, appended to `container`). */\n el: HTMLElement;\n /** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */\n close(result?: unknown): void;\n /** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */\n result: Promise<unknown>;\n}\n\nconst FOCUSABLE =\n 'a[href],area[href],button:not([disabled]),input:not([disabled]),'\n + 'select:not([disabled]),textarea:not([disabled]),iframe,'\n + '[tabindex]:not([tabindex=\"-1\"]),[contenteditable=\"true\"]';\n\nfunction focusable(root: Element): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hasAttribute('hidden'),\n );\n}\n\n/**\n * Open an overlay: append a wrapper to `container`, `mount()` `content` inside\n * it, wire the requested dismissals + (optionally) a focus trap, and return a\n * handle. See {@link OverlayOptions}.\n */\nexport function overlay(content: OverlayContent, options: OverlayOptions = {}): OverlayHandle {\n const {\n container = document.body,\n className = 'kerf-overlay',\n dismiss = ['escape', 'backdrop'],\n initialFocus = true,\n trap = true,\n role = 'dialog',\n onDismiss,\n outsideIgnore,\n } = options;\n\n const triggers: readonly DismissTrigger[] =\n dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];\n const restoreTo = document.activeElement;\n\n const wrapper = document.createElement('div');\n wrapper.className = className;\n if (trap) {\n wrapper.setAttribute('role', role);\n wrapper.setAttribute('aria-modal', 'true');\n }\n container.appendChild(wrapper);\n\n const disposeMount = mount(wrapper, typeof content === 'function' ? content : () => content);\n\n const removers: Array<() => void> = [];\n const resultBox: { resolve?: (value: unknown) => void } = {};\n const result = new Promise<unknown>((resolve) => {\n resultBox.resolve = resolve;\n });\n const state = { closed: false };\n\n function close(value?: unknown): void {\n if (state.closed) return;\n state.closed = true;\n for (const remove of removers) remove();\n disposeMount();\n wrapper.remove();\n if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();\n resultBox.resolve?.(value);\n }\n\n function userDismiss(): void {\n onDismiss?.();\n close();\n }\n\n const wantEscape = triggers.includes('escape');\n if (wantEscape || trap) {\n const onKeydown = (event: KeyboardEvent): void => {\n if (wantEscape && event.key === 'Escape') {\n event.stopPropagation();\n userDismiss();\n return;\n }\n if (trap && event.key === 'Tab') {\n const items = focusable(wrapper);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n const active = document.activeElement;\n const outside = !wrapper.contains(active);\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n };\n document.addEventListener('keydown', onKeydown, true);\n removers.push(() => document.removeEventListener('keydown', onKeydown, true));\n }\n\n if (triggers.includes('backdrop')) {\n const onClick = (event: Event): void => {\n if (event.target === wrapper) userDismiss();\n };\n wrapper.addEventListener('click', onClick);\n removers.push(() => wrapper.removeEventListener('click', onClick));\n }\n\n if (triggers.includes('outside')) {\n const ignore = outsideIgnore === undefined\n ? []\n : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];\n // Capture phase: the click that opened this overlay already passed\n // document's capture phase, so this never fires for that opening click.\n const onDocClick = (event: Event): void => {\n const target = event.target as Node | null;\n if (target === null) return;\n if (wrapper.contains(target)) return;\n if (ignore.some((el) => el === target || el.contains(target))) return;\n userDismiss();\n };\n document.addEventListener('click', onDocClick, true);\n removers.push(() => document.removeEventListener('click', onDocClick, true));\n }\n\n if (initialFocus !== false) {\n if (typeof initialFocus === 'string') {\n wrapper.querySelector<HTMLElement>(initialFocus)?.focus();\n } else {\n const first = focusable(wrapper)[0];\n if (first !== undefined) {\n first.focus();\n } else {\n wrapper.tabIndex = -1;\n wrapper.focus();\n }\n }\n }\n\n return { el: wrapper, close, result };\n}\n\n/** Options for {@link confirm}. */\nexport interface ConfirmOptions {\n /** Where to append the overlay. Default `document.body`. */\n container?: Element;\n /** Wrapper class. Default `'kerf-overlay'`. */\n className?: string;\n /** Optional heading above the message. */\n title?: string;\n /** Confirm button label. Default `'OK'`. */\n okText?: string;\n /** Cancel button label. Default `'Cancel'`. */\n cancelText?: string;\n /** Add a `kerf-confirm--danger` class to the wrapper for destructive actions. */\n danger?: boolean;\n}\n\n/**\n * A promise-based `window.confirm` replacement (that global is a no-op in Tauri\n * webviews). Renders a two-button dialog and resolves `true` for OK, `false`\n * for Cancel or any dismissal (Escape / backdrop). Message + labels are\n * auto-escaped (rendered through the JSX runtime).\n */\nexport function confirm(message: string, options: ConfirmOptions = {}): Promise<boolean> {\n const {\n container,\n className = 'kerf-overlay',\n title,\n okText = 'OK',\n cancelText = 'Cancel',\n danger = false,\n } = options;\n\n const body: SafeHtml = jsx('div', {\n class: 'kerf-confirm',\n children: [\n title !== undefined ? jsx('h2', { class: 'kerf-confirm__title', children: title }) : '',\n jsx('p', { class: 'kerf-confirm__message', children: message }),\n jsx('div', {\n class: 'kerf-confirm__actions',\n children: [\n jsx('button', { type: 'button', 'data-confirm': 'cancel', children: cancelText }),\n jsx('button', {\n type: 'button',\n 'data-confirm': 'ok',\n class: 'kerf-confirm__ok',\n children: okText,\n }),\n ],\n }),\n ],\n });\n\n const handle = overlay(body, {\n container,\n className: danger ? `${className} kerf-confirm--danger` : className,\n dismiss: ['escape', 'backdrop'],\n initialFocus: '.kerf-confirm__ok',\n trap: true,\n });\n\n delegate(handle.el, 'click', '[data-confirm]', (_event, el) => {\n handle.close(el.getAttribute('data-confirm') === 'ok');\n });\n\n return handle.result.then((value) => value === true);\n}\n\n/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */\nexport type ToastContent = string | SafeHtml | (() => MountResult);\n\n/** Options for {@link toast}. */\nexport interface ToastOptions {\n /** Where toasts stack. Default: a lazily-created `<div class=\"kerf-toasts\">` on `document.body`. */\n container?: Element;\n /** Class on the toast element. Default `'kerf-toast'`. */\n className?: string;\n /** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */\n duration?: number;\n /** ARIA role. Default `'status'`. */\n role?: string;\n}\n\n/** The singleton toast region lives in the DOM (queried, not held in a module variable). */\nfunction toastRegion(container?: Element): Element {\n if (container !== undefined) return container;\n const existing = document.querySelector('.kerf-toasts');\n if (existing !== null) return existing;\n const region = document.createElement('div');\n region.className = 'kerf-toasts';\n region.setAttribute('aria-live', 'polite');\n document.body.appendChild(region);\n return region;\n}\n\n/**\n * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level\n * region (or your `container`). Returns a `() => void` that dismisses it early.\n */\nexport function toast(content: ToastContent, options: ToastOptions = {}): () => void {\n const { container, className = 'kerf-toast', duration = 4000, role = 'status' } = options;\n\n const el = document.createElement('div');\n el.className = className;\n el.setAttribute('role', role);\n toastRegion(container).appendChild(el);\n\n const disposeMount = mount(el, typeof content === 'function' ? content : () => content);\n const state: { dismissed: boolean; timer: ReturnType<typeof setTimeout> | undefined } = {\n dismissed: false,\n timer: undefined,\n };\n\n function dismiss(): void {\n if (state.dismissed) return;\n state.dismissed = true;\n if (state.timer !== undefined) clearTimeout(state.timer);\n disposeMount();\n el.remove();\n }\n\n if (duration > 0) state.timer = setTimeout(dismiss, duration);\n return dismiss;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/overlay.ts"],"names":[],"mappings":"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","file":"overlay.js","sourcesContent":["/**\n * `kerfjs/overlay` — the modal / overlay + dismiss manager.\n *\n * Every real kerf app hand-rolls this: `toElement → body.appendChild → mount →\n * wire dismissal → remove`, plus the fiddly parts (Escape, backdrop / outside\n * click, focus trap, restoring focus on close). `window.confirm` is a no-op in\n * Tauri WKWebViews, so a hand-built overlay is mandatory there. This subpath\n * blesses the pattern as three functions over `mount()` — `overlay()`, and the\n * `confirm()` / `toast()` conveniences built on it. No per-instance framework\n * state: each call owns its DOM + listeners in a closure and returns a handle.\n *\n * import { overlay, confirm, toast } from 'kerfjs/overlay';\n *\n * const ok = await confirm('Delete this file?', { danger: true });\n * toast('Saved');\n * const dialog = overlay(<Settings />, { dismiss: ['escape', 'backdrop'] });\n * // …later: dialog.close(); or await dialog.result;\n *\n * Structural only — kerf ships no CSS. The wrapper gets your `className`; style\n * the backdrop / centering / animation yourself.\n */\nimport { delegate } from './delegate.js';\nimport { jsx, type SafeHtml } from './jsx-runtime.js';\nimport { mount, type MountResult } from './mount.js';\n\n/** A user-initiated dismissal trigger. */\nexport type DismissTrigger = 'escape' | 'backdrop' | 'outside';\n\n/** Content for an overlay: static `SafeHtml`, or a render function `mount()` drives reactively. */\nexport type OverlayContent = SafeHtml | (() => MountResult);\n\n/** Options for {@link overlay}. */\nexport interface OverlayOptions {\n /** Where to append the overlay wrapper. Default `document.body`. */\n container?: Element;\n /** Class on the wrapper element (you style it — kerf ships no CSS). Default `'kerf-overlay'`. */\n className?: string;\n /**\n * Which user actions dismiss the overlay. Default `['escape', 'backdrop']`.\n * `'backdrop'` = a click on the wrapper itself (not its content); `'outside'`\n * = a click anywhere outside the wrapper (for anchored popovers). `false`\n * disables user dismissal (close it programmatically).\n */\n dismiss?: DismissTrigger | DismissTrigger[] | false;\n /**\n * Where focus lands on open: a selector, `true` (first focusable element, or\n * the wrapper if none), or `false` (leave focus alone). Default `true`.\n */\n initialFocus?: string | boolean;\n /**\n * Trap Tab / Shift+Tab within the overlay while open and mark it\n * `role=\"dialog\"` / `aria-modal=\"true\"`. Default `true`. Set `false` for a\n * non-modal popover.\n */\n trap?: boolean;\n /** ARIA role for the wrapper when `trap` is on. Default `'dialog'`. */\n role?: string;\n /** Called on any user-initiated dismissal (before `close()` runs). */\n onDismiss?: () => void;\n /** For `'outside'` dismissal: clicks on these elements do NOT count as outside (e.g. the trigger button). */\n outsideIgnore?: Element | readonly Element[];\n}\n\n/** Handle returned by {@link overlay}. Holds no framework state — it's a closure. */\nexport interface OverlayHandle {\n /** The wrapper element (mounted into, appended to `container`). */\n el: HTMLElement;\n /** Tear down: dispose the mount, remove listeners + the node, restore focus, resolve `result`. Idempotent. */\n close(result?: unknown): void;\n /** Resolves with the value passed to `close()` (or `undefined` on user dismissal). */\n result: Promise<unknown>;\n}\n\nconst FOCUSABLE =\n 'a[href],area[href],button:not([disabled]),input:not([disabled]),'\n + 'select:not([disabled]),textarea:not([disabled]),iframe,'\n + '[tabindex]:not([tabindex=\"-1\"]),[contenteditable=\"true\"]';\n\nfunction focusable(root: Element): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hasAttribute('hidden'),\n );\n}\n\n/**\n * Open an overlay: append a wrapper to `container`, `mount()` `content` inside\n * it, wire the requested dismissals + (optionally) a focus trap, and return a\n * handle. See {@link OverlayOptions}.\n */\nexport function overlay(content: OverlayContent, options: OverlayOptions = {}): OverlayHandle {\n const {\n container = document.body,\n className = 'kerf-overlay',\n dismiss = ['escape', 'backdrop'],\n initialFocus = true,\n trap = true,\n role = 'dialog',\n onDismiss,\n outsideIgnore,\n } = options;\n\n const triggers: readonly DismissTrigger[] =\n dismiss === false ? [] : Array.isArray(dismiss) ? dismiss : [dismiss];\n const restoreTo = document.activeElement;\n\n const wrapper = document.createElement('div');\n wrapper.className = className;\n if (trap) {\n wrapper.setAttribute('role', role);\n wrapper.setAttribute('aria-modal', 'true');\n }\n container.appendChild(wrapper);\n\n const disposeMount = mount(wrapper, typeof content === 'function' ? content : () => content);\n\n const removers: Array<() => void> = [];\n const resultBox: { resolve?: (value: unknown) => void } = {};\n const result = new Promise<unknown>((resolve) => {\n resultBox.resolve = resolve;\n });\n const state = { closed: false };\n\n function close(value?: unknown): void {\n if (state.closed) return;\n state.closed = true;\n for (const remove of removers) remove();\n disposeMount();\n wrapper.remove();\n if (restoreTo instanceof HTMLElement && restoreTo.isConnected) restoreTo.focus();\n resultBox.resolve?.(value);\n }\n\n function userDismiss(): void {\n onDismiss?.();\n close();\n }\n\n const wantEscape = triggers.includes('escape');\n if (wantEscape || trap) {\n const onKeydown = (event: KeyboardEvent): void => {\n if (wantEscape && event.key === 'Escape') {\n event.stopPropagation();\n userDismiss();\n return;\n }\n if (trap && event.key === 'Tab') {\n const items = focusable(wrapper);\n if (items.length === 0) {\n event.preventDefault();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n const active = document.activeElement;\n const outside = !wrapper.contains(active);\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n };\n document.addEventListener('keydown', onKeydown, true);\n removers.push(() => document.removeEventListener('keydown', onKeydown, true));\n }\n\n if (triggers.includes('backdrop')) {\n const onClick = (event: Event): void => {\n if (event.target === wrapper) userDismiss();\n };\n wrapper.addEventListener('click', onClick);\n removers.push(() => wrapper.removeEventListener('click', onClick));\n }\n\n if (triggers.includes('outside')) {\n const ignore = outsideIgnore === undefined\n ? []\n : Array.isArray(outsideIgnore) ? outsideIgnore : [outsideIgnore];\n // Capture phase: the click that opened this overlay already passed\n // document's capture phase, so this never fires for that opening click.\n const onDocClick = (event: Event): void => {\n const target = event.target as Node | null;\n if (target === null) return;\n if (wrapper.contains(target)) return;\n if (ignore.some((el) => el === target || el.contains(target))) return;\n userDismiss();\n };\n document.addEventListener('click', onDocClick, true);\n removers.push(() => document.removeEventListener('click', onDocClick, true));\n }\n\n if (initialFocus !== false) {\n if (typeof initialFocus === 'string') {\n wrapper.querySelector<HTMLElement>(initialFocus)?.focus();\n } else {\n const first = focusable(wrapper)[0];\n if (first !== undefined) {\n first.focus();\n } else {\n wrapper.tabIndex = -1;\n wrapper.focus();\n }\n }\n }\n\n return { el: wrapper, close, result };\n}\n\n/** Options for {@link confirm}. */\nexport interface ConfirmOptions {\n /** Where to append the overlay. Default `document.body`. */\n container?: Element;\n /** Wrapper class. Default `'kerf-overlay'`. */\n className?: string;\n /** Optional heading above the message. */\n title?: string;\n /** Confirm button label. Default `'OK'`. */\n okText?: string;\n /** Cancel button label. Default `'Cancel'`. */\n cancelText?: string;\n /** Add a `kerf-confirm--danger` class to the wrapper for destructive actions. */\n danger?: boolean;\n}\n\n/**\n * A promise-based `window.confirm` replacement (that global is a no-op in Tauri\n * webviews). Renders a two-button dialog and resolves `true` for OK, `false`\n * for Cancel or any dismissal (Escape / backdrop). Message + labels are\n * auto-escaped (rendered through the JSX runtime).\n */\nexport function confirm(message: string, options: ConfirmOptions = {}): Promise<boolean> {\n const {\n container,\n className = 'kerf-overlay',\n title,\n okText = 'OK',\n cancelText = 'Cancel',\n danger = false,\n } = options;\n\n const body: SafeHtml = jsx('div', {\n class: 'kerf-confirm',\n children: [\n title !== undefined ? jsx('h2', { class: 'kerf-confirm__title', children: title }) : '',\n jsx('p', { class: 'kerf-confirm__message', children: message }),\n jsx('div', {\n class: 'kerf-confirm__actions',\n children: [\n jsx('button', { type: 'button', 'data-confirm': 'cancel', children: cancelText }),\n jsx('button', {\n type: 'button',\n 'data-confirm': 'ok',\n class: 'kerf-confirm__ok',\n children: okText,\n }),\n ],\n }),\n ],\n });\n\n const handle = overlay(body, {\n container,\n className: danger ? `${className} kerf-confirm--danger` : className,\n dismiss: ['escape', 'backdrop'],\n initialFocus: '.kerf-confirm__ok',\n trap: true,\n });\n\n delegate(handle.el, 'click', '[data-confirm]', (_event, el) => {\n handle.close(el.getAttribute('data-confirm') === 'ok');\n });\n\n return handle.result.then((value) => value === true);\n}\n\n/**\n * Validate a single field's value. Return a non-empty error string to BLOCK\n * submission (shown inline next to the field); return `undefined`/`null`/`''` to\n * allow it.\n */\nexport type FieldValidator = (value: string) => string | null | undefined | void;\n\n/** Options for {@link prompt}. */\nexport interface PromptOptions {\n /** Where to append the overlay. Default `document.body`. */\n container?: Element;\n /** Wrapper class. Default `'kerf-overlay'`. */\n className?: string;\n /** Optional heading above the message. */\n title?: string;\n /** Pre-filled input value. Default `''`. */\n defaultValue?: string;\n /** Input placeholder. */\n placeholder?: string;\n /** `type` attribute of the input (`'text'`, `'email'`, `'password'`, …). Default `'text'`. */\n inputType?: string;\n /** Confirm button label. Default `'OK'`. */\n okText?: string;\n /** Cancel button label. Default `'Cancel'`. */\n cancelText?: string;\n /** Block OK while this returns an error string; the message shows inline. */\n validate?: FieldValidator;\n}\n\n/**\n * A promise-based `window.prompt` replacement (that global is a no-op in Tauri\n * webviews). Renders a one-field dialog and resolves the entered **string** on OK\n * (an empty string is a valid result) or `null` on Cancel / dismissal. Enter in\n * the input submits. `message`, the default value, and labels are auto-escaped\n * (rendered through the JSX runtime). Optional `validate` blocks OK inline.\n */\nexport function prompt(message: string, options: PromptOptions = {}): Promise<string | null> {\n const {\n container,\n className = 'kerf-overlay',\n title,\n defaultValue = '',\n placeholder,\n inputType = 'text',\n okText = 'OK',\n cancelText = 'Cancel',\n validate,\n } = options;\n\n const body: SafeHtml = jsx('div', {\n class: 'kerf-prompt',\n children: [\n title !== undefined ? jsx('h2', { class: 'kerf-prompt__title', children: title }) : '',\n jsx('label', { class: 'kerf-prompt__message', children: message }),\n jsx('input', {\n class: 'kerf-prompt__input',\n type: inputType,\n value: defaultValue,\n ...(placeholder !== undefined ? { placeholder } : {}),\n 'data-prompt-input': '',\n }),\n jsx('p', { class: 'kerf-prompt__error', 'data-prompt-error': '', children: '' }),\n jsx('div', {\n class: 'kerf-prompt__actions',\n children: [\n jsx('button', { type: 'button', 'data-prompt': 'cancel', children: cancelText }),\n jsx('button', {\n type: 'button',\n 'data-prompt': 'ok',\n class: 'kerf-prompt__ok',\n children: okText,\n }),\n ],\n }),\n ],\n });\n\n const handle = overlay(body, {\n container,\n className,\n dismiss: ['escape', 'backdrop'],\n initialFocus: '.kerf-prompt__input',\n trap: true,\n });\n\n // Both elements are rendered unconditionally into this call's own wrapper, so\n // the queries cannot miss (asserted non-null rather than guarded).\n const input = handle.el.querySelector<HTMLInputElement>('[data-prompt-input]')!;\n const errorEl = handle.el.querySelector<HTMLElement>('[data-prompt-error]')!;\n errorEl.hidden = true;\n\n function attemptOk(): void {\n const value = input.value;\n const error = validate?.(value);\n if (typeof error === 'string' && error.length > 0) {\n errorEl.textContent = error;\n errorEl.hidden = false;\n input.focus();\n return;\n }\n handle.close(value);\n }\n\n delegate(handle.el, 'click', '[data-prompt]', (_event, el) => {\n if (el.getAttribute('data-prompt') === 'ok') attemptOk();\n else handle.close(null);\n });\n\n // Enter in the field submits, like the native prompt.\n handle.el.addEventListener('keydown', (event: KeyboardEvent) => {\n if (event.key === 'Enter' && event.target === input) {\n event.preventDefault();\n attemptOk();\n }\n });\n\n return handle.result.then((value) => (typeof value === 'string' ? value : null));\n}\n\n/** A single field in a {@link form}. */\nexport interface FormField {\n /** Field name — the key in the resolved record (and the input's `name`). */\n name: string;\n /** Label shown above the input. Defaults to `name`. */\n label?: string;\n /** Pre-filled value. Default `''`. */\n defaultValue?: string;\n /** Input placeholder. */\n placeholder?: string;\n /** `type` attribute of the input. Default `'text'`. */\n type?: string;\n /** Block OK while this returns an error string; the message shows inline for this field. */\n validate?: FieldValidator;\n}\n\n/** Options for {@link form}. */\nexport interface FormOptions {\n /** Where to append the overlay. Default `document.body`. */\n container?: Element;\n /** Wrapper class. Default `'kerf-overlay'`. */\n className?: string;\n /** Optional heading above the fields. */\n title?: string;\n /** Confirm button label. Default `'OK'`. */\n okText?: string;\n /** Cancel button label. Default `'Cancel'`. */\n cancelText?: string;\n}\n\n/**\n * A promise-based multi-field dialog — the two-or-three-input sibling of\n * {@link prompt}. Renders one labeled input per {@link FormField} and resolves a\n * `Record<name, value>` on OK (after every field's `validate` passes) or `null`\n * on Cancel / dismissal. Enter in any field submits. All labels, defaults, and\n * the title are auto-escaped through the JSX runtime.\n */\nexport function form(\n fields: readonly FormField[],\n options: FormOptions = {},\n): Promise<Record<string, string> | null> {\n const { container, className = 'kerf-overlay', title, okText = 'OK', cancelText = 'Cancel' } = options;\n\n const body: SafeHtml = jsx('div', {\n class: 'kerf-form',\n children: [\n title !== undefined ? jsx('h2', { class: 'kerf-form__title', children: title }) : '',\n ...fields.map((field) =>\n jsx('div', {\n class: 'kerf-form__field',\n children: [\n jsx('label', { class: 'kerf-form__label', children: field.label ?? field.name }),\n jsx('input', {\n class: 'kerf-form__input',\n type: field.type ?? 'text',\n name: field.name,\n value: field.defaultValue ?? '',\n ...(field.placeholder !== undefined ? { placeholder: field.placeholder } : {}),\n 'data-field': field.name,\n }),\n jsx('p', {\n class: 'kerf-form__error',\n 'data-field-error': field.name,\n children: '',\n }),\n ],\n }),\n ),\n jsx('div', {\n class: 'kerf-form__actions',\n children: [\n jsx('button', { type: 'button', 'data-form': 'cancel', children: cancelText }),\n jsx('button', {\n type: 'button',\n 'data-form': 'ok',\n class: 'kerf-form__ok',\n children: okText,\n }),\n ],\n }),\n ],\n });\n\n const handle = overlay(body, {\n container,\n className,\n dismiss: ['escape', 'backdrop'],\n initialFocus: '.kerf-form__input',\n trap: true,\n });\n\n // Look up a field's input / error node by attribute value (no selector\n // escaping needed — field names are developer-supplied identifiers). Every\n // field renders both nodes into this wrapper, so the lookup cannot miss.\n const byAttr = <E extends HTMLElement>(attr: string, name: string): E =>\n Array.from(handle.el.querySelectorAll<E>(`[${attr}]`)).find(\n (el) => el.getAttribute(attr) === name,\n )!;\n\n // Start with every field's error hidden.\n for (const field of fields) {\n byAttr<HTMLElement>('data-field-error', field.name).hidden = true;\n }\n\n function attemptOk(): void {\n const record: Record<string, string> = {};\n let firstInvalid: HTMLInputElement | null = null;\n for (const field of fields) {\n const el = byAttr<HTMLInputElement>('data-field', field.name);\n const value = el.value;\n record[field.name] = value;\n const error = field.validate?.(value);\n const errorEl = byAttr<HTMLElement>('data-field-error', field.name);\n if (typeof error === 'string' && error.length > 0) {\n errorEl.textContent = error;\n errorEl.hidden = false;\n if (firstInvalid === null) firstInvalid = el;\n } else {\n errorEl.hidden = true;\n }\n }\n if (firstInvalid !== null) {\n firstInvalid.focus();\n return;\n }\n handle.close(record);\n }\n\n delegate(handle.el, 'click', '[data-form]', (_event, el) => {\n if (el.getAttribute('data-form') === 'ok') attemptOk();\n else handle.close(null);\n });\n\n handle.el.addEventListener('keydown', (event: KeyboardEvent) => {\n if (event.key === 'Enter' && (event.target as Element | null)?.matches('[data-field]')) {\n event.preventDefault();\n attemptOk();\n }\n });\n\n return handle.result.then((value) =>\n value !== null && typeof value === 'object' ? (value as Record<string, string>) : null,\n );\n}\n\n/** Vertical placement of a {@link popover} relative to its anchor. */\nexport type PopoverPlacement = 'bottom' | 'top';\n\n/** Options for {@link popover}. */\nexport interface PopoverOptions {\n /** Where to append the popover wrapper. Default `document.body`. */\n container?: Element;\n /** Class on the wrapper. Default `'kerf-popover'`. */\n className?: string;\n /** Preferred side of the anchor. Flips to the other side if it would overflow the viewport. Default `'bottom'`. */\n placement?: PopoverPlacement;\n /** Horizontal edge to line up with the anchor: `'start'` (left edges) or `'end'` (right edges). Default `'start'`. */\n align?: 'start' | 'end';\n /** Gap in px between the anchor and the popover. Default `4`. */\n gap?: number;\n /**\n * Which user actions dismiss the popover. Default `['outside']` (a click\n * outside the popover, the anchor exempt). Pass `false` to close only via `close()`.\n */\n dismiss?: DismissTrigger | DismissTrigger[] | false;\n /** Focus behavior on open. Default `false` (non-modal — leave focus alone). */\n initialFocus?: string | boolean;\n /** Extra elements (besides the anchor) whose clicks do NOT count as outside. */\n outsideIgnore?: Element | readonly Element[];\n /** Called on any user-initiated dismissal. */\n onDismiss?: () => void;\n}\n\n/**\n * Anchored, non-modal overlay: positions `content` relative to `anchor` (below by\n * default, flipping above if it would overflow, and clamped horizontally to the\n * viewport) and repositions on scroll / resize while open. A thin wrapper over\n * {@link overlay} with non-modal defaults — `trap: false`, `dismiss: ['outside']`,\n * and the anchor added to `outsideIgnore` so the trigger click doesn't self-close.\n * Returns the same {@link OverlayHandle}; `close()` also drops the reposition\n * listeners. `position: fixed` is set inline (you style everything else).\n */\nexport function popover(\n anchor: Element,\n content: OverlayContent,\n options: PopoverOptions = {},\n): OverlayHandle {\n const {\n container,\n className = 'kerf-popover',\n placement = 'bottom',\n align = 'start',\n gap = 4,\n dismiss = ['outside'],\n initialFocus = false,\n outsideIgnore,\n onDismiss,\n } = options;\n\n const extraIgnore = outsideIgnore === undefined\n ? []\n : Array.isArray(outsideIgnore) ? [...outsideIgnore] : [outsideIgnore];\n\n const handle = overlay(content, {\n container,\n className,\n dismiss,\n trap: false,\n initialFocus,\n onDismiss,\n outsideIgnore: [anchor, ...extraIgnore],\n });\n\n handle.el.style.position = 'fixed';\n handle.el.style.margin = '0';\n\n const reposition = (): void => {\n const a = anchor.getBoundingClientRect();\n const p = handle.el.getBoundingClientRect();\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n // Vertical: preferred side, flipped only if it overflows and the other side fits.\n const belowTop = a.bottom + gap;\n const aboveTop = a.top - gap - p.height;\n let below = placement !== 'top';\n if (below && belowTop + p.height > vh && aboveTop >= 0) below = false;\n else if (!below && aboveTop < 0 && belowTop + p.height <= vh) below = true;\n\n // Horizontal: align to an anchor edge, then clamp into the viewport.\n let left = align === 'end' ? a.right - p.width : a.left;\n left = Math.max(0, Math.min(left, vw - p.width));\n\n handle.el.style.left = `${left}px`;\n handle.el.style.top = `${below ? belowTop : aboveTop}px`;\n };\n\n reposition();\n window.addEventListener('scroll', reposition, true); // capture: catch scrolls in any container\n window.addEventListener('resize', reposition);\n void handle.result.then(() => {\n window.removeEventListener('scroll', reposition, true);\n window.removeEventListener('resize', reposition);\n });\n\n return handle;\n}\n\n/** Content for a {@link toast}: text, `SafeHtml`, or a render function. */\nexport type ToastContent = string | SafeHtml | (() => MountResult);\n\n/** Options for {@link toast}. */\nexport interface ToastOptions {\n /** Where toasts stack. Default: a lazily-created `<div class=\"kerf-toasts\">` on `document.body`. */\n container?: Element;\n /** Class on the toast element. Default `'kerf-toast'`. */\n className?: string;\n /** Auto-dismiss after this many ms. `0` keeps it until dismissed by hand. Default `4000`. */\n duration?: number;\n /** ARIA role. Default `'status'`. */\n role?: string;\n}\n\n/** The singleton toast region lives in the DOM (queried, not held in a module variable). */\nfunction toastRegion(container?: Element): Element {\n if (container !== undefined) return container;\n const existing = document.querySelector('.kerf-toasts');\n if (existing !== null) return existing;\n const region = document.createElement('div');\n region.className = 'kerf-toasts';\n region.setAttribute('aria-live', 'polite');\n document.body.appendChild(region);\n return region;\n}\n\n/**\n * Show a non-modal, auto-dismissing notification. Stacks in a shared body-level\n * region (or your `container`). Returns a `() => void` that dismisses it early.\n */\nexport function toast(content: ToastContent, options: ToastOptions = {}): () => void {\n const { container, className = 'kerf-toast', duration = 4000, role = 'status' } = options;\n\n const el = document.createElement('div');\n el.className = className;\n el.setAttribute('role', role);\n toastRegion(container).appendChild(el);\n\n const disposeMount = mount(el, typeof content === 'function' ? content : () => content);\n const state: { dismissed: boolean; timer: ReturnType<typeof setTimeout> | undefined } = {\n dismissed: false,\n timer: undefined,\n };\n\n function dismiss(): void {\n if (state.dismissed) return;\n state.dismissed = true;\n if (state.timer !== undefined) clearTimeout(state.timer);\n disposeMount();\n el.remove();\n }\n\n if (duration > 0) state.timer = setTimeout(dismiss, duration);\n return dismiss;\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { M as MountResult } from './mount-Bo2qOx25.js';
|
|
2
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
3
|
+
import './jsx-runtime.js';
|
|
4
|
+
import './bindings-CYwoJpQb.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `kerfjs/remount` — force a subtree to be REPLACED, not morphed, when a key changes.
|
|
8
|
+
*
|
|
9
|
+
* kerf morphs by default, which is almost always right. The exception is a
|
|
10
|
+
* library-owned subtree (a highlighted diff, a chart, an editor) that must be
|
|
11
|
+
* torn down and rebuilt on fresh DOM when its identity changes — so the library
|
|
12
|
+
* re-initializes instead of the morph patching stale internals underneath it.
|
|
13
|
+
* The folk pattern is a monotonic counter spent as `data-key={`gen-${n}`}` on a
|
|
14
|
+
* `data-morph-skip` div; `remountOn` names that pattern.
|
|
15
|
+
*
|
|
16
|
+
* import { remountOn } from 'kerfjs/remount';
|
|
17
|
+
*
|
|
18
|
+
* // Replace the diff pane whenever the file (or diff mode) changes:
|
|
19
|
+
* const stop = remountOn(paneEl, () => fileId.value, () => <DiffView id={fileId.value} />);
|
|
20
|
+
* // same key -> the subtree is left entirely alone
|
|
21
|
+
* // key change -> old subtree + its mounts disposed, a fresh one mounted
|
|
22
|
+
*
|
|
23
|
+
* `remountOn` owns `parent`'s children (like `mount()` / `bindList`). It returns
|
|
24
|
+
* a disposer that tears down the current subtree and stops watching the key.
|
|
25
|
+
* Pairs with `kerfjs/imperative`: put the widget's setup/teardown on the fresh
|
|
26
|
+
* node, and `remountOn` drives its re-creation.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The key that drives a {@link remountOn}: a signal, or a thunk that reads signals. */
|
|
30
|
+
type RemountKey<K> = ReadonlySignal<K> | (() => K);
|
|
31
|
+
/** Options for {@link remountOn}. */
|
|
32
|
+
interface RemountOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Called after each (re)mount with `parent` — the live, freshly-rendered
|
|
35
|
+
* subtree. This is where you bind a widget to the new DOM (e.g.
|
|
36
|
+
* `imperative(parent.querySelector('.host'), setup)` from `kerfjs/imperative`),
|
|
37
|
+
* because `render` returns a string and has no live node yet. May return a
|
|
38
|
+
* cleanup `() => void` that runs before the NEXT remount and on dispose — return
|
|
39
|
+
* the disposer from `imperative()` here for synchronous teardown.
|
|
40
|
+
*/
|
|
41
|
+
onMount?: (root: HTMLElement) => (() => void) | void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Watch `key` and, whenever it changes (by `Object.is`), dispose the current
|
|
45
|
+
* subtree + its mounts and render a fresh one into `parent` via `mount(render)`.
|
|
46
|
+
* An unchanged key leaves the subtree untouched. `options.onMount(parent)` runs
|
|
47
|
+
* after each (re)mount to bind widgets to the fresh DOM. Returns a disposer that
|
|
48
|
+
* tears down the current subtree and stops watching.
|
|
49
|
+
*/
|
|
50
|
+
declare function remountOn<K>(parent: HTMLElement, key: RemountKey<K>, render: () => MountResult, options?: RemountOptions): () => void;
|
|
51
|
+
|
|
52
|
+
export { type RemountKey, type RemountOptions, remountOn };
|
package/dist/remount.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mount } from './chunk-4MY2656S.js';
|
|
2
|
+
import './chunk-QIP723L4.js';
|
|
3
|
+
import './chunk-YHH7OUFA.js';
|
|
4
|
+
import './chunk-FSAQR6IU.js';
|
|
5
|
+
import { effect } from './chunk-3APBEVHF.js';
|
|
6
|
+
import './chunk-GY4XV2UV.js';
|
|
7
|
+
import './chunk-VVDJLWMP.js';
|
|
8
|
+
|
|
9
|
+
// src/remount.ts
|
|
10
|
+
var UNSET = /* @__PURE__ */ Symbol("kerf.remount.unset");
|
|
11
|
+
function remountOn(parent, key, render, options = {}) {
|
|
12
|
+
const { onMount } = options;
|
|
13
|
+
const readKey = typeof key === "function" ? key : () => key.value;
|
|
14
|
+
let currentKey = UNSET;
|
|
15
|
+
let disposeMount;
|
|
16
|
+
let onMountCleanup;
|
|
17
|
+
function tearDown() {
|
|
18
|
+
if (onMountCleanup !== void 0) {
|
|
19
|
+
onMountCleanup();
|
|
20
|
+
onMountCleanup = void 0;
|
|
21
|
+
}
|
|
22
|
+
if (disposeMount !== void 0) {
|
|
23
|
+
disposeMount();
|
|
24
|
+
disposeMount = void 0;
|
|
25
|
+
}
|
|
26
|
+
parent.replaceChildren();
|
|
27
|
+
}
|
|
28
|
+
const stopWatch = effect(() => {
|
|
29
|
+
const next = readKey();
|
|
30
|
+
if (!Object.is(next, currentKey)) {
|
|
31
|
+
currentKey = next;
|
|
32
|
+
tearDown();
|
|
33
|
+
disposeMount = mount(parent, render);
|
|
34
|
+
onMountCleanup = onMount?.(parent) ?? void 0;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
stopWatch();
|
|
39
|
+
tearDown();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { remountOn };
|
|
44
|
+
//# sourceMappingURL=remount.js.map
|
|
45
|
+
//# sourceMappingURL=remount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/remount.ts"],"names":[],"mappings":";;;;;;;;;AA0CA,IAAM,KAAA,0BAAe,oBAAoB,CAAA;AASlC,SAAS,UACd,MAAA,EACA,GAAA,EACA,MAAA,EACA,OAAA,GAA0B,EAAC,EACf;AACZ,EAAA,MAAM,EAAE,SAAQ,GAAI,OAAA;AACpB,EAAA,MAAM,UAAU,OAAO,GAAA,KAAQ,UAAA,GAAa,GAAA,GAAM,MAAS,GAAA,CAAI,KAAA;AAC/D,EAAA,IAAI,UAAA,GAA+B,KAAA;AACnC,EAAA,IAAI,YAAA;AACJ,EAAA,IAAI,cAAA;AAEJ,EAAA,SAAS,QAAA,GAAiB;AAIxB,IAAA,IAAI,mBAAmB,MAAA,EAAW;AAChC,MAAA,cAAA,EAAe;AACf,MAAA,cAAA,GAAiB,MAAA;AAAA,IACnB;AACA,IAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,MAAA,YAAA,EAAa;AACb,MAAA,YAAA,GAAe,MAAA;AAAA,IACjB;AAGA,IAAA,MAAA,CAAO,eAAA,EAAgB;AAAA,EACzB;AAKA,EAAA,MAAM,SAAA,GAAY,OAAO,MAAM;AAC7B,IAAA,MAAM,OAAO,OAAA,EAAQ;AACrB,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,IAAA,EAAM,UAAU,CAAA,EAAG;AAChC,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,QAAA,EAAS;AACT,MAAA,YAAA,GAAe,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnC,MAAA,cAAA,GAAiB,OAAA,GAAU,MAAM,CAAA,IAAK,MAAA;AAAA,IACxC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,EAAU;AACV,IAAA,QAAA,EAAS;AAAA,EACX,CAAA;AACF","file":"remount.js","sourcesContent":["/**\n * `kerfjs/remount` — force a subtree to be REPLACED, not morphed, when a key changes.\n *\n * kerf morphs by default, which is almost always right. The exception is a\n * library-owned subtree (a highlighted diff, a chart, an editor) that must be\n * torn down and rebuilt on fresh DOM when its identity changes — so the library\n * re-initializes instead of the morph patching stale internals underneath it.\n * The folk pattern is a monotonic counter spent as `data-key={`gen-${n}`}` on a\n * `data-morph-skip` div; `remountOn` names that pattern.\n *\n * import { remountOn } from 'kerfjs/remount';\n *\n * // Replace the diff pane whenever the file (or diff mode) changes:\n * const stop = remountOn(paneEl, () => fileId.value, () => <DiffView id={fileId.value} />);\n * // same key -> the subtree is left entirely alone\n * // key change -> old subtree + its mounts disposed, a fresh one mounted\n *\n * `remountOn` owns `parent`'s children (like `mount()` / `bindList`). It returns\n * a disposer that tears down the current subtree and stops watching the key.\n * Pairs with `kerfjs/imperative`: put the widget's setup/teardown on the fresh\n * node, and `remountOn` drives its re-creation.\n */\nimport { mount, type MountResult } from './mount.js';\nimport { effect, type ReadonlySignal } from './reactive.js';\n\n/** The key that drives a {@link remountOn}: a signal, or a thunk that reads signals. */\nexport type RemountKey<K> = ReadonlySignal<K> | (() => K);\n\n/** Options for {@link remountOn}. */\nexport interface RemountOptions {\n /**\n * Called after each (re)mount with `parent` — the live, freshly-rendered\n * subtree. This is where you bind a widget to the new DOM (e.g.\n * `imperative(parent.querySelector('.host'), setup)` from `kerfjs/imperative`),\n * because `render` returns a string and has no live node yet. May return a\n * cleanup `() => void` that runs before the NEXT remount and on dispose — return\n * the disposer from `imperative()` here for synchronous teardown.\n */\n onMount?: (root: HTMLElement) => (() => void) | void;\n}\n\n/** Distinguishes \"no key seen yet\" from any real key (including `undefined`). */\nconst UNSET = Symbol('kerf.remount.unset');\n\n/**\n * Watch `key` and, whenever it changes (by `Object.is`), dispose the current\n * subtree + its mounts and render a fresh one into `parent` via `mount(render)`.\n * An unchanged key leaves the subtree untouched. `options.onMount(parent)` runs\n * after each (re)mount to bind widgets to the fresh DOM. Returns a disposer that\n * tears down the current subtree and stops watching.\n */\nexport function remountOn<K>(\n parent: HTMLElement,\n key: RemountKey<K>,\n render: () => MountResult,\n options: RemountOptions = {},\n): () => void {\n const { onMount } = options;\n const readKey = typeof key === 'function' ? key : (): K => key.value;\n let currentKey: K | typeof UNSET = UNSET;\n let disposeMount: (() => void) | undefined;\n let onMountCleanup: (() => void) | undefined;\n\n function tearDown(): void {\n // Run the onMount cleanup BEFORE tearing down the DOM, so a synchronous\n // teardown (e.g. an imperative() disposer returned from onMount) fires while\n // its node is still attached.\n if (onMountCleanup !== undefined) {\n onMountCleanup();\n onMountCleanup = undefined;\n }\n if (disposeMount !== undefined) {\n disposeMount();\n disposeMount = undefined;\n }\n // Owning parent's children: clear whatever the old mount left so widgets\n // under it see a real removal (their MutationObserver teardown fires).\n parent.replaceChildren();\n }\n\n // The outer effect tracks ONLY the key. `mount()` starts its own independent\n // effect for `render`, so render's signal reads attach there, not here — the\n // key is the sole dependency that triggers a remount.\n const stopWatch = effect(() => {\n const next = readKey();\n if (!Object.is(next, currentKey)) {\n currentKey = next;\n tearDown();\n disposeMount = mount(parent, render);\n onMountCleanup = onMount?.(parent) ?? undefined;\n }\n });\n\n return () => {\n stopWatch();\n tearDown();\n };\n}\n"]}
|
package/dist/scope.js
CHANGED
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `kerfjs/timing` — the small timing primitives every app hand-rolls.
|
|
5
|
+
*
|
|
6
|
+
* kerf already replaced most imperative bookkeeping — `delegate` for listeners,
|
|
7
|
+
* `mount`/`effect` for render, `defineStore` for state — but debouncing and
|
|
8
|
+
* throttling still get written by hand as `let timer; clearTimeout(timer);
|
|
9
|
+
* timer = setTimeout(fn, ms)`. This subpath blesses that with disposer-shaped
|
|
10
|
+
* ergonomics (`.cancel()` / `.flush()`), plus `debouncedSignal` so a trailing
|
|
11
|
+
* value composes inside the reactive graph instead of beside it.
|
|
12
|
+
*
|
|
13
|
+
* import { debounce, throttle, debouncedSignal } from 'kerfjs/timing';
|
|
14
|
+
*
|
|
15
|
+
* const save = debounce(() => persist(state), 300);
|
|
16
|
+
* input.addEventListener('input', save); // save.cancel() on teardown
|
|
17
|
+
*
|
|
18
|
+
* const query = signal('');
|
|
19
|
+
* const debouncedQuery = debouncedSignal(query, 250); // trails query by 250ms
|
|
20
|
+
*
|
|
21
|
+
* Tree-shakeable and tiny — `debounce`/`throttle` are dependency-free; only
|
|
22
|
+
* `debouncedSignal` pulls in signals (no render core).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A debounced function: call it like the original, plus `cancel()` / `flush()`. */
|
|
26
|
+
interface Debounced<A extends unknown[]> {
|
|
27
|
+
(...args: A): void;
|
|
28
|
+
/** Drop any pending trailing call without invoking it. */
|
|
29
|
+
cancel(): void;
|
|
30
|
+
/** Invoke the pending trailing call now (if any) and clear the timer. */
|
|
31
|
+
flush(): void;
|
|
32
|
+
}
|
|
33
|
+
/** A throttled function: call it like the original, plus `cancel()` / `flush()`. */
|
|
34
|
+
interface Throttled<A extends unknown[]> {
|
|
35
|
+
(...args: A): void;
|
|
36
|
+
/** Drop any pending trailing call and reset the rate window. */
|
|
37
|
+
cancel(): void;
|
|
38
|
+
/** Invoke the pending trailing call now (if any). */
|
|
39
|
+
flush(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Trailing-edge debounce: `fn` runs `ms` after calls STOP, with the most recent
|
|
43
|
+
* arguments. Every call within the quiet window resets the timer. `cancel()`
|
|
44
|
+
* drops a pending call; `flush()` runs it immediately.
|
|
45
|
+
*/
|
|
46
|
+
declare function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A>;
|
|
47
|
+
/**
|
|
48
|
+
* Leading-plus-trailing throttle: `fn` runs immediately on the first call, then
|
|
49
|
+
* at most once per `ms`. Calls during a cooldown collapse to a single trailing
|
|
50
|
+
* call at the window's end (with the latest arguments). `cancel()` drops a
|
|
51
|
+
* pending trailing call and resets the window; `flush()` runs it now.
|
|
52
|
+
*/
|
|
53
|
+
declare function throttle<A extends unknown[]>(fn: (...args: A) => void, ms: number): Throttled<A>;
|
|
54
|
+
/**
|
|
55
|
+
* A read-only signal that trails `source` by `ms` (trailing-edge). Writes to
|
|
56
|
+
* `source` reschedule; the derived value updates once writes go quiet, so it
|
|
57
|
+
* composes with `computed()`/`effect()`/`mount()` like any signal.
|
|
58
|
+
*
|
|
59
|
+
* Holds a live subscription to `source` for its lifetime (like a module-scope
|
|
60
|
+
* `effect`) — intended for app-lifetime signals, not throwaway ones. For a
|
|
61
|
+
* disposable variant, drive your own `effect` with {@link debounce}.
|
|
62
|
+
*/
|
|
63
|
+
declare function debouncedSignal<T>(source: ReadonlySignal<T>, ms: number): ReadonlySignal<T>;
|
|
64
|
+
|
|
65
|
+
export { type Debounced, type Throttled, debounce, debouncedSignal, throttle };
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { signal, effect } from './chunk-3APBEVHF.js';
|
|
2
|
+
import './chunk-VVDJLWMP.js';
|
|
3
|
+
|
|
4
|
+
// src/timing.ts
|
|
5
|
+
function debounce(fn, ms) {
|
|
6
|
+
let timer;
|
|
7
|
+
let lastArgs;
|
|
8
|
+
const invoke = () => {
|
|
9
|
+
timer = void 0;
|
|
10
|
+
const args = lastArgs;
|
|
11
|
+
lastArgs = void 0;
|
|
12
|
+
fn(...args);
|
|
13
|
+
};
|
|
14
|
+
const debounced = ((...args) => {
|
|
15
|
+
lastArgs = args;
|
|
16
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
17
|
+
timer = setTimeout(invoke, ms);
|
|
18
|
+
});
|
|
19
|
+
debounced.cancel = () => {
|
|
20
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
21
|
+
timer = void 0;
|
|
22
|
+
lastArgs = void 0;
|
|
23
|
+
};
|
|
24
|
+
debounced.flush = () => {
|
|
25
|
+
if (timer !== void 0) {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
invoke();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return debounced;
|
|
31
|
+
}
|
|
32
|
+
function throttle(fn, ms) {
|
|
33
|
+
let timer;
|
|
34
|
+
let trailingArgs;
|
|
35
|
+
const runTrailing = () => {
|
|
36
|
+
const args = trailingArgs;
|
|
37
|
+
trailingArgs = void 0;
|
|
38
|
+
fn(...args);
|
|
39
|
+
};
|
|
40
|
+
const startCooldown = () => {
|
|
41
|
+
timer = setTimeout(() => {
|
|
42
|
+
timer = void 0;
|
|
43
|
+
if (trailingArgs !== void 0) {
|
|
44
|
+
runTrailing();
|
|
45
|
+
startCooldown();
|
|
46
|
+
}
|
|
47
|
+
}, ms);
|
|
48
|
+
};
|
|
49
|
+
const throttled = ((...args) => {
|
|
50
|
+
if (timer === void 0) {
|
|
51
|
+
fn(...args);
|
|
52
|
+
startCooldown();
|
|
53
|
+
} else {
|
|
54
|
+
trailingArgs = args;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
throttled.cancel = () => {
|
|
58
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
59
|
+
timer = void 0;
|
|
60
|
+
trailingArgs = void 0;
|
|
61
|
+
};
|
|
62
|
+
throttled.flush = () => {
|
|
63
|
+
if (trailingArgs !== void 0) runTrailing();
|
|
64
|
+
};
|
|
65
|
+
return throttled;
|
|
66
|
+
}
|
|
67
|
+
function debouncedSignal(source, ms) {
|
|
68
|
+
const out = signal(source.value);
|
|
69
|
+
const write = debounce((value) => {
|
|
70
|
+
out.value = value;
|
|
71
|
+
}, ms);
|
|
72
|
+
effect(() => {
|
|
73
|
+
write(source.value);
|
|
74
|
+
});
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { debounce, debouncedSignal, throttle };
|
|
79
|
+
//# sourceMappingURL=timing.js.map
|
|
80
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/timing.ts"],"names":[],"mappings":";;;;AA8CO,SAAS,QAAA,CAA8B,IAA0B,EAAA,EAA0B;AAChG,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,MAAM,IAAA,GAAO,QAAA;AACb,IAAA,QAAA,GAAW,MAAA;AACX,IAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,SAAA,IAAa,IAAI,IAAA,KAAkB;AACvC,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,UAAA,CAAW,QAAQ,EAAE,CAAA;AAAA,EAC/B,CAAA,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,QAAA,GAAW,MAAA;AAAA,EACb,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAC5B,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,MAAA,EAAO;AAAA,IACT;AAAA,EACF,CAAA;AAEA,EAAA,OAAO,SAAA;AACT;AAQO,SAAS,QAAA,CAA8B,IAA0B,EAAA,EAA0B;AAChG,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,YAAA;AAEJ,EAAA,MAAM,cAAc,MAAY;AAC9B,IAAA,MAAM,IAAA,GAAO,YAAA;AACb,IAAA,YAAA,GAAe,MAAA;AACf,IAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,gBAAgB,MAAY;AAChC,IAAA,KAAA,GAAQ,WAAW,MAAM;AACvB,MAAA,KAAA,GAAQ,MAAA;AACR,MAAA,IAAI,iBAAiB,MAAA,EAAW;AAC9B,QAAA,WAAA,EAAY;AACZ,QAAA,aAAA,EAAc;AAAA,MAChB;AAAA,IACF,GAAG,EAAE,CAAA;AAAA,EACP,CAAA;AAEA,EAAA,MAAM,SAAA,IAAa,IAAI,IAAA,KAAkB;AACvC,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,EAAA,CAAG,GAAG,IAAI,CAAA;AACV,MAAA,aAAA,EAAc;AAAA,IAChB,CAAA,MAAO;AACL,MAAA,YAAA,GAAe,IAAA;AAAA,IACjB;AAAA,EACF,CAAA,CAAA;AAEA,EAAA,SAAA,CAAU,SAAS,MAAY;AAC7B,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAK,CAAA;AAC3C,IAAA,KAAA,GAAQ,MAAA;AACR,IAAA,YAAA,GAAe,MAAA;AAAA,EACjB,CAAA;AAEA,EAAA,SAAA,CAAU,QAAQ,MAAY;AAC5B,IAAA,IAAI,YAAA,KAAiB,QAAW,WAAA,EAAY;AAAA,EAC9C,CAAA;AAEA,EAAA,OAAO,SAAA;AACT;AAWO,SAAS,eAAA,CAAmB,QAA2B,EAAA,EAA+B;AAC3F,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA;AAC/B,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,CAAC,KAAA,KAAa;AACnC,IAAA,GAAA,CAAI,KAAA,GAAQ,KAAA;AAAA,EACd,GAAG,EAAE,CAAA;AACL,EAAA,MAAA,CAAO,MAAM;AACX,IAAA,KAAA,CAAM,OAAO,KAAK,CAAA;AAAA,EACpB,CAAC,CAAA;AACD,EAAA,OAAO,GAAA;AACT","file":"timing.js","sourcesContent":["/**\n * `kerfjs/timing` — the small timing primitives every app hand-rolls.\n *\n * kerf already replaced most imperative bookkeeping — `delegate` for listeners,\n * `mount`/`effect` for render, `defineStore` for state — but debouncing and\n * throttling still get written by hand as `let timer; clearTimeout(timer);\n * timer = setTimeout(fn, ms)`. This subpath blesses that with disposer-shaped\n * ergonomics (`.cancel()` / `.flush()`), plus `debouncedSignal` so a trailing\n * value composes inside the reactive graph instead of beside it.\n *\n * import { debounce, throttle, debouncedSignal } from 'kerfjs/timing';\n *\n * const save = debounce(() => persist(state), 300);\n * input.addEventListener('input', save); // save.cancel() on teardown\n *\n * const query = signal('');\n * const debouncedQuery = debouncedSignal(query, 250); // trails query by 250ms\n *\n * Tree-shakeable and tiny — `debounce`/`throttle` are dependency-free; only\n * `debouncedSignal` pulls in signals (no render core).\n */\nimport { effect, type ReadonlySignal, signal } from './reactive.js';\n\n/** A debounced function: call it like the original, plus `cancel()` / `flush()`. */\nexport interface Debounced<A extends unknown[]> {\n (...args: A): void;\n /** Drop any pending trailing call without invoking it. */\n cancel(): void;\n /** Invoke the pending trailing call now (if any) and clear the timer. */\n flush(): void;\n}\n\n/** A throttled function: call it like the original, plus `cancel()` / `flush()`. */\nexport interface Throttled<A extends unknown[]> {\n (...args: A): void;\n /** Drop any pending trailing call and reset the rate window. */\n cancel(): void;\n /** Invoke the pending trailing call now (if any). */\n flush(): void;\n}\n\n/**\n * Trailing-edge debounce: `fn` runs `ms` after calls STOP, with the most recent\n * arguments. Every call within the quiet window resets the timer. `cancel()`\n * drops a pending call; `flush()` runs it immediately.\n */\nexport function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): Debounced<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let lastArgs: A | undefined;\n\n const invoke = (): void => {\n timer = undefined;\n const args = lastArgs as A;\n lastArgs = undefined;\n fn(...args);\n };\n\n const debounced = ((...args: A): void => {\n lastArgs = args;\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(invoke, ms);\n }) as Debounced<A>;\n\n debounced.cancel = (): void => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n lastArgs = undefined;\n };\n\n debounced.flush = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n invoke();\n }\n };\n\n return debounced;\n}\n\n/**\n * Leading-plus-trailing throttle: `fn` runs immediately on the first call, then\n * at most once per `ms`. Calls during a cooldown collapse to a single trailing\n * call at the window's end (with the latest arguments). `cancel()` drops a\n * pending trailing call and resets the window; `flush()` runs it now.\n */\nexport function throttle<A extends unknown[]>(fn: (...args: A) => void, ms: number): Throttled<A> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let trailingArgs: A | undefined;\n\n const runTrailing = (): void => {\n const args = trailingArgs as A;\n trailingArgs = undefined;\n fn(...args);\n };\n\n const startCooldown = (): void => {\n timer = setTimeout(() => {\n timer = undefined;\n if (trailingArgs !== undefined) {\n runTrailing();\n startCooldown(); // hold the rate limit for a beat after a trailing call\n }\n }, ms);\n };\n\n const throttled = ((...args: A): void => {\n if (timer === undefined) {\n fn(...args); // leading edge\n startCooldown();\n } else {\n trailingArgs = args; // collapse into one trailing call\n }\n }) as Throttled<A>;\n\n throttled.cancel = (): void => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n trailingArgs = undefined;\n };\n\n throttled.flush = (): void => {\n if (trailingArgs !== undefined) runTrailing();\n };\n\n return throttled;\n}\n\n/**\n * A read-only signal that trails `source` by `ms` (trailing-edge). Writes to\n * `source` reschedule; the derived value updates once writes go quiet, so it\n * composes with `computed()`/`effect()`/`mount()` like any signal.\n *\n * Holds a live subscription to `source` for its lifetime (like a module-scope\n * `effect`) — intended for app-lifetime signals, not throwaway ones. For a\n * disposable variant, drive your own `effect` with {@link debounce}.\n */\nexport function debouncedSignal<T>(source: ReadonlySignal<T>, ms: number): ReadonlySignal<T> {\n const out = signal(source.value);\n const write = debounce((value: T) => {\n out.value = value;\n }, ms);\n effect(() => {\n write(source.value); // tracks source; reschedules on every change\n });\n return out;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kerfjs",
|
|
3
|
-
"version": "4.2.0-beta.
|
|
3
|
+
"version": "4.2.0-beta.2",
|
|
4
4
|
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -84,6 +84,18 @@
|
|
|
84
84
|
"types": "./dist/list.d.ts",
|
|
85
85
|
"import": "./dist/list.js"
|
|
86
86
|
},
|
|
87
|
+
"./timing": {
|
|
88
|
+
"types": "./dist/timing.d.ts",
|
|
89
|
+
"import": "./dist/timing.js"
|
|
90
|
+
},
|
|
91
|
+
"./remount": {
|
|
92
|
+
"types": "./dist/remount.d.ts",
|
|
93
|
+
"import": "./dist/remount.js"
|
|
94
|
+
},
|
|
95
|
+
"./imperative": {
|
|
96
|
+
"types": "./dist/imperative.d.ts",
|
|
97
|
+
"import": "./dist/imperative.js"
|
|
98
|
+
},
|
|
87
99
|
"./ai/*": "./ai/*"
|
|
88
100
|
},
|
|
89
101
|
"files": [
|