@loom-dev/vide 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/reactive.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;KAgBK;;UAgBY,OAAO;MACnB;GACH,OAAO,IAAI;;;iBAIG,OAAO,GAAG,SAAS,IAAI,OAAO;;iBA8B9B,OAAO;;iBAwBP,OAAO,GAAG,UAAU,UAAU;;iBAS9B,QAAQ,IAAI;;;;;;iBASZ,KAAK,GAAG,KAAK,wBAAwB,IAAI;;;cCpEnD;;UAGW;YACN;WACD;WACA,OAAO,SAAS;;;;;;;iBAgBV,OACf,qBACG,QAAQ,4BAA4B;;KA8T5B,iBACX,MAAM,WACN,UAAU,aACN;UAEY;;EAEhB,gBAAgB;;;;;;;;;;iBAWD,MACf,iBAAiB,UACjB,SAAS,aACT,UAAU"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/reactive.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;KAgBK;;UAgBY,OAAO;MACnB;GACH,OAAO,IAAI;;;iBAIG,OAAO,GAAG,SAAS,IAAI,OAAO;;iBA8B9B,OAAO;;iBAwBP,OAAO,GAAG,UAAU,UAAU;;iBAS9B,QAAQ,IAAI;;;;;;iBASZ,KAAK,GAAG,KAAK,wBAAwB,IAAI;;;cCpEnD;;UAGW;YACN;WACD;WACA,OAAO,SAAS;;;;;;;iBAgBV,OACf,qBACG,QAAQ,4BAA4B;;KA4T5B,iBACX,MAAM,WACN,UAAU,aACN;UAEY;;EAEhB,gBAAgB;;;;;;;;;;iBAWD,MACf,iBAAiB,UACjB,SAAS,aACT,UAAU"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { computeLayout, initLayout } from "@loom-dev/layout";
2
2
  import { fontShorthand, instanceFont, onFontsChanged, renderScene } from "@loom-dev/renderer";
3
- import { EnumItem, toPropertyValue } from "@loom-dev/runtime";
3
+ import { enumName, toPropertyValue } from "@loom-dev/runtime";
4
4
  import { fontSizeToPx, prop } from "@loom-dev/scene";
5
5
  //#region src/reactive.ts
6
6
  let currentReaction;
@@ -169,8 +169,7 @@ function getMeasureCtx() {
169
169
  }
170
170
  /** Is `AutomaticSize` covering the X axis, so the width is content-derived? */
171
171
  function autoOnX(live) {
172
- const auto = live.props.get("AutomaticSize");
173
- const name = auto instanceof EnumItem ? auto.Name : void 0;
172
+ const name = enumName(live.props.get("AutomaticSize"));
174
173
  return name === "X" || name === "XY";
175
174
  }
176
175
  /**
@@ -222,8 +221,7 @@ function wrapWidth(live, room, own) {
222
221
  */
223
222
  function measureTextBounds(live, wrapAt) {
224
223
  if (!TEXT_CLASSES.has(live.className)) return void 0;
225
- const auto = live.props.get("AutomaticSize");
226
- const autoName = auto instanceof EnumItem ? auto.Name : void 0;
224
+ const autoName = enumName(live.props.get("AutomaticSize"));
227
225
  if (autoName !== "X" && autoName !== "Y" && autoName !== "XY") return void 0;
228
226
  const text = live.props.get("Text");
229
227
  if (typeof text !== "string" || text === "") return void 0;
@@ -231,7 +229,7 @@ function measureTextBounds(live, wrapAt) {
231
229
  if (!ctx) return void 0;
232
230
  const rawSize = live.props.get("TextSize");
233
231
  const rawFontSize = live.props.get("FontSize");
234
- const size = typeof rawSize === "number" ? rawSize : fontSizeToPx(rawFontSize instanceof EnumItem ? rawFontSize.Name : void 0) ?? 14;
232
+ const size = typeof rawSize === "number" ? rawSize : fontSizeToPx(enumName(rawFontSize)) ?? 14;
235
233
  ctx.font = fontShorthand(instanceFont({
236
234
  Font: live.props.get("Font"),
237
235
  FontFace: live.props.get("FontFace")
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/reactive.ts","../src/index.ts"],"sourcesContent":["/**\n * A minimal fine-grained reactive core, mirroring vide's `source`/`derive`/\n * `effect` primitives. Its only job is to track which effects read which sources,\n * so a source write re-runs exactly the effects that depend on it. The loom vide\n * adapter wires reactive props/children onto these so a source change reschedules\n * a Scene IR rebuild (see `index.ts`).\n *\n * Unlike React (a VDOM reconciler), vide is push-based: there is no diff, signals\n * notify their subscribers directly. Both paradigms feed the *same* Scene IR.\n *\n * Reactions form an ownership tree: an effect created while another reaction runs\n * becomes that reaction's child and is disposed when the parent re-runs (or when\n * the enclosing `root` is disposed). This is what keeps dynamic effects — e.g. a\n * `derive` or a reactive child rebuilt inside an effect — from leaking.\n */\n\ntype CleanupFn = () => void;\n\ninterface Reaction {\n\trun: () => void;\n\t/** Subscriber sets this reaction belongs to, cleared before each re-run. */\n\tdeps: Set<Set<Reaction>>;\n\tcleanups: CleanupFn[];\n\t/** Effects created while this reaction was running; disposed before re-run. */\n\tchildren: Reaction[];\n}\n\n// The running reaction doubles as the current owner: dependency reads subscribe\n// it, and effects created during its run nest under it.\nlet currentReaction: Reaction | undefined;\n\n/** A vide source: call with no args to read (tracked), with one arg to write. */\nexport interface Source<T> {\n\t(): T;\n\t(value: T): T;\n}\n\n/** Create a reactive source seeded with `initial`. */\nexport function source<T>(initial: T): Source<T> {\n\tlet value = initial;\n\tconst subscribers = new Set<Reaction>();\n\tfunction accessor(...args: [T] | []): T {\n\t\tif (args.length === 0) {\n\t\t\tif (currentReaction) {\n\t\t\t\tsubscribers.add(currentReaction);\n\t\t\t\tcurrentReaction.deps.add(subscribers);\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tvalue = args[0];\n\t\t// Snapshot: a reaction re-subscribes (mutating the set) as it re-runs.\n\t\tfor (const reaction of [...subscribers]) reaction.run();\n\t\treturn value;\n\t}\n\treturn accessor as Source<T>;\n}\n\nfunction disposeReaction(reaction: Reaction): void {\n\t// Dispose nested effects first, depth-first, so their cleanups run too.\n\tfor (const child of reaction.children) disposeReaction(child);\n\treaction.children = [];\n\tfor (const dep of reaction.deps) dep.delete(reaction);\n\treaction.deps.clear();\n\tfor (const fn of reaction.cleanups) fn();\n\treaction.cleanups = [];\n}\n\n/** Run `fn` now and re-run it whenever a source it read changes. */\nexport function effect(fn: () => void): void {\n\tconst reaction: Reaction = {\n\t\trun,\n\t\tdeps: new Set(),\n\t\tcleanups: [],\n\t\tchildren: [],\n\t};\n\tfunction run(): void {\n\t\t// Tear down the previous run (deps, cleanups, and any nested effects) so a\n\t\t// re-run starts clean and dynamic children don't accumulate.\n\t\tdisposeReaction(reaction);\n\t\tconst prev = currentReaction;\n\t\tcurrentReaction = reaction;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tcurrentReaction = prev;\n\t\t}\n\t}\n\tcurrentReaction?.children.push(reaction);\n\trun();\n}\n\n/** A memoized computation: a source kept in sync with `fn` via an effect. */\nexport function derive<T>(fn: () => T): () => T {\n\tconst s = source<T>(undefined as T);\n\teffect(() => {\n\t\ts(fn());\n\t});\n\treturn () => s();\n}\n\n/** Register a cleanup to run before the enclosing effect re-runs or disposes. */\nexport function cleanup(fn: CleanupFn): void {\n\tcurrentReaction?.cleanups.push(fn);\n}\n\n/**\n * Open a disposal scope. `fn` receives a `dispose` that tears down every effect\n * created within (recursively). `mount` runs the component tree inside a root so\n * unmounting disposes all its reactive bindings.\n */\nexport function root<T>(fn: (dispose: () => void) => T): T {\n\tconst owner: Reaction = {\n\t\trun: () => {},\n\t\tdeps: new Set(),\n\t\tcleanups: [],\n\t\tchildren: [],\n\t};\n\tconst prev = currentReaction;\n\tcurrentReaction = owner;\n\ttry {\n\t\treturn fn(() => disposeReaction(owner));\n\t} finally {\n\t\tcurrentReaction = prev;\n\t}\n}\n","/**\n * `@loom-dev/vide` — a vide frontend adapter for loom.\n *\n * The second proof that the Scene IR is the real contract: vide is push-based\n * fine-grained reactivity (no VDOM, no reconciler — see `./reactive`), yet it\n * feeds the *exact same* `SceneNode` tree into the *same* WASM layout engine and\n * DOM renderer that `@loom-dev/react` uses. Nothing in `@loom-dev/layout` or\n * `@loom-dev/renderer` knows which frontend produced the tree.\n *\n * Authoring mirrors vide: `create(\"Frame\")({ ...props, [1]: child })`, where\n * string keys are Roblox properties (a function value is a reactive binding) and\n * number keys are children (vide's array-part).\n */\nimport { computeLayout, initLayout } from \"@loom-dev/layout\";\nimport {\n\tfontShorthand,\n\tinstanceFont,\n\tonFontsChanged,\n\trenderScene,\n} from \"@loom-dev/renderer\";\nimport { EnumItem, toPropertyValue } from \"@loom-dev/runtime\";\nimport {\n\tfontSizeToPx,\n\ttype LayoutResult,\n\ttype PropertyValue,\n\tprop,\n\ttype SceneNode,\n\ttype Viewport,\n} from \"@loom-dev/scene\";\nimport { effect, root } from \"./reactive\";\n\nexport {\n\tcleanup,\n\tderive,\n\teffect,\n\troot,\n\ttype Source,\n\tsource,\n} from \"./reactive\";\n\n// --- authoring: create() -----------------------------------------------------\n\nconst VIDE_NODE = Symbol(\"loom.vide.node\");\n\n/** A vide element descriptor produced by `create`. */\nexport interface VideNode {\n\treadonly [VIDE_NODE]: true;\n\treadonly className: string;\n\treadonly props: Readonly<Record<string, unknown>>;\n}\n\nfunction isVideNode(value: unknown): value is VideNode {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as { [VIDE_NODE]?: unknown })[VIDE_NODE] === true\n\t);\n}\n\n/**\n * `create(\"Frame\")` returns a constructor taking a props table. String keys are\n * Roblox properties (a function value is bound reactively at mount); number keys\n * are children.\n */\nexport function create(\n\tclassName: string,\n): (props?: Record<string, unknown>) => VideNode {\n\treturn (props = {}) => ({ [VIDE_NODE]: true, className, props });\n}\n\n// --- live tree ---------------------------------------------------------------\n\n/**\n * A resolved, mutable mirror of a `VideNode`. Reactive props write their current\n * value into `props`; `toScene` reads it. Children are grouped into slots so a\n * reactive (function) child can swap its subtree in place without disturbing its\n * siblings; a static child is a one-shot slot.\n */\ninterface LiveNode {\n\tclassName: string;\n\tname: string;\n\tid: string;\n\tprops: Map<string, unknown>;\n\tchildren: ChildSlot[];\n}\n\n/** A positional group of children — its `nodes` are re-derived if reactive. */\ninterface ChildSlot {\n\tnodes: LiveNode[];\n}\n\nlet nextId = 0;\n\n/** Resolve a child value (a node, or nested arrays of nodes) to LiveNodes. */\nfunction resolveChildren(value: unknown, schedule: () => void): LiveNode[] {\n\tif (isVideNode(value)) return [build(value, schedule)];\n\tif (Array.isArray(value)) {\n\t\treturn value.flatMap((item) => resolveChildren(item, schedule));\n\t}\n\treturn [];\n}\n\nfunction addChildSlot(\n\tparent: LiveNode,\n\tvalue: unknown,\n\tschedule: () => void,\n): void {\n\tif (typeof value === \"function\") {\n\t\t// A reactive child (vide's control-flow pattern): rebuild this slot's\n\t\t// subtree whenever the function's sources change. The rebuilt nodes' own\n\t\t// reactive bindings nest under this effect, so they dispose on each re-run.\n\t\tconst slot: ChildSlot = { nodes: [] };\n\t\tparent.children.push(slot);\n\t\teffect(() => {\n\t\t\tslot.nodes = resolveChildren((value as () => unknown)(), schedule);\n\t\t\tschedule();\n\t\t});\n\t} else {\n\t\tparent.children.push({ nodes: resolveChildren(value, schedule) });\n\t}\n}\n\n/** Build a `LiveNode` from a `VideNode`, wiring reactive props/children. */\nfunction build(node: VideNode, schedule: () => void): LiveNode {\n\tconst live: LiveNode = {\n\t\tclassName: node.className,\n\t\tname: node.className,\n\t\tid: `v${nextId++}`,\n\t\tprops: new Map(),\n\t\tchildren: [],\n\t};\n\tfor (const [key, value] of Object.entries(node.props)) {\n\t\tif (/^\\d+$/.test(key)) {\n\t\t\taddChildSlot(live, value, schedule);\n\t\t\tcontinue;\n\t\t}\n\t\tif (key === \"Name\" && typeof value === \"string\") {\n\t\t\tlive.name = value;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"function\") {\n\t\t\t// A reactive prop: re-resolve and repaint whenever its sources change.\n\t\t\teffect(() => {\n\t\t\t\tlive.props.set(key, (value as () => unknown)());\n\t\t\t\tschedule();\n\t\t\t});\n\t\t} else {\n\t\t\tlive.props.set(key, value);\n\t\t}\n\t}\n\treturn live;\n}\n\n// --- live tree → Scene IR ----------------------------------------------------\n\nconst TEXT_CLASSES = new Set([\"TextLabel\", \"TextButton\", \"TextBox\"]);\nlet measureCtx: CanvasRenderingContext2D | null | undefined;\nfunction getMeasureCtx(): CanvasRenderingContext2D | null {\n\tif (measureCtx === undefined) {\n\t\tmeasureCtx =\n\t\t\ttypeof document !== \"undefined\"\n\t\t\t\t? document.createElement(\"canvas\").getContext(\"2d\")\n\t\t\t\t: null;\n\t}\n\treturn measureCtx;\n}\n\n/** Is `AutomaticSize` covering the X axis, so the width is content-derived? */\nfunction autoOnX(live: LiveNode): boolean {\n\tconst auto = live.props.get(\"AutomaticSize\");\n\tconst name = auto instanceof EnumItem ? auto.Name : undefined;\n\treturn name === \"X\" || name === \"XY\";\n}\n\n/**\n * Horizontal `UIPadding` on `live`, in pixels — the room it takes away from\n * whatever it holds. Offsets only, which is what the layout engine resolves a\n * scale inset to on an automatic axis. Mirrors the react adapter.\n */\nfunction horizontalPadding(live: LiveNode): number {\n\tfor (const slot of live.children) {\n\t\tfor (const child of slot.nodes) {\n\t\t\tif (child.className !== \"UIPadding\") continue;\n\t\t\tconst side = (name: string): number => {\n\t\t\t\tconst udim = child.props.get(name) as { Offset?: unknown } | undefined;\n\t\t\t\treturn typeof udim?.Offset === \"number\" ? udim.Offset : 0;\n\t\t\t};\n\t\t\treturn side(\"PaddingLeft\") + side(\"PaddingRight\");\n\t\t}\n\t}\n\treturn 0;\n}\n\n/**\n * The width `TextWrapped` text wraps at, or `undefined` when it does not wrap.\n * `TextWrap` is the engine's own deprecated alias for the same property, and\n * `TextWrapped` still wins when both are set.\n *\n * `room` is what the parent had left for its children (see {@link toScene}) and\n * `own` is this node's own width from the last layout:\n * - **X is not automatic** — the node's own width is fixed, so it is the\n * constraint and the height grows to however many lines result.\n * - **X is automatic** — the node's width is the thing being computed, so\n * wrapping against it would settle at one word per line. The room the nearest\n * ancestor with a width of its own leaves is the constraint instead, which is\n * what such a label does in Studio.\n *\n * Both come from the layout that last ran, so the first snapshot of a label\n * measures unwrapped; `paint` re-snapshots until the widths it measured against\n * are the ones the layout produced.\n */\nfunction wrapWidth(\n\tlive: LiveNode,\n\troom: number | undefined,\n\town: number | undefined,\n): number | undefined {\n\tconst wrapped = live.props.get(\"TextWrapped\") ?? live.props.get(\"TextWrap\");\n\tif (wrapped !== true) return undefined;\n\tconst width = autoOnX(live) ? room : own;\n\treturn width !== undefined && width > 0 ? width : undefined;\n}\n\n/**\n * Measure an auto-sizing text node with the font the renderer paints and emit a\n * `TextBounds` Vector2 the layout engine reads for AutomaticSize (font metrics\n * live browser-side, not in the WASM engine). Mirrors the react adapter.\n *\n * With `wrapAt` set the words are laid into lines greedily, which is what\n * `TextWrapped` asks for; without it each newline is the only line break.\n */\nfunction measureTextBounds(\n\tlive: LiveNode,\n\twrapAt?: number,\n): PropertyValue | undefined {\n\tif (!TEXT_CLASSES.has(live.className)) return undefined;\n\tconst auto = live.props.get(\"AutomaticSize\");\n\tconst autoName = auto instanceof EnumItem ? auto.Name : undefined;\n\tif (autoName !== \"X\" && autoName !== \"Y\" && autoName !== \"XY\")\n\t\treturn undefined;\n\tconst text = live.props.get(\"Text\");\n\tif (typeof text !== \"string\" || text === \"\") return undefined;\n\tconst ctx = getMeasureCtx();\n\tif (!ctx) return undefined;\n\n\t// `TextSize` wins, legacy `FontSize` fills in, 14 is the Roblox default —\n\t// same precedence as `@loom-dev/scene`'s `getTextSize` and the react adapter.\n\tconst rawSize = live.props.get(\"TextSize\");\n\tconst rawFontSize = live.props.get(\"FontSize\");\n\tconst size =\n\t\ttypeof rawSize === \"number\"\n\t\t\t? rawSize\n\t\t\t: (fontSizeToPx(\n\t\t\t\t\trawFontSize instanceof EnumItem ? rawFontSize.Name : undefined,\n\t\t\t\t) ?? 14);\n\tctx.font = fontShorthand(\n\t\tinstanceFont({\n\t\t\tFont: live.props.get(\"Font\"),\n\t\t\tFontFace: live.props.get(\"FontFace\"),\n\t\t}),\n\t\tsize,\n\t);\n\t// `LineHeight` stretches the gap between lines, so it starts paying from the\n\t// second one — same rule as `@loom-dev/scene`'s `getLineHeight`.\n\tconst rawLineHeight = live.props.get(\"LineHeight\");\n\tconst lineHeight =\n\t\ttypeof rawLineHeight === \"number\"\n\t\t\t? Math.min(3, Math.max(1, rawLineHeight))\n\t\t\t: 1;\n\n\tlet width = 0;\n\tlet height = 0;\n\tlet lines = 0;\n\tconst endLine = (lineWidth: number): void => {\n\t\twidth = Math.max(width, lineWidth);\n\t\theight += lines === 0 ? size : size * lineHeight;\n\t\tlines += 1;\n\t};\n\tfor (const line of text.split(\"\\n\")) {\n\t\tif (wrapAt === undefined) {\n\t\t\tendLine(ctx.measureText(line).width);\n\t\t\tcontinue;\n\t\t}\n\t\tlet lineWidth = 0;\n\t\t// Split *keeping* the whitespace, so the gaps between words are measured\n\t\t// rather than assumed.\n\t\tfor (const piece of line.split(/(\\s+)/)) {\n\t\t\tif (piece === \"\") continue;\n\t\t\tconst pieceWidth = ctx.measureText(piece).width;\n\t\t\tif (lineWidth > 0 && lineWidth + pieceWidth > wrapAt) {\n\t\t\t\tendLine(lineWidth);\n\t\t\t\tlineWidth = 0;\n\t\t\t\t// A run of spaces that would overflow is dropped rather than carried\n\t\t\t\t// to the start of the next line, as every text shaper does.\n\t\t\t\tif (piece.trim() === \"\") continue;\n\t\t\t}\n\t\t\tlineWidth += pieceWidth;\n\t\t}\n\t\tendLine(lineWidth);\n\t}\n\treturn prop.vector2({ x: Math.ceil(width), y: height });\n}\n\n/** What one snapshot of the live tree needs from the layout that last ran. */\ninterface SnapshotContext {\n\t/** Rects from the previous layout — where wrapped text learns its room. */\n\treadonly rects: LayoutResult[\"rects\"];\n\t/** id → the wrap width each measured text node was measured against. */\n\treadonly wrap: Map<string, number>;\n}\n\n/**\n * Snapshot the live tree as Scene IR (called fresh on every paint). `room` is\n * what the parent has left for its children: the width of the nearest ancestor\n * that has one of its own, less every `UIPadding` in between.\n */\nfunction toScene(\n\tlive: LiveNode,\n\tctx: SnapshotContext,\n\troom?: number,\n): SceneNode {\n\tconst node: SceneNode = {\n\t\tclassName: live.className,\n\t\tname: live.name,\n\t\tid: live.id,\n\t};\n\tconst properties: Record<string, PropertyValue> = {};\n\tfor (const [key, value] of live.props) {\n\t\tconst pv = toPropertyValue(value);\n\t\tif (pv !== undefined) properties[key] = pv;\n\t}\n\tconst own = ctx.rects[live.id]?.rect.width;\n\tconst wrapAt = wrapWidth(live, room, own);\n\tconst textBounds = measureTextBounds(live, wrapAt);\n\tif (textBounds) {\n\t\tproperties.TextBounds = textBounds;\n\t\tctx.wrap.set(live.id, wrapAt ?? 0);\n\t}\n\tif (Object.keys(properties).length > 0) node.properties = properties;\n\tconst children = live.children.flatMap((slot) => slot.nodes);\n\tif (children.length > 0) {\n\t\t// An auto-sized node was itself sized by what is inside it, so it passes\n\t\t// its parent's room down untouched; a node with a width of its own becomes\n\t\t// the constraint. Either way its own padding comes off.\n\t\tconst base = autoOnX(live) ? room : own;\n\t\tconst inner =\n\t\t\tbase === undefined ? undefined : base - horizontalPadding(live);\n\t\tnode.children = children.map((child) => toScene(child, ctx, inner));\n\t}\n\treturn node;\n}\n\n/** Whether two snapshots measured their wrapped text against the same widths. */\nfunction sameWrap(a: Map<string, number>, b: Map<string, number>): boolean {\n\tif (a.size !== b.size) return false;\n\tfor (const [id, width] of a) if (b.get(id) !== width) return false;\n\treturn true;\n}\n\n// How many times one paint will re-snapshot wrapped text against the width the\n// layout it just ran produced. Two is the settling case (measure unwrapped →\n// learn the width → measure wrapped); the rest is headroom for a label whose\n// wrapping changes the width it wraps against. Mirrors the react adapter.\nconst MAX_WRAP_PASSES = 4;\n\n// --- mount -------------------------------------------------------------------\n\nconst HOST_ID = \"loom-root\";\n\n/** The outer preview viewport (`#loom-root`), created if the host page lacks it. */\nfunction resolveHost(target?: HTMLElement): HTMLElement {\n\tif (target) return target;\n\tconst existing = document.getElementById(HOST_ID);\n\tif (existing) return existing;\n\tconst el = document.createElement(\"div\");\n\tel.id = HOST_ID;\n\tel.style.position = \"relative\";\n\tel.style.width = \"100vw\";\n\tel.style.height = \"100vh\";\n\tel.style.overflow = \"hidden\";\n\tdocument.body.appendChild(el);\n\treturn el;\n}\n\n/** The layout pass a mount runs; the WASM engine unless one is injected. */\nexport type ComputeLayout = (\n\troot: SceneNode,\n\tviewport: Viewport,\n) => LayoutResult;\n\nexport interface MountOptions {\n\t/** Replace the WASM layout engine (tests). Skips {@link initLayout}. */\n\tcomputeLayout?: ComputeLayout;\n}\n\n/**\n * Mount a vide component into the preview DOM and return an unmount function.\n * `component` is run once to build the tree; reactive props/children drive\n * subsequent repaints. Each mount gets its own container under `#loom-root`, so\n * independent mounts don't clobber each other (the renderer replaces its own\n * container's children every commit). The tree is laid out (WASM) and rendered\n * with a ResizeObserver re-laying-out on viewport changes — same as react.\n */\nexport function mount(\n\tcomponent: () => VideNode,\n\ttarget?: HTMLElement,\n\toptions?: MountOptions,\n): () => void {\n\tconst host = document.createElement(\"div\");\n\thost.style.position = \"absolute\";\n\thost.style.inset = \"0\";\n\tresolveHost(target).appendChild(host);\n\n\tconst layoutOf = options?.computeLayout ?? computeLayout;\n\n\treturn root((dispose) => {\n\t\tlet ready = false;\n\t\tlet scheduled = false;\n\t\tlet disposed = false;\n\t\tlet live: LiveNode | undefined;\n\t\t/** The rects the last paint produced — this one's wrap widths come from it. */\n\t\tlet rects: LayoutResult[\"rects\"] = {};\n\n\t\tconst paint = (): void => {\n\t\t\tscheduled = false;\n\t\t\tif (disposed || !ready || !live) return;\n\t\t\tconst width = host.clientWidth;\n\t\t\tconst height = host.clientHeight;\n\t\t\tif (width === 0 || height === 0) return; // wait for the mount to be sized\n\t\t\ttry {\n\t\t\t\t// Wrapped text is measured against a width that only exists once the\n\t\t\t\t// layout has run, so the first snapshot of a label is unwrapped.\n\t\t\t\t// Rendering that pass would put a label wider than its container into\n\t\t\t\t// the DOM, and during a live window resize — where every frame brings\n\t\t\t\t// a fresh width — the stale pass is what stays on screen. So keep\n\t\t\t\t// re-snapshotting until the widths measured against are the ones the\n\t\t\t\t// layout produced, and render once, with the settled result.\n\t\t\t\tlet scene: SceneNode | undefined;\n\t\t\t\tlet layout: LayoutResult | undefined;\n\t\t\t\tlet measured: Map<string, number> | undefined;\n\t\t\t\tfor (let pass = 0; pass < MAX_WRAP_PASSES; pass++) {\n\t\t\t\t\tconst wrap = new Map<string, number>();\n\t\t\t\t\tconst next = toScene(live, { rects, wrap });\n\t\t\t\t\t// The snapshot this pass would lay out is the one already laid\n\t\t\t\t\t// out: nothing moved, so it has settled.\n\t\t\t\t\tif (measured && sameWrap(measured, wrap)) break;\n\t\t\t\t\tscene = next;\n\t\t\t\t\tmeasured = wrap;\n\t\t\t\t\tlayout = layoutOf(scene, { width, height });\n\t\t\t\t\trects = layout.rects;\n\t\t\t\t}\n\t\t\t\tif (!scene || !layout) return;\n\t\t\t\trenderScene(scene, layout, host);\n\t\t\t} catch (err) {\n\t\t\t\t// Never let a malformed scene escape an effect or the RO callback.\n\t\t\t\tconsole.error(\"loom vide:\", err);\n\t\t\t}\n\t\t};\n\t\tconst schedule = (): void => {\n\t\t\tif (scheduled) return;\n\t\t\tscheduled = true;\n\t\t\tqueueMicrotask(paint);\n\t\t};\n\n\t\t// Build inside this root so the reactive bindings are disposable.\n\t\tlive = build(component(), schedule);\n\n\t\tconst observer = new ResizeObserver(() => paint());\n\t\tobserver.observe(host);\n\t\t// Text bounds were measured against the faces the browser had at the time,\n\t\t// so a face arriving later invalidates the layout that came out of them.\n\t\tconst stopFontWatch = onFontsChanged(() => paint());\n\t\tif (options?.computeLayout) {\n\t\t\tready = true;\n\t\t\tpaint();\n\t\t} else {\n\t\t\tvoid initLayout().then(() => {\n\t\t\t\tready = true;\n\t\t\t\tpaint();\n\t\t\t});\n\t\t}\n\n\t\treturn () => {\n\t\t\tdisposed = true; // a queued microtask paint must not resurrect the DOM\n\t\t\tobserver.disconnect();\n\t\t\tstopFontWatch();\n\t\t\tdispose();\n\t\t\thost.remove();\n\t\t};\n\t});\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/reactive.ts","../src/index.ts"],"sourcesContent":["/**\n * A minimal fine-grained reactive core, mirroring vide's `source`/`derive`/\n * `effect` primitives. Its only job is to track which effects read which sources,\n * so a source write re-runs exactly the effects that depend on it. The loom vide\n * adapter wires reactive props/children onto these so a source change reschedules\n * a Scene IR rebuild (see `index.ts`).\n *\n * Unlike React (a VDOM reconciler), vide is push-based: there is no diff, signals\n * notify their subscribers directly. Both paradigms feed the *same* Scene IR.\n *\n * Reactions form an ownership tree: an effect created while another reaction runs\n * becomes that reaction's child and is disposed when the parent re-runs (or when\n * the enclosing `root` is disposed). This is what keeps dynamic effects — e.g. a\n * `derive` or a reactive child rebuilt inside an effect — from leaking.\n */\n\ntype CleanupFn = () => void;\n\ninterface Reaction {\n\trun: () => void;\n\t/** Subscriber sets this reaction belongs to, cleared before each re-run. */\n\tdeps: Set<Set<Reaction>>;\n\tcleanups: CleanupFn[];\n\t/** Effects created while this reaction was running; disposed before re-run. */\n\tchildren: Reaction[];\n}\n\n// The running reaction doubles as the current owner: dependency reads subscribe\n// it, and effects created during its run nest under it.\nlet currentReaction: Reaction | undefined;\n\n/** A vide source: call with no args to read (tracked), with one arg to write. */\nexport interface Source<T> {\n\t(): T;\n\t(value: T): T;\n}\n\n/** Create a reactive source seeded with `initial`. */\nexport function source<T>(initial: T): Source<T> {\n\tlet value = initial;\n\tconst subscribers = new Set<Reaction>();\n\tfunction accessor(...args: [T] | []): T {\n\t\tif (args.length === 0) {\n\t\t\tif (currentReaction) {\n\t\t\t\tsubscribers.add(currentReaction);\n\t\t\t\tcurrentReaction.deps.add(subscribers);\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\t\tvalue = args[0];\n\t\t// Snapshot: a reaction re-subscribes (mutating the set) as it re-runs.\n\t\tfor (const reaction of [...subscribers]) reaction.run();\n\t\treturn value;\n\t}\n\treturn accessor as Source<T>;\n}\n\nfunction disposeReaction(reaction: Reaction): void {\n\t// Dispose nested effects first, depth-first, so their cleanups run too.\n\tfor (const child of reaction.children) disposeReaction(child);\n\treaction.children = [];\n\tfor (const dep of reaction.deps) dep.delete(reaction);\n\treaction.deps.clear();\n\tfor (const fn of reaction.cleanups) fn();\n\treaction.cleanups = [];\n}\n\n/** Run `fn` now and re-run it whenever a source it read changes. */\nexport function effect(fn: () => void): void {\n\tconst reaction: Reaction = {\n\t\trun,\n\t\tdeps: new Set(),\n\t\tcleanups: [],\n\t\tchildren: [],\n\t};\n\tfunction run(): void {\n\t\t// Tear down the previous run (deps, cleanups, and any nested effects) so a\n\t\t// re-run starts clean and dynamic children don't accumulate.\n\t\tdisposeReaction(reaction);\n\t\tconst prev = currentReaction;\n\t\tcurrentReaction = reaction;\n\t\ttry {\n\t\t\tfn();\n\t\t} finally {\n\t\t\tcurrentReaction = prev;\n\t\t}\n\t}\n\tcurrentReaction?.children.push(reaction);\n\trun();\n}\n\n/** A memoized computation: a source kept in sync with `fn` via an effect. */\nexport function derive<T>(fn: () => T): () => T {\n\tconst s = source<T>(undefined as T);\n\teffect(() => {\n\t\ts(fn());\n\t});\n\treturn () => s();\n}\n\n/** Register a cleanup to run before the enclosing effect re-runs or disposes. */\nexport function cleanup(fn: CleanupFn): void {\n\tcurrentReaction?.cleanups.push(fn);\n}\n\n/**\n * Open a disposal scope. `fn` receives a `dispose` that tears down every effect\n * created within (recursively). `mount` runs the component tree inside a root so\n * unmounting disposes all its reactive bindings.\n */\nexport function root<T>(fn: (dispose: () => void) => T): T {\n\tconst owner: Reaction = {\n\t\trun: () => {},\n\t\tdeps: new Set(),\n\t\tcleanups: [],\n\t\tchildren: [],\n\t};\n\tconst prev = currentReaction;\n\tcurrentReaction = owner;\n\ttry {\n\t\treturn fn(() => disposeReaction(owner));\n\t} finally {\n\t\tcurrentReaction = prev;\n\t}\n}\n","/**\n * `@loom-dev/vide` — a vide frontend adapter for loom.\n *\n * The second proof that the Scene IR is the real contract: vide is push-based\n * fine-grained reactivity (no VDOM, no reconciler — see `./reactive`), yet it\n * feeds the *exact same* `SceneNode` tree into the *same* WASM layout engine and\n * DOM renderer that `@loom-dev/react` uses. Nothing in `@loom-dev/layout` or\n * `@loom-dev/renderer` knows which frontend produced the tree.\n *\n * Authoring mirrors vide: `create(\"Frame\")({ ...props, [1]: child })`, where\n * string keys are Roblox properties (a function value is a reactive binding) and\n * number keys are children (vide's array-part).\n */\nimport { computeLayout, initLayout } from \"@loom-dev/layout\";\nimport {\n\tfontShorthand,\n\tinstanceFont,\n\tonFontsChanged,\n\trenderScene,\n} from \"@loom-dev/renderer\";\nimport { enumName, toPropertyValue } from \"@loom-dev/runtime\";\nimport {\n\tfontSizeToPx,\n\ttype LayoutResult,\n\ttype PropertyValue,\n\tprop,\n\ttype SceneNode,\n\ttype Viewport,\n} from \"@loom-dev/scene\";\nimport { effect, root } from \"./reactive\";\n\nexport {\n\tcleanup,\n\tderive,\n\teffect,\n\troot,\n\ttype Source,\n\tsource,\n} from \"./reactive\";\n\n// --- authoring: create() -----------------------------------------------------\n\nconst VIDE_NODE = Symbol(\"loom.vide.node\");\n\n/** A vide element descriptor produced by `create`. */\nexport interface VideNode {\n\treadonly [VIDE_NODE]: true;\n\treadonly className: string;\n\treadonly props: Readonly<Record<string, unknown>>;\n}\n\nfunction isVideNode(value: unknown): value is VideNode {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as { [VIDE_NODE]?: unknown })[VIDE_NODE] === true\n\t);\n}\n\n/**\n * `create(\"Frame\")` returns a constructor taking a props table. String keys are\n * Roblox properties (a function value is bound reactively at mount); number keys\n * are children.\n */\nexport function create(\n\tclassName: string,\n): (props?: Record<string, unknown>) => VideNode {\n\treturn (props = {}) => ({ [VIDE_NODE]: true, className, props });\n}\n\n// --- live tree ---------------------------------------------------------------\n\n/**\n * A resolved, mutable mirror of a `VideNode`. Reactive props write their current\n * value into `props`; `toScene` reads it. Children are grouped into slots so a\n * reactive (function) child can swap its subtree in place without disturbing its\n * siblings; a static child is a one-shot slot.\n */\ninterface LiveNode {\n\tclassName: string;\n\tname: string;\n\tid: string;\n\tprops: Map<string, unknown>;\n\tchildren: ChildSlot[];\n}\n\n/** A positional group of children — its `nodes` are re-derived if reactive. */\ninterface ChildSlot {\n\tnodes: LiveNode[];\n}\n\nlet nextId = 0;\n\n/** Resolve a child value (a node, or nested arrays of nodes) to LiveNodes. */\nfunction resolveChildren(value: unknown, schedule: () => void): LiveNode[] {\n\tif (isVideNode(value)) return [build(value, schedule)];\n\tif (Array.isArray(value)) {\n\t\treturn value.flatMap((item) => resolveChildren(item, schedule));\n\t}\n\treturn [];\n}\n\nfunction addChildSlot(\n\tparent: LiveNode,\n\tvalue: unknown,\n\tschedule: () => void,\n): void {\n\tif (typeof value === \"function\") {\n\t\t// A reactive child (vide's control-flow pattern): rebuild this slot's\n\t\t// subtree whenever the function's sources change. The rebuilt nodes' own\n\t\t// reactive bindings nest under this effect, so they dispose on each re-run.\n\t\tconst slot: ChildSlot = { nodes: [] };\n\t\tparent.children.push(slot);\n\t\teffect(() => {\n\t\t\tslot.nodes = resolveChildren((value as () => unknown)(), schedule);\n\t\t\tschedule();\n\t\t});\n\t} else {\n\t\tparent.children.push({ nodes: resolveChildren(value, schedule) });\n\t}\n}\n\n/** Build a `LiveNode` from a `VideNode`, wiring reactive props/children. */\nfunction build(node: VideNode, schedule: () => void): LiveNode {\n\tconst live: LiveNode = {\n\t\tclassName: node.className,\n\t\tname: node.className,\n\t\tid: `v${nextId++}`,\n\t\tprops: new Map(),\n\t\tchildren: [],\n\t};\n\tfor (const [key, value] of Object.entries(node.props)) {\n\t\tif (/^\\d+$/.test(key)) {\n\t\t\taddChildSlot(live, value, schedule);\n\t\t\tcontinue;\n\t\t}\n\t\tif (key === \"Name\" && typeof value === \"string\") {\n\t\t\tlive.name = value;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"function\") {\n\t\t\t// A reactive prop: re-resolve and repaint whenever its sources change.\n\t\t\teffect(() => {\n\t\t\t\tlive.props.set(key, (value as () => unknown)());\n\t\t\t\tschedule();\n\t\t\t});\n\t\t} else {\n\t\t\tlive.props.set(key, value);\n\t\t}\n\t}\n\treturn live;\n}\n\n// --- live tree → Scene IR ----------------------------------------------------\n\nconst TEXT_CLASSES = new Set([\"TextLabel\", \"TextButton\", \"TextBox\"]);\nlet measureCtx: CanvasRenderingContext2D | null | undefined;\nfunction getMeasureCtx(): CanvasRenderingContext2D | null {\n\tif (measureCtx === undefined) {\n\t\tmeasureCtx =\n\t\t\ttypeof document !== \"undefined\"\n\t\t\t\t? document.createElement(\"canvas\").getContext(\"2d\")\n\t\t\t\t: null;\n\t}\n\treturn measureCtx;\n}\n\n/** Is `AutomaticSize` covering the X axis, so the width is content-derived? */\nfunction autoOnX(live: LiveNode): boolean {\n\tconst auto = live.props.get(\"AutomaticSize\");\n\tconst name = enumName(auto);\n\treturn name === \"X\" || name === \"XY\";\n}\n\n/**\n * Horizontal `UIPadding` on `live`, in pixels — the room it takes away from\n * whatever it holds. Offsets only, which is what the layout engine resolves a\n * scale inset to on an automatic axis. Mirrors the react adapter.\n */\nfunction horizontalPadding(live: LiveNode): number {\n\tfor (const slot of live.children) {\n\t\tfor (const child of slot.nodes) {\n\t\t\tif (child.className !== \"UIPadding\") continue;\n\t\t\tconst side = (name: string): number => {\n\t\t\t\tconst udim = child.props.get(name) as { Offset?: unknown } | undefined;\n\t\t\t\treturn typeof udim?.Offset === \"number\" ? udim.Offset : 0;\n\t\t\t};\n\t\t\treturn side(\"PaddingLeft\") + side(\"PaddingRight\");\n\t\t}\n\t}\n\treturn 0;\n}\n\n/**\n * The width `TextWrapped` text wraps at, or `undefined` when it does not wrap.\n * `TextWrap` is the engine's own deprecated alias for the same property, and\n * `TextWrapped` still wins when both are set.\n *\n * `room` is what the parent had left for its children (see {@link toScene}) and\n * `own` is this node's own width from the last layout:\n * - **X is not automatic** — the node's own width is fixed, so it is the\n * constraint and the height grows to however many lines result.\n * - **X is automatic** — the node's width is the thing being computed, so\n * wrapping against it would settle at one word per line. The room the nearest\n * ancestor with a width of its own leaves is the constraint instead, which is\n * what such a label does in Studio.\n *\n * Both come from the layout that last ran, so the first snapshot of a label\n * measures unwrapped; `paint` re-snapshots until the widths it measured against\n * are the ones the layout produced.\n */\nfunction wrapWidth(\n\tlive: LiveNode,\n\troom: number | undefined,\n\town: number | undefined,\n): number | undefined {\n\tconst wrapped = live.props.get(\"TextWrapped\") ?? live.props.get(\"TextWrap\");\n\tif (wrapped !== true) return undefined;\n\tconst width = autoOnX(live) ? room : own;\n\treturn width !== undefined && width > 0 ? width : undefined;\n}\n\n/**\n * Measure an auto-sizing text node with the font the renderer paints and emit a\n * `TextBounds` Vector2 the layout engine reads for AutomaticSize (font metrics\n * live browser-side, not in the WASM engine). Mirrors the react adapter.\n *\n * With `wrapAt` set the words are laid into lines greedily, which is what\n * `TextWrapped` asks for; without it each newline is the only line break.\n */\nfunction measureTextBounds(\n\tlive: LiveNode,\n\twrapAt?: number,\n): PropertyValue | undefined {\n\tif (!TEXT_CLASSES.has(live.className)) return undefined;\n\tconst auto = live.props.get(\"AutomaticSize\");\n\tconst autoName = enumName(auto);\n\tif (autoName !== \"X\" && autoName !== \"Y\" && autoName !== \"XY\")\n\t\treturn undefined;\n\tconst text = live.props.get(\"Text\");\n\tif (typeof text !== \"string\" || text === \"\") return undefined;\n\tconst ctx = getMeasureCtx();\n\tif (!ctx) return undefined;\n\n\t// `TextSize` wins, legacy `FontSize` fills in, 14 is the Roblox default —\n\t// same precedence as `@loom-dev/scene`'s `getTextSize` and the react adapter.\n\tconst rawSize = live.props.get(\"TextSize\");\n\tconst rawFontSize = live.props.get(\"FontSize\");\n\tconst size =\n\t\ttypeof rawSize === \"number\"\n\t\t\t? rawSize\n\t\t\t: (fontSizeToPx(enumName(rawFontSize)) ?? 14);\n\tctx.font = fontShorthand(\n\t\tinstanceFont({\n\t\t\tFont: live.props.get(\"Font\"),\n\t\t\tFontFace: live.props.get(\"FontFace\"),\n\t\t}),\n\t\tsize,\n\t);\n\t// `LineHeight` stretches the gap between lines, so it starts paying from the\n\t// second one — same rule as `@loom-dev/scene`'s `getLineHeight`.\n\tconst rawLineHeight = live.props.get(\"LineHeight\");\n\tconst lineHeight =\n\t\ttypeof rawLineHeight === \"number\"\n\t\t\t? Math.min(3, Math.max(1, rawLineHeight))\n\t\t\t: 1;\n\n\tlet width = 0;\n\tlet height = 0;\n\tlet lines = 0;\n\tconst endLine = (lineWidth: number): void => {\n\t\twidth = Math.max(width, lineWidth);\n\t\theight += lines === 0 ? size : size * lineHeight;\n\t\tlines += 1;\n\t};\n\tfor (const line of text.split(\"\\n\")) {\n\t\tif (wrapAt === undefined) {\n\t\t\tendLine(ctx.measureText(line).width);\n\t\t\tcontinue;\n\t\t}\n\t\tlet lineWidth = 0;\n\t\t// Split *keeping* the whitespace, so the gaps between words are measured\n\t\t// rather than assumed.\n\t\tfor (const piece of line.split(/(\\s+)/)) {\n\t\t\tif (piece === \"\") continue;\n\t\t\tconst pieceWidth = ctx.measureText(piece).width;\n\t\t\tif (lineWidth > 0 && lineWidth + pieceWidth > wrapAt) {\n\t\t\t\tendLine(lineWidth);\n\t\t\t\tlineWidth = 0;\n\t\t\t\t// A run of spaces that would overflow is dropped rather than carried\n\t\t\t\t// to the start of the next line, as every text shaper does.\n\t\t\t\tif (piece.trim() === \"\") continue;\n\t\t\t}\n\t\t\tlineWidth += pieceWidth;\n\t\t}\n\t\tendLine(lineWidth);\n\t}\n\treturn prop.vector2({ x: Math.ceil(width), y: height });\n}\n\n/** What one snapshot of the live tree needs from the layout that last ran. */\ninterface SnapshotContext {\n\t/** Rects from the previous layout — where wrapped text learns its room. */\n\treadonly rects: LayoutResult[\"rects\"];\n\t/** id → the wrap width each measured text node was measured against. */\n\treadonly wrap: Map<string, number>;\n}\n\n/**\n * Snapshot the live tree as Scene IR (called fresh on every paint). `room` is\n * what the parent has left for its children: the width of the nearest ancestor\n * that has one of its own, less every `UIPadding` in between.\n */\nfunction toScene(\n\tlive: LiveNode,\n\tctx: SnapshotContext,\n\troom?: number,\n): SceneNode {\n\tconst node: SceneNode = {\n\t\tclassName: live.className,\n\t\tname: live.name,\n\t\tid: live.id,\n\t};\n\tconst properties: Record<string, PropertyValue> = {};\n\tfor (const [key, value] of live.props) {\n\t\tconst pv = toPropertyValue(value);\n\t\tif (pv !== undefined) properties[key] = pv;\n\t}\n\tconst own = ctx.rects[live.id]?.rect.width;\n\tconst wrapAt = wrapWidth(live, room, own);\n\tconst textBounds = measureTextBounds(live, wrapAt);\n\tif (textBounds) {\n\t\tproperties.TextBounds = textBounds;\n\t\tctx.wrap.set(live.id, wrapAt ?? 0);\n\t}\n\tif (Object.keys(properties).length > 0) node.properties = properties;\n\tconst children = live.children.flatMap((slot) => slot.nodes);\n\tif (children.length > 0) {\n\t\t// An auto-sized node was itself sized by what is inside it, so it passes\n\t\t// its parent's room down untouched; a node with a width of its own becomes\n\t\t// the constraint. Either way its own padding comes off.\n\t\tconst base = autoOnX(live) ? room : own;\n\t\tconst inner =\n\t\t\tbase === undefined ? undefined : base - horizontalPadding(live);\n\t\tnode.children = children.map((child) => toScene(child, ctx, inner));\n\t}\n\treturn node;\n}\n\n/** Whether two snapshots measured their wrapped text against the same widths. */\nfunction sameWrap(a: Map<string, number>, b: Map<string, number>): boolean {\n\tif (a.size !== b.size) return false;\n\tfor (const [id, width] of a) if (b.get(id) !== width) return false;\n\treturn true;\n}\n\n// How many times one paint will re-snapshot wrapped text against the width the\n// layout it just ran produced. Two is the settling case (measure unwrapped →\n// learn the width → measure wrapped); the rest is headroom for a label whose\n// wrapping changes the width it wraps against. Mirrors the react adapter.\nconst MAX_WRAP_PASSES = 4;\n\n// --- mount -------------------------------------------------------------------\n\nconst HOST_ID = \"loom-root\";\n\n/** The outer preview viewport (`#loom-root`), created if the host page lacks it. */\nfunction resolveHost(target?: HTMLElement): HTMLElement {\n\tif (target) return target;\n\tconst existing = document.getElementById(HOST_ID);\n\tif (existing) return existing;\n\tconst el = document.createElement(\"div\");\n\tel.id = HOST_ID;\n\tel.style.position = \"relative\";\n\tel.style.width = \"100vw\";\n\tel.style.height = \"100vh\";\n\tel.style.overflow = \"hidden\";\n\tdocument.body.appendChild(el);\n\treturn el;\n}\n\n/** The layout pass a mount runs; the WASM engine unless one is injected. */\nexport type ComputeLayout = (\n\troot: SceneNode,\n\tviewport: Viewport,\n) => LayoutResult;\n\nexport interface MountOptions {\n\t/** Replace the WASM layout engine (tests). Skips {@link initLayout}. */\n\tcomputeLayout?: ComputeLayout;\n}\n\n/**\n * Mount a vide component into the preview DOM and return an unmount function.\n * `component` is run once to build the tree; reactive props/children drive\n * subsequent repaints. Each mount gets its own container under `#loom-root`, so\n * independent mounts don't clobber each other (the renderer replaces its own\n * container's children every commit). The tree is laid out (WASM) and rendered\n * with a ResizeObserver re-laying-out on viewport changes — same as react.\n */\nexport function mount(\n\tcomponent: () => VideNode,\n\ttarget?: HTMLElement,\n\toptions?: MountOptions,\n): () => void {\n\tconst host = document.createElement(\"div\");\n\thost.style.position = \"absolute\";\n\thost.style.inset = \"0\";\n\tresolveHost(target).appendChild(host);\n\n\tconst layoutOf = options?.computeLayout ?? computeLayout;\n\n\treturn root((dispose) => {\n\t\tlet ready = false;\n\t\tlet scheduled = false;\n\t\tlet disposed = false;\n\t\tlet live: LiveNode | undefined;\n\t\t/** The rects the last paint produced — this one's wrap widths come from it. */\n\t\tlet rects: LayoutResult[\"rects\"] = {};\n\n\t\tconst paint = (): void => {\n\t\t\tscheduled = false;\n\t\t\tif (disposed || !ready || !live) return;\n\t\t\tconst width = host.clientWidth;\n\t\t\tconst height = host.clientHeight;\n\t\t\tif (width === 0 || height === 0) return; // wait for the mount to be sized\n\t\t\ttry {\n\t\t\t\t// Wrapped text is measured against a width that only exists once the\n\t\t\t\t// layout has run, so the first snapshot of a label is unwrapped.\n\t\t\t\t// Rendering that pass would put a label wider than its container into\n\t\t\t\t// the DOM, and during a live window resize — where every frame brings\n\t\t\t\t// a fresh width — the stale pass is what stays on screen. So keep\n\t\t\t\t// re-snapshotting until the widths measured against are the ones the\n\t\t\t\t// layout produced, and render once, with the settled result.\n\t\t\t\tlet scene: SceneNode | undefined;\n\t\t\t\tlet layout: LayoutResult | undefined;\n\t\t\t\tlet measured: Map<string, number> | undefined;\n\t\t\t\tfor (let pass = 0; pass < MAX_WRAP_PASSES; pass++) {\n\t\t\t\t\tconst wrap = new Map<string, number>();\n\t\t\t\t\tconst next = toScene(live, { rects, wrap });\n\t\t\t\t\t// The snapshot this pass would lay out is the one already laid\n\t\t\t\t\t// out: nothing moved, so it has settled.\n\t\t\t\t\tif (measured && sameWrap(measured, wrap)) break;\n\t\t\t\t\tscene = next;\n\t\t\t\t\tmeasured = wrap;\n\t\t\t\t\tlayout = layoutOf(scene, { width, height });\n\t\t\t\t\trects = layout.rects;\n\t\t\t\t}\n\t\t\t\tif (!scene || !layout) return;\n\t\t\t\trenderScene(scene, layout, host);\n\t\t\t} catch (err) {\n\t\t\t\t// Never let a malformed scene escape an effect or the RO callback.\n\t\t\t\tconsole.error(\"loom vide:\", err);\n\t\t\t}\n\t\t};\n\t\tconst schedule = (): void => {\n\t\t\tif (scheduled) return;\n\t\t\tscheduled = true;\n\t\t\tqueueMicrotask(paint);\n\t\t};\n\n\t\t// Build inside this root so the reactive bindings are disposable.\n\t\tlive = build(component(), schedule);\n\n\t\tconst observer = new ResizeObserver(() => paint());\n\t\tobserver.observe(host);\n\t\t// Text bounds were measured against the faces the browser had at the time,\n\t\t// so a face arriving later invalidates the layout that came out of them.\n\t\tconst stopFontWatch = onFontsChanged(() => paint());\n\t\tif (options?.computeLayout) {\n\t\t\tready = true;\n\t\t\tpaint();\n\t\t} else {\n\t\t\tvoid initLayout().then(() => {\n\t\t\t\tready = true;\n\t\t\t\tpaint();\n\t\t\t});\n\t\t}\n\n\t\treturn () => {\n\t\t\tdisposed = true; // a queued microtask paint must not resurrect the DOM\n\t\t\tobserver.disconnect();\n\t\t\tstopFontWatch();\n\t\t\tdispose();\n\t\t\thost.remove();\n\t\t};\n\t});\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loom-dev/vide",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Vide adapter for loom — fine-grained reactivity producing the same Scene IR as the React renderer.",
5
5
  "keywords": [
6
6
  "loom",
@@ -34,10 +34,10 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@loom-dev/layout": "0.9.0",
38
- "@loom-dev/scene": "0.9.0",
39
- "@loom-dev/runtime": "0.9.0",
40
- "@loom-dev/renderer": "0.9.0"
37
+ "@loom-dev/layout": "0.9.2",
38
+ "@loom-dev/renderer": "0.9.2",
39
+ "@loom-dev/runtime": "0.9.2",
40
+ "@loom-dev/scene": "0.9.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "tsdown": "^0.22.12",