@mindees/renderer 0.10.0 → 0.12.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/dom.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dom.d.ts","names":[],"sources":["../src/dom.ts"],"mappings":";;;;UAgBU,WAAA;EACR,aAAA,CAAc,GAAA,WAAc,UAAA;EAC5B,cAAA,CAAe,IAAA,WAAe,OAAO;AAAA;AAAA,UAE7B,OAAA;EACR,UAAA,EAAY,OAAA;EACZ,WAAA,EAAa,OAAA;EACb,QAAA;EACA,WAAA,CAAY,KAAA,EAAO,OAAA,GAAU,OAAA;EAC7B,YAAA,CAAa,IAAA,EAAM,OAAA,EAAS,GAAA,EAAK,OAAA,UAAiB,OAAA;AAAA;AAAA,UAE1C,UAAA,SAAmB,OAAO;EAClC,YAAA,CAAa,IAAA,UAAc,KAAA;EAC3B,eAAA,CAAgB,IAAA;EAChB,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC1C,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC7C,KAAA,EAAO,MAAA;IAA2B,OAAA;EAAA;AAAA;AAAA,UAE1B,OAAA,SAAgB,OAAO;EAC/B,IAAI;AAAA;;iBAgBU,SAAA,CAAU,IAAY;;;;;;AA1BqB;iBAgG3C,gBAAA,CAAiB,GAAA,GAAM,WAAA,GAAc,WAAA,CAAY,OAAA"}
1
+ {"version":3,"file":"dom.d.ts","names":[],"sources":["../src/dom.ts"],"mappings":";;;;UAgBU,WAAA;EACR,aAAA,CAAc,GAAA,WAAc,UAAA;EAC5B,cAAA,CAAe,IAAA,WAAe,OAAO;AAAA;AAAA,UAE7B,OAAA;EACR,UAAA,EAAY,OAAA;EACZ,WAAA,EAAa,OAAA;EACb,QAAA;EACA,WAAA,CAAY,KAAA,EAAO,OAAA,GAAU,OAAA;EAC7B,YAAA,CAAa,IAAA,EAAM,OAAA,EAAS,GAAA,EAAK,OAAA,UAAiB,OAAA;AAAA;AAAA,UAE1C,UAAA,SAAmB,OAAO;EAClC,YAAA,CAAa,IAAA,UAAc,KAAA;EAC3B,eAAA,CAAgB,IAAA;EAChB,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC1C,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC7C,KAAA,EAAO,MAAA;IAA2B,OAAA;EAAA;AAAA;AAAA,UAE1B,OAAA,SAAgB,OAAO;EAC/B,IAAI;AAAA;;iBAiBU,SAAA,CAAU,IAAY;;;;;;AA3BqB;iBAiG3C,gBAAA,CAAiB,GAAA,GAAM,WAAA,GAAc,WAAA,CAAY,OAAA"}
package/dist/dom.js CHANGED
@@ -7,6 +7,7 @@ const TAG_ALIASES = {
7
7
  text: "span",
8
8
  image: "img",
9
9
  scrollview: "div",
10
+ horizontalscrollview: "div",
10
11
  textinput: "input",
11
12
  button: "button"
12
13
  };
package/dist/dom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["/**\n * DOM host backend — renders to real `Node`s in a browser (or any DOM, such as\n * happy-dom/jsdom in tests).\n *\n * MindeesNative element tags map to DOM via a small alias table so the same app\n * tree (`view`, `text`, …) renders to sensible HTML; unknown tags pass through\n * as custom elements. Props become attributes, except `onX` event props (added\n * as listeners) and `style` objects (applied to `style`).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\nimport { styleValue } from './css'\n\n/** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */\ninterface DomDocument {\n createElement(tag: string): DomElement\n createTextNode(data: string): DomText\n}\ninterface DomNode {\n parentNode: DomNode | null\n nextSibling: DomNode | null\n nodeType: number\n removeChild(child: DomNode): DomNode\n insertBefore(node: DomNode, ref: DomNode | null): DomNode\n}\ninterface DomElement extends DomNode {\n setAttribute(name: string, value: string): void\n removeAttribute(name: string): void\n addEventListener(type: string, listener: (e: unknown) => void): void\n removeEventListener(type: string, listener: (e: unknown) => void): void\n style: Record<string, string> & { cssText: string }\n}\ninterface DomText extends DomNode {\n data: string\n}\n\nconst TEXT_NODE = 3\n\n/** Tag aliases: MindeesNative semantic tags → HTML elements on the web target. */\nconst TAG_ALIASES: Record<string, string> = {\n view: 'div',\n text: 'span',\n image: 'img',\n scrollview: 'div',\n textinput: 'input',\n button: 'button',\n}\n\n/** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */\nexport function domTagFor(type: string): string {\n return TAG_ALIASES[type] ?? type\n}\n\n/**\n * Inject the keyframes the `activityindicator` spinner needs, once per document. Uses a\n * fuller DOM shape than {@link DomDocument}; on minimal/headless documents (no `head`) it's a\n * no-op (tests don't need the animation, and the element still renders).\n */\nfunction ensureSpinnerKeyframes(document: DomDocument): void {\n const doc = document as unknown as {\n getElementById?: (id: string) => unknown\n head?: { appendChild: (node: unknown) => void }\n createElement: (tag: string) => { id: string; textContent: string }\n }\n if (!doc.head || typeof doc.getElementById !== 'function') return\n if (doc.getElementById('mindees-keyframes')) return\n const style = doc.createElement('style')\n style.id = 'mindees-keyframes'\n style.textContent = '@keyframes mindees-spin{to{transform:rotate(360deg)}}'\n doc.head.appendChild(style)\n}\n\n/**\n * Build the web `activityindicator`: a CSS border-spinner. Size comes from the element's\n * `width`/`height` style and the active arc from its `color` (`currentColor`), both applied\n * normally afterwards — so the backend owns only the animation + ring shape.\n */\nfunction createSpinner(document: DomDocument): DomElement {\n ensureSpinnerKeyframes(document)\n const el = document.createElement('div')\n const s = el.style\n s.boxSizing = 'border-box'\n s.display = 'inline-block'\n s.borderStyle = 'solid'\n s.borderWidth = '3px'\n s.borderColor = 'rgba(127, 127, 127, 0.30)'\n s.borderTopColor = 'currentColor'\n s.borderRadius = '50%'\n s.animation = 'mindees-spin 0.8s linear infinite'\n return el\n}\n\nfunction isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/**\n * Form-control props that must be written as the live DOM **property** (not an attribute). The\n * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and\n * attribute diverge, so a controlled update must set the property to change what's shown.\n */\nconst FORM_PROPERTIES = new Set(['value', 'checked', 'selected', 'indeterminate'])\n\n/** `onClick` → `click`, `onPointerDown` → `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/** Listeners we've attached, so reactive updates can swap them cleanly. */\nconst listeners = new WeakMap<object, Map<string, (e: unknown) => void>>()\n\n/**\n * Create a {@link HostBackend} that renders to real DOM nodes.\n *\n * @param doc - The document to create nodes with. Defaults to the global\n * `document`; pass a happy-dom/jsdom document for headless tests.\n */\nexport function createDomBackend(doc?: DomDocument): HostBackend<DomNode> {\n const documentRef = doc ?? (globalThis as unknown as { document?: DomDocument }).document\n if (!documentRef) {\n throw new Error(\n 'createDomBackend: no document available (pass one explicitly outside a browser)',\n )\n }\n const document = documentRef\n let overlay: DomElement | null = null // lazily-created portal overlay layer (see overlayRoot)\n\n return {\n createElement: (type) =>\n type === 'activityindicator'\n ? createSpinner(document)\n : document.createElement(domTagFor(type)),\n createText: (value) => document.createTextNode(value),\n\n setProp(node, key, value, prev): void {\n const el = node as DomElement\n if (isEventProp(key)) {\n const event = eventNameFor(key)\n let map = listeners.get(el)\n if (!map) {\n map = new Map()\n listeners.set(el, map)\n }\n const old = map.get(event)\n if (old) el.removeEventListener(event, old)\n if (typeof value === 'function') {\n const fn = value as (e: unknown) => void\n el.addEventListener(event, fn)\n map.set(event, fn)\n } else {\n map.delete(event)\n }\n return\n }\n if (key === 'style') {\n const style = el.style\n const next = value && typeof value === 'object' ? (value as Record<string, unknown>) : null\n const prevObj = prev && typeof prev === 'object' ? (prev as Record<string, unknown>) : null\n // Clear keys present in the previous style object but absent from the\n // new one, so stale inline styles don't persist across reactive updates.\n if (prevObj) {\n for (const prop of Object.keys(prevObj)) {\n if (!next || !(prop in next)) style[prop] = ''\n }\n }\n if (next) {\n for (const [prop, v] of Object.entries(next)) {\n // Nullish → unset (don't write the literal \"undefined\"/\"null\").\n style[prop] = v === null || v === undefined ? '' : styleValue(prop, v)\n }\n }\n return\n }\n if (FORM_PROPERTIES.has(key)) {\n // Live property, so a controlled `value`/`checked` updates what the user sees.\n ;(el as unknown as Record<string, unknown>)[key] =\n value === null || value === undefined ? '' : value\n return\n }\n if (value === false || value === null || value === undefined) {\n el.removeAttribute(key)\n } else {\n el.setAttribute(key, value === true ? '' : String(value))\n }\n void prev\n },\n\n setText(node, value): void {\n ;(node as DomText).data = value\n },\n\n insert(parent, node, anchor): void {\n ;(parent as DomNode).insertBefore(node, anchor)\n },\n\n remove(parent, node): void {\n ;(parent as DomNode).removeChild(node)\n },\n\n parentOf: (node) => node.parentNode,\n nextSibling: (node) => node.nextSibling,\n isText: (node) => node.nodeType === TEXT_NODE,\n\n overlayRoot(): DomNode | null {\n if (overlay) return overlay\n // Lazily create ONE overlay layer on <body> (fallback <html>) — zero-config portals.\n const host = document as unknown as {\n body?: { appendChild(n: DomNode): void }\n documentElement?: { appendChild(n: DomNode): void }\n }\n const mount = host.body ?? host.documentElement\n if (!mount) return null // no DOM host (e.g. a bare document) → portal falls back in place\n const layer = document.createElement('div')\n layer.setAttribute('data-mindees-overlay', '')\n mount.appendChild(layer)\n overlay = layer\n return overlay\n },\n }\n}\n\nexport type { DomDocument, DomElement, DomNode, DomText }\n"],"mappings":"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"}
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["/**\n * DOM host backend — renders to real `Node`s in a browser (or any DOM, such as\n * happy-dom/jsdom in tests).\n *\n * MindeesNative element tags map to DOM via a small alias table so the same app\n * tree (`view`, `text`, …) renders to sensible HTML; unknown tags pass through\n * as custom elements. Props become attributes, except `onX` event props (added\n * as listeners) and `style` objects (applied to `style`).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\nimport { styleValue } from './css'\n\n/** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */\ninterface DomDocument {\n createElement(tag: string): DomElement\n createTextNode(data: string): DomText\n}\ninterface DomNode {\n parentNode: DomNode | null\n nextSibling: DomNode | null\n nodeType: number\n removeChild(child: DomNode): DomNode\n insertBefore(node: DomNode, ref: DomNode | null): DomNode\n}\ninterface DomElement extends DomNode {\n setAttribute(name: string, value: string): void\n removeAttribute(name: string): void\n addEventListener(type: string, listener: (e: unknown) => void): void\n removeEventListener(type: string, listener: (e: unknown) => void): void\n style: Record<string, string> & { cssText: string }\n}\ninterface DomText extends DomNode {\n data: string\n}\n\nconst TEXT_NODE = 3\n\n/** Tag aliases: MindeesNative semantic tags → HTML elements on the web target. */\nconst TAG_ALIASES: Record<string, string> = {\n view: 'div',\n text: 'span',\n image: 'img',\n scrollview: 'div',\n horizontalscrollview: 'div',\n textinput: 'input',\n button: 'button',\n}\n\n/** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */\nexport function domTagFor(type: string): string {\n return TAG_ALIASES[type] ?? type\n}\n\n/**\n * Inject the keyframes the `activityindicator` spinner needs, once per document. Uses a\n * fuller DOM shape than {@link DomDocument}; on minimal/headless documents (no `head`) it's a\n * no-op (tests don't need the animation, and the element still renders).\n */\nfunction ensureSpinnerKeyframes(document: DomDocument): void {\n const doc = document as unknown as {\n getElementById?: (id: string) => unknown\n head?: { appendChild: (node: unknown) => void }\n createElement: (tag: string) => { id: string; textContent: string }\n }\n if (!doc.head || typeof doc.getElementById !== 'function') return\n if (doc.getElementById('mindees-keyframes')) return\n const style = doc.createElement('style')\n style.id = 'mindees-keyframes'\n style.textContent = '@keyframes mindees-spin{to{transform:rotate(360deg)}}'\n doc.head.appendChild(style)\n}\n\n/**\n * Build the web `activityindicator`: a CSS border-spinner. Size comes from the element's\n * `width`/`height` style and the active arc from its `color` (`currentColor`), both applied\n * normally afterwards — so the backend owns only the animation + ring shape.\n */\nfunction createSpinner(document: DomDocument): DomElement {\n ensureSpinnerKeyframes(document)\n const el = document.createElement('div')\n const s = el.style\n s.boxSizing = 'border-box'\n s.display = 'inline-block'\n s.borderStyle = 'solid'\n s.borderWidth = '3px'\n s.borderColor = 'rgba(127, 127, 127, 0.30)'\n s.borderTopColor = 'currentColor'\n s.borderRadius = '50%'\n s.animation = 'mindees-spin 0.8s linear infinite'\n return el\n}\n\nfunction isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/**\n * Form-control props that must be written as the live DOM **property** (not an attribute). The\n * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and\n * attribute diverge, so a controlled update must set the property to change what's shown.\n */\nconst FORM_PROPERTIES = new Set(['value', 'checked', 'selected', 'indeterminate'])\n\n/** `onClick` → `click`, `onPointerDown` → `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/** Listeners we've attached, so reactive updates can swap them cleanly. */\nconst listeners = new WeakMap<object, Map<string, (e: unknown) => void>>()\n\n/**\n * Create a {@link HostBackend} that renders to real DOM nodes.\n *\n * @param doc - The document to create nodes with. Defaults to the global\n * `document`; pass a happy-dom/jsdom document for headless tests.\n */\nexport function createDomBackend(doc?: DomDocument): HostBackend<DomNode> {\n const documentRef = doc ?? (globalThis as unknown as { document?: DomDocument }).document\n if (!documentRef) {\n throw new Error(\n 'createDomBackend: no document available (pass one explicitly outside a browser)',\n )\n }\n const document = documentRef\n let overlay: DomElement | null = null // lazily-created portal overlay layer (see overlayRoot)\n\n return {\n createElement: (type) =>\n type === 'activityindicator'\n ? createSpinner(document)\n : document.createElement(domTagFor(type)),\n createText: (value) => document.createTextNode(value),\n\n setProp(node, key, value, prev): void {\n const el = node as DomElement\n if (isEventProp(key)) {\n const event = eventNameFor(key)\n let map = listeners.get(el)\n if (!map) {\n map = new Map()\n listeners.set(el, map)\n }\n const old = map.get(event)\n if (old) el.removeEventListener(event, old)\n if (typeof value === 'function') {\n const fn = value as (e: unknown) => void\n el.addEventListener(event, fn)\n map.set(event, fn)\n } else {\n map.delete(event)\n }\n return\n }\n if (key === 'style') {\n const style = el.style\n const next = value && typeof value === 'object' ? (value as Record<string, unknown>) : null\n const prevObj = prev && typeof prev === 'object' ? (prev as Record<string, unknown>) : null\n // Clear keys present in the previous style object but absent from the\n // new one, so stale inline styles don't persist across reactive updates.\n if (prevObj) {\n for (const prop of Object.keys(prevObj)) {\n if (!next || !(prop in next)) style[prop] = ''\n }\n }\n if (next) {\n for (const [prop, v] of Object.entries(next)) {\n // Nullish → unset (don't write the literal \"undefined\"/\"null\").\n style[prop] = v === null || v === undefined ? '' : styleValue(prop, v)\n }\n }\n return\n }\n if (FORM_PROPERTIES.has(key)) {\n // Live property, so a controlled `value`/`checked` updates what the user sees.\n ;(el as unknown as Record<string, unknown>)[key] =\n value === null || value === undefined ? '' : value\n return\n }\n if (value === false || value === null || value === undefined) {\n el.removeAttribute(key)\n } else {\n el.setAttribute(key, value === true ? '' : String(value))\n }\n void prev\n },\n\n setText(node, value): void {\n ;(node as DomText).data = value\n },\n\n insert(parent, node, anchor): void {\n ;(parent as DomNode).insertBefore(node, anchor)\n },\n\n remove(parent, node): void {\n ;(parent as DomNode).removeChild(node)\n },\n\n parentOf: (node) => node.parentNode,\n nextSibling: (node) => node.nextSibling,\n isText: (node) => node.nodeType === TEXT_NODE,\n\n overlayRoot(): DomNode | null {\n if (overlay) return overlay\n // Lazily create ONE overlay layer on <body> (fallback <html>) — zero-config portals.\n const host = document as unknown as {\n body?: { appendChild(n: DomNode): void }\n documentElement?: { appendChild(n: DomNode): void }\n }\n const mount = host.body ?? host.documentElement\n if (!mount) return null // no DOM host (e.g. a bare document) → portal falls back in place\n const layer = document.createElement('div')\n layer.setAttribute('data-mindees-overlay', '')\n mount.appendChild(layer)\n overlay = layer\n return overlay\n },\n }\n}\n\nexport type { DomDocument, DomElement, DomNode, DomText }\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
17
17
  /** The npm package name. */
18
18
  declare const name = "@mindees/renderer";
19
19
  /** The package version. All `@mindees/*` packages share one locked version line. */
20
- declare const VERSION = "0.10.0";
20
+ declare const VERSION = "0.12.0";
21
21
  /**
22
22
  * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
23
23
  * headless backend, SSR + hydration) is implemented and tested. Native
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
16
16
  /** The npm package name. */
17
17
  const name = "@mindees/renderer";
18
18
  /** The package version. All `@mindees/*` packages share one locked version line. */
19
- const VERSION = "0.10.0";
19
+ const VERSION = "0.12.0";
20
20
  /**
21
21
  * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
22
22
  * headless backend, SSR + hydration) is implemented and tested. Native
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Host-backend contract + capability detection. */\nexport {\n type HostBackend,\n isSerializable,\n type SerializableBackend,\n} from './backend'\n/** Helix Canvas strand — a 2D scene graph driven by the reconciler, painted to a 2D context (§6.2). */\nexport {\n type Canvas2DBackend,\n createCanvas2DBackend,\n type Scene2DContext,\n type SceneNode,\n} from './canvas'\n/** DOM (web) backend. */\nexport {\n createDomBackend,\n type DomDocument,\n type DomElement,\n type DomNode,\n type DomText,\n domTagFor,\n} from './dom'\n/** Keyed list reconciliation (the renderer side of core's KeyedRegion). */\nexport { bindKeyedChild } from './for'\n/** Headless (in-memory) backend — the reference/test target. */\nexport {\n createHeadlessBackend,\n createHeadlessRoot,\n type HeadlessNode,\n isEventProp,\n} from './headless'\n/**\n * Native backends. `createNativeCommandBackend` is implemented (emits a native\n * command stream); `createNativeBackend`/`createCanvasBackend` are research\n * tracks that throw `NotImplementedError`.\n */\nexport {\n type CanvasBackend,\n createCanvasBackend,\n createNativeBackend,\n createNativeCommandBackend,\n type NativeBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native'\n/** One-call native app entry — wires the command backend + host contract. */\nexport {\n type CreateNativeAppOptions,\n createNativeApp,\n type NativeApp,\n} from './native-app'\n/**\n * The strict reference native host — applies a command stream to a model tree and\n * validates it (the executable conformance contract real native hosts implement).\n */\nexport {\n createReferenceHost,\n NativeHostError,\n type ReferenceHost,\n type ReferenceHostNode,\n} from './native-host'\n/** The native command protocol: command types + serialization-safe helpers. */\nexport {\n type CreateNodeCommand,\n type CreateTextCommand,\n createNativeNodeIdFactory,\n type DisposeNodeCommand,\n type InsertChildCommand,\n isNativeCommand,\n isNativePropValue,\n type NativeCommand,\n type NativeNodeId,\n type NativePropValue,\n normalizeNativeProp,\n type RegisterEventCommand,\n type RemoveChildCommand,\n type RemovePropCommand,\n type SetPropCommand,\n type UnregisterEventCommand,\n type UpdateTextCommand,\n} from './native-protocol'\n/** Portal reconciliation (the renderer side of core's PortalRegion). */\nexport { bindPortalChild } from './portal'\n/** The fine-grained reactive reconciler. */\nexport { type Mounted, mountNode, render } from './render'\n/** Server-side rendering + hydration (web). */\nexport { hydrate, renderToString } from './ssr'\n\n/** The npm package name. */\nexport const name = '@mindees/renderer'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.10.0'\n\n/**\n * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,\n * headless backend, SSR + hydration) is implemented and tested. Native\n * (iOS/Android) and the GPU canvas are research tracks (throw\n * `NotImplementedError`). See the repository `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;;;;AA6FA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;AAQvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** Host-backend contract + capability detection. */\nexport {\n type HostBackend,\n isSerializable,\n type SerializableBackend,\n} from './backend'\n/** Helix Canvas strand — a 2D scene graph driven by the reconciler, painted to a 2D context (§6.2). */\nexport {\n type Canvas2DBackend,\n createCanvas2DBackend,\n type Scene2DContext,\n type SceneNode,\n} from './canvas'\n/** DOM (web) backend. */\nexport {\n createDomBackend,\n type DomDocument,\n type DomElement,\n type DomNode,\n type DomText,\n domTagFor,\n} from './dom'\n/** Keyed list reconciliation (the renderer side of core's KeyedRegion). */\nexport { bindKeyedChild } from './for'\n/** Headless (in-memory) backend — the reference/test target. */\nexport {\n createHeadlessBackend,\n createHeadlessRoot,\n type HeadlessNode,\n isEventProp,\n} from './headless'\n/**\n * Native backends. `createNativeCommandBackend` is implemented (emits a native\n * command stream); `createNativeBackend`/`createCanvasBackend` are research\n * tracks that throw `NotImplementedError`.\n */\nexport {\n type CanvasBackend,\n createCanvasBackend,\n createNativeBackend,\n createNativeCommandBackend,\n type NativeBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native'\n/** One-call native app entry — wires the command backend + host contract. */\nexport {\n type CreateNativeAppOptions,\n createNativeApp,\n type NativeApp,\n} from './native-app'\n/**\n * The strict reference native host — applies a command stream to a model tree and\n * validates it (the executable conformance contract real native hosts implement).\n */\nexport {\n createReferenceHost,\n NativeHostError,\n type ReferenceHost,\n type ReferenceHostNode,\n} from './native-host'\n/** The native command protocol: command types + serialization-safe helpers. */\nexport {\n type CreateNodeCommand,\n type CreateTextCommand,\n createNativeNodeIdFactory,\n type DisposeNodeCommand,\n type InsertChildCommand,\n isNativeCommand,\n isNativePropValue,\n type NativeCommand,\n type NativeNodeId,\n type NativePropValue,\n normalizeNativeProp,\n type RegisterEventCommand,\n type RemoveChildCommand,\n type RemovePropCommand,\n type SetPropCommand,\n type UnregisterEventCommand,\n type UpdateTextCommand,\n} from './native-protocol'\n/** Portal reconciliation (the renderer side of core's PortalRegion). */\nexport { bindPortalChild } from './portal'\n/** The fine-grained reactive reconciler. */\nexport { type Mounted, mountNode, render } from './render'\n/** Server-side rendering + hydration (web). */\nexport { hydrate, renderToString } from './ssr'\n\n/** The npm package name. */\nexport const name = '@mindees/renderer'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.12.0'\n\n/**\n * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,\n * headless backend, SSR + hydration) is implemented and tested. Native\n * (iOS/Android) and the GPU canvas are research tracks (throw\n * `NotImplementedError`). See the repository `STATUS.md`.\n */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;;;;AA6FA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;;;;;;AAQvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
@@ -6,8 +6,14 @@ import { MindeesNode } from "@mindees/core";
6
6
  interface NativeApp {
7
7
  /** Mount the app and flush the initial command batch to the host. */
8
8
  start(): void;
9
- /** Invoke a registered handler (a native event fired), then flush the resulting batch. */
10
- dispatchEvent(handlerId: string, event?: unknown): boolean;
9
+ /**
10
+ * Invoke a registered handler (a native event fired), then flush the resulting batch.
11
+ * `dispatchEvent(handlerId)` for notify-only events (press/click); `dispatchEvent(handlerId, value)`
12
+ * for value-carrying events (text change) — JS wraps `value` as `{ target: { value } }` so handlers
13
+ * read it via the standard event shape. The host passes the RAW string value (never the object);
14
+ * an absent/null value (no second arg) leaves notify-only behavior unchanged.
15
+ */
16
+ dispatchEvent(handlerId: string, value?: string | null): boolean;
11
17
  /**
12
18
  * Advance animations by one frame: forward the host's vsync timestamp (ms) to the animation
13
19
  * engine, then flush the resulting command batch. The host calls this each vsync while the frame
@@ -1 +1 @@
1
- {"version":3,"file":"native-app.d.ts","names":[],"sources":["../src/native-app.ts"],"mappings":";;;;;UAmDiB,SAAA;EA2Ed;EAzED,KAAA;EAyEU;EAvEV,aAAA,CAAc,SAAA,UAAmB,KAAA;EAqEjC;;;;;EA/DA,SAAA,CAAU,KAAA;AAAA;;UASK,sBAAA;;;;;WAKN,MAAA,GAAS,YAAY;;;;;WAKrB,IAAA,IAAQ,IAAA;;;;;;WAMR,MAAA;;;;;;;;WAQA,SAAA;;;;;;;WAOA,WAAA;AAAA;;;;;iBAsBK,eAAA,CACd,IAAA,EAAM,WAAA,EACN,OAAA,GAAS,sBAAA,GACR,SAAA"}
1
+ {"version":3,"file":"native-app.d.ts","names":[],"sources":["../src/native-app.ts"],"mappings":";;;;;UAmDiB,SAAA;EAiFd;EA/ED,KAAA;EA+EU;;;;;;;EAvEV,aAAA,CAAc,SAAA,UAAmB,KAAA;;;;;;EAMjC,SAAA,CAAU,KAAA;AAAA;;UASK,sBAAA;;;;;WAKN,MAAA,GAAS,YAAY;;;;;WAKrB,IAAA,IAAQ,IAAA;;;;;;WAMR,MAAA;;;;;;;;WAQA,SAAA;;;;;;;WAOA,WAAA;AAAA;;;;;iBAsBK,eAAA,CACd,IAAA,EAAM,WAAA,EACN,OAAA,GAAS,sBAAA,GACR,SAAA"}
@@ -100,7 +100,8 @@ function createNativeApp(root, options = {}) {
100
100
  render(root, backend, backend.root);
101
101
  flush();
102
102
  },
103
- dispatchEvent(handlerId, event) {
103
+ dispatchEvent(handlerId, value) {
104
+ const event = value === void 0 || value === null ? void 0 : { target: { value } };
104
105
  const handled = backend.dispatchEvent(handlerId, event);
105
106
  flush();
106
107
  return handled;
@@ -1 +1 @@
1
- {"version":3,"file":"native-app.js","names":[],"sources":["../src/native-app.ts"],"sourcesContent":["/**\n * `createNativeApp` — the one-call entry point for a MindeesNative app on an embedded\n * native host. It hides the wiring an app author should never have to write: creating\n * the {@link createNativeCommandBackend}, rendering the root, flushing the command\n * batch to the host, and exposing the start/dispatch/frame contract the host calls.\n *\n * ```tsx\n * import { createNativeApp } from '@mindees/renderer'\n * import { App } from './App'\n *\n * createNativeApp(<App />)\n * ```\n *\n * That's the whole entry file. The native host (see `examples/native-hosts/`) injects a\n * `MindeesHost.emit(json)` global and calls `MindeesApp.start()` once, then\n * `MindeesApp.dispatchEvent(handlerId)` per native event — both of which this wires.\n *\n * On a host it also makes animations + concurrency **work by default**: it installs a\n * reactive {@link Scheduler} (so `startTransition`/`deferred`/normal-lane effects run) and a\n * vsync-driven {@link FrameSource} (so `timing`/`spring`/gesture animations advance). The host\n * drives frames by calling `MindeesApp.frameTick(nowMs)` each vsync, and the engine signals when\n * to start/stop that loop through a `MindeesHostFrame.setFrameLoopActive(boolean)` global — so the\n * vsync loop runs **only while something is animating** (battery-friendly), tied to the animation\n * engine's own arm/sleep. With no host (SSR / Node / tests) nothing is installed and animations\n * jump straight to their final value, exactly as before.\n *\n * @module\n */\n\nimport {\n createScheduler,\n type FrameSource,\n type MindeesNode,\n setFrameSource,\n setReactiveScheduler,\n} from '@mindees/core'\nimport { createNativeCommandBackend } from './native-command-backend'\nimport type { NativeNodeId } from './native-protocol'\nimport { render } from './render'\n\n// The reactive scheduler + animation frame source are PROCESS globals (one per runtime). This guard\n// makes a second `createNativeApp` that would re-wire them fail loudly instead of silently stealing\n// the first app's engines. Reset with `_resetNativeAppEngines()` between tests.\nlet enginesWired = false\n\n/** @internal Test-only: reset the process-global engine-wiring guard. */\nexport function _resetNativeAppEngines(): void {\n enginesWired = false\n}\n\n/** The contract a native host drives: render once, dispatch events, and tick frames. */\nexport interface NativeApp {\n /** Mount the app and flush the initial command batch to the host. */\n start(): void\n /** Invoke a registered handler (a native event fired), then flush the resulting batch. */\n dispatchEvent(handlerId: string, event?: unknown): boolean\n /**\n * Advance animations by one frame: forward the host's vsync timestamp (ms) to the animation\n * engine, then flush the resulting command batch. The host calls this each vsync while the frame\n * loop is active (see {@link CreateNativeAppOptions}). A no-op when no frame source is installed.\n */\n frameTick(nowMs: number): void\n}\n\n/** The JS→host battery signal: the engine asks the host to run / stop its vsync loop. */\ninterface HostFrameApi {\n setFrameLoopActive?: (active: boolean) => void\n}\n\n/** Options for {@link createNativeApp}. */\nexport interface CreateNativeAppOptions {\n /**\n * Id of the host's pre-existing root container. Defaults to `\"host-root\"` — the\n * convention the reference hosts register — so the common case needs no config.\n */\n readonly rootId?: NativeNodeId\n /**\n * How to deliver a command batch to the host. Defaults to `globalThis.MindeesHost.emit`\n * (what the embedded hosts inject). Override in tests or alternative transports.\n */\n readonly emit?: (json: string) => void\n /**\n * Expose the app on a global so the host can call `start()`/`dispatchEvent()`/`frameTick()`.\n * `true` (default) → `globalThis.MindeesApp`; a string → that global name; `false` →\n * don't expose (use the returned handle directly, e.g. in Node tests).\n */\n readonly expose?: boolean | string\n /**\n * Install the reactive scheduler that powers `startTransition`/`deferred`/normal-lane effects.\n * `createNativeApp` owns this scheduler so it can flush the command batch after each microtask\n * drain (otherwise deferred mutations would recompute but never reach the host). Pass `false` to\n * run the pure synchronous lane; to use a fully custom scheduler, pass `false` and call\n * `setReactiveScheduler` yourself (you then own flushing).\n */\n readonly scheduler?: false\n /**\n * Install the reactive scheduler + the vsync frame source. Default: `true` when the app is\n * exposed AND a host (`globalThis.MindeesHost`) is present — so SSR/Node/tests install nothing\n * and keep the synchronous jump-to-final behavior. Pass `true` to force-wire (e.g. tests driving\n * `frameTick` directly).\n */\n readonly wireEngines?: boolean\n}\n\nfunction defaultEmit(json: string): void {\n const host = (globalThis as { MindeesHost?: { emit?: (json: string) => void } }).MindeesHost\n if (!host || typeof host.emit !== 'function') {\n throw new Error(\n 'createNativeApp: no `emit` was provided and globalThis.MindeesHost.emit is unavailable',\n )\n }\n host.emit(json)\n}\n\nfunction hostIsPresent(): boolean {\n const host = (globalThis as { MindeesHost?: { emit?: unknown } }).MindeesHost\n return typeof host?.emit === 'function'\n}\n\n/**\n * Wire a root node to a native command host. Returns the {@link NativeApp} handle and\n * (unless `expose: false`) publishes it as `globalThis.MindeesApp` for the host to call.\n */\nexport function createNativeApp(\n root: MindeesNode,\n options: CreateNativeAppOptions = {},\n): NativeApp {\n const backend = createNativeCommandBackend({ rootId: options.rootId ?? 'host-root' })\n const emit = options.emit ?? defaultEmit\n\n const flush = (): void => {\n const batch = backend.flushCommands()\n if (batch.length > 0) emit(JSON.stringify(batch))\n }\n\n const expose = options.expose ?? true\n const wireEngines = options.wireEngines ?? (expose !== false && hostIsPresent())\n\n // The engine's onFrame, captured when the animation loop subscribes; `frameTick` drives it.\n let storedTick: ((nowMs: number) => void) | null = null\n\n if (wireEngines) {\n if (enginesWired) {\n // The reactive scheduler + frame source are process globals; a second wiring would silently\n // steal the first app's engines. Fail loudly instead. (One app per runtime; tests reset via\n // _resetNativeAppEngines.)\n throw new Error(\n 'createNativeApp: the reactive engines are already wired by another app instance. Create one ' +\n 'app per runtime, or pass `wireEngines: false` (and wire the scheduler/frame source yourself).',\n )\n }\n enginesWired = true\n\n // A microtask-drained scheduler. After each drained task we run ONE coalesced trailing flush so\n // deferred/startTransition/normal-lane tree mutations (which land on a microtask, outside the\n // frameTick + dispatchEvent windows) still reach the host — one frame late, never dropped. We\n // OWN this scheduler so the trailing flush is always wired (a custom scheduler couldn't be).\n if (options.scheduler !== false) {\n let flushQueued = false\n const trailingFlush = (): void => {\n if (flushQueued) return\n flushQueued = true\n queueMicrotask(() => {\n flushQueued = false\n flush()\n })\n }\n setReactiveScheduler(\n createScheduler({\n scheduleMicrotask: (cb) =>\n queueMicrotask(() => {\n cb()\n trailingFlush()\n }),\n onError: (error) => {\n // Surface a scheduled-task error instead of swallowing it (uncaught → host log).\n queueMicrotask(() => {\n throw error\n })\n },\n }),\n )\n }\n\n // The vsync frame source: capture the engine's tick + signal the host to run/stop its loop. The\n // subscribe (START) fires the instant the first animation driver arms the loop; the unsubscribe\n // (STOP) fires the instant the last driver settles — so the host's vsync loop runs ONLY while\n // something animates (the battery win), with no separate heuristic to keep in sync.\n //\n // CRITICAL: only install the source when a host can actually DRIVE it (MindeesHostFrame present,\n // i.e. there's a `frameTick` caller + a `setFrameLoopActive` listener). Arming a loop that\n // nothing ticks would FREEZE animations at their start value — strictly worse than jumping to\n // the final value. With no driver, leave the source null → animations jump-to-final (safe).\n const hostFrame = (globalThis as { MindeesHostFrame?: HostFrameApi }).MindeesHostFrame\n if (hostFrame) {\n const frameSource: FrameSource = (tick) => {\n storedTick = tick\n try {\n hostFrame.setFrameLoopActive?.(true)\n } catch {\n // a throwing host signal must not break arming the loop\n }\n return () => {\n storedTick = null\n try {\n hostFrame.setFrameLoopActive?.(false)\n } catch {\n // ditto: always stop the loop even if the host signal throws\n }\n }\n }\n setFrameSource(frameSource)\n }\n }\n\n const app: NativeApp = {\n start(): void {\n render(root, backend, backend.root)\n flush()\n },\n dispatchEvent(handlerId: string, event?: unknown): boolean {\n const handled = backend.dispatchEvent(handlerId, event)\n flush()\n return handled\n },\n frameTick(nowMs: number): void {\n if (storedTick) storedTick(nowMs)\n flush()\n },\n }\n\n if (expose !== false) {\n const name = typeof expose === 'string' ? expose : 'MindeesApp'\n ;(globalThis as Record<string, unknown>)[name] = app\n }\n\n return app\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAI,eAAe;AA6DnB,SAAS,YAAY,MAAoB;CACvC,MAAM,OAAQ,WAAmE;CACjF,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,YAChC,MAAM,IAAI,MACR,wFACF;CAEF,KAAK,KAAK,IAAI;AAChB;AAEA,SAAS,gBAAyB;CAEhC,OAAO,OADO,WAAoD,aAC9C,SAAS;AAC/B;;;;;AAMA,SAAgB,gBACd,MACA,UAAkC,CAAC,GACxB;CACX,MAAM,UAAU,2BAA2B,EAAE,QAAQ,QAAQ,UAAU,YAAY,CAAC;CACpF,MAAM,OAAO,QAAQ,QAAQ;CAE7B,MAAM,cAAoB;EACxB,MAAM,QAAQ,QAAQ,cAAc;EACpC,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,UAAU,KAAK,CAAC;CAClD;CAEA,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,cAAc,QAAQ,gBAAgB,WAAW,SAAS,cAAc;CAG9E,IAAI,aAA+C;CAEnD,IAAI,aAAa;EACf,IAAI,cAIF,MAAM,IAAI,MACR,2LAEF;EAEF,eAAe;EAMf,IAAI,QAAQ,cAAc,OAAO;GAC/B,IAAI,cAAc;GAClB,MAAM,sBAA4B;IAChC,IAAI,aAAa;IACjB,cAAc;IACd,qBAAqB;KACnB,cAAc;KACd,MAAM;IACR,CAAC;GACH;GACA,qBACE,gBAAgB;IACd,oBAAoB,OAClB,qBAAqB;KACnB,GAAG;KACH,cAAc;IAChB,CAAC;IACH,UAAU,UAAU;KAElB,qBAAqB;MACnB,MAAM;KACR,CAAC;IACH;GACF,CAAC,CACH;EACF;EAWA,MAAM,YAAa,WAAmD;EACtE,IAAI,WAAW;GACb,MAAM,eAA4B,SAAS;IACzC,aAAa;IACb,IAAI;KACF,UAAU,qBAAqB,IAAI;IACrC,QAAQ,CAER;IACA,aAAa;KACX,aAAa;KACb,IAAI;MACF,UAAU,qBAAqB,KAAK;KACtC,QAAQ,CAER;IACF;GACF;GACA,eAAe,WAAW;EAC5B;CACF;CAEA,MAAM,MAAiB;EACrB,QAAc;GACZ,OAAO,MAAM,SAAS,QAAQ,IAAI;GAClC,MAAM;EACR;EACA,cAAc,WAAmB,OAA0B;GACzD,MAAM,UAAU,QAAQ,cAAc,WAAW,KAAK;GACtD,MAAM;GACN,OAAO;EACT;EACA,UAAU,OAAqB;GAC7B,IAAI,YAAY,WAAW,KAAK;GAChC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,OAAO;EACpB,MAAM,OAAO,OAAO,WAAW,WAAW,SAAS;EAClD,WAAwC,QAAQ;CACnD;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"native-app.js","names":[],"sources":["../src/native-app.ts"],"sourcesContent":["/**\n * `createNativeApp` — the one-call entry point for a MindeesNative app on an embedded\n * native host. It hides the wiring an app author should never have to write: creating\n * the {@link createNativeCommandBackend}, rendering the root, flushing the command\n * batch to the host, and exposing the start/dispatch/frame contract the host calls.\n *\n * ```tsx\n * import { createNativeApp } from '@mindees/renderer'\n * import { App } from './App'\n *\n * createNativeApp(<App />)\n * ```\n *\n * That's the whole entry file. The native host (see `examples/native-hosts/`) injects a\n * `MindeesHost.emit(json)` global and calls `MindeesApp.start()` once, then\n * `MindeesApp.dispatchEvent(handlerId)` per native event — both of which this wires.\n *\n * On a host it also makes animations + concurrency **work by default**: it installs a\n * reactive {@link Scheduler} (so `startTransition`/`deferred`/normal-lane effects run) and a\n * vsync-driven {@link FrameSource} (so `timing`/`spring`/gesture animations advance). The host\n * drives frames by calling `MindeesApp.frameTick(nowMs)` each vsync, and the engine signals when\n * to start/stop that loop through a `MindeesHostFrame.setFrameLoopActive(boolean)` global — so the\n * vsync loop runs **only while something is animating** (battery-friendly), tied to the animation\n * engine's own arm/sleep. With no host (SSR / Node / tests) nothing is installed and animations\n * jump straight to their final value, exactly as before.\n *\n * @module\n */\n\nimport {\n createScheduler,\n type FrameSource,\n type MindeesNode,\n setFrameSource,\n setReactiveScheduler,\n} from '@mindees/core'\nimport { createNativeCommandBackend } from './native-command-backend'\nimport type { NativeNodeId } from './native-protocol'\nimport { render } from './render'\n\n// The reactive scheduler + animation frame source are PROCESS globals (one per runtime). This guard\n// makes a second `createNativeApp` that would re-wire them fail loudly instead of silently stealing\n// the first app's engines. Reset with `_resetNativeAppEngines()` between tests.\nlet enginesWired = false\n\n/** @internal Test-only: reset the process-global engine-wiring guard. */\nexport function _resetNativeAppEngines(): void {\n enginesWired = false\n}\n\n/** The contract a native host drives: render once, dispatch events, and tick frames. */\nexport interface NativeApp {\n /** Mount the app and flush the initial command batch to the host. */\n start(): void\n /**\n * Invoke a registered handler (a native event fired), then flush the resulting batch.\n * `dispatchEvent(handlerId)` for notify-only events (press/click); `dispatchEvent(handlerId, value)`\n * for value-carrying events (text change) — JS wraps `value` as `{ target: { value } }` so handlers\n * read it via the standard event shape. The host passes the RAW string value (never the object);\n * an absent/null value (no second arg) leaves notify-only behavior unchanged.\n */\n dispatchEvent(handlerId: string, value?: string | null): boolean\n /**\n * Advance animations by one frame: forward the host's vsync timestamp (ms) to the animation\n * engine, then flush the resulting command batch. The host calls this each vsync while the frame\n * loop is active (see {@link CreateNativeAppOptions}). A no-op when no frame source is installed.\n */\n frameTick(nowMs: number): void\n}\n\n/** The JS→host battery signal: the engine asks the host to run / stop its vsync loop. */\ninterface HostFrameApi {\n setFrameLoopActive?: (active: boolean) => void\n}\n\n/** Options for {@link createNativeApp}. */\nexport interface CreateNativeAppOptions {\n /**\n * Id of the host's pre-existing root container. Defaults to `\"host-root\"` — the\n * convention the reference hosts register — so the common case needs no config.\n */\n readonly rootId?: NativeNodeId\n /**\n * How to deliver a command batch to the host. Defaults to `globalThis.MindeesHost.emit`\n * (what the embedded hosts inject). Override in tests or alternative transports.\n */\n readonly emit?: (json: string) => void\n /**\n * Expose the app on a global so the host can call `start()`/`dispatchEvent()`/`frameTick()`.\n * `true` (default) → `globalThis.MindeesApp`; a string → that global name; `false` →\n * don't expose (use the returned handle directly, e.g. in Node tests).\n */\n readonly expose?: boolean | string\n /**\n * Install the reactive scheduler that powers `startTransition`/`deferred`/normal-lane effects.\n * `createNativeApp` owns this scheduler so it can flush the command batch after each microtask\n * drain (otherwise deferred mutations would recompute but never reach the host). Pass `false` to\n * run the pure synchronous lane; to use a fully custom scheduler, pass `false` and call\n * `setReactiveScheduler` yourself (you then own flushing).\n */\n readonly scheduler?: false\n /**\n * Install the reactive scheduler + the vsync frame source. Default: `true` when the app is\n * exposed AND a host (`globalThis.MindeesHost`) is present — so SSR/Node/tests install nothing\n * and keep the synchronous jump-to-final behavior. Pass `true` to force-wire (e.g. tests driving\n * `frameTick` directly).\n */\n readonly wireEngines?: boolean\n}\n\nfunction defaultEmit(json: string): void {\n const host = (globalThis as { MindeesHost?: { emit?: (json: string) => void } }).MindeesHost\n if (!host || typeof host.emit !== 'function') {\n throw new Error(\n 'createNativeApp: no `emit` was provided and globalThis.MindeesHost.emit is unavailable',\n )\n }\n host.emit(json)\n}\n\nfunction hostIsPresent(): boolean {\n const host = (globalThis as { MindeesHost?: { emit?: unknown } }).MindeesHost\n return typeof host?.emit === 'function'\n}\n\n/**\n * Wire a root node to a native command host. Returns the {@link NativeApp} handle and\n * (unless `expose: false`) publishes it as `globalThis.MindeesApp` for the host to call.\n */\nexport function createNativeApp(\n root: MindeesNode,\n options: CreateNativeAppOptions = {},\n): NativeApp {\n const backend = createNativeCommandBackend({ rootId: options.rootId ?? 'host-root' })\n const emit = options.emit ?? defaultEmit\n\n const flush = (): void => {\n const batch = backend.flushCommands()\n if (batch.length > 0) emit(JSON.stringify(batch))\n }\n\n const expose = options.expose ?? true\n const wireEngines = options.wireEngines ?? (expose !== false && hostIsPresent())\n\n // The engine's onFrame, captured when the animation loop subscribes; `frameTick` drives it.\n let storedTick: ((nowMs: number) => void) | null = null\n\n if (wireEngines) {\n if (enginesWired) {\n // The reactive scheduler + frame source are process globals; a second wiring would silently\n // steal the first app's engines. Fail loudly instead. (One app per runtime; tests reset via\n // _resetNativeAppEngines.)\n throw new Error(\n 'createNativeApp: the reactive engines are already wired by another app instance. Create one ' +\n 'app per runtime, or pass `wireEngines: false` (and wire the scheduler/frame source yourself).',\n )\n }\n enginesWired = true\n\n // A microtask-drained scheduler. After each drained task we run ONE coalesced trailing flush so\n // deferred/startTransition/normal-lane tree mutations (which land on a microtask, outside the\n // frameTick + dispatchEvent windows) still reach the host — one frame late, never dropped. We\n // OWN this scheduler so the trailing flush is always wired (a custom scheduler couldn't be).\n if (options.scheduler !== false) {\n let flushQueued = false\n const trailingFlush = (): void => {\n if (flushQueued) return\n flushQueued = true\n queueMicrotask(() => {\n flushQueued = false\n flush()\n })\n }\n setReactiveScheduler(\n createScheduler({\n scheduleMicrotask: (cb) =>\n queueMicrotask(() => {\n cb()\n trailingFlush()\n }),\n onError: (error) => {\n // Surface a scheduled-task error instead of swallowing it (uncaught → host log).\n queueMicrotask(() => {\n throw error\n })\n },\n }),\n )\n }\n\n // The vsync frame source: capture the engine's tick + signal the host to run/stop its loop. The\n // subscribe (START) fires the instant the first animation driver arms the loop; the unsubscribe\n // (STOP) fires the instant the last driver settles — so the host's vsync loop runs ONLY while\n // something animates (the battery win), with no separate heuristic to keep in sync.\n //\n // CRITICAL: only install the source when a host can actually DRIVE it (MindeesHostFrame present,\n // i.e. there's a `frameTick` caller + a `setFrameLoopActive` listener). Arming a loop that\n // nothing ticks would FREEZE animations at their start value — strictly worse than jumping to\n // the final value. With no driver, leave the source null → animations jump-to-final (safe).\n const hostFrame = (globalThis as { MindeesHostFrame?: HostFrameApi }).MindeesHostFrame\n if (hostFrame) {\n const frameSource: FrameSource = (tick) => {\n storedTick = tick\n try {\n hostFrame.setFrameLoopActive?.(true)\n } catch {\n // a throwing host signal must not break arming the loop\n }\n return () => {\n storedTick = null\n try {\n hostFrame.setFrameLoopActive?.(false)\n } catch {\n // ditto: always stop the loop even if the host signal throws\n }\n }\n }\n setFrameSource(frameSource)\n }\n }\n\n const app: NativeApp = {\n start(): void {\n render(root, backend, backend.root)\n flush()\n },\n dispatchEvent(handlerId: string, value?: string | null): boolean {\n // No value (press/click) → undefined event (unchanged). A string (incl. \"\" — a cleared field)\n // → { target: { value } } so eventValue() reads the typed text. `== null` treats both undefined\n // and a host-passed null as \"no value\" while still delivering an empty-string clear.\n const event = value === undefined || value === null ? undefined : { target: { value } }\n const handled = backend.dispatchEvent(handlerId, event)\n flush()\n return handled\n },\n frameTick(nowMs: number): void {\n if (storedTick) storedTick(nowMs)\n flush()\n },\n }\n\n if (expose !== false) {\n const name = typeof expose === 'string' ? expose : 'MindeesApp'\n ;(globalThis as Record<string, unknown>)[name] = app\n }\n\n return app\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAI,eAAe;AAmEnB,SAAS,YAAY,MAAoB;CACvC,MAAM,OAAQ,WAAmE;CACjF,IAAI,CAAC,QAAQ,OAAO,KAAK,SAAS,YAChC,MAAM,IAAI,MACR,wFACF;CAEF,KAAK,KAAK,IAAI;AAChB;AAEA,SAAS,gBAAyB;CAEhC,OAAO,OADO,WAAoD,aAC9C,SAAS;AAC/B;;;;;AAMA,SAAgB,gBACd,MACA,UAAkC,CAAC,GACxB;CACX,MAAM,UAAU,2BAA2B,EAAE,QAAQ,QAAQ,UAAU,YAAY,CAAC;CACpF,MAAM,OAAO,QAAQ,QAAQ;CAE7B,MAAM,cAAoB;EACxB,MAAM,QAAQ,QAAQ,cAAc;EACpC,IAAI,MAAM,SAAS,GAAG,KAAK,KAAK,UAAU,KAAK,CAAC;CAClD;CAEA,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,cAAc,QAAQ,gBAAgB,WAAW,SAAS,cAAc;CAG9E,IAAI,aAA+C;CAEnD,IAAI,aAAa;EACf,IAAI,cAIF,MAAM,IAAI,MACR,2LAEF;EAEF,eAAe;EAMf,IAAI,QAAQ,cAAc,OAAO;GAC/B,IAAI,cAAc;GAClB,MAAM,sBAA4B;IAChC,IAAI,aAAa;IACjB,cAAc;IACd,qBAAqB;KACnB,cAAc;KACd,MAAM;IACR,CAAC;GACH;GACA,qBACE,gBAAgB;IACd,oBAAoB,OAClB,qBAAqB;KACnB,GAAG;KACH,cAAc;IAChB,CAAC;IACH,UAAU,UAAU;KAElB,qBAAqB;MACnB,MAAM;KACR,CAAC;IACH;GACF,CAAC,CACH;EACF;EAWA,MAAM,YAAa,WAAmD;EACtE,IAAI,WAAW;GACb,MAAM,eAA4B,SAAS;IACzC,aAAa;IACb,IAAI;KACF,UAAU,qBAAqB,IAAI;IACrC,QAAQ,CAER;IACA,aAAa;KACX,aAAa;KACb,IAAI;MACF,UAAU,qBAAqB,KAAK;KACtC,QAAQ,CAER;IACF;GACF;GACA,eAAe,WAAW;EAC5B;CACF;CAEA,MAAM,MAAiB;EACrB,QAAc;GACZ,OAAO,MAAM,SAAS,QAAQ,IAAI;GAClC,MAAM;EACR;EACA,cAAc,WAAmB,OAAgC;GAI/D,MAAM,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO,KAAA,IAAY,EAAE,QAAQ,EAAE,MAAM,EAAE;GACtF,MAAM,UAAU,QAAQ,cAAc,WAAW,KAAK;GACtD,MAAM;GACN,OAAO;EACT;EACA,UAAU,OAAqB;GAC7B,IAAI,YAAY,WAAW,KAAK;GAChC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,OAAO;EACpB,MAAM,OAAO,OAAO,WAAW,WAAW,SAAS;EAClD,WAAwC,QAAQ;CACnD;CAEA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/renderer",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "MindeesNative Helix — fine-grained reactive renderer with a web/DOM backend, SSR + hydration, and a headless test backend. Native and GPU-canvas backends are research tracks.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "directory": "packages/renderer"
24
24
  },
25
25
  "dependencies": {
26
- "@mindees/core": "0.10.0"
26
+ "@mindees/core": "0.12.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "happy-dom": "20.9.0"