@cplieger/ui 0.0.0-stage → 1.0.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +3 -0
  3. package/README.md +157 -2
  4. package/THIRD_PARTY_NOTICES.md +8 -0
  5. package/css/ui-primitives.css +344 -0
  6. package/jsr.json +44 -0
  7. package/package.json +105 -4
  8. package/src/actions/api.ts +268 -0
  9. package/src/actions/async-feedback.ts +217 -0
  10. package/src/actions/cleanup.ts +66 -0
  11. package/src/actions/debounce.ts +150 -0
  12. package/src/actions/define-helpers.ts +87 -0
  13. package/src/actions/define.ts +851 -0
  14. package/src/actions/error.ts +161 -0
  15. package/src/actions/index.ts +60 -0
  16. package/src/actions/loading.ts +131 -0
  17. package/src/actions/notifier.ts +61 -0
  18. package/src/actions/poll-until.ts +133 -0
  19. package/src/actions/poll.ts +156 -0
  20. package/src/actions/registry.ts +259 -0
  21. package/src/actions/retry.ts +79 -0
  22. package/src/actions/testing.ts +29 -0
  23. package/src/actions/transport.ts +104 -0
  24. package/src/actions/types.ts +249 -0
  25. package/src/dom/bind-list.ts +103 -0
  26. package/src/dom/el.ts +67 -0
  27. package/src/dom/index.ts +9 -0
  28. package/src/dom/reconcile-tree.ts +268 -0
  29. package/src/dom/reconcile.ts +76 -0
  30. package/src/fetch/index.ts +18 -0
  31. package/src/fetch/instance.ts +37 -0
  32. package/src/fetch/request.ts +437 -0
  33. package/src/fetch/timeout.ts +67 -0
  34. package/src/fetch/types.ts +206 -0
  35. package/src/fetch/verbs.ts +103 -0
  36. package/src/index.ts +4 -0
  37. package/src/primitives/announce.ts +134 -0
  38. package/src/primitives/ask.ts +406 -0
  39. package/src/primitives/dialog-engine.ts +500 -0
  40. package/src/primitives/dialog.ts +263 -0
  41. package/src/primitives/disclosure.ts +287 -0
  42. package/src/primitives/focus-trap.ts +157 -0
  43. package/src/primitives/fx.ts +204 -0
  44. package/src/primitives/live-regions.ts +83 -0
  45. package/src/primitives/modal-host.ts +214 -0
  46. package/src/primitives/modal.ts +247 -0
  47. package/src/primitives/owner.ts +322 -0
  48. package/src/primitives/popover.ts +500 -0
  49. package/src/primitives/popup-core.ts +603 -0
  50. package/src/primitives/popup.ts +23 -0
  51. package/src/primitives/roving-focus.ts +198 -0
  52. package/src/primitives/scroll-lock.ts +119 -0
  53. package/src/primitives/skeleton.ts +142 -0
  54. package/src/primitives/theme.ts +202 -0
  55. package/src/primitives/toast/engine.ts +327 -0
  56. package/src/primitives/toast/index.ts +139 -0
  57. package/src/primitives/toast/view.ts +244 -0
  58. package/src/primitives/tooltip.ts +377 -0
  59. package/src/primitives/transition.ts +119 -0
  60. package/src/primitives/view-transition.ts +57 -0
  61. package/src/signal/bus.ts +126 -0
  62. package/src/signal/collection.ts +182 -0
  63. package/src/signal/index.ts +34 -0
  64. package/src/signal/resource.ts +102 -0
  65. package/src/signal/signal-map.ts +46 -0
  66. package/src/signal/signal.ts +1002 -0
  67. package/src/signal/store.ts +84 -0
@@ -0,0 +1,249 @@
1
+ /** Lifecycle status of a single dispatched action instance.
2
+ *
3
+ * - `"pending"` — optimistic ran (if any), run() in flight
4
+ * - `"success"` — run() resolved
5
+ * - `"error"` — run() threw; rollback ran
6
+ * - `"cancelled"` — action.cancel() called or signal aborted externally */
7
+ export type ActionLifecycleStatus = "pending" | "success" | "error" | "cancelled";
8
+
9
+ /** Errors thrown by an action's run() function. ActionError subclass
10
+ * in error.ts attaches HTTP status + server error code metadata. */
11
+ export interface ActionErrorLike {
12
+ readonly message: string;
13
+ /** HTTP status if applicable. */
14
+ readonly status?: number;
15
+ /** Server-side error code. */
16
+ readonly code?: string;
17
+ readonly cause?: unknown;
18
+ }
19
+
20
+ /** @internal The lifecycle fields every action-log record carries, sensitive
21
+ * or not. Each is a number or a string the framework sets, so none can hold
22
+ * a value from args, result or run(); a payload-bearing field belongs on the
23
+ * non-sensitive arm of {@link ActionInstance}. */
24
+ export interface ActionInstanceFields {
25
+ /** Unique per dispatch. */
26
+ readonly id: string;
27
+ /** Matches ActionDefinition.name. */
28
+ readonly name: string;
29
+ readonly status: ActionLifecycleStatus;
30
+ /** Date.now() when dispatch() was called. */
31
+ readonly dispatchedAt: number;
32
+ /** Date.now() when run() begins (after scope queue). */
33
+ readonly startedAt: number;
34
+ /** Date.now() at terminal state. */
35
+ readonly completedAt?: number;
36
+ /** Total run() invocations (1 = no retry; >1 = retries fired). */
37
+ readonly attempts?: number;
38
+ }
39
+
40
+ /** Snapshot of a single in-flight or historical action invocation, stored in
41
+ * the registry log. A `sensitive` definition's records carry no `args`,
42
+ * `result` or `error`; narrow on `sensitive` before reading `args`. */
43
+ export type ActionInstance<TArgs = unknown, TResult = unknown> =
44
+ | (ActionInstanceFields & {
45
+ readonly sensitive?: never;
46
+ readonly args: TArgs;
47
+ /** Present iff status === "success". */
48
+ readonly result?: TResult;
49
+ /** Present iff status === "error". */
50
+ readonly error?: ActionErrorLike;
51
+ })
52
+ | (ActionInstanceFields & {
53
+ readonly sensitive: true;
54
+ readonly args?: never;
55
+ readonly result?: never;
56
+ readonly error?: never;
57
+ });
58
+
59
+ /** Notification wiring: either a literal string (used as-is), or a function
60
+ * computed from action args + result/error at call time. Pass false
61
+ * to opt out of the default notification for that branch. */
62
+ export type NotificationSpec<TArgs, TPayload> =
63
+ string | ((args: TArgs, payload: TPayload) => string) | false;
64
+
65
+ /** Per-dispatch context passed to run() as the 3rd argument. Mostly
66
+ * populated by the framework so adapters (apiAction, transportAction)
67
+ * can read out values like the idempotency key without the caller
68
+ * having to plumb them. */
69
+ export interface ActionContext {
70
+ /** Stable identifier for this dispatch (matches the registry's
71
+ * ActionInstance.id). */
72
+ readonly instanceID: string;
73
+ /** Set when ActionDefinition.idempotencyKey is configured. The
74
+ * framework generates this once per dispatch (not per retry). */
75
+ readonly idempotencyKey?: string;
76
+ }
77
+
78
+ /** Configuration for automatic retry of transient failures. */
79
+ export interface RetryConfig {
80
+ /** Additional attempts beyond the first (e.g. 2 = up to 3 total). */
81
+ readonly count: number;
82
+ /** Milliseconds before each retry. Number form: exponential backoff via
83
+ * `delay × factor^(attempt-1)`, capped at 5s. Function form: full
84
+ * control — receives the attempt number and the triggering error. */
85
+ readonly delay: number | ((attempt: number, err: ActionErrorLike) => number);
86
+ /** Backoff multiplier per retry. Default 2. Ignored when `delay` is a function. */
87
+ readonly factor?: number;
88
+ }
89
+
90
+ /** Standard retry config: 2 retries, 300ms initial delay. */
91
+ export const RETRY_STANDARD: RetryConfig = { count: 2, delay: 300 } as const;
92
+
93
+ /** Declarative description of an action, consulted on every dispatch. Only
94
+ * `name` and `run` are required; function-valued members are re-evaluated
95
+ * per dispatch. A throw from a callback, `rollback` or a notification spec
96
+ * is logged and leaves the outcome unchanged; a throw from `optimistic`
97
+ * fails the dispatch before run(), with code `"optimistic_failed"` unless
98
+ * the error carries its own. */
99
+ export interface ActionDefinition<TArgs, TResult, TOp = unknown> {
100
+ /** Stable identifier, e.g. "chat.delete", "files.create".
101
+ * Used in the registry log + as a default notification prefix. */
102
+ readonly name: string;
103
+
104
+ /** The work the action performs. Must throw ActionError on failure
105
+ * (or any Error — wrappers will normalise). */
106
+ run: (args: TArgs, signal: AbortSignal, ctx?: ActionContext) => Promise<TResult>;
107
+
108
+ /** Optional optimistic mutation. Runs synchronously before run(). */
109
+ optimistic?: (args: TArgs) => TOp | undefined;
110
+
111
+ /** Undo the optimistic mutation. Called on error OR cancellation (including a
112
+ * cancellation that lands after run() resolves), never on success. The `err`
113
+ * carries {code:'cancelled'} on the cancellation path. */
114
+ rollback?: (args: TArgs, op: TOp | undefined, err: ActionErrorLike) => void;
115
+
116
+ /** Notification on success. Default: no notification. */
117
+ success?: NotificationSpec<TArgs, TResult>;
118
+
119
+ /** Notification on error. Default: action name humanised + error message. */
120
+ error?: NotificationSpec<TArgs, ActionErrorLike>;
121
+
122
+ /** Definition-level success callback. Fires on every successful dispatch.
123
+ * Mirrors TanStack Query's mutation-level onSuccess. */
124
+ onSuccess?: (result: TResult, args: TArgs) => void;
125
+
126
+ /** Definition-level error callback. Fires on every failed dispatch.
127
+ * Mirrors TanStack Query's mutation-level onError. */
128
+ onError?: (err: ActionErrorLike, args: TArgs) => void;
129
+
130
+ /** Definition-level settled callback. Fires after every dispatch
131
+ * (success, error, or cancellation). Mirrors TanStack Query's onSettled. */
132
+ onSettled?: (args: TArgs) => void;
133
+
134
+ /** Classify whether an error qualifies for retry. */
135
+ retryable?: (err: ActionErrorLike) => boolean;
136
+
137
+ /** Auto-retry transient failures before surfacing the error notification. */
138
+ retry?: RetryConfig;
139
+
140
+ /** Auto-retry behavior when offline. Default `"online"`. */
141
+ networkMode?: "online" | "always";
142
+
143
+ /** Timeout in milliseconds for the run() function. Uses AbortSignal.timeout()
144
+ * composed with the cancellation signal. If run() exceeds this duration,
145
+ * the signal aborts with a TimeoutError. */
146
+ timeout?: number;
147
+
148
+ /** Serialize concurrent dispatches sharing the same scope key. */
149
+ scope?: string | ((args: TArgs) => string);
150
+
151
+ /** Generate an idempotency key per dispatch. */
152
+ idempotencyKey?: boolean | ((args: TArgs) => string);
153
+
154
+ /** Collapse concurrent dispatches with matching key into one in-flight promise. */
155
+ dedupe?: boolean | ((args: TArgs) => string);
156
+
157
+ /** For a call carrying a plaintext secret: the action log and every
158
+ * registry listener get each instance without `args`, `result` and
159
+ * `error`, so the result and the error reach only the dispatch's handle,
160
+ * the callbacks and the notifications (the default and string error
161
+ * notifications quote the error's message). Combined with `retry` or
162
+ * `retryable`, defineAction() throws: a retry button would hold the args. */
163
+ sensitive?: true;
164
+ }
165
+
166
+ /** A registered action, returned by defineAction(). Can be dispatched
167
+ * many times; each dispatch is a separate instance with its own
168
+ * cancellation token. */
169
+ export interface Action<TArgs, TResult> {
170
+ readonly name: string;
171
+
172
+ /** Run the action. Returns a DispatchHandle with the result promise
173
+ * and a per-dispatch abort() method (RTK pattern). */
174
+ dispatch(args: TArgs, opts?: DispatchOptions<TArgs, TResult>): DispatchHandle<TResult>;
175
+
176
+ /** Cancel all in-flight instances. Each instance moves to status
177
+ * "cancelled" and run()'s signal aborts. */
178
+ cancel(): void;
179
+ }
180
+
181
+ /** Typed terminal outcome of a single dispatch, resolved by
182
+ * {@link DispatchHandle.outcome}. Distinguishes the three states the
183
+ * handle's own `TResult | null` resolution collapses: success (including a
184
+ * legitimate `null` / `undefined` result), failure (carrying the normalized
185
+ * error), and cancellation. */
186
+ export type ActionOutcome<TResult> =
187
+ | { readonly status: "success"; readonly value: TResult; readonly attempts?: number }
188
+ | { readonly status: "error"; readonly error: ActionErrorLike; readonly attempts?: number }
189
+ | { readonly status: "cancelled" };
190
+
191
+ /** Handle returned by dispatch(). An augmented Promise with an abort()
192
+ * method for per-dispatch cancellation (mirrors RTK createAsyncThunk).
193
+ * Can be awaited directly as a Promise. */
194
+ export interface DispatchHandle<TResult> extends Promise<TResult | null> {
195
+ /** Abort this specific dispatch; other in-flight dispatches of the same
196
+ * action are unaffected. One still queued behind its `scope` settles
197
+ * cancelled at once. On a handle that joined through `dedupe`, only this
198
+ * call settles cancelled: the dispatch it joined keeps running. */
199
+ abort(): void;
200
+ /** Opt-in typed terminal outcome. Resolves alongside the handle (it never
201
+ * rejects) with a discriminated {@link ActionOutcome}, letting a caller
202
+ * distinguish a legitimate `null` result from failure or cancellation
203
+ * without wiring callbacks. The never-rejecting `TResult | null`
204
+ * resolution and the callback tiers remain the canonical surface. */
205
+ readonly outcome: Promise<ActionOutcome<TResult>>;
206
+ }
207
+
208
+ /** Per-dispatch overrides. */
209
+ export interface DispatchOptions<TArgs = unknown, TResult = unknown> {
210
+ /** Suppress the success notification for this call. Errors still notify. */
211
+ readonly silent?: boolean;
212
+ /** Per-call success callback. Fires after the action-level notification. */
213
+ readonly onSuccess?: (result: TResult, args: TArgs) => void;
214
+ /** Per-call error callback. Fires after the action-level error notification. */
215
+ readonly onError?: (err: ActionErrorLike, args: TArgs) => void;
216
+ /** Per-call settled callback. Fires for success, error, AND cancellation. */
217
+ readonly onSettled?: (args: TArgs) => void;
218
+ /** Aborting it cancels this dispatch as `abort()` does: before it starts
219
+ * (a dispatch queued behind its `scope` settles at once and never runs),
220
+ * or mid-run, aborting run()'s signal with this signal's reason. An
221
+ * already-aborted signal cancels the dispatch without running it. */
222
+ readonly signal?: AbortSignal;
223
+ }
224
+
225
+ /** HTTP request descriptor used by apiAction(). GET is included for
226
+ * read actions that want notification/cancellation semantics. */
227
+ export type RequestSpec =
228
+ | {
229
+ readonly method: "GET";
230
+ readonly path: string;
231
+ readonly headers?: Readonly<Record<string, string>>;
232
+ }
233
+ | {
234
+ readonly method: "POST" | "PUT" | "PATCH" | "DELETE";
235
+ readonly path: string;
236
+ /** JSON-encoded request body. Mutually exclusive with `rawBody`. */
237
+ readonly body?: unknown;
238
+ /** Pre-encoded request body, sent as-is with NO JSON encoding and NO
239
+ * automatic Content-Type — set the type via `headers` (e.g. a YAML
240
+ * document with `Content-Type: text/yaml`). The request() function is
241
+ * the encoder seam: compute the encoded payload there per dispatch.
242
+ * Mutually exclusive with `body`. */
243
+ readonly rawBody?: BodyInit;
244
+ readonly headers?: Readonly<Record<string, string>>;
245
+ };
246
+
247
+ /** Subscriber callback for the registry. Fires once per state
248
+ * transition (pending -> success/error/cancelled). */
249
+ export type RegistryListener = (instance: ActionInstance) => void;
@@ -0,0 +1,103 @@
1
+ // Two-tier render binding for a reactive list source. One structural effect
2
+ // tracks `source.ids` and reconciles the id list; each row owns an effect that
3
+ // tracks only its entity signal, so an entity update repaints just that row.
4
+ // The engine saves and restores evalContext around nested effects, so a row
5
+ // effect's reads never leak into the structural effect. A `Collection<T>` is a
6
+ // `ListSource<T>`; a filtered, sorted or paginated view is a `computed` id
7
+ // list paired with the collection's `signalFor`.
8
+
9
+ import { reconcile } from "./reconcile.js";
10
+ import {
11
+ createRoot,
12
+ createRootUnder,
13
+ effect,
14
+ type Owner,
15
+ type ReadonlySignal,
16
+ } from "../signal/signal.js";
17
+
18
+ /** The minimal reactive surface `bindList` renders: a structure signal (the
19
+ * ordered ids) and a per-id content-signal lookup. `Collection<T>` satisfies
20
+ * this; so does `{ ids: computed(...), signalFor: collection.signalFor }`.
21
+ * An id with no matching signal (an inconsistent source) is skipped — no row
22
+ * is rendered for it until the source becomes consistent. */
23
+ export interface ListSource<T> {
24
+ readonly ids: ReadonlySignal<readonly string[]>;
25
+ signalFor(id: string): ReadonlySignal<T> | undefined;
26
+ }
27
+
28
+ /** Row lifecycle for `bindList`. Each row has its own owner, so effects and
29
+ * roots created in `mount` and `update` are disposed when the row leaves. */
30
+ export interface ListSpec<T> {
31
+ /** Create the row element for a new entity. Receives the entity's current
32
+ * value, its id and the row's owner, whose `signal` aborts when the row
33
+ * leaves or the list is disposed. Keep this to structural shell creation;
34
+ * let `update` fill reactive content (it runs once at mount and on every
35
+ * later change). */
36
+ mount: (item: T, id: string, owner: Owner) => HTMLElement;
37
+ /** Update a row when its entity changes. Runs immediately at mount and again
38
+ * on every per-entity change (the content tier). Omit for immutable rows
39
+ * whose `mount` renders everything. */
40
+ update?: (el: HTMLElement, item: T, id: string) => void;
41
+ /** Optional cleanup when a row leaves the list, after its owner is disposed
42
+ * and before the element is removed from the DOM. */
43
+ onRemove?: (el: HTMLElement, id: string) => void;
44
+ }
45
+
46
+ /** Bind a list source to a parent node with two-tier reactivity, in a root
47
+ * owned by the current owner when there is one. Returns a dispose function
48
+ * that tears down the structural effect and every row. */
49
+ export function bindList<T>(
50
+ parent: ParentNode,
51
+ source: ListSource<T>,
52
+ spec: ListSpec<T>,
53
+ ): () => void {
54
+ return createRoot((dispose, listOwner) => {
55
+ const rowDisposers = new WeakMap<HTMLElement, () => void>();
56
+
57
+ effect(() => {
58
+ // Structure tier: re-runs on add/remove/reorder only. An id whose entity
59
+ // signal is missing (an inconsistent ListSource) is skipped instead of
60
+ // mounting a row with a manufactured `undefined as T` value; it renders
61
+ // once the source becomes consistent (the next `ids` change).
62
+ const ids = source.ids.value.filter((id) => source.signalFor(id) !== undefined);
63
+ reconcile<string>(parent, ids, {
64
+ key: (id) => id,
65
+ mount: (id) => {
66
+ const sig = source.signalFor(id);
67
+ if (sig === undefined) {
68
+ // Unreachable via the structural filter above (same synchronous
69
+ // pass); reachable only by a source whose signalFor is not stable
70
+ // within one read — surface that loudly instead of manufacturing
71
+ // an entity value. Routed to the effect error handler.
72
+ throw new Error(`bindList: no entity signal for id "${id}"`);
73
+ }
74
+ // A row belongs to the list, not to this structural run, whose
75
+ // scope is disposed on every add, remove or reorder.
76
+ return createRootUnder(listOwner, (disposeRow, rowOwner) => {
77
+ const el = spec.mount(sig.peek(), id, rowOwner);
78
+ rowDisposers.set(el, disposeRow);
79
+ if (spec.update !== undefined) {
80
+ const update = spec.update;
81
+ // content tier: isolated per-row effect; runs now (fills content)
82
+ // and on every later change to THIS entity.
83
+ effect(() => {
84
+ const cur = source.signalFor(id);
85
+ if (cur !== undefined) {
86
+ update(el, cur.value, id);
87
+ }
88
+ });
89
+ }
90
+ return el;
91
+ });
92
+ },
93
+ // No structural `update`: content is owned entirely by the per-row effect.
94
+ onRemove: (el, id) => {
95
+ rowDisposers.get(el)?.();
96
+ spec.onRemove?.(el, id);
97
+ },
98
+ });
99
+ });
100
+
101
+ return dispose;
102
+ });
103
+ }
package/src/dom/el.ts ADDED
@@ -0,0 +1,67 @@
1
+ // el.ts — CSP-safe DOM element factory. The build half of the DOM layer; pairs
2
+ // with reconcile/patch (the commit half). `on*` handlers are assigned as
3
+ // properties AND registered via trackHandler, so patch() reconciles them across
4
+ // re-renders. No innerHTML, no string templating — safe under a strict CSP.
5
+
6
+ import { trackHandler } from "./reconcile-tree.js";
7
+
8
+ /** A value for an element attribute/property/handler. Functions are treated as
9
+ * event handlers when the key starts with `on`. */
10
+ export type AttrValue =
11
+ string | number | boolean | null | undefined | ((...args: never[]) => unknown);
12
+
13
+ // Keys set as DOM *properties* (not attributes) because the attribute form
14
+ // either doesn't reflect (value/checked) or is awkward (boolean presence).
15
+ const BOOL_PROPS = new Set([
16
+ "hidden",
17
+ "disabled",
18
+ "checked",
19
+ "selected",
20
+ "multiple",
21
+ "readOnly",
22
+ "required",
23
+ "open",
24
+ "default",
25
+ ]);
26
+ const VALUE_PROPS = new Set(["value", "colSpan", "rowSpan", "tabIndex", "htmlFor"]);
27
+
28
+ /** Create an element: `el("button", { className: "x", onclick: fn }, "Label")`.
29
+ * - `className` → the class property
30
+ * - `on*` → handler property + `trackHandler` (reconciled by patch)
31
+ * - boolean DOM props (hidden/disabled/checked/selected/…) → property
32
+ * - value/colSpan/rowSpan/tabIndex/htmlFor → property
33
+ * - everything else → `setAttribute` (incl. `data-*`, `aria-*`, `style`, `id`)
34
+ * Null/undefined attrs and children are skipped. String children become text
35
+ * nodes (never parsed as HTML). */
36
+ export function el(
37
+ tag: string,
38
+ attrs?: Record<string, AttrValue> | null,
39
+ ...children: (string | Node | null | undefined)[]
40
+ ): HTMLElement {
41
+ const e = document.createElement(tag);
42
+ if (attrs) {
43
+ for (const [k, v] of Object.entries(attrs)) {
44
+ if (v === null || v === undefined) {
45
+ continue;
46
+ }
47
+ if (k === "className") {
48
+ e.className = v as string;
49
+ } else if (/^on/i.test(k)) {
50
+ (e as unknown as Record<string, unknown>)[k] = v;
51
+ trackHandler(e, k);
52
+ } else if (BOOL_PROPS.has(k) || VALUE_PROPS.has(k)) {
53
+ // Both sets are assigned as DOM properties, not attributes.
54
+ (e as unknown as Record<string, unknown>)[k] = v;
55
+ } else {
56
+ e.setAttribute(k, String(v));
57
+ }
58
+ }
59
+ }
60
+ for (const child of children) {
61
+ if (child === null || child === undefined) {
62
+ continue;
63
+ }
64
+ e.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
65
+ }
66
+ return e;
67
+ }
@@ -0,0 +1,9 @@
1
+ /** Build and reconcile DOM: `el`, `patch`, `reconcile` and `bindList`. @module */
2
+
3
+ export { bindList } from "./bind-list.js";
4
+ export type { ListSpec, ListSource } from "./bind-list.js";
5
+ export { el } from "./el.js";
6
+ export type { AttrValue } from "./el.js";
7
+ export { reconcile, KEY_ATTR } from "./reconcile.js";
8
+ export type { ReconcileSpec } from "./reconcile.js";
9
+ export { patch, trackHandler } from "./reconcile-tree.js";