@unoverse-platform/studio 0.1.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/bin/studio.mjs +124 -0
- package/index.html +12 -0
- package/local/catalog.ts +105 -0
- package/local/keys.ts +118 -0
- package/local/nodes.ts +77 -0
- package/local/plugin.ts +401 -0
- package/local/scaffold.d.mts +13 -0
- package/local/scaffold.mjs +85 -0
- package/package.json +43 -0
- package/public/logo.png +0 -0
- package/screens/AtomsView.tsx +206 -0
- package/screens/ComponentsView.tsx +551 -0
- package/screens/ConfigForm.tsx +217 -0
- package/screens/DesignSystemView.tsx +324 -0
- package/screens/IdentityHeader.tsx +88 -0
- package/screens/PromptBlocksView.tsx +137 -0
- package/screens/SkillsView.tsx +167 -0
- package/screens/TemplatesView.tsx +1005 -0
- package/screens/index.ts +26 -0
- package/screens/states.ts +44 -0
- package/src/App.tsx +252 -0
- package/src/ConnectUniverse.tsx +232 -0
- package/src/NewProject.tsx +89 -0
- package/src/NodeKeys.tsx +98 -0
- package/src/NodesView.tsx +443 -0
- package/src/PublishDialog.tsx +309 -0
- package/src/UniverseSession.tsx +121 -0
- package/src/host.ts +165 -0
- package/src/index.css +7 -0
- package/src/main.tsx +11 -0
- package/src/universes.ts +210 -0
- package/vendor/sdk/appEngine.tsx +219 -0
- package/vendor/sdk/lib/UnoverseComponent.tsx +120 -0
- package/vendor/sdk/lib/connection.tsx +302 -0
- package/vendor/sdk/lib/core/actions.ts +84 -0
- package/vendor/sdk/lib/core/client.ts +134 -0
- package/vendor/sdk/lib/core/conditions.ts +43 -0
- package/vendor/sdk/lib/core/connection.ts +142 -0
- package/vendor/sdk/lib/core/index.ts +32 -0
- package/vendor/sdk/lib/core/store.ts +455 -0
- package/vendor/sdk/lib/core/types.ts +194 -0
- package/vendor/sdk/lib/index.ts +59 -0
- package/vendor/sdk/lib/isolate.tsx +70 -0
- package/vendor/sdk/lib/primitives.tsx +453 -0
- package/vendor/sdk/lib/realtime/audioUtils.ts +40 -0
- package/vendor/sdk/lib/realtime/types.ts +45 -0
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +237 -0
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +145 -0
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +164 -0
- package/vendor/sdk/lib/render.tsx +178 -0
- package/vendor/sdk/lib/streamed.tsx +65 -0
- package/vendor/sdk/lib/style.ts +227 -0
- package/vendor/sdk/lib/template.tsx +521 -0
- package/vendor/sdk/lib/theme.ts +55 -0
- package/vendor/sdk/lib/voice.tsx +311 -0
- package/vendor/sdk/main.tsx +96 -0
- package/vite.config.ts +70 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atoms view — the design system's building-block tier, made browsable. Atoms are
|
|
3
|
+
* normally authoring-time only (Ref-inlined into components/templates, no canvas node),
|
|
4
|
+
* so this view PREVIEWS one via its `unoverse://atoms/{name}` resource (served by the
|
|
5
|
+
* MCP server solely for this view). Header + controls mirror ComponentsView (responsive
|
|
6
|
+
* width toggle, collapsible drag-resize dock) — minus Copy-for-Canvas (not a node) and
|
|
7
|
+
* Meta (no manifest). Ported from apps/unoverse/web/studio (kept diffable).
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useMemo, useState, type MouseEvent as ReactMouseEvent } from "react";
|
|
10
|
+
import { UnoverseComponent, type ResolvedTheme } from "@unoverse/sdk";
|
|
11
|
+
import { client, authedFetch } from "studio-host";
|
|
12
|
+
import { usePersistentState } from "studio-host";
|
|
13
|
+
|
|
14
|
+
interface PropDef {
|
|
15
|
+
type?: string;
|
|
16
|
+
default?: unknown;
|
|
17
|
+
}
|
|
18
|
+
interface AtomDef {
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
whenToUse?: string;
|
|
22
|
+
props?: Record<string, PropDef>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const VIEWPORTS: { id: string; label: string; w: number | null }[] = [
|
|
26
|
+
{ id: "mobile", label: "Mobile", w: 390 },
|
|
27
|
+
{ id: "tablet", label: "Tablet", w: 768 },
|
|
28
|
+
{ id: "desktop", label: "Desktop", w: 1280 },
|
|
29
|
+
{ id: "full", label: "Full", w: null },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const toText = (v: unknown) => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
33
|
+
const defaultsOf = (a: AtomDef): Record<string, unknown> =>
|
|
34
|
+
Object.fromEntries(
|
|
35
|
+
Object.entries(a.props ?? {}).map(([k, v]) => [k, v.default ?? (v.type === "array" ? [] : v.type === "object" ? {} : v.type === "boolean" ? false : "")]),
|
|
36
|
+
);
|
|
37
|
+
const slug = (n: string) => n.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
38
|
+
|
|
39
|
+
export function AtomsView({ theme }: { theme: ResolvedTheme }) {
|
|
40
|
+
const [atoms, setAtoms] = useState<AtomDef[]>([]);
|
|
41
|
+
const [selected, setSelected] = usePersistentState<string>("wb:atom", "");
|
|
42
|
+
const [values, setValues] = useState<Record<string, unknown>>({});
|
|
43
|
+
const [text, setText] = useState<Record<string, string>>({});
|
|
44
|
+
const [panelOpen, setPanelOpen] = useState(true);
|
|
45
|
+
const [panelHeight, setPanelHeight] = useState(200);
|
|
46
|
+
const [stageWidth, setStageWidth] = useState<number | null>(768);
|
|
47
|
+
|
|
48
|
+
// Drag the divider to resize the controls dock (up = taller). Same as ComponentsView.
|
|
49
|
+
const onDragStart = (e: ReactMouseEvent) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
const startY = e.clientY;
|
|
52
|
+
const startH = panelHeight;
|
|
53
|
+
const onMove = (ev: globalThis.MouseEvent) => setPanelHeight(Math.max(120, Math.min(window.innerHeight - 200, startH + (startY - ev.clientY))));
|
|
54
|
+
const onUp = () => {
|
|
55
|
+
window.removeEventListener("mousemove", onMove);
|
|
56
|
+
window.removeEventListener("mouseup", onUp);
|
|
57
|
+
};
|
|
58
|
+
window.addEventListener("mousemove", onMove);
|
|
59
|
+
window.addEventListener("mouseup", onUp);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const choose = (a: AtomDef) => {
|
|
63
|
+
setSelected(a.name);
|
|
64
|
+
const defs = defaultsOf(a);
|
|
65
|
+
setValues(defs);
|
|
66
|
+
setText(Object.fromEntries(Object.entries(defs).map(([k, v]) => [k, toText(v)])));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
authedFetch("/dev/atoms")
|
|
71
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
72
|
+
.then((d) => {
|
|
73
|
+
const list: AtomDef[] = (d.atoms ?? []).sort((a: AtomDef, b: AtomDef) => a.name.localeCompare(b.name));
|
|
74
|
+
setAtoms(list);
|
|
75
|
+
const restore = list.find((a) => a.name === selected) ?? list[0];
|
|
76
|
+
if (restore) choose(restore);
|
|
77
|
+
})
|
|
78
|
+
.catch(() => setAtoms([]));
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const current = atoms.find((a) => a.name === selected);
|
|
83
|
+
|
|
84
|
+
const setField = (key: string, raw: string, type?: string) => {
|
|
85
|
+
setText((t) => ({ ...t, [key]: raw }));
|
|
86
|
+
let val: unknown = raw;
|
|
87
|
+
if (type === "array" || type === "object") {
|
|
88
|
+
try {
|
|
89
|
+
val = JSON.parse(raw);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
} else if (type === "boolean") {
|
|
94
|
+
val = raw === "true";
|
|
95
|
+
}
|
|
96
|
+
setValues((v) => ({ ...v, [key]: val }));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const uri = useMemo(() => (selected ? `unoverse://atoms/${slug(selected)}` : ""), [selected]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="grid min-h-0 flex-1 grid-cols-[190px_1fr] overflow-hidden">
|
|
103
|
+
{/* NAVIGATOR */}
|
|
104
|
+
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#1b2129] p-2">
|
|
105
|
+
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Atoms</div>
|
|
106
|
+
{atoms.length === 0 && (
|
|
107
|
+
<div className="p-2 text-xs text-gray-400">No atoms — restart the Unoverse server so /dev/atoms is available.</div>
|
|
108
|
+
)}
|
|
109
|
+
{atoms.map((a) => (
|
|
110
|
+
<button
|
|
111
|
+
key={a.name}
|
|
112
|
+
onClick={() => choose(a)}
|
|
113
|
+
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${
|
|
114
|
+
selected === a.name ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
115
|
+
}`}
|
|
116
|
+
>
|
|
117
|
+
{a.name}
|
|
118
|
+
</button>
|
|
119
|
+
))}
|
|
120
|
+
</nav>
|
|
121
|
+
|
|
122
|
+
{/* STAGE + CONTROLS */}
|
|
123
|
+
<div className="flex min-h-0 flex-col overflow-hidden">
|
|
124
|
+
<section className="flex min-h-0 flex-1 flex-col overflow-hidden" style={{ background: theme.color["surface.sunken"] }}>
|
|
125
|
+
{/* HEADER — matches ComponentsView (name + responsive toggle); no Copy-for-Canvas
|
|
126
|
+
(atoms are not canvas nodes) and no Meta panel (atoms carry no manifest). */}
|
|
127
|
+
<div className="flex shrink-0 items-center gap-3 border-b px-4 py-2.5" style={{ borderColor: theme.color["border.subtle"], background: theme.color["surface.base"] }}>
|
|
128
|
+
<span className="text-sm font-semibold uppercase tracking-wide" style={{ color: theme.color["text.primary"] }}>{current?.name ?? ""}</span>
|
|
129
|
+
<div className="ml-auto flex items-center gap-1 rounded p-0.5 text-[11px]" style={{ background: theme.color["border.subtle"] }}>
|
|
130
|
+
{VIEWPORTS.map((v) => (
|
|
131
|
+
<button
|
|
132
|
+
key={v.id}
|
|
133
|
+
onClick={() => setStageWidth(v.w)}
|
|
134
|
+
className="rounded px-2 py-0.5 font-medium transition"
|
|
135
|
+
style={
|
|
136
|
+
stageWidth === v.w
|
|
137
|
+
? { background: theme.color["surface.base"], color: theme.color["text.primary"] }
|
|
138
|
+
: { color: theme.color["text.secondary"] }
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
{v.label}
|
|
142
|
+
</button>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
<span className="tabular-nums text-[11px]" style={{ color: theme.color["text.tertiary"] }}>
|
|
146
|
+
{stageWidth ? `${stageWidth}px` : "100%"}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="flex-1 overflow-auto p-10">
|
|
151
|
+
<div className="mx-auto max-w-full transition-[width] duration-200" style={{ width: stageWidth ?? "100%" }}>
|
|
152
|
+
{uri && <UnoverseComponent client={client} uri={uri} data={values} theme={theme} />}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
|
|
157
|
+
{/* CONTROLS — collapsible + drag-to-resize dock, same as ComponentsView */}
|
|
158
|
+
{panelOpen && (
|
|
159
|
+
<div
|
|
160
|
+
onMouseDown={onDragStart}
|
|
161
|
+
className="h-1.5 shrink-0 cursor-row-resize bg-gray-700 transition-colors hover:bg-emerald-600/50"
|
|
162
|
+
title="Drag to resize"
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
<div className="flex min-h-0 shrink-0 flex-col bg-[#232a34] text-gray-200" style={panelOpen ? { height: panelHeight } : undefined}>
|
|
166
|
+
<div
|
|
167
|
+
onClick={() => setPanelOpen((o) => !o)}
|
|
168
|
+
title={panelOpen ? "Collapse controls" : "Expand controls"}
|
|
169
|
+
className="flex cursor-pointer select-none items-center gap-2 border-y border-gray-700 px-3 py-2 text-[11px] uppercase tracking-wider text-gray-400 hover:bg-white/5"
|
|
170
|
+
>
|
|
171
|
+
<span className="leading-none text-gray-300">{panelOpen ? "▾" : "▸"}</span>
|
|
172
|
+
<span>controls</span>
|
|
173
|
+
{current && <span className="text-gray-400">· {current.name}</span>}
|
|
174
|
+
</div>
|
|
175
|
+
{panelOpen && (
|
|
176
|
+
<div className="min-h-0 flex-1 overflow-auto p-3">
|
|
177
|
+
{current && Object.keys(current.props ?? {}).length > 0 ? (
|
|
178
|
+
<div className="grid grid-cols-2 gap-3">
|
|
179
|
+
{Object.entries(current.props ?? {}).map(([key, def]) => (
|
|
180
|
+
<label key={key} className="flex flex-col gap-1 text-xs">
|
|
181
|
+
<span className="text-gray-400">
|
|
182
|
+
{key} <span className="text-gray-600">{def.type ?? "string"}</span>
|
|
183
|
+
</span>
|
|
184
|
+
{def.type === "boolean" ? (
|
|
185
|
+
<select value={text[key] ?? "false"} onChange={(e) => setField(key, e.target.value, "boolean")} className="rounded border border-gray-600 bg-[#1b2129] px-2 py-1 text-gray-200">
|
|
186
|
+
<option value="false">false</option>
|
|
187
|
+
<option value="true">true</option>
|
|
188
|
+
</select>
|
|
189
|
+
) : def.type === "array" || def.type === "object" ? (
|
|
190
|
+
<textarea value={text[key] ?? ""} onChange={(e) => setField(key, e.target.value, def.type)} rows={3} className="rounded border border-gray-600 bg-[#1b2129] px-2 py-1 font-mono text-[11px] text-gray-200" />
|
|
191
|
+
) : (
|
|
192
|
+
<input value={text[key] ?? ""} onChange={(e) => setField(key, e.target.value, def.type)} className="rounded border border-gray-600 bg-[#1b2129] px-2 py-1 text-gray-200" />
|
|
193
|
+
)}
|
|
194
|
+
</label>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
) : (
|
|
198
|
+
<div className="text-xs text-gray-500">This atom has no props.</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|