@unoverse-platform/studio 0.1.41 → 0.1.42
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
CHANGED
|
@@ -27,6 +27,8 @@ interface PropDef {
|
|
|
27
27
|
}
|
|
28
28
|
interface ComponentDef {
|
|
29
29
|
name: string;
|
|
30
|
+
/** The owning org for an org component (server-tagged); marketplace tier has none. */
|
|
31
|
+
org?: string;
|
|
30
32
|
/** Display name from the manifest ("Card Finder"); falls back to `name`. */
|
|
31
33
|
title?: string;
|
|
32
34
|
description?: string;
|
|
@@ -119,17 +121,22 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
119
121
|
// SDK's `Each` saw a string instead of a list and rendered nothing. Editor display
|
|
120
122
|
// strings are derived separately into `text`.
|
|
121
123
|
const toText = (v: unknown) => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v, null, 2));
|
|
124
|
+
// The component's IDENTITY everywhere identity matters (selection, store slices,
|
|
125
|
+
// Copy for Canvas, URIs): the qualified ref for an org component, bare for the
|
|
126
|
+
// marketplace tier. Names are unique only within a tier, so keying on the bare name
|
|
127
|
+
// would collide two orgs' same-named components. Display keeps the bare name.
|
|
128
|
+
const refOf = (c: ComponentDef): string => (c.org ? `${c.org}/${c.name}` : c.name);
|
|
122
129
|
const defaultsOf = (c: ComponentDef): Record<string, unknown> =>
|
|
123
130
|
Object.fromEntries(
|
|
124
131
|
Object.entries(c.props ?? {}).map(([k, v]) => [k, v.preview ?? v.default ?? (v.type === "array" ? [] : v.type === "object" ? {} : "")]),
|
|
125
132
|
);
|
|
126
133
|
|
|
127
134
|
function choose(c: ComponentDef) {
|
|
128
|
-
setSelected(c
|
|
135
|
+
setSelected(refOf(c));
|
|
129
136
|
const defaults = defaultsOf(c);
|
|
130
137
|
setValues(defaults);
|
|
131
138
|
setText(Object.fromEntries(Object.entries(defaults).map(([k, v]) => [k, toText(v)])));
|
|
132
|
-
store.apply({ type: "COMPONENT_INIT", chatId: CHAT_ID, nodeId: c
|
|
139
|
+
store.apply({ type: "COMPONENT_INIT", chatId: CHAT_ID, nodeId: refOf(c), component: { type: refOf(c), props: defaults } });
|
|
133
140
|
// SEED THE ARRIVAL FACE, from the one place it lives. `manifest.defaultState` is the
|
|
134
141
|
// render contract, and the server merges it into the composed `state` block
|
|
135
142
|
// (definitions.ts — "so the SDK, which merges def.state beneath live data, renders
|
|
@@ -142,9 +149,9 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
142
149
|
// the arrival face; `state.defaultState` is the legacy spelling. Seed whichever
|
|
143
150
|
// the def declares, under the same key the renderer resolves.
|
|
144
151
|
const st = c.state as Record<string, unknown> | undefined;
|
|
145
|
-
if (typeof st?.view === "string") store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: c
|
|
152
|
+
if (typeof st?.view === "string") store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { view: st.view } });
|
|
146
153
|
else if (typeof st?.defaultState === "string")
|
|
147
|
-
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: c
|
|
154
|
+
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { defaultState: st.defaultState } });
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
useEffect(() => {
|
|
@@ -156,7 +163,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
156
163
|
const list: ComponentDef[] = d.components ?? [];
|
|
157
164
|
setComponents(list);
|
|
158
165
|
// Restore the persisted selection across reloads; fall back to the first.
|
|
159
|
-
const restore = list.find((c) => c
|
|
166
|
+
const restore = list.find((c) => refOf(c) === selected) ?? list[0];
|
|
160
167
|
if (restore) choose(restore);
|
|
161
168
|
})
|
|
162
169
|
.catch(() => setComponents([]));
|
|
@@ -167,7 +174,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
167
174
|
// toggle highlight + grow container, not just the rendered component).
|
|
168
175
|
useEffect(() => store.subscribe(() => setStoreTick((t) => t + 1)), [store]);
|
|
169
176
|
|
|
170
|
-
const current = components.find((c) => c
|
|
177
|
+
const current = components.find((c) => refOf(c) === selected);
|
|
171
178
|
// The TWO MASTER STATES (inline / focused) are universal — every component's root
|
|
172
179
|
// switches its face on `defaultState` (STATE_MODEL §5b). The toolbar control writes
|
|
173
180
|
// that key into the slice — the same generic setValue/COMPONENT_DATA path a real
|
|
@@ -329,7 +336,9 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
329
336
|
// data. UIComponentNode layers props [schema defaults] < [executed] < [config], so
|
|
330
337
|
// anything written here outranks the live definition permanently: spreading the
|
|
331
338
|
// previews in froze them into the workflow at copy time.
|
|
332
|
-
|
|
339
|
+
// Qualified ref for an org component (`<org>/<name>`): the selector identity in
|
|
340
|
+
// the generic Component node's oneOf, and unambiguous once two orgs share a name.
|
|
341
|
+
config: { component: refOf(current) },
|
|
333
342
|
},
|
|
334
343
|
style: { width: size.width },
|
|
335
344
|
draggable: true,
|
|
@@ -377,14 +386,15 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
377
386
|
<div key={cat} className="mb-3">
|
|
378
387
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">{cat}</div>
|
|
379
388
|
{items.map((c) => (
|
|
380
|
-
<div key={c
|
|
389
|
+
<div key={refOf(c)}>
|
|
381
390
|
<button
|
|
382
391
|
onClick={() => choose(c)}
|
|
383
392
|
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${
|
|
384
|
-
selected === c
|
|
393
|
+
selected === refOf(c) ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
385
394
|
}`}
|
|
386
395
|
>
|
|
387
396
|
{c.name}
|
|
397
|
+
{c.org ? <span className="ml-1.5 text-[10px] text-gray-500">{c.org}</span> : null}
|
|
388
398
|
</button>
|
|
389
399
|
{/* The component's OWN states (its states/ folder) live HERE in the rail.
|
|
390
400
|
The two MASTER states (inline/focused — the `defaultState` face switch,
|
|
@@ -399,7 +409,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
399
409
|
Clicking a step's layout puts the preview into that step. The
|
|
400
410
|
states themselves live in the toolbar pills above — one control
|
|
401
411
|
per axis, no duplication. */}
|
|
402
|
-
{selected === c
|
|
412
|
+
{selected === refOf(c) && !sourceOpen && stateTree?.length ? (
|
|
403
413
|
(() => {
|
|
404
414
|
const st = stateTree.find((s) => s.name === masterState);
|
|
405
415
|
if (!st) return null;
|
|
@@ -409,14 +419,14 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
409
419
|
// choice either: nothing lists.
|
|
410
420
|
const entries = (st.states ?? []).map((sub) => ({ label: sub.name, step: sub.name }));
|
|
411
421
|
if (entries.length < 2 || !st.on) return null;
|
|
412
|
-
const cur = (store.get(CHAT_ID, c
|
|
422
|
+
const cur = (store.get(CHAT_ID, refOf(c)) as Record<string, unknown>)[st.on] ?? st.initial;
|
|
413
423
|
return (
|
|
414
424
|
<div className="my-0.5 ml-2 flex flex-col gap-px border-l border-gray-700 pl-2">
|
|
415
425
|
{entries.map((e) => (
|
|
416
426
|
<button
|
|
417
427
|
key={e.label}
|
|
418
428
|
onClick={() => {
|
|
419
|
-
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: c
|
|
429
|
+
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { [st.on!]: e.step } });
|
|
420
430
|
setStoreTick((v) => v + 1);
|
|
421
431
|
}}
|
|
422
432
|
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
@@ -430,20 +440,20 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
430
440
|
);
|
|
431
441
|
})()
|
|
432
442
|
) : null}
|
|
433
|
-
{selected === c
|
|
443
|
+
{selected === refOf(c) && !sourceOpen && !stateTree?.length && (c.states?.length ?? 0) > 0 && (
|
|
434
444
|
<div className="my-0.5 ml-2 flex flex-col gap-px border-l border-gray-700 pl-2">
|
|
435
445
|
{(c.states ?? []).map((s) => {
|
|
436
446
|
const data = stateActivation(c.root, s);
|
|
437
447
|
const activeKey = data ? Object.keys(data)[0] : null;
|
|
438
448
|
const isActive =
|
|
439
449
|
Boolean(activeKey) &&
|
|
440
|
-
(store.get(CHAT_ID, c
|
|
450
|
+
(store.get(CHAT_ID, refOf(c)) as Record<string, unknown>)[activeKey!] === (data as Record<string, unknown>)[activeKey!];
|
|
441
451
|
return (
|
|
442
452
|
<button
|
|
443
453
|
key={s.name}
|
|
444
454
|
onClick={() => {
|
|
445
455
|
if (!data) return;
|
|
446
|
-
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: c
|
|
456
|
+
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data });
|
|
447
457
|
setStoreTick((v) => v + 1);
|
|
448
458
|
}}
|
|
449
459
|
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
@@ -459,16 +469,16 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
459
469
|
{/* CONTEXTUAL SUB-STATES (LEGACY Switch-scan only — a v2 tree renders
|
|
460
470
|
its steps nested above): the ACTIVE face's internal steps (its
|
|
461
471
|
private Switch). Listed only while their face is active. */}
|
|
462
|
-
{selected === c
|
|
472
|
+
{selected === refOf(c) && !sourceOpen && !stateTree?.length && subStates.length > 0 && (
|
|
463
473
|
<div className="my-0.5 ml-2 flex flex-col gap-px border-l border-gray-700 pl-2">
|
|
464
474
|
{subStates.map((s, i) => {
|
|
465
|
-
const cur = (store.get(CHAT_ID, c
|
|
475
|
+
const cur = (store.get(CHAT_ID, refOf(c)) as Record<string, unknown>)[s.on];
|
|
466
476
|
const on = cur === s.name || (i === 0 && (cur === undefined || cur === ""));
|
|
467
477
|
return (
|
|
468
478
|
<button
|
|
469
479
|
key={s.name}
|
|
470
480
|
onClick={() => {
|
|
471
|
-
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: c
|
|
481
|
+
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { [s.on]: s.name } });
|
|
472
482
|
setStoreTick((v) => v + 1);
|
|
473
483
|
}}
|
|
474
484
|
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
@@ -582,8 +592,8 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
582
592
|
nothing. This one is honoured — off means no canvas can install it. */}
|
|
583
593
|
{current?.spatial && (
|
|
584
594
|
<ToggleSwitch
|
|
585
|
-
on={available.isEnabled(current
|
|
586
|
-
onClick={() => available.toggle(current
|
|
595
|
+
on={available.isEnabled(refOf(current))}
|
|
596
|
+
onClick={() => available.toggle(refOf(current))}
|
|
587
597
|
label="Available"
|
|
588
598
|
/>
|
|
589
599
|
)}
|
|
@@ -33,7 +33,11 @@ export function useUnoverseInstance(store?: ComponentStore, chatId = "", nodeId
|
|
|
33
33
|
subscribe,
|
|
34
34
|
useCallback(() => (store ? store.getType(chatId, nodeId) : undefined), [store, chatId, nodeId]),
|
|
35
35
|
);
|
|
36
|
-
|
|
36
|
+
const componentUrl = useSyncExternalStore(
|
|
37
|
+
subscribe,
|
|
38
|
+
useCallback(() => (store ? store.getComponentUrl(chatId, nodeId) : undefined), [store, chatId, nodeId]),
|
|
39
|
+
);
|
|
40
|
+
return { type, data, componentUrl };
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface UnoverseComponentProps {
|
|
@@ -83,11 +87,14 @@ export function UnoverseComponent({ client, uri, data, store, chatId, nodeId, ex
|
|
|
83
87
|
// LIVE SOURCE. The hook runs either way (hooks cannot be conditional) and no-ops without a
|
|
84
88
|
// store, so one component serves both paths without a wrapper deciding for it.
|
|
85
89
|
const live = useUnoverseInstance(store, chatId, nodeId);
|
|
86
|
-
// The
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
90
|
+
// The SERVER-SENT address first: an org component's URI is org-qualified
|
|
91
|
+
// (`unoverse://components/<org>/<name>`) and the bare type cannot rebuild it once two
|
|
92
|
+
// orgs share a name. The rebuild-from-type stays as the fallback for events that
|
|
93
|
+
// carry no componentUrl. (Still the plain URI, never the withdrawn `@refs` form —
|
|
94
|
+
// that suffix broke Studio's local definition route.)
|
|
95
|
+
const resolvedUri = store
|
|
96
|
+
? (live.componentUrl ?? (live.type ? `unoverse://components/${live.type}` : null))
|
|
97
|
+
: (uri ?? null);
|
|
91
98
|
const sourceData = store ? (extraData ? { ...(live.data ?? {}), ...extraData } : live.data) : data;
|
|
92
99
|
// No theme passed → fetch it from the server (the SDK owns zero token values). The
|
|
93
100
|
// theme REF comes from the definition itself: a template's `org` names its client's
|
|
@@ -71,6 +71,10 @@ export class ComponentStore {
|
|
|
71
71
|
// --- data plane ---
|
|
72
72
|
private data = new Map<string, Record<string, unknown>>();
|
|
73
73
|
private types = new Map<string, string>();
|
|
74
|
+
// The server-sent render address (`unoverse://components/<org>/<name>` for an org
|
|
75
|
+
// component). Kept because the TYPE alone cannot rebuild it: names are unique only
|
|
76
|
+
// within a tier, so a bare type two orgs share is ambiguous without this.
|
|
77
|
+
private urls = new Map<string, string>();
|
|
74
78
|
// --- timeline plane (pointers only) ---
|
|
75
79
|
private timeline: Turn[] = [];
|
|
76
80
|
// --- write recency per slice (monotonic seq stamped on every merge) — powers
|
|
@@ -275,6 +279,7 @@ export class ComponentStore {
|
|
|
275
279
|
const key = `${event.chatId}:${event.nodeId}`;
|
|
276
280
|
if (event.type === "COMPONENT_INIT") {
|
|
277
281
|
this.types.set(key, event.component.type);
|
|
282
|
+
if (event.component.componentUrl) this.urls.set(key, event.component.componentUrl);
|
|
278
283
|
this.placeInTimeline(event.chatId, key); // structure: place the pointer ONCE
|
|
279
284
|
this.merge(key, event.component.props ?? {}); // data: seed
|
|
280
285
|
// Rule 5 — an arrival carrying a public state refreshes that state.
|
|
@@ -376,6 +381,7 @@ export class ComponentStore {
|
|
|
376
381
|
for (const ptr of dropped.components) {
|
|
377
382
|
this.data.delete(ptr);
|
|
378
383
|
this.types.delete(ptr);
|
|
384
|
+
this.urls.delete(ptr);
|
|
379
385
|
this.touches.delete(ptr);
|
|
380
386
|
this.interactionTouches.delete(ptr);
|
|
381
387
|
}
|
|
@@ -460,6 +466,10 @@ export class ComponentStore {
|
|
|
460
466
|
getType(chatId: string, nodeId: string): string | undefined {
|
|
461
467
|
return this.types.get(`${chatId}:${nodeId}`);
|
|
462
468
|
}
|
|
469
|
+
/** The server-sent render address for a slice, when the INIT carried one. */
|
|
470
|
+
getComponentUrl(chatId: string, nodeId: string): string | undefined {
|
|
471
|
+
return this.urls.get(`${chatId}:${nodeId}`);
|
|
472
|
+
}
|
|
463
473
|
getVersion(): number {
|
|
464
474
|
return this.version;
|
|
465
475
|
}
|
|
@@ -132,8 +132,11 @@ place. Work through them in order.
|
|
|
132
132
|
2. **Rename the templates.** Folder AND the id inside the manifest. `bpp-chat-layout`
|
|
133
133
|
becomes `lyceum-chat-layout` in both places. Template ids are org-qualified and they
|
|
134
134
|
are referenced by string.
|
|
135
|
-
3. **
|
|
136
|
-
|
|
135
|
+
3. **Component names stay as copied.** Names are unique per org, not across orgs, so a
|
|
136
|
+
cloned pack carrying the same component names as its source is legal: each org's copy
|
|
137
|
+
is addressed `unoverse://components/<org>/<name>`. The one forbidden case is a name
|
|
138
|
+
that shadows a design-system (marketplace) component; the publish lint catches it.
|
|
139
|
+
Update template manifest `name`s only where the id itself is org-branded.
|
|
137
140
|
4. **Register the client surface** if the demo client serves it
|
|
138
141
|
(`packages/demo/src/demo/clients.js`).
|
|
139
142
|
5. **Point the app at the new theme.** Templates reference `unoverse://theme/<org>/<name>`.
|
|
@@ -154,9 +154,11 @@ template states by name exactly as in chat. Audio is never wired in a definition
|
|
|
154
154
|
|
|
155
155
|
## New project?
|
|
156
156
|
|
|
157
|
-
`rx/<project>/` = `templates/` + optional `components/` (project-private microapps
|
|
158
|
-
names unique
|
|
159
|
-
|
|
157
|
+
`rx/<project>/` = `templates/` + optional `components/` (project-private microapps;
|
|
158
|
+
names unique within the project and never shadowing a marketplace name; two projects
|
|
159
|
+
may share a name, each addressed `<org>/<name>`) + `styles/` holding ONLY the org's
|
|
160
|
+
overrides (the foundation cascades per token; the theme file is the one you copy
|
|
161
|
+
whole, see brand.md). Addresses: apps
|
|
160
162
|
`unoverse://apps/<project>/<name>`, project components
|
|
161
163
|
`unoverse://components/<project>/<name>`, themes `unoverse://theme/<project>/<theme>`.
|
|
162
164
|
|