@unoverse-platform/studio 0.1.102 → 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 -393
  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 -383
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -0,0 +1,195 @@
1
+ /**
2
+ * A REGION: a part of the page whose shape is worked out from the store on every update (what
3
+ * a slot holds, a place, a mounted template, the conversation). It is described as data, a
4
+ * `Region`, and a region view brings the page to that description, keeping whatever it can:
5
+ * a component that stays in the list keeps its elements, and a tree that keeps its key is
6
+ * updated in place.
7
+ */
8
+ import type { UnoverseNode } from "../core/types";
9
+ import type { CssStyle } from "../style";
10
+ import { setChildren, setStyle } from "./element";
11
+ import { renderNode } from "./render";
12
+ import type { Data, RenderCtx, View } from "./types";
13
+
14
+ /** Something a region holds whole (a component instance): its elements, and its end. */
15
+ export interface Leaf {
16
+ readonly nodes: Node[];
17
+ destroy(): void;
18
+ }
19
+
20
+ /** What a slot inside a tree draws, worked out from the node and its data. */
21
+ export type SlotCompute = (node: UnoverseNode, data: Data) => Region;
22
+
23
+ export type Region =
24
+ | { kind: "none" }
25
+ /** A definition subtree, drawn with these facts; its slots are worked out by `slot`. */
26
+ | { kind: "tree"; key: string; node: UnoverseNode; data: Data; ctx: Pick<RenderCtx, "theme" | "onAction" | "defs">; slot?: SlotCompute }
27
+ /** Instances by key. `flip`: each sits in a measuring box and slides when the order changes. */
28
+ | { kind: "list"; key: string; flip: boolean; items: { key: string; make(): Leaf; refresh(leaf: Leaf): void }[] }
29
+ /** A plain box around regions. */
30
+ | { kind: "box"; key: string; style: CssStyle; parts: Region[] }
31
+ /** Elements the host made (a host's own chrome). */
32
+ | { kind: "nodes"; key: string; nodes: Node[] };
33
+
34
+ const NONE: Region = { kind: "none" };
35
+
36
+ export interface RegionView {
37
+ readonly nodes: Node[];
38
+ set(region: Region): void;
39
+ destroy(): void;
40
+ }
41
+
42
+ /** A slot whose content is a region, worked out again on every update. */
43
+ function slotView(doc: Document, mounted: RenderCtx["mounted"], holder: { slot?: SlotCompute }, node: UnoverseNode, data: Data): View {
44
+ const rv = regionView(doc, mounted);
45
+ const view: View = {
46
+ get nodes() {
47
+ return rv.nodes;
48
+ },
49
+ update(n, d) {
50
+ rv.set(holder.slot ? holder.slot(n, d) : NONE);
51
+ },
52
+ destroy: () => rv.destroy(),
53
+ };
54
+ view.update(node, data);
55
+ return view;
56
+ }
57
+
58
+ export function regionView(doc: Document, mounted: RenderCtx["mounted"]): RegionView {
59
+ let current: Region = NONE;
60
+ let destroyCurrent = () => {};
61
+ let nodesOf = (): Node[] => [];
62
+ let updateCurrent: (r: Region) => void = () => {};
63
+
64
+ const build = (r: Region) => {
65
+ destroyCurrent();
66
+ current = r;
67
+ switch (r.kind) {
68
+ case "none":
69
+ nodesOf = () => [];
70
+ destroyCurrent = () => {};
71
+ updateCurrent = () => {};
72
+ return;
73
+ case "nodes": {
74
+ let nodes = r.nodes;
75
+ nodesOf = () => nodes;
76
+ destroyCurrent = () => {};
77
+ updateCurrent = (n) => void (nodes = (n as Extract<Region, { kind: "nodes" }>).nodes);
78
+ return;
79
+ }
80
+ case "tree": {
81
+ const holder: { slot?: SlotCompute } = { slot: r.slot };
82
+ const ctx: RenderCtx = { doc, mounted, ...r.ctx };
83
+ // A slot exists only while the region names how to fill it.
84
+ const withSlot = () => (ctx.slot = holder.slot ? (n, d) => slotView(doc, mounted, holder, n, d) : undefined);
85
+ withSlot();
86
+ const view = renderNode(r.node, r.data, ctx);
87
+ nodesOf = () => view.nodes;
88
+ destroyCurrent = () => view.destroy();
89
+ updateCurrent = (n) => {
90
+ const t = n as Extract<Region, { kind: "tree" }>;
91
+ Object.assign(ctx, t.ctx);
92
+ holder.slot = t.slot;
93
+ withSlot();
94
+ view.update(t.node, t.data);
95
+ };
96
+ return;
97
+ }
98
+ case "box": {
99
+ const el = doc.createElement("div");
100
+ const parts: RegionView[] = [];
101
+ nodesOf = () => [el];
102
+ destroyCurrent = () => parts.forEach((p) => p.destroy());
103
+ updateCurrent = (n) => {
104
+ const b = n as Extract<Region, { kind: "box" }>;
105
+ setStyle(el, b.style);
106
+ b.parts.forEach((p, i) => (parts[i] ??= regionView(doc, mounted)).set(p));
107
+ for (const p of parts.splice(b.parts.length)) p.destroy();
108
+ setChildren(el, parts.flatMap((p) => p.nodes));
109
+ };
110
+ updateCurrent(r);
111
+ return;
112
+ }
113
+ case "list": {
114
+ const leaves = new Map<string, { leaf: Leaf; box?: HTMLDivElement }>();
115
+ let order: string[] = [];
116
+ let flip = r.flip;
117
+ // FLIP: what was where, by key, to slide a reorder from.
118
+ const positions = new Map<string, { left: number; top: number }>();
119
+ let lastOrder = "";
120
+ const slide = () => {
121
+ const nextOrder = order.join(" ");
122
+ // The guest's reduce-motion setting wins: reorders land instantly.
123
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
124
+ // ANIMATE ONLY A REORDER: chrome growing above or a scroll shifts every position a
125
+ // little, and playing that back made cards wobble while the guest typed.
126
+ const moved = !reduced && lastOrder !== "" && lastOrder !== nextOrder;
127
+ for (const key of order) {
128
+ const box = leaves.get(key)?.box;
129
+ if (!box) continue;
130
+ const pos = { left: box.offsetLeft, top: box.offsetTop };
131
+ const prev = positions.get(key);
132
+ positions.set(key, pos);
133
+ const dx = prev && moved ? prev.left - pos.left : 0;
134
+ const dy = prev && moved ? prev.top - pos.top : 0;
135
+ if (dx || dy) {
136
+ box.style.transition = "none";
137
+ box.style.transform = `translate(${dx}px, ${dy}px)`;
138
+ requestAnimationFrame(() => {
139
+ box.style.transition = "transform 240ms ease";
140
+ box.style.transform = "";
141
+ });
142
+ }
143
+ }
144
+ for (const key of positions.keys()) if (!leaves.has(key)) positions.delete(key);
145
+ lastOrder = nextOrder;
146
+ };
147
+ nodesOf = () => order.flatMap((k) => {
148
+ const e = leaves.get(k)!;
149
+ return e.box ? [e.box] : e.leaf.nodes;
150
+ });
151
+ destroyCurrent = () => leaves.forEach((e) => e.leaf.destroy());
152
+ updateCurrent = (n) => {
153
+ const l = n as Extract<Region, { kind: "list" }>;
154
+ flip = l.flip;
155
+ const keep = new Set(l.items.map((i) => i.key));
156
+ for (const [k, e] of leaves) if (!keep.has(k)) {
157
+ e.leaf.destroy();
158
+ leaves.delete(k);
159
+ }
160
+ for (const item of l.items) {
161
+ let e = leaves.get(item.key);
162
+ if (!e) {
163
+ e = { leaf: item.make() };
164
+ leaves.set(item.key, e);
165
+ } else item.refresh(e.leaf);
166
+ if (flip) {
167
+ e.box ??= doc.createElement("div");
168
+ setChildren(e.box, e.leaf.nodes);
169
+ } else e.box = undefined;
170
+ }
171
+ order = l.items.map((i) => i.key);
172
+ if (flip) mounted(slide);
173
+ };
174
+ updateCurrent(r);
175
+ return;
176
+ }
177
+ }
178
+ };
179
+
180
+ return {
181
+ get nodes() {
182
+ return nodesOf();
183
+ },
184
+ set(r) {
185
+ if (r.kind === current.kind && (r.kind === "none" || (r as { key: string }).key === (current as { key: string }).key)) updateCurrent(r);
186
+ else build(r);
187
+ },
188
+ destroy() {
189
+ destroyCurrent();
190
+ current = NONE;
191
+ nodesOf = () => [];
192
+ destroyCurrent = () => {};
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * ════════════════════════════════════════════════════════════════════════════
3
+ * THE DISPATCHER, IN PLAIN DOM: UnoverseNode → View. ⛔ NO REACT ⛔
4
+ * ════════════════════════════════════════════════════════════════════════════
5
+ *
6
+ * This file draws nothing. It walks the tree and hands each node to one primitive in
7
+ * ./primitives (or ./markdown), deciding exactly what ../render.tsx decides: visibleWhen,
8
+ * skeleton, appWidth, Ref, Switch, Each and the slot types. A raw element belongs in a
9
+ * primitive, never here.
10
+ *
11
+ * A node's View is built once and updated in place. When an update changes WHAT a node
12
+ * draws (a Switch branch of another type, a node that hides), its primitive is replaced;
13
+ * otherwise the same elements are kept, so focus, scroll and a half-typed draft survive.
14
+ *
15
+ * The two laws hold here as in the whole SDK: own zero style values (every value is
16
+ * served), own zero UX shape (a composite is a definition, never a primitive).
17
+ * ════════════════════════════════════════════════════════════════════════════
18
+ */
19
+ import type { UnoverseNode } from "../core/types";
20
+ import { visible, selectCase } from "../core/conditions";
21
+ import { cssWidth } from "../core/apps";
22
+ import { resolveRef } from "../core/refs";
23
+ import { boxView, buttonView, iconView, imageView, inputView, orbView, selectView, skeletonView, textView, unknownView, type Prim } from "./primitives";
24
+ import { markdownView } from "./markdown";
25
+ import type { Data, RenderCtx, View } from "./types";
26
+
27
+ type Kind = "none" | "skeleton-box" | "Skeleton" | "Icon" | "Orb" | "Input" | "Markdown" | "slot" | "box" | "Text" | "Image" | "Select" | "Button" | "Unknown";
28
+
29
+ const SLOT_TYPES = new Set(["ComponentSlot", "Timeline", "Template", "Place"]);
30
+ // The places whose `appWidth` sizes their FRAME (the app draws it), not a box of their own.
31
+ const FRAMED_PLACES = new Set(["ComponentSlot", "Template", "Place"]);
32
+ const BOX_TYPES = new Set(["Box", "Stack", "Row", "Column", "Each"]);
33
+
34
+ /** What a node draws, after its visibility, its loading state, its references and its branch. */
35
+ function resolve(node: UnoverseNode, data: Data, ctx: RenderCtx): { kind: Kind; node: UnoverseNode; variant?: string } {
36
+ for (;;) {
37
+ // visibleWhen: a bare field is a truthiness test, an object a Condition (core/conditions.ts).
38
+ if (node.visibleWhen != null && !visible(node.visibleWhen, data)) return { kind: "none", node };
39
+ // `skeleton: true`: while the bound field has no data, its ghost inside the node's own box.
40
+ if (node.skeleton) {
41
+ const field = node.bind?.value ?? node.bind?.src ?? node.bind?.items;
42
+ if (field && !visible(field, data)) {
43
+ const variant = node.type === "Image" ? "image" : node.type === "Each" ? "card" : "text";
44
+ return { kind: "skeleton-box", node, variant };
45
+ }
46
+ }
47
+ // STATE-OWNED SIZING: a plain node's `appWidth` fixes the panel's box. Slot places keep
48
+ // theirs, since it applies to their frame.
49
+ const panelW = (node as { appWidth?: string }).appWidth;
50
+ if (panelW && !FRAMED_PLACES.has(node.type)) {
51
+ const w = cssWidth(ctx.theme.appSize?.[panelW] ?? panelW);
52
+ if (w) node = { ...node, appWidth: undefined, style: { ...(node.style ?? {}), width: w, flex: "0 0 auto" } } as UnoverseNode;
53
+ }
54
+ if (node.type === "Ref") {
55
+ // REFERENCE, NOT COPY (core/refs.ts). An unknown name draws nothing.
56
+ const resolved = resolveRef(node, ctx.defs?.[String((node as { ref?: string }).ref ?? "")]);
57
+ if (!resolved) return { kind: "none", node };
58
+ node = resolved;
59
+ continue;
60
+ }
61
+ if (node.type === "Switch") {
62
+ // Exactly one branch, the case matching `data[on]`; no wrapper element.
63
+ const branch = selectCase(node, data);
64
+ if (!branch) return { kind: "none", node };
65
+ node = branch;
66
+ continue;
67
+ }
68
+ if (SLOT_TYPES.has(node.type)) return { kind: ctx.slot ? "slot" : "none", node };
69
+ if (BOX_TYPES.has(node.type)) return { kind: "box", node };
70
+ switch (node.type) {
71
+ case "Skeleton":
72
+ case "Icon":
73
+ case "Orb":
74
+ case "Input":
75
+ case "Markdown":
76
+ case "Text":
77
+ case "Image":
78
+ case "Select":
79
+ case "Button":
80
+ return { kind: node.type, node };
81
+ default:
82
+ return { kind: "Unknown", node };
83
+ }
84
+ }
85
+ }
86
+
87
+ /** Where an `Each` item sits, and the item itself as the scope (a non-object item is `value`). */
88
+ const scopeOf = (item: unknown, i: number): Data => {
89
+ const base = { index: i, position: i + 1 };
90
+ return item != null && typeof item === "object" && !Array.isArray(item) ? { ...base, ...(item as Data) } : { ...base, value: item };
91
+ };
92
+
93
+ const PRIMS: Partial<Record<Kind, (ctx: RenderCtx) => Prim>> = {
94
+ Skeleton: skeletonView,
95
+ Icon: iconView,
96
+ Orb: orbView,
97
+ Input: inputView,
98
+ Markdown: markdownView,
99
+ box: boxView,
100
+ "skeleton-box": boxView,
101
+ Text: textView,
102
+ Image: imageView,
103
+ Select: selectView,
104
+ Button: buttonView,
105
+ Unknown: unknownView,
106
+ };
107
+
108
+ /** Draw one definition node: the View for it, already filled with `data`. */
109
+ export function renderNode(node: UnoverseNode, data: Data, ctx: RenderCtx): View {
110
+ let kind: Kind | null = null;
111
+ let prim: Prim | null = null;
112
+ let slot: View | null = null;
113
+ let ghost: ReturnType<typeof skeletonView> | null = null;
114
+ const kids: View[] = [];
115
+
116
+ const clear = () => {
117
+ prim?.destroy();
118
+ slot?.destroy();
119
+ for (const k of kids.splice(0)) k.destroy();
120
+ prim = slot = ghost = null;
121
+ };
122
+
123
+ /** Bring the children up to `specs`, by position. */
124
+ const fillKids = (specs: [UnoverseNode, Data][]): Node[] => {
125
+ specs.forEach(([n, d], i) => {
126
+ if (kids[i]) kids[i].update(n, d);
127
+ else kids[i] = renderNode(n, d, ctx);
128
+ });
129
+ for (const k of kids.splice(specs.length)) k.destroy();
130
+ return kids.flatMap((k) => k.nodes);
131
+ };
132
+
133
+ const view: View = {
134
+ get nodes() {
135
+ return slot ? slot.nodes : (prim?.nodes ?? []);
136
+ },
137
+ update(n0, data) {
138
+ const r = resolve(n0, data, ctx);
139
+ if (r.kind !== kind) {
140
+ clear();
141
+ kind = r.kind;
142
+ if (kind === "slot") slot = ctx.slot!(r.node, data);
143
+ else prim = PRIMS[kind]?.(ctx) ?? null;
144
+ if (kind === "skeleton-box") ghost = skeletonView(ctx);
145
+ } else if (kind === "slot") slot?.update(r.node, data);
146
+ const n = r.node;
147
+ switch (kind) {
148
+ case "none":
149
+ case "slot":
150
+ return;
151
+ case "skeleton-box":
152
+ ghost!.variant(r.variant);
153
+ prim!.update({ type: "Box", style: n.style } as UnoverseNode, data, ghost!.nodes);
154
+ return;
155
+ case "box": {
156
+ if (n.type === "Each") {
157
+ // Literal `items` is the fallback for a static list in the definition; bind wins.
158
+ const src = n.bind?.items ? data[n.bind.items] : n.items;
159
+ const items = Array.isArray(src) ? src : [];
160
+ const tpl = typeof n.item === "object" ? n.item : undefined;
161
+ prim!.update(n, data, tpl ? fillKids(items.map((item, i) => [tpl, scopeOf(item, i)])) : fillKids([]));
162
+ } else prim!.update(n, data, fillKids((n.children ?? []).map((c) => [c, data])));
163
+ return;
164
+ }
165
+ case "Button":
166
+ case "Unknown":
167
+ prim!.update(n, data, fillKids(kind === "Button" && !n.children?.length ? [] : (n.children ?? []).map((c) => [c, data])));
168
+ return;
169
+ default:
170
+ prim!.update(n, data);
171
+ }
172
+ },
173
+ destroy: clear,
174
+ };
175
+ view.update(node, data);
176
+ return view;
177
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The theme, fetched from the server by name (the SDK owns zero token values), and its
3
+ * webfonts registered in the document.
4
+ */
5
+ import type { ResolvedTheme } from "../core/types";
6
+
7
+ /** What reads a theme: the MCP client, or anything that answers the same call. */
8
+ export interface ThemeSource {
9
+ readTheme(name: string): Promise<ResolvedTheme>;
10
+ }
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, doc: Document | undefined = typeof document !== "undefined" ? document : undefined): void {
20
+ if (!theme?.fonts?.length || !doc) return;
21
+ for (const href of theme.fonts) {
22
+ if (doc.head.querySelector(`link[data-unoverse-font="${CSS.escape(href)}"]`)) continue;
23
+ const link = doc.createElement("link");
24
+ link.rel = "stylesheet";
25
+ link.href = href;
26
+ link.setAttribute("data-unoverse-font", href);
27
+ doc.head.appendChild(link);
28
+ }
29
+ }
30
+
31
+ /** The named theme; an unknown name falls back to the neutral `light`, so a pack without a
32
+ * theme still renders. */
33
+ export function fetchTheme(client: ThemeSource, name: string): Promise<ResolvedTheme> {
34
+ return client.readTheme(name).catch((e) => {
35
+ if (name === "light") throw e;
36
+ return client.readTheme("light");
37
+ });
38
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The plain-DOM renderer's two shapes: a View (what a node puts on the page) and the render
3
+ * context passed down the walk (the theme, the action handler, slots, shared pieces).
4
+ */
5
+ import type { ResolvedTheme, UnoverseNode } from "../core/types";
6
+ import type { ActionHandler } from "../actions";
7
+
8
+ export type Data = Record<string, unknown>;
9
+
10
+ /** What one definition node puts on the page, kept up to date in place. */
11
+ export interface View {
12
+ /** The DOM nodes this view puts in its parent, in order: none while hidden. */
13
+ readonly nodes: Node[];
14
+ update(node: UnoverseNode, data: Data): void;
15
+ destroy(): void;
16
+ }
17
+
18
+ /** Fills a `ComponentSlot`, `Timeline`, `Template` or `Place` (the page layer, step 3). */
19
+ export type SlotResolver = (node: UnoverseNode, data: Data) => View | null;
20
+
21
+ /** A parsed markdown element: a tag, its props and its children, or a run of text. */
22
+ export type MarkdownTree = string | { tag: string; props: Record<string, unknown>; children: MarkdownTree[] };
23
+
24
+ export interface RenderCtx {
25
+ doc: Document;
26
+ theme: ResolvedTheme;
27
+ onAction?: ActionHandler;
28
+ slot?: SlotResolver;
29
+ /** The shared pieces a definition references by name (`Ref`). */
30
+ defs?: Record<string, UnoverseNode>;
31
+ /**
32
+ * Work that needs the element on the page (measuring, observers, the orb's drawing). Run by
33
+ * whoever puts the view on the page, once it is there.
34
+ */
35
+ mounted(fn: () => void): void;
36
+ }
@@ -1,20 +1,29 @@
1
- export { UnoverseComponent } from "./UnoverseComponent";
2
- export type { UnoverseComponentProps } from "./UnoverseComponent";
3
- export { StreamedUnoverseComponent, useUnoverseInstance } from "./streamed";
4
- export type { StreamedUnoverseComponentProps } from "./streamed";
5
- export { StreamedUnoverseApp, INERT_VOICE, selectPointers, computeAppWidth, resolveActiveLayout } from "./app";
6
- export type { StreamedUnoverseAppProps } from "./app";
7
- export { renderNode, styleToCss, keyframesCss } from "./render";
8
- export type { ActionHandler, ActionMeta, SlotResolver } from "./render";
9
- export { useUnoverseTheme } from "./theme";
10
- export { IsolatedRoot } from "./isolate";
11
- export { useUnoverseConnection } from "./connection";
12
- export type { UnoverseConnectionConfig, UnoverseSessionParams, UnoverseConnection } from "./connection";
13
- // The `voice` native service (UNOVERSE_SPEC §2e-1 Tier-3). Channel-instantiated; its
14
- // neutral state is spread into a `service: "voice"` template's scope, its actions
15
- // dispatched from the template. Audio rides the /ws/gravity WS lane (§5), not MCP.
16
- export { useVoiceService } from "./voice";
17
- export type { UseVoiceServiceConfig, VoiceService, VoiceServiceState, VoiceSession, VoiceConnectionStatus } from "./voice";
1
+ /**
2
+ * The Unoverse SDK's public API. ⛔ NO REACT (web/sdk/CLAUDE.md): screens are data, drawn by
3
+ * plain DOM. A React host wraps these itself (packages/studio/screens/react/unoverse.tsx).
4
+ */
5
+ // Drawing: one component, an app or template, and the definition walk itself.
6
+ export { mountComponent } from "./dom/component";
7
+ export type { ComponentMount, ComponentOptions, DefinitionSource } from "./dom/component";
8
+ export { mountApp } from "./dom/app";
9
+ export type { AppMount, AppOptions, VoiceVerbs } from "./dom/app";
10
+ export { renderNode } from "./dom/render";
11
+ export type { View, RenderCtx, SlotResolver } from "./dom/types";
12
+ export { parseMarkdown } from "./dom/markdownParse";
13
+ export { styleToCss, keyframesCss, cssDecls } from "./style";
14
+ export type { CssStyle } from "./style";
15
+ export { nodeAction } from "./actions";
16
+ export type { ActionHandler, ActionMeta } from "./actions";
17
+ export { fetchTheme, ensureFontStylesheets } from "./dom/theme";
18
+ // The live connection (the data plane, §5b).
19
+ export { openConnection, sessionUserId, resolveGuestId } from "./dom/connection";
20
+ export type { UnoverseConnection, UnoverseConnectionConfig, UnoverseSessionParams, ConnectionStatus, ConnectionOptions } from "./dom/connection";
21
+ // The `voice` native service (UNOVERSE_SPEC §2e-1 Tier-3). Its neutral state is spread into a
22
+ // `service: "voice"` template's scope; audio rides the /ws/gravity WS lane (§5), not MCP.
23
+ export { createVoiceService } from "./voiceService";
24
+ export type { VoiceService, VoiceServiceConfig, VoiceServiceState, VoiceSession, VoiceConnectionStatus, VoiceCallState, StartCall } from "./voiceService";
25
+ // The pure template derivations a host reads (app width, the active layout).
26
+ export { selectPointers, computeAppWidth, resolveActiveLayout } from "./core/apps";
18
27
  // Re-export core's FULL public API. core is BUNDLED into this package (tsup noExternal), so
19
28
  // consumers import EVERYTHING from `@unoverse/sdk` and never depend on a
20
29
  // separate `-core` install — eliminating react↔core version/export drift entirely. EXPLICIT
@@ -0,0 +1,139 @@
1
+ // The orb on a canvas, in plain DOM. One shared clock (performance.now) keeps every orb in
2
+ // phase; each runs its own rAF loop, paused while offscreen (IntersectionObserver) or while
3
+ // the tab is hidden. Reduced-motion users get one static frame that still follows the theme.
4
+ // The theme is the explicit one, else an ancestor's data-theme/.dark|.light (watched live),
5
+ // else prefers-color-scheme (subscribed live).
6
+
7
+ import { MODE_DRAWS } from './engine/registry';
8
+ import { setInk } from './engine/core';
9
+ import { resolvePreset } from './presets';
10
+ import type { OrbSize, OrbState, OrbTheme } from './states';
11
+
12
+ export type { OrbState, OrbTheme } from './states';
13
+
14
+ export function ancestorTheme(el: Element | null): boolean | null {
15
+ let node: Element | null = el;
16
+ while (node) {
17
+ const attr = node.getAttribute('data-theme');
18
+ if (attr === 'dark') return true;
19
+ if (attr === 'light') return false;
20
+ if (node.classList.contains('dark')) return true;
21
+ if (node.classList.contains('light')) return false;
22
+ node = node.parentElement;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function systemDark(): boolean {
28
+ return typeof matchMedia === 'undefined' || matchMedia('(prefers-color-scheme: dark)').matches;
29
+ }
30
+
31
+ export interface OrbOptions {
32
+ state: OrbState;
33
+ /** Any positive size: the canvas draws at it, the tuning snaps to the nearest preset (20 / 64). */
34
+ size: number;
35
+ speed: number;
36
+ theme: OrbTheme;
37
+ ink: [number, number, number] | null;
38
+ }
39
+
40
+ /** Start drawing on a canvas that is on the page; returns the stop. */
41
+ export function startOrb(canvas: HTMLCanvasElement, o: OrbOptions): () => void {
42
+ let stopDraw = () => {};
43
+ const resolveDark = () => (o.theme === 'dark' ? true : o.theme === 'light' ? false : (ancestorTheme(canvas) ?? systemDark()));
44
+ const reducedMq = typeof matchMedia !== 'undefined' ? matchMedia('(prefers-reduced-motion: reduce)') : null;
45
+ let dark = resolveDark();
46
+ let reduced = reducedMq?.matches ?? false;
47
+
48
+ const draw = () => {
49
+ stopDraw();
50
+ stopDraw = drawLoop(canvas, o, dark, reduced);
51
+ };
52
+ draw();
53
+
54
+ const redraw = () => {
55
+ const d = resolveDark();
56
+ const r = reducedMq?.matches ?? false;
57
+ if (d === dark && r === reduced) return;
58
+ dark = d;
59
+ reduced = r;
60
+ draw();
61
+ };
62
+ reducedMq?.addEventListener('change', redraw);
63
+ const schemeMq = o.theme === 'auto' && typeof matchMedia !== 'undefined' ? matchMedia('(prefers-color-scheme: dark)') : null;
64
+ schemeMq?.addEventListener('change', redraw);
65
+ // App-level toggles: class/data-theme flips on ancestors.
66
+ let mo: MutationObserver | null = null;
67
+ if (o.theme === 'auto' && typeof MutationObserver !== 'undefined') {
68
+ mo = new MutationObserver(redraw);
69
+ mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'], subtree: true });
70
+ }
71
+ return () => {
72
+ stopDraw();
73
+ reducedMq?.removeEventListener('change', redraw);
74
+ schemeMq?.removeEventListener('change', redraw);
75
+ mo?.disconnect();
76
+ };
77
+ }
78
+
79
+ function drawLoop(canvas: HTMLCanvasElement, o: OrbOptions, dark: boolean, reduced: boolean): () => void {
80
+ const { size } = o;
81
+ const dpr = Math.min(2, (typeof devicePixelRatio !== 'undefined' && devicePixelRatio) || 1);
82
+ canvas.width = Math.round(size * dpr);
83
+ canvas.height = Math.round(size * dpr);
84
+ const ctx = canvas.getContext('2d');
85
+ if (!ctx) return () => {};
86
+ const preset: OrbSize = size <= 32 ? 20 : 64;
87
+ const { mode, speed: baseSpeed, opts } = resolvePreset(o.state, preset);
88
+ const paint = MODE_DRAWS[mode];
89
+ const effSpeed = baseSpeed * o.speed;
90
+ const frame = (tSec: number) => {
91
+ // Ink is a module-level current (frames are synchronous); set per frame so orbs with
92
+ // different brand tokens on one page never bleed into each other.
93
+ setInk(o.ink);
94
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
95
+ ctx.clearRect(0, 0, size, size);
96
+ paint(ctx, size, tSec, dark, opts);
97
+ };
98
+ if (reduced) {
99
+ frame(0.6);
100
+ return () => {};
101
+ }
102
+ let raf = 0;
103
+ let running = false;
104
+ const loop = () => {
105
+ frame((performance.now() / 1000) * effSpeed);
106
+ if (running) raf = requestAnimationFrame(loop);
107
+ };
108
+ const start = () => {
109
+ if (running) return;
110
+ running = true;
111
+ raf = requestAnimationFrame(loop);
112
+ };
113
+ const stop = () => {
114
+ running = false;
115
+ cancelAnimationFrame(raf);
116
+ };
117
+ frame((performance.now() / 1000) * effSpeed);
118
+ let visible = true;
119
+ const io =
120
+ typeof IntersectionObserver !== 'undefined'
121
+ ? new IntersectionObserver(([entry]) => {
122
+ visible = entry.isIntersecting;
123
+ if (visible && document.visibilityState !== 'hidden') start();
124
+ else stop();
125
+ })
126
+ : null;
127
+ io?.observe(canvas);
128
+ const onVis = () => {
129
+ if (document.visibilityState === 'hidden') stop();
130
+ else if (visible) start();
131
+ };
132
+ document.addEventListener('visibilitychange', onVis);
133
+ if (!io) start();
134
+ return () => {
135
+ stop();
136
+ io?.disconnect();
137
+ document.removeEventListener('visibilitychange', onVis);
138
+ };
139
+ }
@@ -1,20 +1,10 @@
1
1
  /**
2
- * The Orb primitive's drawing engine — VENDORED from `thinking-orbs`
3
- * (https://github.com/Jakubantalik/thinking-orbs, MIT © 2026 Jakub Antalik),
4
- * with two LOCAL EXTENSIONS, each marked at its site: free numeric `size`
5
- * (tuning snaps to the nearest baked preset — ThinkingOrb.tsx) and a brand
6
- * `ink` the painters tint toward, fed from the served `color` token
7
- * (engine/core.ts setInk). Nine hand-tuned "assistant is doing something" animations,
8
- * drawn procedurally on a plain 2D canvas — no WebGL, no filters — which is
9
- * what makes the spec portable: each native SDK (iOS/Compose/Flutter/RN)
10
- * reimplements the same draw loops with its platform's own 2D canvas.
11
- *
12
- * This is the sanctioned shape for rich motion (the SkeletonView precedent):
13
- * the SDK owns ONE generic draw behavior; every authoring decision — which
14
- * state, what size, what speed — stays DATA in the definition (`type: Orb`).
15
- * Keyframe animations (styles/semantic/keyframes) remain the tool for
16
- * element motion; the Orb is for pictures computed per-frame, which keyframe
17
- * interpolation cannot express (no per-frame math, no depth occlusion).
2
+ * The Orb primitive's drawing engine, VENDORED from `thinking-orbs`
3
+ * (https://github.com/Jakubantalik/thinking-orbs, MIT © 2026 Jakub Antalik), with two LOCAL
4
+ * EXTENSIONS, each marked at its site: free numeric `size` (tuning snaps to the nearest baked
5
+ * preset, canvas.ts) and a brand `ink` the painters tint toward, fed from the served `color`
6
+ * token (engine/core.ts setInk). Drawn procedurally on a plain 2D canvas, no WebGL, so each
7
+ * native SDK can reimplement the same draw loops on its platform's own canvas.
18
8
  */
19
- export { ThinkingOrb } from "./ThinkingOrb";
20
- export type { ThinkingOrbProps, OrbState, OrbSize, OrbTheme } from "./types";
9
+ export { startOrb, type OrbOptions } from "./canvas";
10
+ export type { OrbState, OrbSize, OrbTheme } from "./states";