@lumencast/runtime 0.7.0 → 0.9.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/.tsbuildinfo +1 -1
- package/dist/animate/keyframes.js +8 -1
- package/dist/animate/keyframes.js.map +1 -1
- package/dist/app.d.ts +4 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +2 -1
- package/dist/app.js.map +1 -1
- package/dist/{broadcast-DUYqvcgo.js → broadcast-ryjLRD5q.js} +3 -3
- package/dist/{broadcast-DUYqvcgo.js.map → broadcast-ryjLRD5q.js.map} +1 -1
- package/dist/{control-CL8TWXaE.js → control-AgxbXOVS.js} +4 -4
- package/dist/{control-CL8TWXaE.js.map → control-AgxbXOVS.js.map} +1 -1
- package/dist/{index-C6viWFcT.js → index-DrXsLYhe.js} +309 -212
- package/dist/index-DrXsLYhe.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/lumencast.js +14 -9
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +5 -0
- package/dist/mount.js.map +1 -1
- package/dist/overlay/runtime-context.d.ts +11 -0
- package/dist/overlay/runtime-context.d.ts.map +1 -1
- package/dist/overlay/runtime-context.js +8 -0
- package/dist/overlay/runtime-context.js.map +1 -1
- package/dist/render/asset-resolve.d.ts +27 -0
- package/dist/render/asset-resolve.d.ts.map +1 -0
- package/dist/render/asset-resolve.js +86 -0
- package/dist/render/asset-resolve.js.map +1 -0
- package/dist/render/bundle.d.ts +1 -1
- package/dist/render/bundle.d.ts.map +1 -1
- package/dist/render/bundle.js +4 -0
- package/dist/render/bundle.js.map +1 -1
- package/dist/render/headless.d.ts +39 -0
- package/dist/render/headless.d.ts.map +1 -0
- package/dist/render/headless.js +83 -0
- package/dist/render/headless.js.map +1 -0
- package/dist/render/keyframe-player.d.ts.map +1 -1
- package/dist/render/keyframe-player.js +15 -1
- package/dist/render/keyframe-player.js.map +1 -1
- package/dist/render/primitives/capture.d.ts +40 -0
- package/dist/render/primitives/capture.d.ts.map +1 -0
- package/dist/render/primitives/capture.js +171 -0
- package/dist/render/primitives/capture.js.map +1 -0
- package/dist/render/primitives/index.d.ts.map +1 -1
- package/dist/render/primitives/index.js +3 -0
- package/dist/render/primitives/index.js.map +1 -1
- package/dist/render/primitives/media.d.ts +11 -2
- package/dist/render/primitives/media.d.ts.map +1 -1
- package/dist/render/primitives/media.js +14 -3
- package/dist/render/primitives/media.js.map +1 -1
- package/dist/render/prop-allowlist.d.ts.map +1 -1
- package/dist/render/prop-allowlist.js +5 -0
- package/dist/render/prop-allowlist.js.map +1 -1
- package/dist/{status-pill-jJT54n07.js → status-pill-BxCdj-KZ.js} +2 -2
- package/dist/{status-pill-jJT54n07.js.map → status-pill-BxCdj-KZ.js.map} +1 -1
- package/dist/{test-84XodL1c.js → test-CaRHj_J6.js} +4 -4
- package/dist/{test-84XodL1c.js.map → test-CaRHj_J6.js.map} +1 -1
- package/dist/{tree-BIimahCf.js → tree-BLIxJbD3.js} +515 -432
- package/dist/tree-BLIxJbD3.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/animate/keyframes.ts +8 -1
- package/src/app.tsx +5 -0
- package/src/index.ts +29 -0
- package/src/mount.ts +5 -0
- package/src/overlay/runtime-context.tsx +14 -0
- package/src/render/asset-resolve.ts +97 -0
- package/src/render/bundle.ts +9 -1
- package/src/render/headless.tsx +129 -0
- package/src/render/keyframe-player.tsx +14 -1
- package/src/render/primitives/capture.tsx +210 -0
- package/src/render/primitives/index.ts +3 -0
- package/src/render/primitives/media.tsx +14 -3
- package/src/render/prop-allowlist.ts +5 -0
- package/src/types.ts +10 -0
- package/dist/index-C6viWFcT.js.map +0 -1
- package/dist/tree-BIimahCf.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ErrorCode } from "@lumencast/protocol";
|
|
2
|
+
import type { ResolveCaptureDevice } from "./render/primitives/capture";
|
|
2
3
|
export type LumencastMode = "broadcast" | "control" | "test";
|
|
3
4
|
export type LumencastStatus = "disconnected" | "connecting" | "live";
|
|
4
5
|
export interface LumencastTokenProvider {
|
|
@@ -53,6 +54,15 @@ export interface MountOptions {
|
|
|
53
54
|
* logs — `broadcast` builds stay console-silent. When omitted, the
|
|
54
55
|
* runtime falls back to a DEV-only console.warn. */
|
|
55
56
|
onDiagnostic?: (diagnostic: LumencastDiagnostic) => void;
|
|
57
|
+
/** ADR 004 §A1.3 — host resolver for the `x-zab.capture` primitive's ACQUIRE
|
|
58
|
+
* mode. Given the LOGICAL `(deviceRef, sourceKind)` from the bundle, return
|
|
59
|
+
* `{ deviceId }` to pin a physical device, or `null` for the host's default
|
|
60
|
+
* device. The runtime passes `deviceId` only as a live `getUserMedia`
|
|
61
|
+
* constraint — it NEVER enters the bundle or the content hash. Omit it and
|
|
62
|
+
* ACQUIRE uses the default device ("the cam traverses"), never throwing.
|
|
63
|
+
* Only consulted on a capture-capable host (e.g. the Electron preview
|
|
64
|
+
* webview) ; ignored on-air (CEF/Pulsar render the placeholder). */
|
|
65
|
+
resolveCaptureDevice?: ResolveCaptureDevice;
|
|
56
66
|
}
|
|
57
67
|
export interface LumencastHandle {
|
|
58
68
|
/** Tear down the WS, unmount the React tree, release timers. Idempotent. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,WAAW,GACX,mBAAmB,GACnB,eAAe,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;+BAE+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C;;;yDAGqD;IACrD,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACzD;;;;;;;yEAOqE;IACrE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;CAC7C;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumencast/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Browser runtime for Lumencast — mount(), LSDP/1 transport, leaf-grain store, LSML render, animations, overlays.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"framer-motion": "^12.0.0",
|
|
37
37
|
"react": "^19.0.0",
|
|
38
38
|
"react-dom": "^19.0.0",
|
|
39
|
-
"@lumencast/protocol": "0.
|
|
39
|
+
"@lumencast/protocol": "0.9.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@playwright/test": "^1.49.1",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"vite-plugin-dts": "^4.5.0",
|
|
51
51
|
"vitest": "^4.1.5",
|
|
52
52
|
"ws": "^8.18.0",
|
|
53
|
-
"@lumencast/
|
|
54
|
-
"@lumencast/server": "0.
|
|
53
|
+
"@lumencast/server": "0.9.0",
|
|
54
|
+
"@lumencast/dev-server": "0.9.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"dev": "vite",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"test:watch": "vitest",
|
|
62
62
|
"test:e2e": "playwright test",
|
|
63
|
-
"check:bundle": "node scripts/check-bundle-size.mjs"
|
|
63
|
+
"check:bundle": "node scripts/check-bundle-size.mjs",
|
|
64
|
+
"check:no-fetch": "node scripts/check-no-fetch.mjs"
|
|
64
65
|
}
|
|
65
66
|
}
|
package/src/animate/keyframes.ts
CHANGED
|
@@ -151,7 +151,14 @@ function pullTransform(
|
|
|
151
151
|
if (prop === "rotate") {
|
|
152
152
|
out.rotate = values.map((n) => `${n}deg`);
|
|
153
153
|
} else {
|
|
154
|
-
|
|
154
|
+
// ADR 011 I7 live-bug fix (2nd half): framer-motion animates transform
|
|
155
|
+
// through its shorthand motion keys `x`/`y`, NOT `translateX`/`translateY`
|
|
156
|
+
// — emitting the authored names verbatim left framer with unknown keys
|
|
157
|
+
// it silently dropped, so the box never translated at the antenna (only
|
|
158
|
+
// the opacity fade survived). Map the translate channels onto the framer
|
|
159
|
+
// keys. `scale`/`rotate` already match framer's vocabulary.
|
|
160
|
+
const framerKey = prop === "translateX" ? "x" : prop === "translateY" ? "y" : prop;
|
|
161
|
+
out[framerKey] = values;
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
}
|
package/src/app.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import type { Store } from "./state/store.js";
|
|
|
18
18
|
import type { RenderBundle } from "./render/bundle.js";
|
|
19
19
|
import type { ConnectionStatus } from "./transport/ws.js";
|
|
20
20
|
import { LumencastRuntimeProvider } from "./overlay/runtime-context.js";
|
|
21
|
+
import type { ResolveCaptureDevice } from "./render/primitives/capture.js";
|
|
21
22
|
import type { LumencastMode } from "./types.js";
|
|
22
23
|
|
|
23
24
|
const LazyBroadcastMode = lazy(() =>
|
|
@@ -35,6 +36,8 @@ export interface LumencastAppProps {
|
|
|
35
36
|
statusSignal: Signal<ConnectionStatus>;
|
|
36
37
|
crossfadeKeySignal: Signal<string>;
|
|
37
38
|
sendInput: (patches: Patch[]) => void;
|
|
39
|
+
/** ADR 004 §A1.3 — host resolver for `x-zab.capture` ACQUIRE mode. */
|
|
40
|
+
resolveCaptureDevice?: ResolveCaptureDevice;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
export function LumencastApp({
|
|
@@ -44,6 +47,7 @@ export function LumencastApp({
|
|
|
44
47
|
statusSignal,
|
|
45
48
|
crossfadeKeySignal,
|
|
46
49
|
sendInput,
|
|
50
|
+
resolveCaptureDevice,
|
|
47
51
|
}: LumencastAppProps) {
|
|
48
52
|
useSignals();
|
|
49
53
|
|
|
@@ -72,6 +76,7 @@ export function LumencastApp({
|
|
|
72
76
|
bundle,
|
|
73
77
|
status,
|
|
74
78
|
sendInput,
|
|
79
|
+
...(resolveCaptureDevice !== undefined ? { resolveCaptureDevice } : {}),
|
|
75
80
|
}}
|
|
76
81
|
>
|
|
77
82
|
<Suspense fallback={null}>
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,11 @@ export {
|
|
|
26
26
|
} from "./render/diagnostics.js";
|
|
27
27
|
export { PRIMITIVE_PROP_ALLOWLIST } from "./render/prop-allowlist.js";
|
|
28
28
|
|
|
29
|
+
// ADR 004 §A1.3 — host resolver type for the `x-zab.capture` ACQUIRE mode,
|
|
30
|
+
// supplied via `MountOptions.resolveCaptureDevice`. Exported so the consuming
|
|
31
|
+
// app (Prism/Solar) types its injected resolver against the runtime's contract.
|
|
32
|
+
export type { ResolveCaptureDevice } from "./render/primitives/capture.js";
|
|
33
|
+
|
|
29
34
|
// Bundle types are useful for hosts that want to typecheck pre-compiled scenes.
|
|
30
35
|
export type {
|
|
31
36
|
RenderBundle,
|
|
@@ -46,3 +51,27 @@ export {
|
|
|
46
51
|
isAuthoringProfile,
|
|
47
52
|
validateBundleProfiles,
|
|
48
53
|
} from "./render/bundle.js";
|
|
54
|
+
|
|
55
|
+
// Headless render (ADR 003) — render an already-compiled RenderBundle into a
|
|
56
|
+
// live DOM node, no WS, ready when layout + fonts settle. Hosts (Solar headless
|
|
57
|
+
// entry, ZabCanvas render worker, the zero-loss harness) screenshot the target
|
|
58
|
+
// once `ready` resolves. The runtime does DOM + readiness only — no fetch, no
|
|
59
|
+
// screenshot. Dynamically pulls BroadcastMode so it adds no eager weight to the
|
|
60
|
+
// `mount`/broadcast path (RC6).
|
|
61
|
+
export { renderBundleHeadless } from "./render/headless.js";
|
|
62
|
+
export type { HeadlessRenderOptions, HeadlessRenderHandle } from "./render/headless.js";
|
|
63
|
+
|
|
64
|
+
// Asset / font resolution helpers for headless hosts (ADR 003 §3.2). No-fetch:
|
|
65
|
+
// they only rewrite a bundle's `src`s against a caller table and load fonts
|
|
66
|
+
// from caller-supplied `data:` URIs. The host-allow gate stays the sole
|
|
67
|
+
// authority. (`FontFace` is the public type name per ADR 003 RC5; it is the
|
|
68
|
+
// spec object — distinct from the DOM `FontFace` constructor.)
|
|
69
|
+
export {
|
|
70
|
+
resolveSrc,
|
|
71
|
+
rewriteLayoutSrcs,
|
|
72
|
+
rewriteDefaultsSrcs,
|
|
73
|
+
injectFonts,
|
|
74
|
+
type AssetTable,
|
|
75
|
+
type FontFaceSpec,
|
|
76
|
+
type FontFaceSpec as FontFace,
|
|
77
|
+
} from "./render/asset-resolve.js";
|
package/src/mount.ts
CHANGED
|
@@ -118,6 +118,11 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
118
118
|
statusSignal,
|
|
119
119
|
crossfadeKeySignal,
|
|
120
120
|
sendInput: (patches) => ws.sendInput(patches),
|
|
121
|
+
// ADR 004 §A1.3 — thread the host capture resolver to the runtime context
|
|
122
|
+
// so the `x-zab.capture` primitive's ACQUIRE mode can pin a device.
|
|
123
|
+
...(options.resolveCaptureDevice !== undefined
|
|
124
|
+
? { resolveCaptureDevice: options.resolveCaptureDevice }
|
|
125
|
+
: {}),
|
|
121
126
|
}),
|
|
122
127
|
);
|
|
123
128
|
|
|
@@ -4,6 +4,7 @@ import type { Store } from "../state/store";
|
|
|
4
4
|
import type { RenderBundle } from "../render/bundle";
|
|
5
5
|
import type { ConnectionStatus } from "../transport/ws";
|
|
6
6
|
import type { LumencastMode } from "../types";
|
|
7
|
+
import type { ResolveCaptureDevice } from "../render/primitives/capture";
|
|
7
8
|
|
|
8
9
|
export interface LumencastRuntime {
|
|
9
10
|
mode: LumencastMode;
|
|
@@ -12,6 +13,10 @@ export interface LumencastRuntime {
|
|
|
12
13
|
status: ConnectionStatus;
|
|
13
14
|
/** Send LSDP/1 input patches to the server. */
|
|
14
15
|
sendInput: (patches: Patch[]) => void;
|
|
16
|
+
/** ADR 004 §A1.3 — host-provided resolver mapping a LOGICAL `deviceRef` to a
|
|
17
|
+
* physical `deviceId` for the `x-zab.capture` primitive's ACQUIRE mode.
|
|
18
|
+
* Injected from `MountOptions`, NOT the bundle. Absent → default device. */
|
|
19
|
+
resolveCaptureDevice?: ResolveCaptureDevice;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const Ctx = createContext<LumencastRuntime | null>(null);
|
|
@@ -35,3 +40,12 @@ export function useLumencastRuntime(): LumencastRuntime {
|
|
|
35
40
|
}
|
|
36
41
|
return v;
|
|
37
42
|
}
|
|
43
|
+
|
|
44
|
+
/** Read the runtime context WITHOUT throwing when no provider is mounted.
|
|
45
|
+
* Render primitives (e.g. `x-zab.capture`) may render via `<Tree>` directly —
|
|
46
|
+
* embedded hosts, tooling, tests — outside `mount()`'s provider. They use this
|
|
47
|
+
* to pick up mount-level host config (the capture resolver) when present and
|
|
48
|
+
* fall back to defaults when not. */
|
|
49
|
+
export function useOptionalLumencastRuntime(): LumencastRuntime | null {
|
|
50
|
+
return useContext(Ctx);
|
|
51
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Public asset + font resolution helpers for headless / host render (ADR 003 §3.2).
|
|
2
|
+
//
|
|
3
|
+
// These utilities let a HOST (Solar headless entry, a ZabCanvas render worker,
|
|
4
|
+
// the zero-loss harness) resolve a bundle's content-addressed asset references
|
|
5
|
+
// to concrete `data:` URIs and inject brand `@font-face`s BEFORE the first
|
|
6
|
+
// frame — exactly as the zero-loss harness has always done (ADR 002 #J). They
|
|
7
|
+
// are promoted here verbatim so every host resolves identically and exercises
|
|
8
|
+
// the SAME host-allow gate the runtime applies internally.
|
|
9
|
+
//
|
|
10
|
+
// ── No-fetch contract (ADR 003 §3.2, D3, Bastion R2) ────────────────────────
|
|
11
|
+
// NONE of these helpers performs a network fetch. They only rewrite a bundle's
|
|
12
|
+
// `src` values against a caller-supplied table (`resolveSrc` /
|
|
13
|
+
// `rewriteLayoutSrcs` / `rewriteDefaultsSrcs`) and load `@font-face`s from
|
|
14
|
+
// caller-supplied `data:`/same-document URLs (`injectFonts`). The runtime never
|
|
15
|
+
// reaches the network; the host owns where bytes come from. Both the input
|
|
16
|
+
// table values and the font `src`s are expected to be `data:` (or otherwise
|
|
17
|
+
// already host-allowed) URIs — substituting one already-admitted scheme for
|
|
18
|
+
// another, so the deny-by-default `allowedHosts` gate stays the sole authority.
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** A table mapping a bundle `src` reference to its resolved `data:` URI.
|
|
22
|
+
* Keys may be either the full `assets/<hash>.ext` path or a bare `<hash>`. */
|
|
23
|
+
export type AssetTable = Readonly<Record<string, string>>;
|
|
24
|
+
|
|
25
|
+
/** Resolve a single `src` value against a table. Non-string or unmatched values
|
|
26
|
+
* pass through unchanged. Matches both `assets/<hash>.ext` and bare `<hash>`. */
|
|
27
|
+
export function resolveSrc(src: unknown, table: AssetTable): unknown {
|
|
28
|
+
if (typeof src !== "string") return src;
|
|
29
|
+
if (table[src]) return table[src];
|
|
30
|
+
const m = /^assets\/([A-Za-z0-9]+)\.[A-Za-z0-9]+$/.exec(src);
|
|
31
|
+
if (m && m[1] !== undefined && table[m[1]]) return table[m[1]];
|
|
32
|
+
return src;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Deep-rewrite every `src` (image-fill, mask source) in a layout subtree,
|
|
36
|
+
* in place, against the supplied asset table. */
|
|
37
|
+
export function rewriteLayoutSrcs(node: unknown, table: AssetTable): void {
|
|
38
|
+
if (node === null || typeof node !== "object") return;
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
for (const n of node) rewriteLayoutSrcs(n, table);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const obj = node as Record<string, unknown>;
|
|
44
|
+
if ("src" in obj) obj["src"] = resolveSrc(obj["src"], table);
|
|
45
|
+
for (const v of Object.values(obj)) {
|
|
46
|
+
if (v && typeof v === "object") rewriteLayoutSrcs(v, table);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Rewrite the `__lit.image.*` defaults (image-primitive `bind.src` targets)
|
|
51
|
+
* against the asset table, returning a new defaults object. */
|
|
52
|
+
export function rewriteDefaultsSrcs(
|
|
53
|
+
defaults: Record<string, unknown>,
|
|
54
|
+
table: AssetTable,
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
const out: Record<string, unknown> = { ...defaults };
|
|
57
|
+
for (const [k, v] of Object.entries(out)) {
|
|
58
|
+
if (k.startsWith("__lit.image.")) out[k] = resolveSrc(v, table);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A brand `@font-face` to inject before render. `src` is a `data:` URI (or any
|
|
64
|
+
* same-document URL) so no network/host-allow surface is involved. */
|
|
65
|
+
export interface FontFaceSpec {
|
|
66
|
+
family: string;
|
|
67
|
+
weight: number | string;
|
|
68
|
+
style?: string;
|
|
69
|
+
/** `url(data:font/woff2;base64,…)` content (the value inside `src:`). */
|
|
70
|
+
src: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Inject `@font-face` rules and block until the faces are loaded, so the very
|
|
74
|
+
* first painted frame already uses the brand glyphs (no fallback-font flash).
|
|
75
|
+
* Returns the families that successfully loaded. Loads only from the supplied
|
|
76
|
+
* `src` (expected `data:`); never fetches a remote host on its own behalf. */
|
|
77
|
+
export async function injectFonts(faces: readonly FontFaceSpec[]): Promise<string[]> {
|
|
78
|
+
const loaded: string[] = [];
|
|
79
|
+
for (const f of faces) {
|
|
80
|
+
try {
|
|
81
|
+
const face = new FontFace(f.family, f.src, {
|
|
82
|
+
weight: String(f.weight),
|
|
83
|
+
style: f.style ?? "normal",
|
|
84
|
+
});
|
|
85
|
+
await face.load();
|
|
86
|
+
(document as Document & { fonts: FontFaceSet }).fonts.add(face);
|
|
87
|
+
loaded.push(f.family);
|
|
88
|
+
} catch {
|
|
89
|
+
// A font that fails to load is a documented gap, not a render-breaker:
|
|
90
|
+
// the fallback glyphs paint and the host can detect the missing family
|
|
91
|
+
// in the returned list. Never throw (would abort an otherwise-good
|
|
92
|
+
// render) and never log the value (R9) — the absent family name is in
|
|
93
|
+
// `f.family`, which the caller already holds.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return loaded;
|
|
97
|
+
}
|
package/src/render/bundle.ts
CHANGED
|
@@ -25,7 +25,11 @@ export type RenderKind =
|
|
|
25
25
|
| "shape"
|
|
26
26
|
| "media"
|
|
27
27
|
| "repeat"
|
|
28
|
-
| "instance"
|
|
28
|
+
| "instance"
|
|
29
|
+
// Zab vendor primitive (RFC-0001, §17.1) — a transparent capture
|
|
30
|
+
// placeholder. Recognised by the Zab-plugin runtime ; reserves a box and
|
|
31
|
+
// renders nothing.
|
|
32
|
+
| "x-zab.capture";
|
|
29
33
|
|
|
30
34
|
export interface RenderNode {
|
|
31
35
|
kind: RenderKind;
|
|
@@ -139,6 +143,10 @@ export interface RenderBundle {
|
|
|
139
143
|
*/
|
|
140
144
|
export const SUPPORTED_PROFILES: ReadonlySet<string> = new Set<string>([
|
|
141
145
|
"x-lumencast.color-srgb-1.0",
|
|
146
|
+
// RFC-0001 / ADR 004 — this runtime ships the Zab capture plugin, so a
|
|
147
|
+
// bundle declaring `x-zab.capture/1` in `profiles[]` is compatible (it is
|
|
148
|
+
// NOT rejected as BUNDLE_INCOMPATIBLE, §17.3.1).
|
|
149
|
+
"x-zab.capture/1",
|
|
142
150
|
]);
|
|
143
151
|
|
|
144
152
|
// LSML 1.1 §17.5.1 + ADR 001 RC#14 — authoring-profile detection.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Public headless render entry — render an already-compiled `RenderBundle`
|
|
2
|
+
// into a live DOM node, no WebSocket, ready when layout + fonts have settled
|
|
3
|
+
// (ADR 003 §3.1). The host (Playwright / Chromium / a CEF offscreen surface)
|
|
4
|
+
// screenshots `target` once `ready` resolves. The runtime does DOM + readiness
|
|
5
|
+
// ONLY — no screenshot, no fetch (ADR 003 D5/D3).
|
|
6
|
+
//
|
|
7
|
+
// This is the zero-loss harness (ADR 002 #J) generalised: it mounts the EXACT
|
|
8
|
+
// production seam —
|
|
9
|
+
// LumencastRuntimeProvider{ mode:"broadcast", status:"live" } > BroadcastMode
|
|
10
|
+
// — into a real `createRoot(target)`, NOT `renderToStaticMarkup` (which yields
|
|
11
|
+
// unlaid-out markup: unmeasured fonts, uncomposited masks → an infidel PNG,
|
|
12
|
+
// ADR 003 §3.1). `BroadcastMode` is dynamically imported so the headless
|
|
13
|
+
// function adds no weight to the eager `mount`/broadcast path (ADR 003 §4,
|
|
14
|
+
// RC6); the heavy render code already lives in the broadcast/tree chunks.
|
|
15
|
+
//
|
|
16
|
+
// Asset resolution is the HOST's job, done in the bundle BEFORE this call
|
|
17
|
+
// (ADR 003 §3.2): the runtime renders the bundle as-is, gating every remaining
|
|
18
|
+
// `src` through the unchanged deny-by-default host-allow gate inside
|
|
19
|
+
// `BroadcastMode` (`AllowedHostsProvider`). A `src` on a host not in the
|
|
20
|
+
// bundle's `allowedHosts` is omitted + a diagnostic is emitted — never faked
|
|
21
|
+
// (ADR 002 borne, D4). Use `render/asset-resolve` helpers to pre-resolve.
|
|
22
|
+
|
|
23
|
+
import { StrictMode } from "react";
|
|
24
|
+
import { createRoot } from "react-dom/client";
|
|
25
|
+
import { createStore } from "../state/store.js";
|
|
26
|
+
import { LumencastRuntimeProvider } from "../overlay/runtime-context.js";
|
|
27
|
+
import { addDiagnosticsHandler, type DiagnosticHandler } from "./diagnostics.js";
|
|
28
|
+
import type { RenderBundle } from "./bundle.js";
|
|
29
|
+
|
|
30
|
+
/** Default stage size — the Figma 817:3 cover frame, the SSIM reference. */
|
|
31
|
+
const DEFAULT_STAGE = { width: 1920, height: 1080 } as const;
|
|
32
|
+
|
|
33
|
+
export interface HeadlessRenderOptions {
|
|
34
|
+
/** Already-compiled bundle (via `@lumencast/compiler` on the host side). */
|
|
35
|
+
bundle: RenderBundle;
|
|
36
|
+
/** A live, mounted DOM node. Its size is set from `stage` unless the host
|
|
37
|
+
* has already dimensioned it (see `stage`). */
|
|
38
|
+
target: HTMLElement;
|
|
39
|
+
/** Initial leaf-grain store state (`store.reset(defaults)`) — the bound
|
|
40
|
+
* values the bundle reads (`__lit.*`, score, names…). */
|
|
41
|
+
defaults?: Record<string, unknown>;
|
|
42
|
+
/** Stage dimensions in CSS px. Defaults to 1920×1080. Applied to `target`
|
|
43
|
+
* as `width`/`height`/`position:relative`/`overflow:hidden` so the
|
|
44
|
+
* screenshot frame matches the reference exactly. */
|
|
45
|
+
stage?: { width: number; height: number };
|
|
46
|
+
/** Anti-drop diagnostics channel (ADR 001 §3.4): omitted assets, unhonoured
|
|
47
|
+
* fields surface here as `{ nodeId, field, reason }` (never a value — R9).
|
|
48
|
+
* Wired to the same global channel `mount()` uses. */
|
|
49
|
+
onDiagnostic?: DiagnosticHandler;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface HeadlessRenderHandle {
|
|
53
|
+
/** Resolves after the scene has rendered, two animation frames have passed
|
|
54
|
+
* AND `document.fonts.ready` (ADR 003 §3.3) — i.e. the DOM is laid out and
|
|
55
|
+
* fonts are loaded, so a screenshot taken now is fidelity-faithful. */
|
|
56
|
+
ready: Promise<void>;
|
|
57
|
+
/** Tear down the React root and detach the diagnostics handler. */
|
|
58
|
+
unmount(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const noop = (): void => {};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render `bundle` into `target` through the production broadcast path and
|
|
65
|
+
* resolve `ready` once it is settled. The runtime performs NO network fetch and
|
|
66
|
+
* takes NO screenshot — it produces a settled live DOM and a readiness signal,
|
|
67
|
+
* nothing more (ADR 003 D5).
|
|
68
|
+
*/
|
|
69
|
+
export function renderBundleHeadless(opts: HeadlessRenderOptions): HeadlessRenderHandle {
|
|
70
|
+
const stage = opts.stage ?? DEFAULT_STAGE;
|
|
71
|
+
const target = opts.target;
|
|
72
|
+
// Pose the stage so the screenshot frame is exact (mirrors harness.html).
|
|
73
|
+
target.style.position ||= "relative";
|
|
74
|
+
target.style.width = `${stage.width}px`;
|
|
75
|
+
target.style.height = `${stage.height}px`;
|
|
76
|
+
target.style.overflow = "hidden";
|
|
77
|
+
|
|
78
|
+
const removeDiagnostics = opts.onDiagnostic
|
|
79
|
+
? addDiagnosticsHandler(opts.onDiagnostic)
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
const store = createStore();
|
|
83
|
+
store.reset(opts.defaults ?? {});
|
|
84
|
+
|
|
85
|
+
const root = createRoot(target);
|
|
86
|
+
|
|
87
|
+
const ready = new Promise<void>((resolve) => {
|
|
88
|
+
// BroadcastMode is dynamically imported so its (and the tree's) weight is
|
|
89
|
+
// not pulled into the eager `mount` entry chunk (RC6). It is already a
|
|
90
|
+
// separate chunk reused from the broadcast path.
|
|
91
|
+
void import("../modes/broadcast.js").then(({ BroadcastMode }) => {
|
|
92
|
+
root.render(
|
|
93
|
+
<StrictMode>
|
|
94
|
+
<LumencastRuntimeProvider
|
|
95
|
+
value={{
|
|
96
|
+
mode: "broadcast",
|
|
97
|
+
store,
|
|
98
|
+
bundle: opts.bundle,
|
|
99
|
+
status: "live",
|
|
100
|
+
sendInput: noop,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<BroadcastMode />
|
|
104
|
+
</LumencastRuntimeProvider>
|
|
105
|
+
</StrictMode>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Settle: two animation frames (layout) AND fonts loaded (ADR 003 §3.3).
|
|
109
|
+
// Both must complete before `ready` resolves, so a screenshot taken on
|
|
110
|
+
// `ready` uses the brand glyphs, not the fallback font (no FOUT freeze).
|
|
111
|
+
const framesSettled = new Promise<void>((res) => {
|
|
112
|
+
requestAnimationFrame(() => requestAnimationFrame(() => res()));
|
|
113
|
+
});
|
|
114
|
+
const fontsReady =
|
|
115
|
+
typeof document !== "undefined" && document.fonts
|
|
116
|
+
? document.fonts.ready.then(() => undefined)
|
|
117
|
+
: Promise.resolve();
|
|
118
|
+
void Promise.all([framesSettled, fontsReady]).then(() => resolve());
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
ready,
|
|
124
|
+
unmount() {
|
|
125
|
+
removeDiagnostics?.();
|
|
126
|
+
root.unmount();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -64,7 +64,20 @@ export function KeyframePlayer({
|
|
|
64
64
|
return (
|
|
65
65
|
<motion.div
|
|
66
66
|
key={replayTokenRef.current}
|
|
67
|
-
|
|
67
|
+
// A `display:contents` element generates NO box, so the browser
|
|
68
|
+
// never composites the animated `transform`/`opacity`/`filter` this
|
|
69
|
+
// player writes — they are silently dropped and the subtree renders
|
|
70
|
+
// dead at its child's default origin (ADR 011 I7 live bug). The
|
|
71
|
+
// player must be a REAL compositing box. `position:absolute; inset:0`
|
|
72
|
+
// overlays the parent without disturbing sibling layout, and — being
|
|
73
|
+
// positioned — becomes the containing block for the absolutely-
|
|
74
|
+
// positioned primitive nested beneath it (Frame is `position:absolute;
|
|
75
|
+
// left:0; top:0`), so the child's authored `x`/`y` resolve against the
|
|
76
|
+
// player's (0,0) exactly as they did against the grandparent under
|
|
77
|
+
// `display:contents`. The animated channels now composite onto a live
|
|
78
|
+
// box and the whole subtree (the nested target's geometry + fill)
|
|
79
|
+
// moves and fades with the keyframes.
|
|
80
|
+
style={{ position: "absolute", inset: 0 }}
|
|
68
81
|
initial={firstFrame(compiled.animate)}
|
|
69
82
|
animate={compiled.animate}
|
|
70
83
|
transition={transition}
|