kerfjs 5.0.0-beta.69 → 5.0.0-beta.70
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 +33 -0
- package/ai/manifest.json +1 -1
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
- **LucideIcon supports filled glyphs.** Pass `appearance="solid"` to use the
|
|
10
|
+
icon's current text color as its fill; outline remains the default.
|
|
11
|
+
|
|
12
|
+
- **Doctor detects borrowed component markup.** Source modules that place a
|
|
13
|
+
foreign public class in JSX, raw HTML, or DOM class writes now receive
|
|
14
|
+
`KUI-L023`; documented placeable classes remain allowed.
|
|
15
|
+
|
|
16
|
+
- **Doctor offers strict same-package selector context checks.**
|
|
17
|
+
`ownershipContext: "any"` reports a sibling component's class even when it is
|
|
18
|
+
only used as selector context; cross-package composition stays available.
|
|
19
|
+
|
|
20
|
+
- **Doctor catches classless descendants of composed components.** Component
|
|
21
|
+
mode uses JSX evidence to identify element selectors below component hooks,
|
|
22
|
+
sibling roots, and direct composed child roots.
|
|
23
|
+
|
|
24
|
+
- **Doctor component ownership recognizes source evidence.** Selection-only
|
|
25
|
+
components and literal BEM classes rendered by a component are protected
|
|
26
|
+
when its stylesheet styles their block. CSS imports can supply the owned
|
|
27
|
+
stylesheet, and uncataloged modules can opt into the same inference.
|
|
28
|
+
|
|
29
|
+
- **`createScope().dispose` is typed as a bound function property.** Consumers
|
|
30
|
+
can store or return it as a teardown without a wrapper or an unbound-method
|
|
31
|
+
lint suppression.
|
|
32
|
+
|
|
33
|
+
- **ToolbarControlGroup grow sizing keeps its basis in wide toolbars.** An
|
|
34
|
+
expanded grow search now contributes its minimum width to the trailing zone's
|
|
35
|
+
intrinsic size, keeping sibling groups beside it instead of wrapping early.
|
|
36
|
+
|
|
37
|
+
- **TabBar can settle overflowing peers at whole-tab starts.** Opt into
|
|
38
|
+
`snapTabs` beside a pinned leading AppTab; selection and scroll gestures
|
|
39
|
+
align peers after the pin in LTR and RTL, with enough end room for the final
|
|
40
|
+
tab instead of leaving a clipped sliver at rest.
|
|
41
|
+
|
|
9
42
|
- **The pre-push check no longer adds timing metadata to Hot Sheet tickets.**
|
|
10
43
|
It still runs the repository gate or skips an identical verified clean tree;
|
|
11
44
|
historical timing notes remain readable through `ticket:timing summary`.
|
package/ai/manifest.json
CHANGED
package/dist/scope.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface DisposerScope {
|
|
|
39
39
|
/** Register a disposer in the current generation and return it. */
|
|
40
40
|
add(dispose: () => void): () => void;
|
|
41
41
|
/** Run the current generation newest first, then abort its signal. */
|
|
42
|
-
dispose()
|
|
42
|
+
readonly dispose: () => void;
|
|
43
43
|
/** Signal for native listeners in the current generation. */
|
|
44
44
|
readonly signal: AbortSignal;
|
|
45
45
|
/** Number of registered disposers in the current generation. */
|
package/dist/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/scope.ts"],"names":[],"mappings":"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","file":"scope.js","sourcesContent":["/**\n * `kerfjs/scope` — group disposers with or without a DOM owner.\n *\n * kerf hands out disposers (`mount()` / `effect()` / `delegate()` all return\n * `() => void`), but nothing scopes them to a subtree's lifetime — so an\n * append-heavy app (a feed, a list of cards) leaks detached-but-subscribed\n * effects, listeners, and observers. Every such app hand-rolls the same\n * `WeakMap<Element, disposers[]>` swept on removal. This subpath blesses it.\n *\n * import { createScope, disposeScope, disposeSubtree, observeRemovals } from 'kerfjs/scope';\n *\n * const s = disposeScope(card);\n * s.mount(card, renderCard); // mounts AND registers its disposer\n * s.effect(() => syncCard(card));\n * s.delegate(card, 'click', '.del', del);\n * s.add(() => observer.disconnect()); // any () => void disposer\n * // …when the card goes away:\n * disposeSubtree(feed); // runs every scope in feed (incl. feed)\n * feed.remove();\n *\n * Or install one observer and let removals auto-dispose:\n * observeRemovals(document.body);\n *\n * Use `createScope()` for a reusable group spanning several event targets.\n * Its `signal` also removes native listeners when the group is disposed.\n *\n * No module-level mutable state: element scopes live in a `WeakMap` (GC-tied),\n * standalone scopes use closure-local state, and `disposeSubtree` walks the DOM.\n */\nimport { delegate, type DelegateOptions } from './delegate.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A reusable disposer group with no DOM owner. */\nexport interface DisposerScope {\n /** Register a disposer in the current generation and return it. */\n add(dispose: () => void): () => void;\n /** Run the current generation newest first, then abort its signal. */\n dispose(): void;\n /** Signal for native listeners in the current generation. */\n readonly signal: AbortSignal;\n /** Number of registered disposers in the current generation. */\n readonly size: number;\n}\n\n/**\n * Create an element-free scope for listeners and other disposers spanning\n * several targets. After disposal, the same scope accepts a new generation.\n */\nexport function createScope(): DisposerScope {\n let disposers: Array<() => void> = [];\n let controller = new AbortController();\n let active = true;\n let disposing = false;\n return {\n add(dispose) {\n disposers.push(dispose);\n active = true;\n return dispose;\n },\n get signal() {\n active = true;\n return controller.signal;\n },\n get size() {\n return disposers.length;\n },\n dispose() {\n if (!active || disposing) return;\n active = false;\n disposing = true;\n const pending = disposers;\n const oldController = controller;\n disposers = [];\n controller = new AbortController();\n try {\n for (let index = pending.length - 1; index >= 0; index--)\n runBestEffort(pending[index]);\n } finally {\n oldController.abort();\n disposing = false;\n }\n },\n };\n}\n\n/** A per-element teardown scope. Calling `disposeScope(el)` again returns the SAME scope. */\nexport interface Scope {\n /** Register any `() => void` disposer (a `mount`/`effect`/`delegate` return, a listener remover, …). Returns it. */\n add(dispose: () => void): () => void;\n /** `mount()` into `el` and register its disposer in one step. Returns the disposer. */\n mount(el: HTMLElement, render: () => MountResult): () => void;\n /** `effect(fn)` and register its disposer in one step. Returns the disposer. */\n effect(fn: () => void | (() => void)): () => void;\n /** `delegate(...)` and register its disposer in one step. Returns the disposer. */\n delegate<T extends Element = Element>(\n root: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n ): () => void;\n /**\n * Run every registered disposer (best-effort — a throwing one won't strand the\n * rest). Idempotent. The handle is then dead: a later `add` / `mount` /\n * `effect` / `delegate` on it (including one made by a disposer while this\n * runs) tears down at once — `add(fn)` calls `fn` immediately — so nothing\n * leaks into a scope that will never run again. `disposeScope(el)` hands out\n * a fresh scope.\n */\n dispose(): void;\n}\n\ninterface ScopeState {\n scope: Scope;\n disposers: Array<() => void>;\n}\n\n// GC-tied cache keyed by element — const + WeakMap, so it is exempt from the\n// \"no module-level mutable state\" rule (like bindings.ts:insertedTextNodes).\nconst scopes = new WeakMap<Element, ScopeState>();\n\n/** Run a disposer; a throwing one must not strand the rest. */\nfunction runBestEffort(dispose: () => void): void {\n try {\n dispose();\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Get (or create) the teardown {@link Scope} for `el`. Repeated calls for the\n * same element return the same scope, so disparate code paths can register into\n * one place. After `dispose()`, a later `disposeScope(el)` starts fresh, and\n * registrations on the old (disposed) handle tear down immediately.\n */\nexport function disposeScope(el: Element): Scope {\n const existing = scopes.get(el);\n if (existing !== undefined) return existing.scope;\n\n const disposers: Array<() => void> = [];\n // Once disposed (including DURING dispose(), from a disposer), the handle is\n // dead: a registration tears down at once instead of landing in an orphaned\n // array nothing will ever run — a leak (KF-DFGD1R).\n let disposed = false;\n const register = (dispose: () => void): (() => void) => {\n if (disposed) runBestEffort(dispose);\n else disposers.push(dispose);\n return dispose;\n };\n const scope: Scope = {\n add: register,\n mount: (target, render) => register(mount(target, render)),\n effect: (fn) => register(effect(fn)),\n delegate: (root, type, selector, handler, options) =>\n register(delegate(root, type, selector, handler, options)),\n dispose() {\n if (disposed) return;\n disposed = true;\n scopes.delete(el);\n for (const d of disposers.splice(0)) runBestEffort(d);\n },\n };\n scopes.set(el, { scope, disposers });\n return scope;\n}\n\n/**\n * Dispose every scope within `root` (including `root`'s own), then leave the DOM\n * to you. Call it right before removing a subtree. Finds scopes by walking the\n * subtree against the `WeakMap` — no marker attributes are added to your DOM.\n */\nexport function disposeSubtree(root: Element): void {\n // Static NodeList snapshot — safe to dispose (which deletes WeakMap entries)\n // while iterating. Root first, then descendants in document order.\n const own = scopes.get(root);\n if (own !== undefined) own.scope.dispose();\n for (const el of root.querySelectorAll('*')) {\n const state = scopes.get(el);\n if (state !== undefined) state.scope.dispose();\n }\n}\n\n/**\n * Install a `MutationObserver` on `root` that auto-disposes a node's scope when\n * that node (or an ancestor) is permanently removed from the subtree. A node\n * moved or reordered within `root` remains live. One observer covers the whole\n * tree. Returns a disconnect function. Note: `MutationObserver` fires\n * asynchronously, so final containment and disposal run after the mutation.\n */\nexport function observeRemovals(root: Element): () => void {\n const observer = new MutationObserver((records) => {\n for (const record of records) {\n for (const node of record.removedNodes) {\n if (node instanceof Element && !root.contains(node))\n disposeSubtree(node);\n }\n }\n });\n observer.observe(root, { childList: true, subtree: true });\n return () => observer.disconnect();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/scope.ts"],"names":[],"mappings":"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","file":"scope.js","sourcesContent":["/**\n * `kerfjs/scope` — group disposers with or without a DOM owner.\n *\n * kerf hands out disposers (`mount()` / `effect()` / `delegate()` all return\n * `() => void`), but nothing scopes them to a subtree's lifetime — so an\n * append-heavy app (a feed, a list of cards) leaks detached-but-subscribed\n * effects, listeners, and observers. Every such app hand-rolls the same\n * `WeakMap<Element, disposers[]>` swept on removal. This subpath blesses it.\n *\n * import { createScope, disposeScope, disposeSubtree, observeRemovals } from 'kerfjs/scope';\n *\n * const s = disposeScope(card);\n * s.mount(card, renderCard); // mounts AND registers its disposer\n * s.effect(() => syncCard(card));\n * s.delegate(card, 'click', '.del', del);\n * s.add(() => observer.disconnect()); // any () => void disposer\n * // …when the card goes away:\n * disposeSubtree(feed); // runs every scope in feed (incl. feed)\n * feed.remove();\n *\n * Or install one observer and let removals auto-dispose:\n * observeRemovals(document.body);\n *\n * Use `createScope()` for a reusable group spanning several event targets.\n * Its `signal` also removes native listeners when the group is disposed.\n *\n * No module-level mutable state: element scopes live in a `WeakMap` (GC-tied),\n * standalone scopes use closure-local state, and `disposeSubtree` walks the DOM.\n */\nimport { delegate, type DelegateOptions } from './delegate.js';\nimport { mount, type MountResult } from './mount.js';\nimport { effect } from './reactive.js';\n\n/** A reusable disposer group with no DOM owner. */\nexport interface DisposerScope {\n /** Register a disposer in the current generation and return it. */\n add(dispose: () => void): () => void;\n /** Run the current generation newest first, then abort its signal. */\n readonly dispose: () => void;\n /** Signal for native listeners in the current generation. */\n readonly signal: AbortSignal;\n /** Number of registered disposers in the current generation. */\n readonly size: number;\n}\n\n/**\n * Create an element-free scope for listeners and other disposers spanning\n * several targets. After disposal, the same scope accepts a new generation.\n */\nexport function createScope(): DisposerScope {\n let disposers: Array<() => void> = [];\n let controller = new AbortController();\n let active = true;\n let disposing = false;\n return {\n add(dispose) {\n disposers.push(dispose);\n active = true;\n return dispose;\n },\n get signal() {\n active = true;\n return controller.signal;\n },\n get size() {\n return disposers.length;\n },\n dispose() {\n if (!active || disposing) return;\n active = false;\n disposing = true;\n const pending = disposers;\n const oldController = controller;\n disposers = [];\n controller = new AbortController();\n try {\n for (let index = pending.length - 1; index >= 0; index--)\n runBestEffort(pending[index]);\n } finally {\n oldController.abort();\n disposing = false;\n }\n },\n };\n}\n\n/** A per-element teardown scope. Calling `disposeScope(el)` again returns the SAME scope. */\nexport interface Scope {\n /** Register any `() => void` disposer (a `mount`/`effect`/`delegate` return, a listener remover, …). Returns it. */\n add(dispose: () => void): () => void;\n /** `mount()` into `el` and register its disposer in one step. Returns the disposer. */\n mount(el: HTMLElement, render: () => MountResult): () => void;\n /** `effect(fn)` and register its disposer in one step. Returns the disposer. */\n effect(fn: () => void | (() => void)): () => void;\n /** `delegate(...)` and register its disposer in one step. Returns the disposer. */\n delegate<T extends Element = Element>(\n root: HTMLElement,\n type: string,\n selector: string,\n handler: (event: Event, target: T) => void,\n options?: DelegateOptions,\n ): () => void;\n /**\n * Run every registered disposer (best-effort — a throwing one won't strand the\n * rest). Idempotent. The handle is then dead: a later `add` / `mount` /\n * `effect` / `delegate` on it (including one made by a disposer while this\n * runs) tears down at once — `add(fn)` calls `fn` immediately — so nothing\n * leaks into a scope that will never run again. `disposeScope(el)` hands out\n * a fresh scope.\n */\n dispose(): void;\n}\n\ninterface ScopeState {\n scope: Scope;\n disposers: Array<() => void>;\n}\n\n// GC-tied cache keyed by element — const + WeakMap, so it is exempt from the\n// \"no module-level mutable state\" rule (like bindings.ts:insertedTextNodes).\nconst scopes = new WeakMap<Element, ScopeState>();\n\n/** Run a disposer; a throwing one must not strand the rest. */\nfunction runBestEffort(dispose: () => void): void {\n try {\n dispose();\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Get (or create) the teardown {@link Scope} for `el`. Repeated calls for the\n * same element return the same scope, so disparate code paths can register into\n * one place. After `dispose()`, a later `disposeScope(el)` starts fresh, and\n * registrations on the old (disposed) handle tear down immediately.\n */\nexport function disposeScope(el: Element): Scope {\n const existing = scopes.get(el);\n if (existing !== undefined) return existing.scope;\n\n const disposers: Array<() => void> = [];\n // Once disposed (including DURING dispose(), from a disposer), the handle is\n // dead: a registration tears down at once instead of landing in an orphaned\n // array nothing will ever run — a leak (KF-DFGD1R).\n let disposed = false;\n const register = (dispose: () => void): (() => void) => {\n if (disposed) runBestEffort(dispose);\n else disposers.push(dispose);\n return dispose;\n };\n const scope: Scope = {\n add: register,\n mount: (target, render) => register(mount(target, render)),\n effect: (fn) => register(effect(fn)),\n delegate: (root, type, selector, handler, options) =>\n register(delegate(root, type, selector, handler, options)),\n dispose() {\n if (disposed) return;\n disposed = true;\n scopes.delete(el);\n for (const d of disposers.splice(0)) runBestEffort(d);\n },\n };\n scopes.set(el, { scope, disposers });\n return scope;\n}\n\n/**\n * Dispose every scope within `root` (including `root`'s own), then leave the DOM\n * to you. Call it right before removing a subtree. Finds scopes by walking the\n * subtree against the `WeakMap` — no marker attributes are added to your DOM.\n */\nexport function disposeSubtree(root: Element): void {\n // Static NodeList snapshot — safe to dispose (which deletes WeakMap entries)\n // while iterating. Root first, then descendants in document order.\n const own = scopes.get(root);\n if (own !== undefined) own.scope.dispose();\n for (const el of root.querySelectorAll('*')) {\n const state = scopes.get(el);\n if (state !== undefined) state.scope.dispose();\n }\n}\n\n/**\n * Install a `MutationObserver` on `root` that auto-disposes a node's scope when\n * that node (or an ancestor) is permanently removed from the subtree. A node\n * moved or reordered within `root` remains live. One observer covers the whole\n * tree. Returns a disconnect function. Note: `MutationObserver` fires\n * asynchronously, so final containment and disposal run after the mutation.\n */\nexport function observeRemovals(root: Element): () => void {\n const observer = new MutationObserver((records) => {\n for (const record of records) {\n for (const node of record.removedNodes) {\n if (node instanceof Element && !root.contains(node))\n disposeSubtree(node);\n }\n }\n });\n observer.observe(root, { childList: true, subtree: true });\n return () => observer.disconnect();\n}\n"]}
|
package/package.json
CHANGED