@unoverse-platform/studio 0.1.103 → 0.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/package.json +4 -2
  2. package/screens/AtomsView.tsx +2 -1
  3. package/screens/ComponentsView.tsx +2 -1
  4. package/screens/NativeAppHost.tsx +1 -2
  5. package/screens/TemplatesLibraryView.tsx +14 -1
  6. package/screens/TemplatesView.tsx +2 -1
  7. package/screens/index.ts +4 -0
  8. package/screens/react/unoverse.tsx +281 -0
  9. package/vendor/base/src/agent-mcp/index.ts +1 -1
  10. package/vendor/base/src/agent-mcp/invoke.ts +6 -2
  11. package/vendor/base/src/agent-mcp/tools.ts +14 -5
  12. package/vendor/base/src/agent-mcp/types.ts +6 -0
  13. package/vendor/base/src/auth/auth.ts +90 -23
  14. package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
  15. package/vendor/base/src/definitions/load/compile/template.ts +23 -2
  16. package/vendor/base/src/definitions/model.ts +9 -0
  17. package/vendor/base/src/definitions/registry/catalog.ts +6 -7
  18. package/vendor/base/src/executor.ts +25 -1
  19. package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
  20. package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
  21. package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
  22. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
  23. package/vendor/base/src/platform/serviceCalls.ts +13 -0
  24. package/vendor/sdk/appEngine.ts +363 -0
  25. package/vendor/sdk/lib/actions.ts +36 -0
  26. package/vendor/sdk/lib/citations.ts +48 -0
  27. package/vendor/sdk/lib/core/apps.ts +18 -0
  28. package/vendor/sdk/lib/core/client.ts +3 -3
  29. package/vendor/sdk/lib/core/store.ts +6 -0
  30. package/vendor/sdk/lib/dom/app.ts +546 -0
  31. package/vendor/sdk/lib/dom/chrome.ts +125 -0
  32. package/vendor/sdk/lib/dom/component.ts +333 -0
  33. package/vendor/sdk/lib/dom/connection.ts +309 -0
  34. package/vendor/sdk/lib/dom/element.ts +69 -0
  35. package/vendor/sdk/lib/dom/isolate.ts +37 -0
  36. package/vendor/sdk/lib/dom/markdown.ts +388 -0
  37. package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
  38. package/vendor/sdk/lib/dom/mounted.ts +29 -0
  39. package/vendor/sdk/lib/dom/primitives.ts +689 -0
  40. package/vendor/sdk/lib/dom/region.ts +195 -0
  41. package/vendor/sdk/lib/dom/render.ts +177 -0
  42. package/vendor/sdk/lib/dom/theme.ts +38 -0
  43. package/vendor/sdk/lib/dom/types.ts +36 -0
  44. package/vendor/sdk/lib/index.ts +26 -17
  45. package/vendor/sdk/lib/orb/canvas.ts +139 -0
  46. package/vendor/sdk/lib/orb/index.ts +8 -18
  47. package/vendor/sdk/lib/orb/presets.ts +1 -1
  48. package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
  49. package/vendor/sdk/lib/realtime/capture.ts +152 -0
  50. package/vendor/sdk/lib/realtime/playback.ts +129 -0
  51. package/vendor/sdk/lib/realtime/socket.ts +117 -0
  52. package/vendor/sdk/lib/style.ts +45 -37
  53. package/vendor/sdk/lib/voiceService.ts +344 -0
  54. package/vendor/sdk/main.ts +107 -0
  55. package/vendor/sdk/mcpAppBridge.ts +125 -102
  56. package/vendor/sdk/pageInterfaces.ts +452 -0
  57. package/vendor/sdk/responseView.ts +193 -0
  58. package/vite.config.ts +3 -3
  59. package/vendor/sdk/appEngine.tsx +0 -395
  60. package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
  61. package/vendor/sdk/lib/app.tsx +0 -730
  62. package/vendor/sdk/lib/connection.tsx +0 -433
  63. package/vendor/sdk/lib/isolate.tsx +0 -70
  64. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
  65. package/vendor/sdk/lib/orb/theme.ts +0 -83
  66. package/vendor/sdk/lib/primitives.tsx +0 -943
  67. package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
  68. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
  69. package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
  70. package/vendor/sdk/lib/render.tsx +0 -244
  71. package/vendor/sdk/lib/streamed.tsx +0 -30
  72. package/vendor/sdk/lib/theme.ts +0 -55
  73. package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
  74. package/vendor/sdk/lib/transition/index.ts +0 -9
  75. package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
  76. package/vendor/sdk/lib/voice.tsx +0 -422
  77. package/vendor/sdk/main.tsx +0 -125
  78. package/vendor/sdk/pageInterfaces.tsx +0 -388
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -1,244 +0,0 @@
1
- /**
2
- * ════════════════════════════════════════════════════════════════════════════
3
- * THE DISPATCHER: UnoverseNode → React. ⛔ READ BEFORE EDITING ⛔
4
- * ════════════════════════════════════════════════════════════════════════════
5
- *
6
- * THIS FILE RENDERS NOTHING. It only WALKS the tree and DISPATCHES `node.type`
7
- * to a primitive component in ./primitives. There is NOT A SINGLE raw element
8
- * here — no `<div>`, `<span>`, `<button>`, `<img>`, `<input>`, `<svg>`. If you
9
- * are about to write one, STOP: it belongs in ./primitives. This is enforced by
10
- * test/dispatcher-only.test.mjs (the build fails if a raw element appears here),
11
- * so the engine/primitive split can't silently rot — which is exactly how
12
- * `Button` (and its styling logic) kept leaking back into the dispatcher.
13
- *
14
- * THE FRAMEWORK IN ONE SENTENCE: this SDK is a FIXED, GENERIC interpreter. ALL
15
- * UX is built in DATA (design/ definitions + the served theme) — an author creates
16
- * ANY interface WITHOUT editing this SDK. (See FRAMEWORK.md.)
17
- *
18
- * WHERE THINGS LIVE:
19
- * • ./render — this dispatcher (control flow: visibleWhen, Each, slots) ONLY.
20
- * • ./primitives — EVERY primitive (Box/Text/Image/Button/Icon/Skeleton/Input/
21
- * Markdown/Unknown) + the shared per-element chrome.
22
- * • ./style — the style + animation interpreter (styleToCss/keyframesCss).
23
- *
24
- * Two laws (govern all three files + the whole package):
25
- * LAW 1 — OWN ZERO STYLE VALUES. No hex/px/rem/em/recipes; resolve token NAMES
26
- * against the SERVED theme. Enforced by test/golden-rule.test.mjs.
27
- * LAW 2 — OWN ZERO UX SHAPE. A primitive renders ONE generic element, nothing
28
- * about a specific UX. A composer/card/loader is a LAYOUT — compose it
29
- * in a DEFINITION. Never a per-UX primitive or per-UX config.
30
- *
31
- * Need something the vocab can't express? Prefer extending the generic interpreter
32
- * ONCE (a new style-vocab key in ./style, a new model projection) over a primitive.
33
- * You almost never need to touch this file — build it in design/ first.
34
- * ════════════════════════════════════════════════════════════════════════════
35
- */
36
- import type { ReactNode } from "react";
37
- import type { ActionSpec, ResolvedTheme, UnoverseNode } from "./core";
38
- import { visible, selectCase, cssWidth } from "./core";
39
- import { resolveRef } from "./core/refs";
40
- import { BoxView, TextView, ImageView, ButtonView, UnknownView, SkeletonView, InputView, SelectView, MarkdownView, IconView, OrbView } from "./primitives";
41
-
42
- // The style + animation interpreter lives in ./style; re-exported so consumers keep
43
- // importing it from "./render".
44
- export { styleToCss, keyframesCss } from "./style";
45
-
46
- // An action is a bare string (→ workflow) or a declarative envelope (§2e-3). The
47
- // binding layer (streamed.tsx) interprets it; primitives just forward node.action.
48
- /** Instance context riding with a bubbled server action — WHICH component fired and its
49
- * current slice, so the channel can forward the submitted state (e.g. a wizard's answers)
50
- * without reaching into the store. Pure transport metadata; the SDK interprets none of it. */
51
- export interface ActionMeta {
52
- chatId?: string;
53
- nodeId?: string;
54
- /** Which component this instance is — so the server needs no registry to find out. */
55
- type?: string;
56
- state?: Record<string, unknown>;
57
- }
58
-
59
- export type ActionHandler = (action: string | ActionSpec, data: Record<string, unknown>, meta?: ActionMeta) => void;
60
-
61
- /**
62
- * A leaf's action, with the node's declared `analytics` attached (UNOVERSE_ANALYTICS.md).
63
- *
64
- * Authors write `analytics:` beside `action:` on the node, where the data it reports is
65
- * already in scope. The two travel together from here so `dispatchAction` remains the ONE
66
- * place an interaction is understood: leaves stay dumb, and no leaf can forget to report.
67
- * A node with no `analytics` block passes its action through untouched, unallocated.
68
- */
69
- export function nodeAction(node: UnoverseNode, fallback: string): string | ActionSpec {
70
- const action = node.action ?? fallback;
71
- if (!node.analytics?.event) return action;
72
- return typeof action === "string"
73
- ? { type: action, analytics: node.analytics }
74
- : { ...action, analytics: node.analytics };
75
- }
76
-
77
- /**
78
- * Resolves a `ComponentSlot` node against the store (provided by the template
79
- * renderer). Returns the rendered leaves (or the slot's fallback). Components
80
- * don't use slots, so this is undefined on the component render path.
81
- */
82
- export type SlotResolver = (node: UnoverseNode, key?: React.Key) => ReactNode;
83
-
84
- // cssWidth moved to core/apps.ts (computeAppWidth needs it and core imports
85
- // nothing from the React layer); re-exported to keep this import path working.
86
- export { cssWidth };
87
-
88
- export function renderNode(
89
- node: UnoverseNode,
90
- data: Record<string, unknown>,
91
- onAction: ActionHandler | undefined,
92
- theme: ResolvedTheme,
93
- key?: React.Key,
94
- slot?: SlotResolver,
95
- /**
96
- * The shared pieces this definition REFERENCES, by name. Present only when the server
97
- * sends references instead of inlining copies of them; absent for every definition
98
- * served the old way, in which case no `Ref` reaches this function at all.
99
- */
100
- defs?: Record<string, UnoverseNode>,
101
- ): ReactNode {
102
- // visibleWhen — conditional visibility (§2e-3). A bare field name is a truthiness test
103
- // (an empty array counts as empty); an object is a `Condition` (eq/ne/in, or truthy) so
104
- // one field can drive tabs/wizard steps without a boolean per state. The predicate is
105
- // shared with `Switch` and `style.when` (core/conditions.ts) — one state semantics.
106
- if (node.visibleWhen != null && !visible(node.visibleWhen, data)) return null;
107
-
108
- // `skeleton: true` — the protocol's LOADING semantic: while the node's bound field has
109
- // no data yet, render its Skeleton ghost IN PLACE (inside the node's own styled box, so
110
- // the ghost occupies exactly the space the content will). Variant inferred from the
111
- // node type; the ghost's entire look is theme data (LAW 1). This is a generic BEHAVIOR
112
- // (no data yet → ghost), not a style or a UX shape — the one judgment only the
113
- // renderer can make, since only it watches binds resolve over time.
114
- if (node.skeleton) {
115
- const field = node.bind?.value ?? node.bind?.src ?? node.bind?.items;
116
- if (field && !visible(field, data)) {
117
- const variant = node.type === "Image" ? "image" : node.type === "Each" ? "card" : "text";
118
- return (
119
- <BoxView key={key} node={{ type: "Box", style: node.style }} data={data} theme={theme}>
120
- <SkeletonView variant={variant} theme={theme} />
121
- </BoxView>
122
- );
123
- }
124
- }
125
-
126
- // STATE-OWNED SIZING (docs/design/05): a plain node's `appWidth` = an ALWAYS-OPEN
127
- // panel (e.g. the core chat column). The ONE declared value both grows the app
128
- // (computeAppWidth) and fixes the panel's box here — they can never disagree, and
129
- // nothing elastic remains for a resize to land on. Slot panels are injected at their
130
- // frame (template.tsx); a layout root never carries appWidth (guard-enforced).
131
- const panelW = (node as { appWidth?: string }).appWidth;
132
- // ComponentSlot AND Template keep their appWidth: both are slot-dispatched places
133
- // whose width applies to their FRAME (app.tsx), not to a box of their own —
134
- // consuming it here left the mount frame width-less (measured live 2026-08-28).
135
- if (panelW && node.type !== "ComponentSlot" && node.type !== "Template" && node.type !== "Place") {
136
- const w = cssWidth(theme.appSize?.[panelW] ?? panelW);
137
- if (w) node = { ...node, appWidth: undefined, style: { ...(node.style ?? {}), width: w, flex: "0 0 auto" } } as UnoverseNode;
138
- }
139
-
140
- // Store-backed / leaf primitives → dispatch to ./primitives.
141
- if (node.type === "Skeleton") return <SkeletonView key={key} variant={node.variant} theme={theme} />;
142
- // Icon: DATA path reads `bind.name`; LITERAL path reads the `icon` attribute. `name` is
143
- // read as a literal too, because an atom that binds `name` (the only spelling the data
144
- // path reads) has that bind turned INTO a `name` attribute when a Ref supplies the glyph
145
- // through `with` — so without this an atom can be data-driven or literal, never both.
146
- if (node.type === "Icon")
147
- return <IconView key={key} name={node.bind?.name ? (data[node.bind.name] as string) : (node.icon ?? node.name)} style={node.style} theme={theme} />;
148
- // Orb: procedural thinking/working animation — state is a literal or bound (`bind.state`).
149
- if (node.type === "Orb") return <OrbView key={key} node={node} data={data} theme={theme} />;
150
- if (node.type === "Input") return <InputView key={key} node={node} data={data} theme={theme} onAction={onAction} />;
151
- if (node.type === "Markdown") return <MarkdownView key={key} node={node} data={data} theme={theme} onAction={onAction} />;
152
- if (node.type === "ComponentSlot" || node.type === "Timeline" || node.type === "Template" || node.type === "Place") return slot ? slot(node, key) : null;
153
-
154
- // `Ref` — REFERENCE, NOT COPY. The design system's shared pieces travel once and are
155
- // cached; a definition names one and specialises it here (core/refs.ts: `props` remap,
156
- // `with` literals, style/action/visibleWhen overrides). This is what stops a button
157
- // being re-sent inside every definition that uses it.
158
- //
159
- // A Ref only arrives when the server sent references, so this branch is inert for
160
- // anything served the old, inlined way. An unknown name renders NOTHING rather than a
161
- // broken box: a hole is easier to diagnose than a mystery element.
162
- if (node.type === "Ref") {
163
- const resolved = resolveRef(node, defs?.[String((node as { ref?: string }).ref ?? "")]);
164
- return resolved ? renderNode(resolved, data, onAction, theme, key, slot, defs) : null;
165
- }
166
-
167
- // `Switch` — control flow: render EXACTLY ONE branch, the case matching `data[on]`
168
- // (else `cases.default`, else nothing). The declarative whole-view swap (wizard step,
169
- // inline↔focused) — mutually exclusive in one place, vs. a `visibleWhen` per branch. No
170
- // wrapper element: it is transparent, like an `if`, delegating straight to the branch.
171
- if (node.type === "Switch") {
172
- const branch = selectCase(node, data);
173
- return branch ? renderNode(branch, data, onAction, theme, key, slot, defs) : null;
174
- }
175
-
176
- // `Each` — control flow: map a bound array (bind.items) → the `item` subtree once
177
- // per item (that item as the data scope), inside the node's own container (a Box).
178
- // A PRIMITIVE item (string/number — e.g. a `string[]` of follow-up questions) is exposed
179
- // as `{ value: item }`, so the item binds `value` like any field; object items pass
180
- // through unchanged. This is what lets an `Each` iterate a plain list of strings.
181
- if (node.type === "Each") {
182
- // Literal `items` fallback: a microapp component hardcodes its static option lists
183
- // in the def (props exist only for state — UNOVERSE_AUTHORING.md). bind wins.
184
- const src = node.bind?.items ? data[node.bind.items] : node.items;
185
- const items = Array.isArray(src) ? src : [];
186
- // `item` is the Each's repeated drawing (renamed from `template` 2026-09-01: one word for
187
- // a holder NAME (string) on Template, which this branch never sees. Narrow rather than
188
- // assert, so a definition that puts the wrong one here renders nothing instead of
189
- // throwing inside the map.
190
- const tpl = typeof node.item === "object" ? node.item : undefined;
191
- // WHERE AN ITEM SITS is something only the renderer knows, and a numbered list cannot
192
- // be authored without it: the data carries no ordinal, and a template cannot count.
193
- // `index` is 0-based for logic, `position` 1-based because that is what a reader sees.
194
- // The item's own fields always win, so data that carries either name is untouched.
195
- // AN ARRAY ITEM IS THE VALUE. `typeof [] === "object"`, so an array took the spread
196
- // branch and its INDICES became scope keys ({0: "London", 1: "+44…"}) while `value` was
197
- // never set. A nested Each bound to `value` — a table's row of cells — then found
198
- // nothing, and every table rendered its headers over empty rows.
199
- const scopeOf = (item: unknown, i: number): Record<string, unknown> => {
200
- const base = { index: i, position: i + 1 };
201
- return item != null && typeof item === "object" && !Array.isArray(item)
202
- ? { ...base, ...(item as Record<string, unknown>) }
203
- : { ...base, value: item };
204
- };
205
- const rendered = tpl ? items.map((item, i) => renderNode(tpl, scopeOf(item, i), onAction, theme, i, slot, defs)) : null;
206
- return (
207
- <BoxView key={key} node={node} data={data} theme={theme}>
208
- {rendered}
209
- </BoxView>
210
- );
211
- }
212
-
213
- // Element primitives → dispatch. Children are rendered here (control flow) and handed down.
214
- const kids = (node.children ?? []).map((c, i) => renderNode(c, data, onAction, theme, i, slot, defs));
215
- switch (node.type) {
216
- case "Box":
217
- case "Stack":
218
- case "Row":
219
- case "Column":
220
- return (
221
- <BoxView key={key} node={node} data={data} theme={theme} onAction={onAction}>
222
- {kids}
223
- </BoxView>
224
- );
225
- case "Text":
226
- return <TextView key={key} node={node} data={data} theme={theme} />;
227
- case "Image":
228
- return <ImageView key={key} node={node} data={data} theme={theme} />;
229
- case "Select":
230
- return <SelectView key={key} node={node} data={data} theme={theme} onAction={onAction} />;
231
- case "Button":
232
- return (
233
- <ButtonView key={key} node={node} data={data} theme={theme} onAction={onAction}>
234
- {kids}
235
- </ButtonView>
236
- );
237
- default:
238
- return (
239
- <UnknownView key={key} node={node} theme={theme}>
240
- {kids}
241
- </UnknownView>
242
- );
243
- }
244
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * DEPRECATED ALIAS. `UnoverseComponent` now renders from either a plain `data` object or a
3
- * live store, so the streamed variant is the same component with the store props filled in.
4
- *
5
- * Kept so existing channels keep working while their call sites move over. Nothing here
6
- * decides anything: it forwards.
7
- */
8
- import { UnoverseComponent, useUnoverseInstance } from "./UnoverseComponent";
9
- import type { ComponentStore, ResolvedTheme, UnoverseClient } from "./core";
10
- import type { ActionHandler } from "./render";
11
-
12
- export { useUnoverseInstance };
13
-
14
- export interface StreamedUnoverseComponentProps {
15
- client: UnoverseClient;
16
- store: ComponentStore;
17
- chatId: string;
18
- nodeId: string;
19
- onAction?: ActionHandler;
20
- theme?: ResolvedTheme;
21
- /** Neutral turn state merged into the component's data scope (e.g. `streaming`). */
22
- extraData?: Record<string, unknown>;
23
- /** A surface's single occupant fills the surface — see UnoverseComponent.fill. */
24
- fill?: boolean;
25
- }
26
-
27
- /** @deprecated Render `UnoverseComponent` with `store`, `chatId` and `nodeId`. */
28
- export function StreamedUnoverseComponent(props: StreamedUnoverseComponentProps) {
29
- return <UnoverseComponent {...props} />;
30
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * useUnoverseTheme — fetch the RESOLVED theme from the MCP server by name.
3
- *
4
- * The SDK owns ZERO token values. The server owns the visual (apps/unoverse/
5
- * design/styles, resolved live) and serves it at `unoverse://theme/{name}`; this hook
6
- * fetches it (cached in the client). A brand change in design/styles is refresh-only
7
- * — there is no baked theme in this bundle to rebuild (UNOVERSE_SPEC §2d-1).
8
- */
9
- import { useEffect, useState } from "react";
10
- import type { ResolvedTheme, UnoverseClient } from "./core";
11
-
12
- /**
13
- * Ensure the theme's webfont stylesheets are loaded ONCE in the document head.
14
- * `theme.fonts` is SERVED DATA (`fonts.stylesheets` in an org's styles) — the SDK
15
- * authors no font choices, it just registers what the server serves. Fonts must live
16
- * at document level: a shadow root cannot register @font-face, but faces declared in
17
- * the document apply inside shadow trees via the font cascade.
18
- */
19
- export function ensureFontStylesheets(theme: ResolvedTheme | null): void {
20
- if (!theme?.fonts?.length || typeof document === "undefined") return;
21
- for (const href of theme.fonts) {
22
- if (document.head.querySelector(`link[data-unoverse-font="${CSS.escape(href)}"]`)) continue;
23
- const link = document.createElement("link");
24
- link.rel = "stylesheet";
25
- link.href = href;
26
- link.setAttribute("data-unoverse-font", href);
27
- document.head.appendChild(link);
28
- }
29
- }
30
-
31
- /** Returns the resolved theme (null until the first fetch resolves).
32
- * `name: null` = don't fetch yet (the caller is still deriving the name — e.g. waiting
33
- * for the definition whose `org` names its pack's theme). An unknown name falls back to
34
- * the neutral `light` so a pack without a theme still renders. */
35
- export function useUnoverseTheme(client: UnoverseClient, name: string | null = "light"): ResolvedTheme | null {
36
- const [theme, setTheme] = useState<ResolvedTheme | null>(null);
37
- useEffect(() => {
38
- if (!name) return;
39
- let alive = true;
40
- client
41
- .readTheme(name)
42
- .then((t) => alive && setTheme(t))
43
- .catch(() => {
44
- if (!alive || name === "light") return;
45
- client
46
- .readTheme("light")
47
- .then((t) => alive && setTheme(t))
48
- .catch(() => {});
49
- });
50
- return () => {
51
- alive = false;
52
- };
53
- }, [client, name]);
54
- return theme;
55
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * The double-buffered mount (UNOVERSE_TEMPLATE_MODEL §13.3): renders its LAST
3
- * COMMITTED arrangement and commits the next one only when the decision allows —
4
- * so a data ripple can never move the shelf, and the boundary commit is the one
5
- * designed (and, with theme motion, animated) change. Content inside held
6
- * instances stays live automatically: the render closure reads slices from the
7
- * store at render time; only membership and order are buffered here.
8
- *
9
- * Fail open (§13.5): a "hold" with no boundary inside the grace window commits
10
- * anyway — the screen never pays for machinery failure.
11
- *
12
- * RENDER-LAYER state, deliberately: the portable core stays frozen and "no UI
13
- * state in the store" holds.
14
- */
15
-
16
- import React, { useEffect, useRef, useState } from "react";
17
- import type { ReactNode } from "react";
18
- import { decideCommit, type Arrangement } from "./decide";
19
- import { commitArrangement } from "./viewTransition";
20
-
21
- /** A held-back change commits anyway after this long without a boundary. */
22
- const GRACE_MS = 4000;
23
-
24
- export function MountArrangement({
25
- next,
26
- directed,
27
- motion,
28
- render,
29
- }: {
30
- /** The arrangement the store derives RIGHT NOW. */
31
- next: Arrangement;
32
- /** The placement grants a director — every delivery settles, so membership
33
- * waits for the settle (decide.ts). */
34
- directed: boolean;
35
- /** Theme-requested motion (§13.4) — absent theme word = instant commits. */
36
- motion: boolean;
37
- /** Build the tree for an arrangement; slices are read live inside. */
38
- render: (arrangement: Arrangement) => ReactNode;
39
- }) {
40
- const [committed, setCommitted] = useState<Arrangement | null>(null);
41
- const graceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
42
- const decision = decideCommit(committed, next, directed);
43
-
44
- useEffect(() => {
45
- if (decision === "none") return;
46
- if (decision === "hold") {
47
- if (!graceTimer.current) {
48
- graceTimer.current = setTimeout(() => {
49
- graceTimer.current = null;
50
- setCommitted(next);
51
- }, GRACE_MS);
52
- }
53
- return;
54
- }
55
- if (graceTimer.current) {
56
- clearTimeout(graceTimer.current);
57
- graceTimer.current = null;
58
- }
59
- if (decision === "transition") commitArrangement(() => setCommitted(next), motion);
60
- else setCommitted(next);
61
- });
62
-
63
- useEffect(
64
- () => () => {
65
- if (graceTimer.current) clearTimeout(graceTimer.current);
66
- },
67
- [],
68
- );
69
-
70
- return <>{render(committed ?? next)}</>;
71
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * The Transition Model (UNOVERSE_TEMPLATE_MODEL §13) — the render layer's
3
- * designed way of moving interfaces in and out of a mounted template:
4
- * `decide.ts` (the pure commit decision), `MountArrangement.tsx` (the
5
- * double-buffer), `viewTransition.ts` (the theme-gated animated commit).
6
- */
7
- export { decideCommit, type Arrangement, type CommitDecision } from "./decide";
8
- export { MountArrangement } from "./MountArrangement";
9
- export { commitArrangement } from "./viewTransition";
@@ -1,29 +0,0 @@
1
- /**
2
- * The commit IS the animation (UNOVERSE_TEMPLATE_MODEL §13.4): when the theme
3
- * asks for motion, the boundary commit is wrapped in the browser's View
4
- * Transitions API — the browser diffs old and new and animates; the THEME's CSS
5
- * owns every duration and ease. The SDK contributes only this generic behaviour
6
- * and NO styles. Engines without the API, and themes that author no motion, get
7
- * the instant commit — today's behaviour, gracefully.
8
- */
9
-
10
- import { flushSync } from "react-dom";
11
-
12
- type WithViewTransition = { startViewTransition?: (cb: () => void) => unknown };
13
-
14
- export function commitArrangement(commit: () => void, motion: boolean): void {
15
- const doc = typeof document !== "undefined" ? (document as unknown as WithViewTransition) : undefined;
16
- if (!motion || typeof doc?.startViewTransition !== "function") {
17
- commit();
18
- return;
19
- }
20
- // From a microtask, outside React's lifecycle, so flushSync is legal: the
21
- // API snapshots, the commit flushes the new DOM, the browser animates.
22
- queueMicrotask(() => {
23
- try {
24
- doc.startViewTransition!(() => flushSync(commit));
25
- } catch {
26
- commit(); // fail open — the change lands even if the ceremony breaks
27
- }
28
- });
29
- }