@unoverse-platform/studio 0.1.88 → 0.1.90
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
|
@@ -23,7 +23,8 @@ import { NAV, SCREEN, navItem } from "./chrome";
|
|
|
23
23
|
|
|
24
24
|
interface TemplateSection {
|
|
25
25
|
/** static = placed as designed; copywriter = written into through its briefs;
|
|
26
|
-
* director =
|
|
26
|
+
* director = the judge: a search place (with `component`) or a delivery band
|
|
27
|
+
* (without) — one word since 2026-09-02 (§9b + §9e). */
|
|
27
28
|
kind: "static" | "copywriter" | "director";
|
|
28
29
|
/** The state instances render in at this section — what the claim matches. */
|
|
29
30
|
face: string;
|
|
@@ -36,9 +37,15 @@ interface TemplateSection {
|
|
|
36
37
|
* previews as written and filled rather than as a page of ghosts. */
|
|
37
38
|
seeds?: Record<string, Record<string, unknown>>;
|
|
38
39
|
state?: string;
|
|
39
|
-
limit?: number;
|
|
40
40
|
rules?: string;
|
|
41
41
|
preview?: string[];
|
|
42
|
+
/** Director places (§9e): the grammar, echoed for the dock table. `ask` appears
|
|
43
|
+
* only after derivation (rule 10d) — never authored. */
|
|
44
|
+
ask?: string;
|
|
45
|
+
type?: "discovery" | "intent";
|
|
46
|
+
include?: string[];
|
|
47
|
+
pick?: number;
|
|
48
|
+
items?: { min: number; max: number };
|
|
42
49
|
}
|
|
43
50
|
interface TemplateDef {
|
|
44
51
|
name: string;
|
|
@@ -73,13 +80,26 @@ const refOf = (t: TemplateDef) => (t.org ? `${t.org}/${t.name}` : t.name);
|
|
|
73
80
|
|
|
74
81
|
/** One section's description for the dock table — the three words (§9b). */
|
|
75
82
|
function sectionLabel(sec: TemplateSection): { name: string; filler: string; rules: string } {
|
|
83
|
+
// ONE WORD, TWO MODES (§9e, 2026-09-02): with `component` = a search place; without =
|
|
84
|
+
// a delivery band. Both show as the director's — the judge is the same.
|
|
85
|
+
if (sec.kind === "director" && sec.component)
|
|
86
|
+
return {
|
|
87
|
+
name: sec.component,
|
|
88
|
+
filler: "director",
|
|
89
|
+
rules: [
|
|
90
|
+
sec.items ? `items ${sec.items.min}–${sec.items.max}` : null,
|
|
91
|
+
sec.type ?? null,
|
|
92
|
+
sec.include?.length ? sec.include.join("+") : null,
|
|
93
|
+
sec.pick != null ? `pick ${sec.pick}` : null,
|
|
94
|
+
].filter(Boolean).join(" · "),
|
|
95
|
+
};
|
|
76
96
|
if (sec.kind === "director")
|
|
77
97
|
return {
|
|
78
98
|
name: "director",
|
|
79
99
|
filler: "director",
|
|
80
100
|
rules: [
|
|
81
101
|
sec.state ? `state ${sec.state}` : null,
|
|
82
|
-
sec.
|
|
102
|
+
sec.pick != null ? `pick ${sec.pick}` : null,
|
|
83
103
|
sec.rules ?? null,
|
|
84
104
|
].filter(Boolean).join(" · "),
|
|
85
105
|
};
|
|
@@ -151,11 +171,16 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
151
171
|
const secs = (base && current?.sections?.[base]) || [];
|
|
152
172
|
const out: { nodeId: string; ref: string; face?: string; framed: boolean; seed?: Record<string, unknown> }[] = [];
|
|
153
173
|
for (const sec of secs) {
|
|
154
|
-
|
|
174
|
+
// ONE word, two modes (§9e rule 10b, 2026-09-02): a director WITH a component is a
|
|
175
|
+
// SEARCH place — its mock is its own component's preview, seeded exactly like a
|
|
176
|
+
// linked part. Only the BAND form (no ref) seeds stand-ins. This branch predates
|
|
177
|
+
// the merge and read every director as a band, so search places seeded nothing and
|
|
178
|
+
// the template previewed with those sections simply missing.
|
|
179
|
+
if (sec.ref) {
|
|
180
|
+
out.push({ nodeId: `${sec.face}~${sec.ref}`, ref: sec.ref, face: sec.face, framed: true, seed: sec.seeds?.[sec.ref] });
|
|
181
|
+
} else if (sec.kind === "director") {
|
|
155
182
|
for (const [i, ref] of (sec.preview ?? []).entries())
|
|
156
183
|
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
184
|
}
|
|
160
185
|
}
|
|
161
186
|
return out;
|
|
@@ -201,28 +226,53 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
|
|
|
201
226
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
227
|
}, [storeVersion, store, current]);
|
|
203
228
|
|
|
229
|
+
// GROUPED BY CATEGORY, exactly as Components groups its rail — the shelf is no longer a
|
|
230
|
+
// flat list the moment small templates land on it (the `Blocks` shelf: pair, feature,
|
|
231
|
+
// single, rail, stack, statement) and a page has to be found among them. The category is
|
|
232
|
+
// the DOMAIN of the job, so the grouping reads as what a template is FOR. Within a
|
|
233
|
+
// category the developer's own list first, ownership marked per item rather than by
|
|
234
|
+
// section — the org chip says "yours"; no chip means the base shelf.
|
|
235
|
+
const byCategory = Object.entries(
|
|
236
|
+
templates.reduce<Record<string, TemplateDef[]>>((acc, t) => {
|
|
237
|
+
(acc[t.category ?? "Uncategorized"] ??= []).push(t);
|
|
238
|
+
return acc;
|
|
239
|
+
}, {}),
|
|
240
|
+
).sort(([a], [b]) => a.localeCompare(b));
|
|
241
|
+
for (const [, items] of byCategory) items.sort((a, b) => (a.org ? 0 : 1) - (b.org ? 0 : 1));
|
|
242
|
+
|
|
204
243
|
return (
|
|
205
244
|
<div className={SCREEN}>
|
|
206
|
-
{/* NAVIGATOR — per-item org chip
|
|
245
|
+
{/* NAVIGATOR — grouped by category, per-item org chip: no chip = BASE shelf. */}
|
|
207
246
|
<nav className={NAV}>
|
|
208
247
|
{templates.length === 0 && (
|
|
209
248
|
<div className="p-2 text-xs text-gray-400">
|
|
210
249
|
No templates — restart the Unoverse server so /dev/templates is available.
|
|
211
250
|
</div>
|
|
212
251
|
)}
|
|
213
|
-
{
|
|
214
|
-
<div key={
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
<span className="
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
252
|
+
{byCategory.map(([cat, items]) => (
|
|
253
|
+
<div key={cat} className="mb-3">
|
|
254
|
+
{/* The same group header Components draws: a hairline that fades right lifts the
|
|
255
|
+
label without adding weight, and wider tracking reads as a label rather than
|
|
256
|
+
another item. One anatomy across the workbench's rails. */}
|
|
257
|
+
<div className="mb-1 mt-1 flex items-center gap-2 px-2">
|
|
258
|
+
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-gray-400">{cat}</span>
|
|
259
|
+
<span className="h-px flex-1 bg-gradient-to-r from-gray-500/40 to-transparent" />
|
|
260
|
+
</div>
|
|
261
|
+
{items.map((t) => (
|
|
262
|
+
<div key={refOf(t)}>
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => setSelected(refOf(t))}
|
|
265
|
+
className={`flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition ${navItem(selected === refOf(t))}`}
|
|
266
|
+
>
|
|
267
|
+
<span className="truncate">{t.title ?? t.name}</span>
|
|
268
|
+
{t.org && (
|
|
269
|
+
<span className="ml-auto shrink-0 rounded bg-emerald-900/50 px-1.5 py-0.5 text-[10px] text-emerald-300">
|
|
270
|
+
{t.org}
|
|
271
|
+
</span>
|
|
272
|
+
)}
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
226
276
|
</div>
|
|
227
277
|
))}
|
|
228
278
|
</nav>
|
|
@@ -590,7 +590,7 @@ export function TemplatesView({
|
|
|
590
590
|
const base = d.defaultLayout ?? Object.keys(d.layouts ?? {})[0];
|
|
591
591
|
const out: { ref: string; face: string; props: Record<string, unknown> }[] = [];
|
|
592
592
|
for (const sec of (base && d.sections?.[base]) || []) {
|
|
593
|
-
const refs = sec.kind === "director" ? (sec.preview ?? []) :
|
|
593
|
+
const refs = sec.ref ? [sec.ref] : sec.kind === "director" ? (sec.preview ?? []) : [];
|
|
594
594
|
for (const ref of refs) out.push({ ref, face: sec.face, props: sec.seeds?.[ref] ?? {} });
|
|
595
595
|
}
|
|
596
596
|
return out;
|
package/vendor/sdk/lib/app.tsx
CHANGED
|
@@ -144,10 +144,10 @@ function collectSlotPointers(node: unknown, store: ComponentStore, scope?: Set<s
|
|
|
144
144
|
const out: string[] = [];
|
|
145
145
|
const walk = (n: unknown): void => {
|
|
146
146
|
if (!n || typeof n !== "object") return;
|
|
147
|
-
const un = n as UnoverseNode & { children?: unknown[];
|
|
147
|
+
const un = n as UnoverseNode & { children?: unknown[]; item?: unknown; frame?: unknown; cases?: Record<string, unknown> };
|
|
148
148
|
if (un.type === "ComponentSlot") out.push(...selectPointers(store, un, scope));
|
|
149
149
|
(un.children ?? []).forEach(walk);
|
|
150
|
-
if (un.
|
|
150
|
+
if (un.item) walk(un.item);
|
|
151
151
|
if (un.frame) walk(un.frame);
|
|
152
152
|
if (un.cases) Object.values(un.cases).forEach(walk);
|
|
153
153
|
};
|
|
@@ -65,7 +65,7 @@ function remapFields(node: AnyNode, propMap: Record<string, string>): void {
|
|
|
65
65
|
for (const v of when as { field?: string }[]) if (v && typeof v.field === "string" && propMap[v.field]) v.field = propMap[v.field];
|
|
66
66
|
}
|
|
67
67
|
if (Array.isArray(node.children)) node.children.forEach((c: AnyNode) => remapFields(c, propMap));
|
|
68
|
-
if (node.
|
|
68
|
+
if (node.item) remapFields(node.item as AnyNode, propMap);
|
|
69
69
|
if (node.cases && typeof node.cases === "object") for (const k of Object.keys(node.cases)) remapFields(node.cases[k] as AnyNode, propMap);
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -102,7 +102,7 @@ function applyLiterals(node: AnyNode, lits: Record<string, unknown>): void {
|
|
|
102
102
|
}
|
|
103
103
|
if (node.style && typeof node.style === "object") applyStyleLiterals(node.style as AnyNode, lits);
|
|
104
104
|
if (Array.isArray(node.children)) node.children.forEach((c: AnyNode) => applyLiterals(c, lits));
|
|
105
|
-
if (node.
|
|
105
|
+
if (node.item) applyLiterals(node.item as AnyNode, lits);
|
|
106
106
|
if (node.cases && typeof node.cases === "object") for (const k of Object.keys(node.cases)) applyLiterals(node.cases[k] as AnyNode, lits);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -78,15 +78,18 @@ export interface UnoverseNode {
|
|
|
78
78
|
user?: UnoverseNode;
|
|
79
79
|
/** Timeline: data sub-tree rendered for an assistant turn (its ComponentSlot is scoped to that turn). */
|
|
80
80
|
assistant?: UnoverseNode;
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
* Each: the per-item subtree, rendered once per element of the bound array (`bind.items`),
|
|
81
|
+
/** Each: the per-item subtree, drawn once per element of the bound array (`bind.items`),
|
|
84
82
|
* with that element as its data scope. The node's own `style` is the list container.
|
|
85
83
|
*
|
|
86
|
-
*
|
|
84
|
+
* RENAMED FROM `template` (2026-09-01). One word named two unrelated things — an Each's
|
|
85
|
+
* repeated drawing and a mounted TEMPLATE — and the only way to tell them apart was the
|
|
86
|
+
* shape of the value, which this type spelled out as `UnoverseNode | string`. A reader
|
|
87
|
+
* hitting it in a layout had to work out which; an author writing one had to know both. */
|
|
88
|
+
item?: UnoverseNode;
|
|
89
|
+
/** Template: a STRING naming what the place holds (UNOVERSE_TEMPLATE_MODEL.md — holder
|
|
87
90
|
* grammar, deliberately the same shape as `interface:`), e.g. `template: grid-page`.
|
|
88
|
-
*
|
|
89
|
-
template?:
|
|
91
|
+
* Unmoved by the rename above: here the word names a template. */
|
|
92
|
+
template?: string;
|
|
90
93
|
/** Template: the judgment granted to THIS placement — which items the director may select,
|
|
91
94
|
* whether it may order them, and the rules it works to. Opaque to the renderer, which
|
|
92
95
|
* carries it to the loader rather than reading into it; the placed node owns the mount,
|
|
@@ -173,21 +173,21 @@ export function renderNode(
|
|
|
173
173
|
return branch ? renderNode(branch, data, onAction, theme, key, slot, defs) : null;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// `Each` — control flow: map a bound array (bind.items) → the `
|
|
176
|
+
// `Each` — control flow: map a bound array (bind.items) → the `item` subtree once
|
|
177
177
|
// per item (that item as the data scope), inside the node's own container (a Box).
|
|
178
178
|
// A PRIMITIVE item (string/number — e.g. a `string[]` of follow-up questions) is exposed
|
|
179
|
-
// as `{ value: item }`, so the
|
|
179
|
+
// as `{ value: item }`, so the item binds `value` like any field; object items pass
|
|
180
180
|
// through unchanged. This is what lets an `Each` iterate a plain list of strings.
|
|
181
181
|
if (node.type === "Each") {
|
|
182
182
|
// Literal `items` fallback: a microapp component hardcodes its static option lists
|
|
183
183
|
// in the def (props exist only for state — UNOVERSE_AUTHORING.md). bind wins.
|
|
184
184
|
const src = node.bind?.items ? data[node.bind.items] : node.items;
|
|
185
185
|
const items = Array.isArray(src) ? src : [];
|
|
186
|
-
// `
|
|
186
|
+
// `item` is the Each's repeated drawing (renamed from `template` 2026-09-01: one word for
|
|
187
187
|
// a holder NAME (string) on Template, which this branch never sees. Narrow rather than
|
|
188
188
|
// assert, so a definition that puts the wrong one here renders nothing instead of
|
|
189
189
|
// throwing inside the map.
|
|
190
|
-
const tpl = typeof node.
|
|
190
|
+
const tpl = typeof node.item === "object" ? node.item : undefined;
|
|
191
191
|
// WHERE AN ITEM SITS is something only the renderer knows, and a numbered list cannot
|
|
192
192
|
// be authored without it: the data carries no ordinal, and a template cannot count.
|
|
193
193
|
// `index` is 0-based for logic, `position` 1-based because that is what a reader sees.
|