defuss-morph 0.1.1 → 0.3.0

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/dist/index.d.ts CHANGED
@@ -24,10 +24,19 @@ interface VNodeAttributes {
24
24
  [attributeName: string]: any;
25
25
  }
26
26
  type VNodeType = string | Function | any;
27
+ /**
28
+ * A virtual node as JSX factories produce it: `type` is a tag name (or `"#comment"`), `attributes`
29
+ * the props, `children` nested VNodes, text, numbers or values that render nothing.
30
+ */
27
31
  interface VNode<A = VNodeAttributes> {
28
32
  type?: VNodeType;
29
33
  attributes?: A;
30
34
  children?: VNodeChildren;
35
+ /**
36
+ * Comment data of a `{ type: "#comment" }` VNode (see `COMMENT_TYPE`).
37
+ * VERIFIED: optional, so defuss core's VNode (no `value`) stays assignable.
38
+ */
39
+ value?: string;
31
40
  sourceInfo?: unknown;
32
41
  /** Original props passed to a function component (set by jsx runtime for SSG hydration). */
33
42
  componentProps?: Record<string, any>;
@@ -38,6 +47,7 @@ type VNodeChildren = VNodeChild[];
38
47
  * Anything the morph engine accepts as new content.
39
48
  */
40
49
  type RenderInput = VNode | object | string | number | boolean | null | undefined | RenderInput[];
50
+ /** The renderer contract returned by `getRenderer()`. */
41
51
  interface DomAbstractionImpl {
42
52
  hasElNamespace(domElement: Element | Document): boolean;
43
53
  hasSvgNamespace(parentElement: Element | Document, type: string): boolean;
@@ -49,30 +59,14 @@ interface DomAbstractionImpl {
49
59
  setAttributes(virtualNode: VNode<VNodeAttributes>, domElement: Element): void;
50
60
  }
51
61
 
52
- type TransitionType = "fade" | "slide-left" | "slide-right" | "shake" | "none";
53
- interface TransitionStyles {
54
- enter: Record<string, string>;
55
- enterActive: Record<string, string>;
56
- exit: Record<string, string>;
57
- exitActive: Record<string, string>;
58
- }
59
- type TransitionsEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end";
60
- interface TransitionConfig {
61
- type?: TransitionType;
62
- styles?: TransitionStyles;
63
- duration?: number;
64
- easing?: TransitionsEasing | string;
65
- delay?: number;
66
- target?: "parent" | "self";
67
- }
68
- declare const getTransitionStyles: (type: TransitionType, duration: number, easing?: string) => TransitionStyles;
69
- declare const applyStyles: (el: HTMLElement, styles: Record<string, string | number>) => void;
70
- declare const DEFAULT_TRANSITION_CONFIG: TransitionConfig;
71
- declare const performTransition: (element: HTMLElement, updateCallback: () => Promise<void>, config?: TransitionConfig) => Promise<void>;
72
-
62
+ /**
63
+ * Wraps `cb` so each call runs it in a microtask, with the same arguments.
64
+ * VERIFIED: renderer.test.ts asserts onMount has not run before that microtask.
65
+ */
73
66
  declare const queueCallback: <T extends any[]>(cb: (...args: T) => void) => (...args: T) => void;
74
67
 
75
68
  type DelegatedPhase = "bubble" | "capture";
69
+ /** Options of {@link registerDelegatedEvent}. */
76
70
  interface DelegatedEventOptions {
77
71
  capture?: boolean;
78
72
  /** If true, allows multiple handlers per element+type (Dequery mode) */
@@ -82,15 +76,42 @@ interface ParsedEventProp {
82
76
  eventType: string;
83
77
  capture: boolean;
84
78
  }
85
- /** non-bubbling events best handled via capture */
79
+ /**
80
+ * Events whose handlers are forced into the capture phase, so an ancestor's
81
+ * handler also sees descendants' events (container onFocus/onBlur). Every
82
+ * other non-bubbling event reaches only its target's handlers, natively.
83
+ */
86
84
  declare const CAPTURE_ONLY_EVENTS: Set<string>;
85
+ /**
86
+ * Parses a JSX event prop name: `onClick` gives `{ eventType: "click", capture: false }`,
87
+ * `onClickCapture` the capture phase. Case-insensitive; `null` for anything not starting with `on`.
88
+ */
87
89
  declare const parseEventPropName: (propName: string) => ParsedEventProp | null;
90
+ /**
91
+ * Attaches `handler` to `element` the way JSX props do: one slot per event type and phase, so
92
+ * registering again replaces the handler without adding a listener. With `{ multi: true }`
93
+ * every handler becomes its own native listener (dequery-style `on()`). `focus`, `blur`,
94
+ * `scroll`, `mouseenter` and `mouseleave` always use the capture phase.
95
+ */
88
96
  declare const registerDelegatedEvent: (element: HTMLElement, eventType: string, handler: EventListener, options?: DelegatedEventOptions) => void;
97
+ /**
98
+ * Removes `handler` (from both phases), or every handler of `eventType` when no handler is given.
99
+ */
89
100
  declare const removeDelegatedEvent: (target: EventTarget, eventType: string, handler?: EventListener, _options?: DelegatedEventOptions) => void;
101
+ /**
102
+ * Why: listeners a facade attaches natively (defuss-query's on()) live
103
+ * outside this registry, yet must go whenever morph clears an element
104
+ * (removal, replacement, clearDelegatedEventsDeep). A hook keeps morph
105
+ * ignorant of who owns them, instead of morph also tracking native listeners.
106
+ * Registering the same hook twice is a no-op; returns an unregister function.
107
+ */
108
+ declare const onClearDelegatedEvents: (hook: (target: EventTarget) => void) => (() => void);
109
+ /** Removes every registered handler of one element and runs the {@link onClearDelegatedEvents} hooks for it. */
90
110
  declare const clearDelegatedEvents: (target: EventTarget) => void;
91
111
  /**
92
- * Clear delegated events for an element and all its descendants.
93
- * Used by empty() to prevent event handler leaks when removing subtrees.
112
+ * Clear delegated events for an element and all its descendants. Morph runs
113
+ * it on every element it removes or replaces; facades call it before removing
114
+ * nodes themselves (defuss-query does in its internal remove()).
94
115
  */
95
116
  declare const clearDelegatedEventsDeep: (root: HTMLElement) => void;
96
117
  /**
@@ -120,9 +141,27 @@ declare const nsMap: {
120
141
  xlink: string;
121
142
  svg: string;
122
143
  };
144
+ /**
145
+ * Calls `onUnmount` once `domNode` leaves the document. A move (removal and re-insertion in the
146
+ * same task) is not an unmount: the observer follows the node to its new parent. Throws when the
147
+ * node has no parent; a no-op where `MutationObserver` does not exist (SSR).
148
+ */
123
149
  declare const observeUnmount: (domNode: Node, onUnmount: () => void) => void;
124
- /** lifecycle event attachment has been implemented separately, because it is also required to run when partially updating the DOM */
150
+ /**
151
+ * Runs the pending `onMount` hook and arms the `onUnmount` observer of an
152
+ * element that was just inserted or patched in place.
153
+ *
154
+ * Why one observer that reads `$onUnmount` when it fires: morph calls this on
155
+ * every in-place patch, and each re-render sets a fresh `onUnmount` closure.
156
+ * Arming one observer per call made a single removal run every handler ever
157
+ * registered (VERIFIED: 1 render + 3 morphs fired onUnmount 10 times).
158
+ */
125
159
  declare const handleLifecycleEventsForOnMount: (newEl: HTMLElement) => void;
160
+ /**
161
+ * Returns the VNode-to-DOM renderer for `document`: element and SVG creation, text and comment
162
+ * nodes, attributes (class arrays, style objects, controlled form props, namespaced SVG
163
+ * attributes), refs, JSX event props and the `onMount`/`onUnmount` hooks.
164
+ */
126
165
  declare const getRenderer: (document: Document) => DomAbstractionImpl;
127
166
 
128
167
  /**
@@ -130,7 +169,7 @@ declare const getRenderer: (document: Document) => DomAbstractionImpl;
130
169
  * 1. Checks for reference equality.
131
170
  * 2. Compares node types.
132
171
  * 3. For Element nodes, compares tag names and attributes.
133
- * 4. For Text nodes, compares text content.
172
+ * 4. For Text and Comment nodes, compares their data.
134
173
  */
135
174
  declare const areDomNodesEqual: (oldNode: Node, newNode: Node) => boolean;
136
175
  /********************************************************
@@ -182,12 +221,36 @@ declare function replaceDomWithVdom(parentElement: Element, newVDOM: RenderInput
182
221
  * (same rationale as uncontrolled form-state preservation).
183
222
  */
184
223
  declare const FROM_DOM_MARKER: unique symbol;
224
+ /**
225
+ * VNode type of an HTML comment: `{ type: "#comment", value }`.
226
+ * Why "#comment": it is the DOM's own nodeName for comments and can never
227
+ * collide with a tag name ("#" is invalid there). The data lives in `value`,
228
+ * not `children`, so generic tree walkers never mistake it for content.
229
+ */
230
+ declare const COMMENT_TYPE = "#comment";
231
+ type CommentVNode = VNode & {
232
+ type: typeof COMMENT_TYPE;
233
+ };
234
+ /** True for a comment VNode (`{ type: "#comment", value }`). */
235
+ declare const isCommentVNode: (value: unknown) => value is CommentVNode;
236
+ /** Parses `input` into a detached document with the given parser. */
185
237
  declare function parseDOM(input: string, type: DOMParserSupportedType, Parser: typeof DOMParser): Document;
238
+ /** True when `input` parses as an SVG document (root element `<svg>`). */
186
239
  declare function isSVG(input: string, Parser: typeof DOMParser): boolean;
240
+ /** True when `input` parses into at least one element besides the implied `<head>` and `<body>`. */
187
241
  declare function isHTML(input: string, Parser: typeof DOMParser): boolean;
242
+ /** True when `input` contains a tag and parses as HTML or SVG. */
188
243
  declare const isMarkup: (input: string, Parser: typeof DOMParser) => boolean;
244
+ /** Parses `markup` (HTML or SVG) and returns its top-level nodes, detached from any live document. */
189
245
  declare function renderMarkup(markup: string, Parser: typeof DOMParser, doc?: Document): ChildNode[];
246
+ /** The `DOMParser` MIME type for `input`: `image/svg+xml` for SVG documents, else `text/html`. */
190
247
  declare function getMimeType(input: string, Parser: typeof DOMParser): DOMParserSupportedType;
248
+ /**
249
+ * The content fragment of a `<template>` element, where the parser puts its markup; `undefined` for other elements.
250
+ * morph reads, creates and patches a template's children there, so markup such as defuss-i18n's translation
251
+ * templates survives a morph.
252
+ */
253
+ declare const templateContent: (element: Element) => DocumentFragment | undefined;
191
254
  /**
192
255
  * Converts a DOM node to a VNode structure for use with updateDomWithVdom.
193
256
  * This allows us to leverage the sophisticated partial update system even for Node inputs.
@@ -199,6 +262,43 @@ declare function domNodeToVNode(node: Node): VNode<VNodeAttributes> | string;
199
262
  */
200
263
  declare function htmlStringToVNodes(html: string, Parser: typeof DOMParser): Array<VNode<VNodeAttributes> | string>;
201
264
 
265
+ type TransitionType = "fade" | "slide-left" | "slide-right" | "shake" | "none";
266
+ interface TransitionStyles {
267
+ enter: Record<string, string>;
268
+ enterActive: Record<string, string>;
269
+ exit: Record<string, string>;
270
+ exitActive: Record<string, string>;
271
+ }
272
+ type TransitionsEasing = "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "step-start" | "step-end";
273
+ /** The `transition` option of `morph()`; every field is optional (see {@link DEFAULT_TRANSITION_CONFIG}). */
274
+ interface TransitionConfig {
275
+ /** Built-in animation; `"none"` keeps `morph()` synchronous. Default `"fade"`. */
276
+ type?: TransitionType;
277
+ /** Own style maps for the four phases (not used by `"fade"`). */
278
+ styles?: TransitionStyles;
279
+ /** Milliseconds per phase. Default 300. */
280
+ duration?: number;
281
+ /** CSS timing function. Default `"ease-in-out"`. */
282
+ easing?: TransitionsEasing | string;
283
+ /** Milliseconds to wait before starting. Default 0. */
284
+ delay?: number;
285
+ /** Animate the morphed element's parent (default) or the element itself. */
286
+ target?: "parent" | "self";
287
+ }
288
+ /** The four phase style maps of a built-in transition type (empty maps for an unknown type). */
289
+ declare const getTransitionStyles: (type: TransitionType, duration: number, easing?: string) => TransitionStyles;
290
+ /** Sets each CSS property of `styles` on `el` (`style.setProperty`). */
291
+ declare const applyStyles: (el: HTMLElement, styles: Record<string, string | number>) => void;
292
+ /** Defaults merged under every transition config. */
293
+ declare const DEFAULT_TRANSITION_CONFIG: TransitionConfig;
294
+ /**
295
+ * Runs `updateCallback` inside a transition on `element`. `fade` cross-fades a snapshot of the old
296
+ * content over the new one; the other types animate out, run the update, then animate in. Styles
297
+ * the transition set are restored afterwards, also when the update throws (the error propagates).
298
+ */
299
+ declare const performTransition: (element: HTMLElement, updateCallback: () => Promise<void>, config?: TransitionConfig) => Promise<void>;
300
+
301
+ /** Options of {@link morph}. */
202
302
  interface MorphOptions {
203
303
  /**
204
304
  * Optional transition to apply around the morph.
@@ -227,7 +327,217 @@ interface MorphOptions {
227
327
  */
228
328
  diff?: boolean;
229
329
  }
330
+ /**
331
+ * Morphs the children of `el` to match the given HTML string or JSX/VNode input.
332
+ *
333
+ * A string is parsed into a DOM tree via the native `DOMParser`
334
+ * of the element's own document (isomorphic: browser, happy-dom, ...);
335
+ * any other `RenderInput` (JSX, VNodes, arrays, text) is used directly.
336
+ * Both paths then morph into the existing DOM using the defuss morph algorithm:
337
+ *
338
+ * - key/id-aware node matching (`key` and `id` attributes)
339
+ * - move-not-replace semantics (preserves node identity, focus, selection
340
+ * and event listeners)
341
+ * - in-place attribute patching for same-tag elements
342
+ * - uncontrolled form-state preservation (live input values survive
343
+ * unless the new HTML explicitly controls them)
344
+ * - shadow DOM / custom element (light DOM) aware
345
+ * - re-entrant morphs on the same or an ancestor element are queued
346
+ * latest-wins and replayed after the active morph finishes
347
+ * - overlapping transitions are latest-wins too: a morph issued while a
348
+ * transition is in flight replaces the content that transition will apply
349
+ *
350
+ * Plain text input morphs as a text node.
351
+ *
352
+ * Unlike an HTML string, a VNode/JSX input can express explicit state
353
+ * (`checked: false`, `value: ""`) and carry event handlers.
354
+ *
355
+ * @param el the element whose children (or shadow root) are morphed
356
+ * @param newContent HTML string, JSX/VNode(s) or text describing the next children
357
+ * @param options `transition` animates the swap; `diff` applies a partial change-set
358
+ * @returns `undefined` without a transition (the morph is synchronous); with
359
+ * one, a Promise that resolves once the transition finished
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * import { morph } from "defuss-morph";
364
+ *
365
+ * morph(document.getElementById("app")!, "<ul><li key='a'>Hello</li></ul>");
366
+ *
367
+ * // JSX / VNodes work too (same API, same algorithm):
368
+ * morph(document.getElementById("app")!, <p>Hi</p>);
369
+ *
370
+ * // with a transition (async):
371
+ * await morph(document.getElementById("app")!, "<p>Hi</p>", {
372
+ * transition: { type: "fade", duration: 200 },
373
+ * });
374
+ * ```
375
+ */
230
376
  declare const morph: (el: Element, newContent: RenderInput, options?: MorphOptions) => void | Promise<void>;
231
377
 
232
- export { CAPTURE_ONLY_EVENTS, CLASS_ATTRIBUTE_NAME, DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE, DEFAULT_TRANSITION_CONFIG, FROM_DOM_MARKER, REF_ATTRIBUTE_NAME, XLINK_ATTRIBUTE_NAME, XMLNS_ATTRIBUTE_NAME, applyStyles, areDomNodesEqual, clearDelegatedEvents, clearDelegatedEventsDeep, domNodeToVNode, getMimeType, getRegisteredEventKeys, getRegisteredEventTypes, getRenderer, getTransitionStyles, handleLifecycleEventsForOnMount, htmlStringToVNodes, isHTML, isMarkup, isSVG, morph, nsMap, observeUnmount, parseDOM, parseEventPropName, performTransition, queueCallback, registerDelegatedEvent, removeDelegatedEvent, removeDelegatedEventByKey, renderMarkup, replaceDomWithVdom, resolveGlobals, updateDomWithVdom };
233
- export type { DefussKey, DelegatedEventOptions, DelegatedPhase, DomAbstractionImpl, Globals, MorphMode, MorphOptions, MountHandler, ParsedEventProp, RefLike, RenderInput, TransitionConfig, TransitionStyles, TransitionType, TransitionsEasing, UnmountHandler, VNode, VNodeAttributes, VNodeChild, VNodeChildren, VNodeType, ValidChild };
378
+ /**
379
+ * Why: morph() takes VNodes, and JSX is the readable way to write them, but a JSX compiler needs a runtime to call.
380
+ * Without one here, using `<Reactive>` or a ref meant installing the whole defuss framework for its `jsx()`. This
381
+ * is the smallest runtime that fits morph: it returns plain `{ type, attributes, children }` objects, calls function
382
+ * components at once (there is no component tree to keep, the next render is a new call) and has no state.
383
+ *
384
+ * - automatic runtime: `jsxImportSource: "defuss-morph"` (TypeScript, Bun, esbuild, Vite) imports `jsx`, `jsxs` and
385
+ * `Fragment` from `defuss-morph/jsx-runtime` (`jsxDEV` from `defuss-morph/jsx-dev-runtime`);
386
+ * - classic runtime and pages without a build step: `h(type, props, ...children)`.
387
+ *
388
+ * VERIFIED: TypeScript, Bun, esbuild and Vite take `jsxImportSource` from tsconfig.json (probed 2026-11-01; Bun
389
+ * imports `jsx-dev-runtime` by default, so both subpaths exist); scripts/e2e/consumer.tsx type-checks JSX against
390
+ * the packed declarations in every `make e2e`.
391
+ */
392
+ /** A function component: called with its props (including `children`, `ref` and `key`), returns what to render. */
393
+ type Component<P = any> = (props: P) => RenderInput;
394
+ type Props = {
395
+ [name: string]: unknown;
396
+ children?: unknown;
397
+ key?: DefussKey;
398
+ };
399
+ /** `<>…</>`: its children, rendered in place of the fragment. */
400
+ declare const Fragment: (props: {
401
+ children?: unknown;
402
+ }) => RenderInput;
403
+ /**
404
+ * The automatic-runtime factory: `jsx(type, props, key)`. A tag name gives a VNode; a function component is called
405
+ * with `props` (children included). A `key` given to a component lands on the element it returns, unless that
406
+ * element has one, so `items.map((item) => <Row key={item.id} item={item} />)` is keyed for morph.
407
+ */
408
+ declare function jsx(type: string | Component, props?: Props | null, key?: DefussKey): RenderInput;
409
+ /** Same as {@link jsx}; compilers call it for elements with several static children. */
410
+ declare const jsxs: typeof jsx;
411
+ /** Same as {@link jsx}; compilers call it in development mode. The extra source arguments are ignored. */
412
+ declare const jsxDEV: (type: string | Component, props?: Props | null, key?: DefussKey) => RenderInput;
413
+ /**
414
+ * The classic factory, for `jsxFactory: "h"` and for pages without a build step:
415
+ * `h("button", { onClick: save }, "Save")` is `<button onClick={save}>Save</button>`.
416
+ */
417
+ declare function h(type: string | Component, props?: Props | null, ...children: unknown[]): RenderInput;
418
+ /**
419
+ * JSX types for `jsxImportSource: "defuss-morph"`. Loose on purpose: any tag, any attribute. A JSX expression is a
420
+ * `RenderInput` (a fragment is an array), and a component may return anything morph renders.
421
+ */
422
+ declare namespace JSX {
423
+ type Element = RenderInput;
424
+ type ElementType = string | Component;
425
+ interface ElementChildrenAttribute {
426
+ children: {};
427
+ }
428
+ interface IntrinsicAttributes {
429
+ key?: DefussKey;
430
+ }
431
+ interface IntrinsicElements {
432
+ [tagName: string]: {
433
+ [attribute: string]: any;
434
+ };
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Why a plain object and not defuss's store-backed Ref: morph only ever assigns `current`, and the forwardRef
440
+ * pattern needs one free slot for a component's API, so `{ current, state }` is all a ref has to be; defuss's richer
441
+ * Ref stays assignable to it (RefLike). VERIFIED: src/reactive.test.ts keeps `current` on the same node across
442
+ * morphs and drives a component through `ref.state`.
443
+ *
444
+ * A reference to a rendered element. morph sets `current` when it creates or patches the element that carries
445
+ * `ref={...}`; a morph that keeps the element keeps `current` pointing at the same node. `state` is free for the
446
+ * component that owns the element: the forwardRef pattern puts the component's API there, for its parent to call.
447
+ */
448
+ interface Ref<T extends Element = HTMLElement, S = unknown> extends RefLike {
449
+ current: T | null;
450
+ state?: S;
451
+ }
452
+ /**
453
+ * Creates an empty {@link Ref}. `current` stays `null` until morph renders the element that carries it, so read it
454
+ * in an event handler, in `onMount` or after the morph call, not in the component body.
455
+ */
456
+ declare const createRef: <T extends Element = HTMLElement, S = unknown>(state?: S) => Ref<T, S>;
457
+ /** True for objects with a `current` property, the shape morph assigns refs to. */
458
+ declare const isRef: (value: unknown) => value is RefLike;
459
+
460
+ /**
461
+ * Why: re-rendering a region whenever a store changes is the loop most pages need, and morph makes the naive
462
+ * version correct: render the whole region again and morph it in, and every node that stays keeps its focus, typed
463
+ * input and listeners. Three entry points share one core, as in defuss: `reactive()` for a known element or ref,
464
+ * `<Reactive />` for a JSX tree, and defuss-query's `df$(selector).reactive()` for markup already in the page.
465
+ */
466
+ /**
467
+ * What `reactive()` subscribes to: a defuss-store store (`createStore()`, `computed()`), or anything whose
468
+ * `subscribe(listener)` returns an unsubscribe function or an object with `unsubscribe()`.
469
+ */
470
+ interface Subscribable {
471
+ subscribe(listener: (...args: any[]) => unknown): unknown;
472
+ }
473
+ /** Configuration of {@link reactive} and {@link Reactive}. */
474
+ interface ReactiveConfig {
475
+ /** One store, or several: a change in any of them re-renders. */
476
+ store: Subscribable | readonly Subscribable[];
477
+ /** Returns the region's next content: JSX/VNodes or an HTML string. Keep it pure; it may run more often than the stores change. */
478
+ render: () => RenderInput;
479
+ /** Runs once, when the region is disposed (the returned function, `signal` or the `<Reactive>` element's unmount). */
480
+ cleanup?: () => void;
481
+ /** Disposes the region when aborted: the way to end a `df$(...).reactive()` binding, which returns the selection. */
482
+ signal?: AbortSignal;
483
+ }
484
+ /** Where {@link reactive} renders: an element, a ref to one, or a list of elements (a defuss-query selection, a NodeList). */
485
+ type ReactiveTarget = Element | RefLike | ArrayLike<Element>;
486
+ /**
487
+ * Renders `config.render()` into `target` now, then again whenever a store changes, by morphing it in: nodes that
488
+ * stay keep their identity, focus, typed input and listeners. The first render runs before the subscriptions start.
489
+ * Returns the dispose function: it unsubscribes from every store and runs `config.cleanup` once.
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * import { reactive } from "defuss-morph";
494
+ * import { createStore } from "defuss-store";
495
+ *
496
+ * const cart = createStore({ count: 0 });
497
+ * const dispose = reactive({ store: cart, render: () => `<p>${cart.value.count} items</p>` }, cartEl);
498
+ * cart.set("count", 2); // <p>2 items</p>, the same <p> as before
499
+ * ```
500
+ */
501
+ declare function reactive(config: ReactiveConfig, target: ReactiveTarget): () => void;
502
+ /** Props of {@link Reactive}: the {@link ReactiveConfig}, the wrapper's tag and any attribute for the wrapper. */
503
+ interface ReactiveProps extends Omit<ReactiveConfig, "signal"> {
504
+ /** The wrapper element's tag. Default: `"div"`. */
505
+ tag?: string;
506
+ /** Set to the wrapper element. */
507
+ ref?: RefLike;
508
+ [attribute: string]: unknown;
509
+ }
510
+ /**
511
+ * A JSX component that re-renders its content whenever a store changes. It renders a wrapper element (`tag`,
512
+ * default `div`) whose children are `render()`; other props go to the wrapper. The subscription starts when the
513
+ * wrapper is mounted and ends when it leaves the document, which also runs `cleanup`. A parent re-render passes the
514
+ * latest `render` and `store`, and the children it produces are morphed in like any other render.
515
+ *
516
+ * @example
517
+ * ```tsx
518
+ * import { Reactive } from "defuss-morph";
519
+ * import { createStore } from "defuss-store";
520
+ *
521
+ * const counter = createStore({ count: 0 });
522
+ *
523
+ * <Reactive tag="section" store={counter} render={() => (
524
+ * <button onClick={() => counter.set("count", counter.value.count + 1)}>{counter.value.count}</button>
525
+ * )} />
526
+ * ```
527
+ */
528
+ declare function Reactive(props: ReactiveProps): VNode;
529
+ /** The `.reactive()` method {@link installReactive} adds to every defuss-query selection. */
530
+ interface ReactiveQueryMethod {
531
+ /** Calls {@link reactive} with this selection as the target and returns the selection; end it with `config.signal`. */
532
+ reactive(config: ReactiveConfig): this;
533
+ }
534
+ /**
535
+ * Adds `.reactive(config)` to defuss-query's selections (`df$.fn`, the shared prototype), so markup already in the
536
+ * page becomes reactive by selector: `df$("#cart").reactive({ store, render })`. Idempotent; returns `df$`. The CDN
537
+ * bundle calls it itself when it loads after defuss-query; with defuss-morph loaded first, call
538
+ * `df$.installReactive(df$)` once defuss-query has loaded.
539
+ */
540
+ declare const installReactive: <Q>(df$: Q) => Q;
541
+
542
+ export { CAPTURE_ONLY_EVENTS, CLASS_ATTRIBUTE_NAME, COMMENT_TYPE, DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE, DEFAULT_TRANSITION_CONFIG, FROM_DOM_MARKER, Fragment, JSX, REF_ATTRIBUTE_NAME, Reactive, XLINK_ATTRIBUTE_NAME, XMLNS_ATTRIBUTE_NAME, applyStyles, areDomNodesEqual, clearDelegatedEvents, clearDelegatedEventsDeep, createRef, domNodeToVNode, getMimeType, getRegisteredEventKeys, getRegisteredEventTypes, getRenderer, getTransitionStyles, h, handleLifecycleEventsForOnMount, htmlStringToVNodes, installReactive, isCommentVNode, isHTML, isMarkup, isRef, isSVG, jsx, jsxDEV, jsxs, morph, nsMap, observeUnmount, onClearDelegatedEvents, parseDOM, parseEventPropName, performTransition, queueCallback, reactive, registerDelegatedEvent, removeDelegatedEvent, removeDelegatedEventByKey, renderMarkup, replaceDomWithVdom, resolveGlobals, templateContent, updateDomWithVdom };
543
+ export type { CommentVNode, Component, DefussKey, DelegatedEventOptions, DelegatedPhase, DomAbstractionImpl, Globals, MorphMode, MorphOptions, MountHandler, ParsedEventProp, ReactiveConfig, ReactiveProps, ReactiveQueryMethod, ReactiveTarget, Ref, RefLike, RenderInput, Subscribable, TransitionConfig, TransitionStyles, TransitionType, TransitionsEasing, UnmountHandler, VNode, VNodeAttributes, VNodeChild, VNodeChildren, VNodeType, ValidChild };