@unoverse-platform/studio 0.1.88 → 0.1.89

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -201,28 +201,53 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
201
201
  // eslint-disable-next-line react-hooks/exhaustive-deps
202
202
  }, [storeVersion, store, current]);
203
203
 
204
+ // GROUPED BY CATEGORY, exactly as Components groups its rail — the shelf is no longer a
205
+ // flat list the moment small templates land on it (the `Blocks` shelf: pair, feature,
206
+ // single, rail, stack, statement) and a page has to be found among them. The category is
207
+ // the DOMAIN of the job, so the grouping reads as what a template is FOR. Within a
208
+ // category the developer's own list first, ownership marked per item rather than by
209
+ // section — the org chip says "yours"; no chip means the base shelf.
210
+ const byCategory = Object.entries(
211
+ templates.reduce<Record<string, TemplateDef[]>>((acc, t) => {
212
+ (acc[t.category ?? "Uncategorized"] ??= []).push(t);
213
+ return acc;
214
+ }, {}),
215
+ ).sort(([a], [b]) => a.localeCompare(b));
216
+ for (const [, items] of byCategory) items.sort((a, b) => (a.org ? 0 : 1) - (b.org ? 0 : 1));
217
+
204
218
  return (
205
219
  <div className={SCREEN}>
206
- {/* NAVIGATOR — per-item org chip, like Components: no chip = BASE shelf. */}
220
+ {/* NAVIGATOR — grouped by category, per-item org chip: no chip = BASE shelf. */}
207
221
  <nav className={NAV}>
208
222
  {templates.length === 0 && (
209
223
  <div className="p-2 text-xs text-gray-400">
210
224
  No templates — restart the Unoverse server so /dev/templates is available.
211
225
  </div>
212
226
  )}
213
- {templates.map((t) => (
214
- <div key={refOf(t)}>
215
- <button
216
- onClick={() => setSelected(refOf(t))}
217
- className={`flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition ${navItem(selected === refOf(t))}`}
218
- >
219
- <span className="truncate">{t.title ?? t.name}</span>
220
- {t.org && (
221
- <span className="ml-auto shrink-0 rounded bg-emerald-900/50 px-1.5 py-0.5 text-[10px] text-emerald-300">
222
- {t.org}
223
- </span>
224
- )}
225
- </button>
227
+ {byCategory.map(([cat, items]) => (
228
+ <div key={cat} className="mb-3">
229
+ {/* The same group header Components draws: a hairline that fades right lifts the
230
+ label without adding weight, and wider tracking reads as a label rather than
231
+ another item. One anatomy across the workbench's rails. */}
232
+ <div className="mb-1 mt-1 flex items-center gap-2 px-2">
233
+ <span className="text-[10px] font-medium uppercase tracking-[0.14em] text-gray-400">{cat}</span>
234
+ <span className="h-px flex-1 bg-gradient-to-r from-gray-500/40 to-transparent" />
235
+ </div>
236
+ {items.map((t) => (
237
+ <div key={refOf(t)}>
238
+ <button
239
+ onClick={() => setSelected(refOf(t))}
240
+ className={`flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition ${navItem(selected === refOf(t))}`}
241
+ >
242
+ <span className="truncate">{t.title ?? t.name}</span>
243
+ {t.org && (
244
+ <span className="ml-auto shrink-0 rounded bg-emerald-900/50 px-1.5 py-0.5 text-[10px] text-emerald-300">
245
+ {t.org}
246
+ </span>
247
+ )}
248
+ </button>
249
+ </div>
250
+ ))}
226
251
  </div>
227
252
  ))}
228
253
  </nav>
@@ -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[]; template?: unknown; frame?: unknown; cases?: Record<string, 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.template) walk(un.template);
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.template) remapFields(node.template as AnyNode, propMap);
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.template) applyLiterals(node.template as AnyNode, lits);
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
- /** TWO node types read this, and the shape says which.
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
- * Template: a STRING naming what the place holds (UNOVERSE_TEMPLATE_MODEL.md — holder
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
- * A renderer branches on the node's `type`, never on this key alone. */
89
- template?: UnoverseNode | string;
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 `template` subtree once
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 template binds `value` like any field; object items pass
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
- // `template` is read by two node types and the shape says which: a SUBTREE here on Each,
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.template === "object" ? node.template : undefined;
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.