@unoverse-platform/studio 0.1.85 → 0.1.87
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/package.json +1 -1
- package/screens/ComponentsView.tsx +4 -3
- package/screens/TemplatesLibraryView.tsx +197 -82
- package/screens/TemplatesView.tsx +49 -8
- package/screens/states.ts +25 -7
- package/src/host.ts +1 -1
- package/vendor/sdk/lib/UnoverseComponent.tsx +49 -3
- package/vendor/sdk/lib/app.tsx +121 -31
- package/vendor/sdk/lib/connection.tsx +34 -33
- package/vendor/sdk/lib/core/actions.ts +2 -2
- package/vendor/sdk/lib/core/apps.ts +31 -18
- package/vendor/sdk/lib/core/conditions.ts +15 -3
- package/vendor/sdk/lib/core/connection.ts +17 -1
- package/vendor/sdk/lib/core/store.ts +104 -21
- package/vendor/sdk/lib/core/types.ts +8 -0
- package/vendor/sdk/lib/transition/MountArrangement.tsx +71 -0
- package/vendor/sdk/lib/transition/decide.ts +53 -0
- package/vendor/sdk/lib/transition/guestHold.ts +34 -0
- package/vendor/sdk/lib/transition/index.ts +9 -0
- package/vendor/sdk/lib/transition/viewTransition.ts +29 -0
- package/vendor/skills/unoverse-create/SKILL.md +11 -5
- package/vendor/skills/unoverse-create/references/app.md +35 -21
- package/vendor/skills/unoverse-create/references/component.md +55 -54
- package/vendor/skills/unoverse-create/references/node.md +3 -1
- package/vendor/skills/unoverse-create/references/template.md +133 -0
package/package.json
CHANGED
|
@@ -150,7 +150,8 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
150
150
|
// the arrival face; `state.defaultState` is the legacy spelling. Seed whichever
|
|
151
151
|
// the def declares, under the same key the renderer resolves.
|
|
152
152
|
const st = c.state as Record<string, unknown> | undefined;
|
|
153
|
-
|
|
153
|
+
const face0 = st?.state ?? st?.view;
|
|
154
|
+
if (typeof face0 === "string") store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { state: face0 } });
|
|
154
155
|
else if (typeof st?.defaultState === "string")
|
|
155
156
|
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { defaultState: st.defaultState } });
|
|
156
157
|
}
|
|
@@ -186,7 +187,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
186
187
|
// on (faceAxisOf), so a v2 component and a legacy one both flip.
|
|
187
188
|
const faceAxis = current?.root ? faceAxisOf(current.root) : "defaultState";
|
|
188
189
|
const masterState =
|
|
189
|
-
((slice?.view ?? slice?.defaultState ?? slice?.displayState) as string | undefined) ??
|
|
190
|
+
((slice?.state ?? slice?.view ?? slice?.defaultState ?? slice?.displayState) as string | undefined) ??
|
|
190
191
|
((current as { initialView?: string } | undefined)?.initialView ?? "inline");
|
|
191
192
|
// A component's FACES come from its OWN root Switch on the public axis — an OPEN
|
|
192
193
|
// name set (inline/focused/course/…), never a hardcoded pair. The BASE face (a v2
|
|
@@ -197,7 +198,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
197
198
|
const walk = (n: any): void => {
|
|
198
199
|
if (!n || typeof n !== "object") return;
|
|
199
200
|
if (Array.isArray(n)) { n.forEach(walk); return; }
|
|
200
|
-
if (n.type === "Switch" && (n.on === "view" || n.on === "defaultState") && n.cases && typeof n.cases === "object") {
|
|
201
|
+
if (n.type === "Switch" && (n.on === "state" || n.on === "view" || n.on === "defaultState") && n.cases && typeof n.cases === "object") {
|
|
201
202
|
for (const k of Object.keys(n.cases)) if (k !== "default" && !found.includes(k)) found.push(k);
|
|
202
203
|
}
|
|
203
204
|
Object.values(n).forEach(walk);
|
|
@@ -1,50 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Templates view — the Template Model's kind (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md
|
|
3
|
-
* made browsable. A template is
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Templates view — the Template Model's kind (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md
|
|
3
|
+
* §9b, the three-word grammar), made browsable. A template is states owning
|
|
4
|
+
* arrangements; a layout's SECTIONS are `static:` (placed as designed),
|
|
5
|
+
* `copywriter:` (written into through briefs) and ONE `director:` (decided from
|
|
6
|
+
* what streams in). Authors write `type: template`; the app kind is `app` in code.
|
|
7
|
+
* Two tiers like components: the marketplace shelf (no chip = BASE, installed,
|
|
8
|
+
* read-only) and org packs (org chip).
|
|
8
9
|
*
|
|
9
10
|
* Header + dock MIRROR ComponentsView/AtomsView exactly (responsive width toggle,
|
|
10
|
-
* collapsible drag-resize dock) — the dock holds the
|
|
11
|
-
* contract: each
|
|
12
|
-
* fills come from deliveries, not typed values. The preview renders the
|
|
13
|
-
* synthesized root:
|
|
14
|
-
*
|
|
11
|
+
* collapsible drag-resize dock) — the dock holds the SECTIONS table (the template's
|
|
12
|
+
* contract: each section and who fills it) instead of prop controls, because a
|
|
13
|
+
* template's fills come from deliveries, not typed values. The preview renders the
|
|
14
|
+
* synthesized root: linked sections seed their own components, the director section
|
|
15
|
+
* seeds its preview mocks (real interfaces, their own defaults).
|
|
15
16
|
*
|
|
16
17
|
* NOT TemplatesView.tsx: that name is taken by the Apps screen (the legacy noun).
|
|
17
18
|
*/
|
|
18
19
|
import { useEffect, useMemo, useState, useSyncExternalStore, useCallback, type MouseEvent as ReactMouseEvent } from "react";
|
|
19
20
|
import { ComponentStore, StreamedUnoverseApp, type ResolvedTheme } from "@unoverse/sdk";
|
|
20
|
-
import { client, authedFetch, usePersistentState } from "studio-host";
|
|
21
|
+
import { client, authedFetch, usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
21
22
|
import { NAV, SCREEN, navItem } from "./chrome";
|
|
22
23
|
|
|
23
|
-
interface
|
|
24
|
-
static
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
interface TemplateSection {
|
|
25
|
+
/** static = placed as designed; copywriter = written into through its briefs;
|
|
26
|
+
* director = decided from what streams in (§9b, the three words). */
|
|
27
|
+
kind: "static" | "copywriter" | "director";
|
|
28
|
+
/** The state instances render in at this section — what the claim matches. */
|
|
29
|
+
face: string;
|
|
30
|
+
/** Linked sections: the authored path (components/masthead). */
|
|
31
|
+
component?: string;
|
|
32
|
+
/** Linked sections: the resolvable scoped ref (email-digest/masthead). */
|
|
33
|
+
ref?: string;
|
|
34
|
+
/** The previewed components' mock values, keyed by ref, compiled by the server (only
|
|
35
|
+
* it can resolve a template-local part). Seeded into each instance so the arrangement
|
|
36
|
+
* previews as written and filled rather than as a page of ghosts. */
|
|
37
|
+
seeds?: Record<string, Record<string, unknown>>;
|
|
33
38
|
state?: string;
|
|
39
|
+
limit?: number;
|
|
40
|
+
rules?: string;
|
|
41
|
+
preview?: string[];
|
|
34
42
|
}
|
|
35
43
|
interface TemplateDef {
|
|
36
44
|
name: string;
|
|
37
45
|
org?: string;
|
|
46
|
+
/** Spatial-CAPABLE (server-projected, §9c): every template is, because its manifest is
|
|
47
|
+
* mandatory. Capability, not activation — the switch below is the opt-in. */
|
|
48
|
+
spatial?: boolean;
|
|
38
49
|
title?: string;
|
|
39
50
|
description?: string;
|
|
40
51
|
whenToUse?: string;
|
|
41
52
|
category?: string;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
/** Compiled per-state sections (§9b) — the loader's projection of the layout's
|
|
54
|
+
* three-word lines; the wire layouts carry the expanded claims. */
|
|
55
|
+
sections?: Record<string, TemplateSection[]>;
|
|
56
|
+
/** The composed arrangements, keyed by state — the pills. */
|
|
57
|
+
layouts?: Record<string, unknown>;
|
|
58
|
+
defaultLayout?: string;
|
|
59
|
+
/** Seeding map the loader assembles: face → interface refs. */
|
|
48
60
|
preview?: Record<string, string[]>;
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -59,22 +71,30 @@ const VIEWPORTS: { id: string; label: string; w: number | null }[] = [
|
|
|
59
71
|
|
|
60
72
|
const refOf = (t: TemplateDef) => (t.org ? `${t.org}/${t.name}` : t.name);
|
|
61
73
|
|
|
62
|
-
/** One
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
/** One section's description for the dock table — the three words (§9b). */
|
|
75
|
+
function sectionLabel(sec: TemplateSection): { name: string; filler: string; rules: string } {
|
|
76
|
+
if (sec.kind === "director")
|
|
77
|
+
return {
|
|
78
|
+
name: "director",
|
|
79
|
+
filler: "director",
|
|
80
|
+
rules: [
|
|
81
|
+
sec.state ? `state ${sec.state}` : null,
|
|
82
|
+
sec.limit != null ? `limit ${sec.limit}` : null,
|
|
83
|
+
sec.rules ?? null,
|
|
84
|
+
].filter(Boolean).join(" · "),
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
name: sec.component ?? sec.face,
|
|
88
|
+
filler: sec.kind === "copywriter" ? "copywriter" : "static",
|
|
89
|
+
rules: "",
|
|
90
|
+
};
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org?: string }) {
|
|
75
94
|
const [templates, setTemplates] = useState<TemplateDef[]>([]);
|
|
76
95
|
const [selected, setSelected] = usePersistentState<string>("wb:template", "");
|
|
77
96
|
const [panelOpen, setPanelOpen] = useState(true);
|
|
97
|
+
const [metaOpen, setMetaOpen] = usePersistentState<boolean>("wb:templates:meta", false); // manifest meta panel
|
|
78
98
|
const [panelHeight, setPanelHeight] = useState(200);
|
|
79
99
|
const [stageWidth, setStageWidth] = useState<number | null>(768);
|
|
80
100
|
|
|
@@ -108,6 +128,10 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
108
128
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
129
|
}, [org]);
|
|
110
130
|
|
|
131
|
+
// AVAILABLE — the same master switch every other kind has (§9c): is this template
|
|
132
|
+
// offered to workflows at all? Level 2 (is it on THIS workflow's map?) stays in
|
|
133
|
+
// Canvas's Content Library, exactly as for a component.
|
|
134
|
+
const available = useRegistryToggles("template");
|
|
111
135
|
const current = templates.find((t) => refOf(t) === selected);
|
|
112
136
|
const uri = useMemo(() => (current ? `unoverse://templates/${refOf(current)}` : ""), [current]);
|
|
113
137
|
|
|
@@ -116,20 +140,40 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
116
140
|
// everything — a tap re-faces an instance, the name-matched layout surfaces, its
|
|
117
141
|
// slot claims the instance, and sub-states stay contained in the instance's slice.
|
|
118
142
|
const store = useMemo(() => new ComponentStore(), [selected]);
|
|
143
|
+
|
|
144
|
+
const seeds = useMemo(() => {
|
|
145
|
+
// THE SEED PLAN, from the BASE state's compiled sections (§9b): a linked
|
|
146
|
+
// section (static/copywriter) seeds ONE instance of its own component at its
|
|
147
|
+
// face — the template's FRAME, which the state pills never re-face. The
|
|
148
|
+
// director section seeds its preview refs at its face — the items the guest
|
|
149
|
+
// actually browses.
|
|
150
|
+
const base = current?.defaultLayout ?? (current?.layouts ? Object.keys(current.layouts)[0] : undefined);
|
|
151
|
+
const secs = (base && current?.sections?.[base]) || [];
|
|
152
|
+
const out: { nodeId: string; ref: string; face?: string; framed: boolean; seed?: Record<string, unknown> }[] = [];
|
|
153
|
+
for (const sec of secs) {
|
|
154
|
+
if (sec.kind === "director") {
|
|
155
|
+
for (const [i, ref] of (sec.preview ?? []).entries())
|
|
156
|
+
out.push({ nodeId: `${sec.face}~${ref}~${i}`, ref, face: sec.face, framed: false, seed: sec.seeds?.[ref] });
|
|
157
|
+
} else if (sec.ref) {
|
|
158
|
+
out.push({ nodeId: `${sec.face}~${sec.ref}`, ref: sec.ref, face: sec.face, framed: true, seed: sec.seeds?.[sec.ref] });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}, [current]);
|
|
163
|
+
const framedIds = useMemo(() => new Set(seeds.filter((s) => s.framed).map((s) => s.nodeId)), [seeds]);
|
|
164
|
+
|
|
119
165
|
useEffect(() => {
|
|
120
166
|
// Seed once per store, and only once `current` has actually loaded — the list
|
|
121
167
|
// arrives async, so this must re-run when it lands, not just on store creation.
|
|
122
168
|
if (!current || store.getTimeline().length) return;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (baseFace) store.mergeComponentState(PREVIEW_CHAT, `${ref}~${i}`, { view: baseFace });
|
|
132
|
-
});
|
|
169
|
+
for (const { nodeId, ref, face, seed } of seeds) {
|
|
170
|
+
// The part's own mock values, exactly as the Components screen seeds a component
|
|
171
|
+
// (`preview ?? default`) — compiled onto the section by the server, because only it
|
|
172
|
+
// can resolve a template-local part. Seeded with nothing, every copywriter part
|
|
173
|
+
// draws as skeletons and a finished arrangement reads as a broken one.
|
|
174
|
+
store.apply({ type: "COMPONENT_INIT", chatId: PREVIEW_CHAT, nodeId, component: { type: ref, props: seed ?? {} } });
|
|
175
|
+
if (face) store.mergeComponentState(PREVIEW_CHAT, nodeId, { state: face });
|
|
176
|
+
}
|
|
133
177
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
178
|
}, [store, current]);
|
|
135
179
|
|
|
@@ -140,7 +184,7 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
140
184
|
useCallback((cb) => store.subscribe(cb), [store]),
|
|
141
185
|
useCallback(() => store.getStructureVersion(), [store]),
|
|
142
186
|
);
|
|
143
|
-
const shapeNames = current?.
|
|
187
|
+
const shapeNames = current?.layouts ? Object.keys(current.layouts) : [];
|
|
144
188
|
const activeState = useMemo(() => {
|
|
145
189
|
void storeVersion;
|
|
146
190
|
for (const name of shapeNames.slice(1)) {
|
|
@@ -149,7 +193,7 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
149
193
|
.some((r) => r.components.some((p) => {
|
|
150
194
|
const { chatId, nodeId } = store.split(p);
|
|
151
195
|
const slice = store.get(chatId, nodeId) as Record<string, unknown> | undefined;
|
|
152
|
-
return (slice?.view ?? slice?.defaultState) === name;
|
|
196
|
+
return (slice?.state ?? slice?.view ?? slice?.defaultState) === name;
|
|
153
197
|
}));
|
|
154
198
|
if (hit) return name;
|
|
155
199
|
}
|
|
@@ -197,6 +241,31 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
197
241
|
{/* THE TEMPLATE'S STATES, across the top — the same pill row the Apps
|
|
198
242
|
screen gives a multi-layout template (states own shapes; the pill puts
|
|
199
243
|
the preview into that state). One state is not a choice: no row. */}
|
|
244
|
+
{current?.spatial && (
|
|
245
|
+
<ToggleSwitch
|
|
246
|
+
on={available.isEnabled(refOf(current))}
|
|
247
|
+
onClick={() => available.toggle(refOf(current))}
|
|
248
|
+
label="Available"
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
{/* META — the same disclosure Components and Apps carry, on the same
|
|
252
|
+
mandatory manifest (§9c: a template is discovered like everything
|
|
253
|
+
else, so what it is DISCOVERED BY must be readable where it is
|
|
254
|
+
designed). Disclosure only; it changes nothing. */}
|
|
255
|
+
{current?.spatial && (
|
|
256
|
+
<button
|
|
257
|
+
onClick={() => setMetaOpen((o) => !o)}
|
|
258
|
+
className="flex items-center gap-1 rounded border px-2 py-0.5 text-[11px] font-medium"
|
|
259
|
+
style={{
|
|
260
|
+
borderColor: theme.color["border.subtle"],
|
|
261
|
+
color: metaOpen ? theme.color["text.primary"] : theme.color["text.secondary"],
|
|
262
|
+
background: metaOpen ? theme.color["surface.sunken"] : "transparent",
|
|
263
|
+
}}
|
|
264
|
+
title="Show the template's manifest (spatial discovery meta)"
|
|
265
|
+
>
|
|
266
|
+
ⓘ Meta <span className="opacity-60">{metaOpen ? "▴" : "▾"}</span>
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
200
269
|
{shapeNames.length > 1 && (
|
|
201
270
|
<div className="flex items-center gap-1 rounded p-0.5 text-[11px]" style={{ background: theme.color["border.subtle"] }}>
|
|
202
271
|
{shapeNames.map((name, i) => {
|
|
@@ -208,16 +277,29 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
208
277
|
// The pill is the guest's hand: it writes FACES, and the
|
|
209
278
|
// state derives — never the other way round. Base pill
|
|
210
279
|
// returns every instance to the base face.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
280
|
+
//
|
|
281
|
+
// THE FRAME IS NOT AN ITEM. Every seeded instance used to be
|
|
282
|
+
// treated as one, which held while a template had a single
|
|
283
|
+
// slot: on email-digest the base pill re-faced the masthead
|
|
284
|
+
// and footer to `grid` (they left their slots and landed in
|
|
285
|
+
// the item band), and the `page` pill wrote the face onto the
|
|
286
|
+
// first component in the response — the masthead — so the
|
|
287
|
+
// detail shape surfaced with nothing in it. Slots with an
|
|
288
|
+
// authored `state:` are skipped, and the detail pill opens
|
|
289
|
+
// the first ITEM.
|
|
290
|
+
const items = store
|
|
291
|
+
.getResponses()
|
|
292
|
+
.flatMap((r) => r.components)
|
|
293
|
+
.filter((ptr) => !framedIds.has(store.split(ptr).nodeId));
|
|
294
|
+
for (const [j, ptr] of items.entries()) {
|
|
295
|
+
const { chatId, nodeId } = store.split(ptr);
|
|
296
|
+
const slice = store.get(chatId, nodeId) as Record<string, unknown> | undefined;
|
|
297
|
+
if (i === 0) {
|
|
298
|
+
if ((slice?.state ?? slice?.view ?? slice?.defaultState) != null) store.mergeComponentState(chatId, nodeId, { state: shapeNames[0] });
|
|
299
|
+
} else if (j === 0) {
|
|
300
|
+
store.mergeComponentState(chatId, nodeId, { state: name });
|
|
220
301
|
}
|
|
302
|
+
}
|
|
221
303
|
}}
|
|
222
304
|
className="rounded px-2 py-0.5 font-medium capitalize transition"
|
|
223
305
|
style={on ? { background: theme.color["surface.base"], color: theme.color["text.primary"] } : { color: theme.color["text.secondary"] }}
|
|
@@ -249,6 +331,42 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
249
331
|
</span>
|
|
250
332
|
</div>
|
|
251
333
|
|
|
334
|
+
{/* MANIFEST META — identical anatomy to the Components and Apps panels:
|
|
335
|
+
title / category chip / muted tag, description, when-to-use callout. */}
|
|
336
|
+
{metaOpen && current?.spatial && (() => {
|
|
337
|
+
const accent = theme.color["action.primary"] ?? "#6366f1";
|
|
338
|
+
return (
|
|
339
|
+
<div className="border-b px-5 py-4" style={{ borderColor: theme.color["border.subtle"], background: theme.color["surface.base"] }}>
|
|
340
|
+
<div className="mx-auto flex max-w-5xl flex-col gap-1">
|
|
341
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
342
|
+
<h2 className="text-[17px] font-semibold leading-tight tracking-tight" style={{ color: theme.color["text.primary"] }}>
|
|
343
|
+
{current.title || current.name}
|
|
344
|
+
</h2>
|
|
345
|
+
{current.category && (
|
|
346
|
+
<span className="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide" style={{ background: `${accent}14`, color: accent }}>
|
|
347
|
+
{current.category}
|
|
348
|
+
</span>
|
|
349
|
+
)}
|
|
350
|
+
<span className="font-mono text-[10px]" style={{ color: theme.color["text.tertiary"] }}>
|
|
351
|
+
manifest.json
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
{current.description && (
|
|
355
|
+
<p className="mt-1 max-w-3xl text-[13px] leading-relaxed" style={{ color: theme.color["text.secondary"] }}>{current.description}</p>
|
|
356
|
+
)}
|
|
357
|
+
{current.whenToUse && (
|
|
358
|
+
<div className="mt-2 max-w-3xl rounded-r py-1.5 pl-3" style={{ borderLeft: `3px solid ${accent}`, background: theme.color["surface.sunken"] }}>
|
|
359
|
+
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-wider" style={{ color: accent }}>
|
|
360
|
+
When to use
|
|
361
|
+
</div>
|
|
362
|
+
<p className="text-[15px] font-medium leading-relaxed" style={{ color: theme.color["text.primary"] }}>{current.whenToUse}</p>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
})()}
|
|
369
|
+
|
|
252
370
|
<div className="flex-1 overflow-auto p-10">
|
|
253
371
|
<div className="mx-auto max-w-full transition-[width] duration-200" style={{ width: stageWidth ?? "100%" }}>
|
|
254
372
|
{uri && <StreamedUnoverseApp key={uri} client={client} store={store} uri={uri} theme={theme} />}
|
|
@@ -267,46 +385,43 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
267
385
|
<div className="flex min-h-0 shrink-0 flex-col bg-[#232a34] text-gray-200" style={panelOpen ? { height: panelHeight } : undefined}>
|
|
268
386
|
<div
|
|
269
387
|
onClick={() => setPanelOpen((o) => !o)}
|
|
270
|
-
title={panelOpen ? "Collapse
|
|
388
|
+
title={panelOpen ? "Collapse sections" : "Expand sections"}
|
|
271
389
|
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"
|
|
272
390
|
>
|
|
273
391
|
<span className="leading-none text-gray-300">{panelOpen ? "▾" : "▸"}</span>
|
|
274
|
-
<span>
|
|
392
|
+
<span>sections</span>
|
|
275
393
|
{current && <span className="text-gray-400">· {current.name}</span>}
|
|
276
394
|
</div>
|
|
277
395
|
{panelOpen && (
|
|
278
396
|
<div className="min-h-0 flex-1 overflow-auto p-3">
|
|
279
|
-
{
|
|
397
|
+
{(() => {
|
|
398
|
+
const base = current?.defaultLayout ?? (current?.layouts ? Object.keys(current.layouts)[0] : undefined);
|
|
399
|
+
const secs = (base && current?.sections?.[base]) || [];
|
|
400
|
+
return secs.length ? (
|
|
280
401
|
<table className="w-full text-left text-xs">
|
|
281
402
|
<thead className="text-gray-500">
|
|
282
403
|
<tr>
|
|
283
|
-
<th className="pb-1 pr-4 font-medium">
|
|
404
|
+
<th className="pb-1 pr-4 font-medium">section</th>
|
|
284
405
|
<th className="pb-1 pr-4 font-medium">filled by</th>
|
|
285
406
|
<th className="pb-1 font-medium">rules</th>
|
|
286
407
|
</tr>
|
|
287
408
|
</thead>
|
|
288
409
|
<tbody className="text-gray-300">
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
p.state ? `state ${p.state}` : null,
|
|
300
|
-
]
|
|
301
|
-
.filter(Boolean)
|
|
302
|
-
.join(" · ")}
|
|
303
|
-
</td>
|
|
304
|
-
</tr>
|
|
305
|
-
))}
|
|
410
|
+
{secs.map((sec, i) => {
|
|
411
|
+
const l = sectionLabel(sec);
|
|
412
|
+
return (
|
|
413
|
+
<tr key={i} className="border-t border-gray-700/60">
|
|
414
|
+
<td className="py-1 pr-4 font-mono">{l.name}</td>
|
|
415
|
+
<td className="py-1 pr-4">{l.filler}</td>
|
|
416
|
+
<td className="py-1 text-gray-500">{l.rules}</td>
|
|
417
|
+
</tr>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
306
420
|
</tbody>
|
|
307
421
|
</table>
|
|
308
|
-
|
|
309
|
-
|
|
422
|
+
) : null;
|
|
423
|
+
})() || (
|
|
424
|
+
<div className="text-xs text-gray-500">This template declares no sections.</div>
|
|
310
425
|
)}
|
|
311
426
|
</div>
|
|
312
427
|
)}
|
|
@@ -77,6 +77,9 @@ interface PropDef {
|
|
|
77
77
|
}
|
|
78
78
|
interface Def {
|
|
79
79
|
name: string;
|
|
80
|
+
/** The artifact's kind. Templates sit beside components in the seed lookup (§9c), and
|
|
81
|
+
* the kind decides which home their definition is addressed at. */
|
|
82
|
+
kind?: string;
|
|
80
83
|
// The client org this app belongs to (server-injected) — names its theme (`<org>/light`).
|
|
81
84
|
org?: string;
|
|
82
85
|
description?: string;
|
|
@@ -172,7 +175,7 @@ export function TemplatesView({
|
|
|
172
175
|
// MOCK (offline) vs LIVE (real data plane). A fresh store per mode keeps the two paths
|
|
173
176
|
// from bleeding into each other.
|
|
174
177
|
const [live, setLive] = useState(defaultLive);
|
|
175
|
-
const available = useRegistryToggles("
|
|
178
|
+
const available = useRegistryToggles("app");
|
|
176
179
|
const [templates, setTemplates] = useState<Def[]>([]);
|
|
177
180
|
const [components, setComponents] = useState<Def[]>([]);
|
|
178
181
|
const [selected, setSelected] = usePersistentState<string | null>("wb:apps:selected", null);
|
|
@@ -578,6 +581,21 @@ export function TemplatesView({
|
|
|
578
581
|
return d.initialView ?? d.publicStates?.[0] ?? d.state?.view?.initial ?? Object.keys(d.state?.view?.states ?? {})[0];
|
|
579
582
|
};
|
|
580
583
|
|
|
584
|
+
/** The instances a seeded TEMPLATE stands for: each linked part at its own face with
|
|
585
|
+
* its own mock values, and the director's stand-ins at the band's face. Empty for a
|
|
586
|
+
* component, which stands for itself. */
|
|
587
|
+
const templateSeeds = (c: Def): { ref: string; face: string; props: Record<string, unknown> }[] => {
|
|
588
|
+
const d = c as { kind?: string; defaultLayout?: string; layouts?: Record<string, unknown>; sections?: Record<string, { kind: string; face: string; ref?: string; preview?: string[]; seeds?: Record<string, Record<string, unknown>> }[]> };
|
|
589
|
+
if (d.kind !== "template") return [];
|
|
590
|
+
const base = d.defaultLayout ?? Object.keys(d.layouts ?? {})[0];
|
|
591
|
+
const out: { ref: string; face: string; props: Record<string, unknown> }[] = [];
|
|
592
|
+
for (const sec of (base && d.sections?.[base]) || []) {
|
|
593
|
+
const refs = sec.kind === "director" ? (sec.preview ?? []) : sec.ref ? [sec.ref] : [];
|
|
594
|
+
for (const ref of refs) out.push({ ref, face: sec.face, props: sec.seeds?.[ref] ?? {} });
|
|
595
|
+
}
|
|
596
|
+
return out;
|
|
597
|
+
};
|
|
598
|
+
|
|
581
599
|
function seed(comps: SeedSpec[]) {
|
|
582
600
|
// nodeId is per-INSTANCE (`name-i`), so previewComponents may repeat a name to
|
|
583
601
|
// seed several instances — e.g. ["course-card", "course-card"] previews a rail of cards.
|
|
@@ -593,7 +611,21 @@ export function TemplatesView({
|
|
|
593
611
|
// instance ARRIVES with its own view, both spellings, exactly as a real moment
|
|
594
612
|
// would deliver it — and the chip never overrides it (the activation loop skips
|
|
595
613
|
// envelope instances).
|
|
596
|
-
const { view: _v, defaultState: _d, ...restState } = ((c as { state?: Record<string, unknown> }).state ?? {}) as Record<string, unknown>;
|
|
614
|
+
const { state: _s, view: _v, defaultState: _d, ...restState } = ((c as { state?: Record<string, unknown> }).state ?? {}) as Record<string, unknown>;
|
|
615
|
+
// A template arrives WITH its page: the parts it links and the stand-ins its band
|
|
616
|
+
// holds, each at its own face, exactly as a delivery would land them.
|
|
617
|
+
for (const [j, part] of templateSeeds(c).entries()) {
|
|
618
|
+
store.apply({
|
|
619
|
+
type: "COMPONENT_INIT",
|
|
620
|
+
chatId: chatIdFor(selected ?? "app"),
|
|
621
|
+
nodeId: `${c.name}-${i}-${part.face}-${j}`,
|
|
622
|
+
component: {
|
|
623
|
+
type: part.ref,
|
|
624
|
+
componentUrl: `unoverse://components/${part.ref}`,
|
|
625
|
+
props: { ...part.props, state: part.face, view: part.face, defaultState: part.face },
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
597
629
|
store.apply({
|
|
598
630
|
type: "COMPONENT_INIT",
|
|
599
631
|
chatId: chatIdFor(selected ?? "app"),
|
|
@@ -604,7 +636,9 @@ export function TemplatesView({
|
|
|
604
636
|
// cannot rebuild it (names are unique only within a tier — store.ts), so a
|
|
605
637
|
// mock seed without it resolved bare `unoverse://components/<name>` and the
|
|
606
638
|
// resolver rightly refused to guess between two orgs' clones.
|
|
607
|
-
|
|
639
|
+
// THE ADDRESS SAYS WHAT IT IS (§9c): a seeded TEMPLATE is served, read and
|
|
640
|
+
// rendered from the templates home, so a preview naming one resolves.
|
|
641
|
+
componentUrl: `unoverse://${c.kind === "template" ? "templates" : "components"}/${c.org ? `${c.org}/` : ""}${c.name}`,
|
|
608
642
|
// ORDER MATTERS: prop DEFAULTS come after the flattened state scalars,
|
|
609
643
|
// because a projected axis (loadState) appears in BOTH — as the tree's first
|
|
610
644
|
// substate in state (its live arrival) and as a prop default (its preview) —
|
|
@@ -657,10 +691,17 @@ export function TemplatesView({
|
|
|
657
691
|
// Mock seeding picked whichever listed first.
|
|
658
692
|
authedFetch(`/dev/apps${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { apps: [] })),
|
|
659
693
|
authedFetch(`/dev/components${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { components: [] })),
|
|
694
|
+
// TEMPLATES SEED LIKE COMPONENTS (§9c). A state whose surface holds a page that
|
|
695
|
+
// STREAMS IN names that page in its preview, so the seed lookup has to be able to
|
|
696
|
+
// find it — otherwise the state's chip seeds nothing and the tab looks broken.
|
|
697
|
+
authedFetch(`/dev/templates${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`).then((r) => (r.ok ? r.json() : { templates: [] })),
|
|
660
698
|
])
|
|
661
|
-
.then(([t, c]) => {
|
|
699
|
+
.then(([t, c, tpl]) => {
|
|
662
700
|
const tl: Def[] = t.apps ?? [];
|
|
663
|
-
const cl: Def[] =
|
|
701
|
+
const cl: Def[] = [
|
|
702
|
+
...((c.components ?? []) as Def[]),
|
|
703
|
+
...((tpl.templates ?? []) as Def[]).map((d) => ({ ...d, kind: "template" as const })),
|
|
704
|
+
];
|
|
664
705
|
setTemplates(tl);
|
|
665
706
|
setComponents(cl);
|
|
666
707
|
if (live) setSeeded([]);
|
|
@@ -737,7 +778,7 @@ export function TemplatesView({
|
|
|
737
778
|
return Object.entries(c.states).flatMap(([stName, st]): (string | Record<string, Record<string, unknown>>)[] => {
|
|
738
779
|
if (typeof st.interface !== "string" || !st.interface) return [];
|
|
739
780
|
const face = st.state ?? (publicStatesOf(st.interface).includes(stName) ? stName : undefined);
|
|
740
|
-
return [face ? { [st.interface]: {
|
|
781
|
+
return [face ? { [st.interface]: { state: face } } : st.interface];
|
|
741
782
|
});
|
|
742
783
|
return typeof c.interface === "string" && c.interface ? [c.interface] : [];
|
|
743
784
|
})
|
|
@@ -836,8 +877,8 @@ export function TemplatesView({
|
|
|
836
877
|
// An ENVELOPE instance is NEVER moved: it declared its own view, and forcing it
|
|
837
878
|
// would recreate the one-view-for-all mock the envelope form exists to retire
|
|
838
879
|
// (UNOVERSE_INTERFACE_MODEL §3, §7).
|
|
839
|
-
const axis = whereSel.field === "view" || whereSel.field === "defaultState";
|
|
840
|
-
const data = axis ? { view: whereSel.eq ?? true, defaultState: whereSel.eq ?? true } : { [whereSel.field!]: whereSel.eq ?? true };
|
|
880
|
+
const axis = whereSel.field === "state" || whereSel.field === "view" || whereSel.field === "defaultState";
|
|
881
|
+
const data = axis ? { state: whereSel.eq ?? true, view: whereSel.eq ?? true, defaultState: whereSel.eq ?? true } : { [whereSel.field!]: whereSel.eq ?? true };
|
|
841
882
|
seededSet.forEach((c, i) => {
|
|
842
883
|
if (c.view || (c as { envelope?: boolean }).envelope) return;
|
|
843
884
|
store.mergeComponentState(chatIdFor(selected ?? "app"), `${c.def.name}-${i}`, data);
|
package/screens/states.ts
CHANGED
|
@@ -21,15 +21,31 @@ function walkTree(n: unknown, visit: (node: Record<string, unknown>) => boolean
|
|
|
21
21
|
return Object.values(o).some((v) => walkTree(v, visit));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The PUBLIC axis: `state` (the ruled word, 2026-08-29), with `view` and `defaultState`
|
|
26
|
+
* as the older spellings a component may still switch on.
|
|
27
|
+
*
|
|
28
|
+
* `state` WAS MISSING HERE, and it broke every face control in Studio. A v2 component
|
|
29
|
+
* compiles its state tree into a root `Switch` on `state` (`{ on: "state", cases:
|
|
30
|
+
* ["grid", "page", …] }`), so with only the two legacy words admitted:
|
|
31
|
+
* - `faceAxisOf` fell through to its `defaultState` default, so clicking a face wrote
|
|
32
|
+
* `{ defaultState: "page" }` while the root watched `state`, and nothing moved. The
|
|
33
|
+
* buttons still RENDERED, because the names come from the served `stateTree` rather
|
|
34
|
+
* than from this file, which is what made it look like dead buttons rather than a
|
|
35
|
+
* missing axis;
|
|
36
|
+
* - `faceOf` could not find a face's subtree;
|
|
37
|
+
* - `switchLayers` counted the public axis as a STEP, because it identifies steps by
|
|
38
|
+
* everything this predicate rejects.
|
|
39
|
+
* One list, three symptoms.
|
|
40
|
+
*/
|
|
41
|
+
export const VIEW_AXES = ["state", "view", "defaultState"] as const;
|
|
42
|
+
const isViewAxis = (on: unknown): boolean => on === "state" || on === "view" || on === "defaultState";
|
|
27
43
|
|
|
28
44
|
/** The state an instance is showing, read off its slice on the public axis. Reading
|
|
29
45
|
* only the legacy alias leaves every migrated component looking state-less. */
|
|
30
46
|
export const viewOfSlice = (slice: unknown): unknown => {
|
|
31
47
|
const s = (slice ?? {}) as Record<string, unknown>;
|
|
32
|
-
return s.view ?? s.defaultState;
|
|
48
|
+
return s.state ?? s.view ?? s.defaultState;
|
|
33
49
|
};
|
|
34
50
|
|
|
35
51
|
/** The subtree a face draws: the root's Switch-on-the-public-axis case of that name. */
|
|
@@ -45,11 +61,13 @@ export function faceOf(root: unknown, face: string): unknown {
|
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
/** Which public-axis spelling the root actually switches on (writes must match it). */
|
|
48
|
-
export function faceAxisOf(root: unknown): "view" | "defaultState" {
|
|
49
|
-
|
|
64
|
+
export function faceAxisOf(root: unknown): "state" | "view" | "defaultState" {
|
|
65
|
+
// The default stays `defaultState`: a tree with no public-axis Switch at all is a
|
|
66
|
+
// legacy component, and writing the modern word to it would flip nothing.
|
|
67
|
+
let axis: "state" | "view" | "defaultState" = "defaultState";
|
|
50
68
|
walkTree(root, (o) => {
|
|
51
69
|
if (o.type === "Switch" && isViewAxis(o.on) && o.cases && typeof o.cases === "object") {
|
|
52
|
-
axis = o.on as "view" | "defaultState";
|
|
70
|
+
axis = o.on as "state" | "view" | "defaultState";
|
|
53
71
|
return true;
|
|
54
72
|
}
|
|
55
73
|
});
|
package/src/host.ts
CHANGED
|
@@ -187,7 +187,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, React.Dispat
|
|
|
187
187
|
return [value, setValue];
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
export type RegistryKind = "node" | "skill" | "
|
|
190
|
+
export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template";
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* Registry toggles decide what a RUNNING universe exposes; they persist to
|