@unoverse-platform/studio 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +46 -0
- package/package.json +4 -3
- package/screens/NativeAppHost.tsx +34 -8
- package/screens/TemplatesView.tsx +18 -1
- package/vendor/sdk/lib/connection.tsx +3 -0
- package/vendor/sdk/lib/template.tsx +9 -1
- package/vite.config.ts +5 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unoverse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @unoverse-platform/studio
|
|
2
|
+
|
|
3
|
+
Unoverse Studio: the standalone authoring app. It reads your own `rx/` project folder
|
|
4
|
+
off disk and previews your components, templates, skills and nodes. No database, no
|
|
5
|
+
Docker, no backend of its own.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @unoverse-platform/studio
|
|
9
|
+
npx unoverse-studio
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Studio opens on your project folder. Author in your editor, see it in Studio.
|
|
13
|
+
|
|
14
|
+
## What you author
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
rx/components/ components: definition-backed UI, rendered by the generic SDK
|
|
18
|
+
rx/templates/ templates: whole app surfaces, states and layouts
|
|
19
|
+
rx/styles/ themes and tokens
|
|
20
|
+
prompts/ skills and prompt blocks
|
|
21
|
+
nodes/ declarative YAML node manifests
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Everything is data. There is no codegen and no build step for your assets: the
|
|
25
|
+
definition IS the artifact, and the same definition renders on web, in ChatGPT, in
|
|
26
|
+
Claude, and on every other channel a universe serves.
|
|
27
|
+
|
|
28
|
+
## Connecting a universe
|
|
29
|
+
|
|
30
|
+
Studio on its own is a viewer for your folder. Connected to a universe it becomes a
|
|
31
|
+
full client:
|
|
32
|
+
|
|
33
|
+
- **Live preview**: Studio hosts the app natively. It asks the universe for its
|
|
34
|
+
MCP-served app shell (`ui://unoverse/app.html`, the same one ChatGPT loads) and the
|
|
35
|
+
app runs its own connections: chat, streaming, voice. Studio ships no renderer of
|
|
36
|
+
its own; the renderer loads when required, from the universe that owns it.
|
|
37
|
+
- **Publish**: your components, templates, skills and prompts publish into the
|
|
38
|
+
universe over its API, authorized by a publish key issued on that universe
|
|
39
|
+
(`unoverse key`). Lint runs first; nothing leaves your machine until the rules pass.
|
|
40
|
+
|
|
41
|
+
The universe is somebody's deployment of the Unoverse platform. Yours, your team's, or
|
|
42
|
+
a customer's. Studio never runs one.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@tailwindcss/vite": "^4.0.0",
|
|
15
|
-
"@unoverse-platform/base": "^0.
|
|
15
|
+
"@unoverse-platform/base": "^0.3.0",
|
|
16
16
|
"@unoverse-platform/marketplace": "*",
|
|
17
17
|
"@vitejs/plugin-react": "^4.3.4",
|
|
18
18
|
"oidc-client-ts": "^3.4.1",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"public",
|
|
37
37
|
"index.html",
|
|
38
38
|
"vite.config.ts",
|
|
39
|
-
"README.md"
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
|
@@ -32,6 +32,21 @@ export function NativeAppHost({
|
|
|
32
32
|
const [html, setHtml] = useState<string | null>(null);
|
|
33
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
34
|
const frame = useRef<HTMLIFrameElement>(null);
|
|
35
|
+
// STATE-OWNED APP WIDTH (docs/design/05): the app computes its width off the active
|
|
36
|
+
// layout and posts it up as `unoverse:size` (appEngine.tsx) — the HOST owns the resize.
|
|
37
|
+
// Without this listener the frame never widens and a panel surface renders clipped.
|
|
38
|
+
const [appWidth, setAppWidth] = useState<string | null>(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const onSize = (e: MessageEvent) => {
|
|
41
|
+
if (e.source === frame.current?.contentWindow && e.data?.type === "unoverse:size" && e.data.width) {
|
|
42
|
+
setAppWidth(String(e.data.width));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
window.addEventListener("message", onSize);
|
|
46
|
+
return () => window.removeEventListener("message", onSize);
|
|
47
|
+
}, []);
|
|
48
|
+
// A different app starts back at the host default until it reports.
|
|
49
|
+
useEffect(() => setAppWidth(null), [templateId]);
|
|
35
50
|
|
|
36
51
|
// Pull the app shell from the universe — one resources/read, then the client closes.
|
|
37
52
|
useEffect(() => {
|
|
@@ -94,13 +109,24 @@ export function NativeAppHost({
|
|
|
94
109
|
if (!html)
|
|
95
110
|
return <div className="flex h-full items-center justify-center text-sm opacity-60">Loading the app from the universe…</div>;
|
|
96
111
|
return (
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
<div style={{ width: "100%", height: "100%", display: "flex", justifyContent: "center" }}>
|
|
113
|
+
<iframe
|
|
114
|
+
ref={frame}
|
|
115
|
+
srcDoc={html}
|
|
116
|
+
onLoad={postConfig}
|
|
117
|
+
title="unoverse app"
|
|
118
|
+
// The app's reported width wins (panel surfaces widen it); capped at the pane.
|
|
119
|
+
// The host animates the resize (appEngine contract).
|
|
120
|
+
style={{
|
|
121
|
+
width: appWidth ?? "100%",
|
|
122
|
+
maxWidth: "100%",
|
|
123
|
+
height: "100%",
|
|
124
|
+
border: 0,
|
|
125
|
+
display: "block",
|
|
126
|
+
transition: "width 300ms ease",
|
|
127
|
+
}}
|
|
128
|
+
allow="microphone; autoplay"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
105
131
|
);
|
|
106
132
|
}
|
|
@@ -285,6 +285,19 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
|
|
|
285
285
|
// because Mock rendering shares this machinery (store, states, width).
|
|
286
286
|
const liveIsNative = live && Boolean(binding?.workflow) && (!hasAuth || Boolean(auth?.isAuthenticated));
|
|
287
287
|
const canConnect = false;
|
|
288
|
+
// NATIVE width authority: the app computes its width off ITS live store and posts
|
|
289
|
+
// `unoverse:size` up (appEngine contract). The in-page store below is DEAD by design
|
|
290
|
+
// (canConnect=false), so computeAppWidth against it pins the wrapper at the base
|
|
291
|
+
// layout's width forever — the wrapper must follow the app's reported width instead.
|
|
292
|
+
const [nativeWidth, setNativeWidth] = useState<string | null>(null);
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
const onSize = (e: MessageEvent) => {
|
|
295
|
+
if (e.data?.type === "unoverse:size" && e.data.width) setNativeWidth(String(e.data.width));
|
|
296
|
+
};
|
|
297
|
+
window.addEventListener("message", onSize);
|
|
298
|
+
return () => window.removeEventListener("message", onSize);
|
|
299
|
+
}, []);
|
|
300
|
+
useEffect(() => setNativeWidth(null), [selected, live]);
|
|
288
301
|
const rawConn = useUnoverseConnection(config, session, store, { enabled: canConnect });
|
|
289
302
|
// HARD INVARIANT (workbench doc Mode A — "Mock does not connect with server"): a Mock
|
|
290
303
|
// session must NEVER reach the backend. The SDK only gates the inbound SSE stream on
|
|
@@ -871,7 +884,11 @@ export function TemplatesView({ theme: defaultTheme, themeName, org = "all" }: {
|
|
|
871
884
|
// TRUE width, never clamped: a panel is its declared width always, so
|
|
872
885
|
// an app wider than the device box CLIPS at the edge (like a real
|
|
873
886
|
// too-small screen) — it must never squeeze into garbage.
|
|
874
|
-
|
|
887
|
+
// NATIVE mode: the app's own reported width wins (unoverse:size) — the
|
|
888
|
+
// in-page store is dead there and computeAppWidth against it lies.
|
|
889
|
+
width: liveIsNative
|
|
890
|
+
? (nativeWidth ?? "100%")
|
|
891
|
+
: (computeAppWidth(resolveActiveLayout(selectedDef as never, store), store, theme?.appSize) ?? "100%"),
|
|
875
892
|
margin: "0 auto",
|
|
876
893
|
transition: "width 200ms",
|
|
877
894
|
...((selectedDef?.fluidHeight ?? true) ? { height: "100%" } : {}),
|
|
@@ -154,6 +154,9 @@ export function useUnoverseConnection(
|
|
|
154
154
|
};
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
|
+
// The SDK fire-and-forgets the standalone GET SSE open on `initialized`; a failure
|
|
158
|
+
// lands ONLY here. Unset, a dead inbound stream is invisible — no events, no error.
|
|
159
|
+
client.onerror = (e) => console.error("[unoverse] client error:", e);
|
|
157
160
|
clientRef.current = client;
|
|
158
161
|
|
|
159
162
|
// The server pushes the live stream as `notifications/unoverse/event` over the
|
|
@@ -129,7 +129,12 @@ export function computeAppWidth(root: unknown, store: ComponentStore, appSize?:
|
|
|
129
129
|
if (n !== r) {
|
|
130
130
|
if (n.type === "ComponentSlot") {
|
|
131
131
|
const claim = n.select?.where;
|
|
132
|
-
|
|
132
|
+
// `appWidth: flex` — the surface takes the REMAINING host space. DATA WINS: an
|
|
133
|
+
// org may define `flex` as a real appSize token (bpp: calc(100% − chat)) and
|
|
134
|
+
// that resolves like any name; the marker below is the theme-free fallback,
|
|
135
|
+
// resolved as "fill the host" when the view is active.
|
|
136
|
+
if (claim?.field === "defaultState" && typeof claim.eq === "string")
|
|
137
|
+
surfaces.set(claim.eq, rw(n.appWidth) ?? (n.appWidth === "flex" ? "flex" : undefined));
|
|
133
138
|
} else if (n.appWidth !== undefined) {
|
|
134
139
|
const w = rw(n.appWidth);
|
|
135
140
|
if (w) widths.push(w); // a plain panel (the core column) is ALWAYS open
|
|
@@ -153,6 +158,9 @@ export function computeAppWidth(root: unknown, store: ComponentStore, appSize?:
|
|
|
153
158
|
}
|
|
154
159
|
}
|
|
155
160
|
const activePanel = active ? surfaces.get(active) : undefined;
|
|
161
|
+
// An ACTIVE flex surface fills the host viewport (the host caps at its pane); the
|
|
162
|
+
// surface flexes into whatever the fixed panels leave.
|
|
163
|
+
if (activePanel === "flex") return "100%";
|
|
156
164
|
if (activePanel) widths.push(activePanel);
|
|
157
165
|
if (!widths.length) return undefined;
|
|
158
166
|
return widths.length === 1 ? widths[0] : `calc(${widths.join(" + ")})`;
|
package/vite.config.ts
CHANGED
|
@@ -44,8 +44,11 @@ export default defineConfig({
|
|
|
44
44
|
// path resolves to nothing). The old order preferred vendor — so the moment a
|
|
45
45
|
// publish ran vendor-sdk.mjs, the monorepo's own Studio silently switched onto a
|
|
46
46
|
// FROZEN SDK copy and live SDK edits stopped appearing (2026-07-28).
|
|
47
|
-
"
|
|
48
|
-
|
|
47
|
+
// apps/, not packages/. This said "../unoverse/..." (packages/unoverse, which does
|
|
48
|
+
// not exist), so existsSync was always false and the monorepo's own Studio silently
|
|
49
|
+
// ran on the STALE vendored copy — the exact failure the comment above warns about.
|
|
50
|
+
"@unoverse/sdk": existsSync(resolve(__dirname, "../../apps/unoverse/web/sdk/src/lib/index.ts"))
|
|
51
|
+
? resolve(__dirname, "../../apps/unoverse/web/sdk/src/lib/index.ts")
|
|
49
52
|
: resolve(__dirname, "vendor/sdk/lib/index.ts"),
|
|
50
53
|
},
|
|
51
54
|
},
|