@unoverse-platform/studio 0.1.55 → 0.1.56
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/AtomsView.tsx +4 -5
- package/screens/ComponentsView.tsx +7 -14
- package/screens/DesignSystemView.tsx +44 -48
- package/screens/PromptBlocksView.tsx +8 -9
- package/screens/SkillsView.tsx +8 -9
- package/screens/TemplatesView.tsx +6 -9
- package/screens/chrome.ts +83 -0
- package/screens/index.ts +4 -0
- package/src/NodesView.tsx +3 -2
- package/vendor/skills/unoverse-create/references/component.md +26 -7
package/package.json
CHANGED
package/screens/AtomsView.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useState, type MouseEvent as ReactMouseEvent } from
|
|
|
10
10
|
import { UnoverseComponent, type ResolvedTheme } from "@unoverse/sdk";
|
|
11
11
|
import { client, authedFetch } from "studio-host";
|
|
12
12
|
import { usePersistentState } from "studio-host";
|
|
13
|
+
import { NAV, SCREEN, navItem } from "./chrome";
|
|
13
14
|
|
|
14
15
|
interface PropDef {
|
|
15
16
|
type?: string;
|
|
@@ -127,9 +128,9 @@ export function AtomsView({ theme }: { theme: ResolvedTheme }) {
|
|
|
127
128
|
const uri = useMemo(() => (selected ? `unoverse://atoms/${slug(selected)}` : ""), [selected]);
|
|
128
129
|
|
|
129
130
|
return (
|
|
130
|
-
<div className=
|
|
131
|
+
<div className={SCREEN}>
|
|
131
132
|
{/* NAVIGATOR */}
|
|
132
|
-
<nav className=
|
|
133
|
+
<nav className={NAV}>
|
|
133
134
|
{atoms.length === 0 && (
|
|
134
135
|
<div className="p-2 text-xs text-gray-400">No atoms — restart the Unoverse server so /dev/atoms is available.</div>
|
|
135
136
|
)}
|
|
@@ -153,9 +154,7 @@ export function AtomsView({ theme }: { theme: ResolvedTheme }) {
|
|
|
153
154
|
<button
|
|
154
155
|
key={a.name}
|
|
155
156
|
onClick={() => choose(a)}
|
|
156
|
-
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${
|
|
157
|
-
selected === a.name ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
158
|
-
}`}
|
|
157
|
+
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${navItem(selected === a.name)}`}
|
|
159
158
|
>
|
|
160
159
|
{a.name}
|
|
161
160
|
</button>
|
|
@@ -10,6 +10,7 @@ import { client, authedFetch, hasSourceAccess } from "studio-host";
|
|
|
10
10
|
import { SourceView } from "./SourceView";
|
|
11
11
|
import { stateActivation, faceOf, faceAxisOf, switchLayers } from "./states";
|
|
12
12
|
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
13
|
+
import { NAV, SCREEN, navItem, navSubItem } from "./chrome";
|
|
13
14
|
|
|
14
15
|
const CHAT_ID = "demo";
|
|
15
16
|
|
|
@@ -382,9 +383,9 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
382
383
|
for (const [, items] of byCategory) items.sort((a, b) => (a.org ? 0 : 1) - (b.org ? 0 : 1));
|
|
383
384
|
|
|
384
385
|
return (
|
|
385
|
-
<div className=
|
|
386
|
+
<div className={SCREEN}>
|
|
386
387
|
{/* NAVIGATOR */}
|
|
387
|
-
<nav className=
|
|
388
|
+
<nav className={NAV}>
|
|
388
389
|
{components.length === 0 && (
|
|
389
390
|
<div className="p-2 text-xs text-gray-400">No components — restart the Unoverse server so /dev/components is available.</div>
|
|
390
391
|
)}
|
|
@@ -400,9 +401,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
400
401
|
<div key={refOf(c)}>
|
|
401
402
|
<button
|
|
402
403
|
onClick={() => choose(c)}
|
|
403
|
-
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${
|
|
404
|
-
selected === refOf(c) ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
405
|
-
}`}
|
|
404
|
+
className={`block w-full rounded px-2 py-1 text-left text-sm transition ${navItem(selected === refOf(c))}`}
|
|
406
405
|
>
|
|
407
406
|
{c.name}
|
|
408
407
|
{/* Ownership mark: tagged = the developer's own (editable); untagged =
|
|
@@ -442,9 +441,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
442
441
|
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { [st.on!]: e.step } });
|
|
443
442
|
setStoreTick((v) => v + 1);
|
|
444
443
|
}}
|
|
445
|
-
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
446
|
-
cur === e.step ? "bg-emerald-500/10 text-emerald-300" : "text-gray-400 hover:bg-white/5 hover:text-gray-200"
|
|
447
|
-
}`}
|
|
444
|
+
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${navSubItem(cur === e.step)}`}
|
|
448
445
|
>
|
|
449
446
|
{e.label}
|
|
450
447
|
</button>
|
|
@@ -469,9 +466,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
469
466
|
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data });
|
|
470
467
|
setStoreTick((v) => v + 1);
|
|
471
468
|
}}
|
|
472
|
-
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
473
|
-
isActive ? "bg-emerald-500/10 text-emerald-300" : "text-gray-400 hover:bg-white/5 hover:text-gray-200"
|
|
474
|
-
}`}
|
|
469
|
+
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${navSubItem(isActive)}`}
|
|
475
470
|
>
|
|
476
471
|
{s.name}
|
|
477
472
|
</button>
|
|
@@ -494,9 +489,7 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
494
489
|
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: refOf(c), data: { [s.on]: s.name } });
|
|
495
490
|
setStoreTick((v) => v + 1);
|
|
496
491
|
}}
|
|
497
|
-
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
498
|
-
on ? "bg-emerald-500/10 text-emerald-300" : "text-gray-400 hover:bg-white/5 hover:text-gray-200"
|
|
499
|
-
}`}
|
|
492
|
+
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${navSubItem(on)}`}
|
|
500
493
|
>
|
|
501
494
|
{s.name}
|
|
502
495
|
</button>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
10
|
+
import { GALLERY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
10
11
|
|
|
11
12
|
const COLOR_GROUP_ORDER = [
|
|
12
13
|
"brand", "primary", "secondary", "success", "action", "text", "surface", "border", "status",
|
|
@@ -65,7 +66,12 @@ export function DesignSystemView({
|
|
|
65
66
|
// loud brand (e.g. an all-red project) doesn't turn the whole page its brand colour. The
|
|
66
67
|
// real token swatches still render their true values; only the chrome is neutralised.
|
|
67
68
|
const accent = "#6366f1";
|
|
68
|
-
|
|
69
|
+
// THE NAVIGATOR IS STUDIO CHROME, NOT THE PREVIEW (2026-08-21). It used to paint itself
|
|
70
|
+
// from the viewed theme's `surface.base` and size itself by hand, which is the same
|
|
71
|
+
// mistake the accent above was already fixed for: a project with a white surface gave
|
|
72
|
+
// Studio a white sub-nav, in the one section where every other section's is dark. It now
|
|
73
|
+
// takes the shared shell from `./chrome` like every other screen. Reading a project's
|
|
74
|
+
// tokens is what the CONTENT pane is for, and that pane still does.
|
|
69
75
|
const bodyFont = (theme.text["body.md"]?.fontFamily as string) ?? "system-ui";
|
|
70
76
|
|
|
71
77
|
// A token is THIS PROJECT'S OVERRIDE when its resolved value differs from the
|
|
@@ -276,55 +282,45 @@ export function DesignSystemView({
|
|
|
276
282
|
const current = SECTIONS.find((s) => s.id === active) ?? SECTIONS[0];
|
|
277
283
|
|
|
278
284
|
return (
|
|
279
|
-
<div className=
|
|
280
|
-
{/*
|
|
281
|
-
<nav className=
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
<span>{s.title}</span>
|
|
302
|
-
{n > 0 && (
|
|
303
|
-
<span
|
|
304
|
-
className="rounded-full px-1.5 py-0.5 text-[10px] font-semibold"
|
|
305
|
-
style={{ background: on ? "rgba(255,255,255,0.25)" : accent, color: onAction }}
|
|
306
|
-
>
|
|
307
|
-
{n}
|
|
308
|
-
</span>
|
|
309
|
-
)}
|
|
310
|
-
</button>
|
|
311
|
-
</li>
|
|
312
|
-
);
|
|
313
|
-
})}
|
|
314
|
-
</ul>
|
|
285
|
+
<div className={SCREEN}>
|
|
286
|
+
{/* NAVIGATOR — jump between token groups; badge = how many tokens THIS project overrides */}
|
|
287
|
+
<nav className={NAV}>
|
|
288
|
+
<div className={NAV_SECTION}>{foundation ? org : "foundation"}</div>
|
|
289
|
+
{SECTIONS.map((s) => {
|
|
290
|
+
const n = foundation && s.bucket ? overrideCount(s.bucket) : 0;
|
|
291
|
+
const on = active === s.id;
|
|
292
|
+
return (
|
|
293
|
+
<button
|
|
294
|
+
key={s.id}
|
|
295
|
+
onClick={() => setActive(s.id)}
|
|
296
|
+
className={`flex w-full items-center justify-between rounded px-2 py-1 text-left text-sm transition ${navItem(on)}`}
|
|
297
|
+
>
|
|
298
|
+
<span>{s.title}</span>
|
|
299
|
+
{n > 0 && (
|
|
300
|
+
<span className="rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-300">
|
|
301
|
+
{n}
|
|
302
|
+
</span>
|
|
303
|
+
)}
|
|
304
|
+
</button>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
315
307
|
</nav>
|
|
316
308
|
|
|
317
|
-
{/* CONTENT — the selected group only
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
309
|
+
{/* CONTENT — the selected group only. The one pane that IS allowed to look like the
|
|
310
|
+
project, because looking at the project is its whole job; `bodyFont` renders the
|
|
311
|
+
typography specimens in the theme's own face. */}
|
|
312
|
+
<div className={PANE}>
|
|
313
|
+
<div className={GALLERY} style={{ fontFamily: bodyFont }}>
|
|
314
|
+
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900">{current.title}</h1>
|
|
315
|
+
<p className="mb-8 mt-1 max-w-3xl text-sm leading-relaxed text-gray-600">
|
|
316
|
+
{foundation ? (
|
|
317
|
+
<>Tokens with an <OverrideMark /> are <b className="text-gray-900">{org}</b>’s own; the rest are inherited from the design-system foundation.</>
|
|
318
|
+
) : (
|
|
319
|
+
<>Live tokens from the design-system foundation (<code>design/marketplace/styles</code>). Toggle the theme to restyle everything.</>
|
|
320
|
+
)}
|
|
321
|
+
</p>
|
|
322
|
+
{current.body}
|
|
323
|
+
</div>
|
|
328
324
|
</div>
|
|
329
325
|
</div>
|
|
330
326
|
);
|
|
@@ -15,6 +15,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
15
15
|
import { fetchUnoverse } from "studio-host";
|
|
16
16
|
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
17
17
|
import { IdentityHeader } from "./IdentityHeader";
|
|
18
|
+
import { DOC, EMPTY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
18
19
|
|
|
19
20
|
interface PromptBlock {
|
|
20
21
|
id: string;
|
|
@@ -61,9 +62,9 @@ export function PromptBlocksView() {
|
|
|
61
62
|
).sort(([a], [b]) => a.localeCompare(b));
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
|
-
<div className=
|
|
65
|
+
<div className={SCREEN}>
|
|
65
66
|
{/* NAVIGATOR */}
|
|
66
|
-
<nav className=
|
|
67
|
+
<nav className={NAV}>
|
|
67
68
|
{blocks.length === 0 && (
|
|
68
69
|
<div className="p-2 text-xs text-gray-400">
|
|
69
70
|
{error
|
|
@@ -73,14 +74,12 @@ export function PromptBlocksView() {
|
|
|
73
74
|
)}
|
|
74
75
|
{byCategory.map(([cat, items]) => (
|
|
75
76
|
<div key={cat} className="mb-3">
|
|
76
|
-
<div className=
|
|
77
|
+
<div className={NAV_SECTION}>{cat}</div>
|
|
77
78
|
{items.map((b) => (
|
|
78
79
|
<button
|
|
79
80
|
key={b.id}
|
|
80
81
|
onClick={() => setSelected(b.id)}
|
|
81
|
-
className={`block w-full rounded px-2 py-1.5 text-left transition ${
|
|
82
|
-
selected === b.id ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
83
|
-
}`}
|
|
82
|
+
className={`block w-full rounded px-2 py-1.5 text-left transition ${navItem(selected === b.id)}`}
|
|
84
83
|
>
|
|
85
84
|
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
86
85
|
{toggles.isEnabled(b.id) && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" title="Enabled" />}
|
|
@@ -94,11 +93,11 @@ export function PromptBlocksView() {
|
|
|
94
93
|
</nav>
|
|
95
94
|
|
|
96
95
|
{/* DETAIL */}
|
|
97
|
-
<div className=
|
|
96
|
+
<div className={PANE}>
|
|
98
97
|
{!detail ? (
|
|
99
|
-
<div className=
|
|
98
|
+
<div className={EMPTY}>Select a prompt block</div>
|
|
100
99
|
) : (
|
|
101
|
-
<div className=
|
|
100
|
+
<div className={DOC}>
|
|
102
101
|
<IdentityHeader
|
|
103
102
|
name={detail.name}
|
|
104
103
|
category={detail.category}
|
package/screens/SkillsView.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { useEffect, useState } from "react";
|
|
|
13
13
|
import { fetchUnoverse } from "studio-host";
|
|
14
14
|
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
15
15
|
import { IdentityHeader } from "./IdentityHeader";
|
|
16
|
+
import { DOC, EMPTY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
16
17
|
|
|
17
18
|
interface SkillSummary {
|
|
18
19
|
name: string;
|
|
@@ -86,9 +87,9 @@ export function SkillsView() {
|
|
|
86
87
|
).sort(([a], [b]) => a.localeCompare(b));
|
|
87
88
|
|
|
88
89
|
return (
|
|
89
|
-
<div className=
|
|
90
|
+
<div className={SCREEN}>
|
|
90
91
|
{/* NAVIGATOR */}
|
|
91
|
-
<nav className=
|
|
92
|
+
<nav className={NAV}>
|
|
92
93
|
{skills.length === 0 && (
|
|
93
94
|
<div className="p-2 text-xs text-gray-400">
|
|
94
95
|
{error
|
|
@@ -98,14 +99,12 @@ export function SkillsView() {
|
|
|
98
99
|
)}
|
|
99
100
|
{byCategory.map(([cat, items]) => (
|
|
100
101
|
<div key={cat} className="mb-3">
|
|
101
|
-
<div className=
|
|
102
|
+
<div className={NAV_SECTION}>{cat}</div>
|
|
102
103
|
{items.map((s) => (
|
|
103
104
|
<button
|
|
104
105
|
key={s.name}
|
|
105
106
|
onClick={() => setSelected(s.name)}
|
|
106
|
-
className={`block w-full rounded px-2 py-1.5 text-left transition ${
|
|
107
|
-
selected === s.name ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
108
|
-
}`}
|
|
107
|
+
className={`block w-full rounded px-2 py-1.5 text-left transition ${navItem(selected === s.name)}`}
|
|
109
108
|
>
|
|
110
109
|
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
111
110
|
{s.name}
|
|
@@ -118,11 +117,11 @@ export function SkillsView() {
|
|
|
118
117
|
</nav>
|
|
119
118
|
|
|
120
119
|
{/* DETAIL */}
|
|
121
|
-
<div className=
|
|
120
|
+
<div className={PANE}>
|
|
122
121
|
{!selected ? (
|
|
123
|
-
<div className=
|
|
122
|
+
<div className={EMPTY}>Select a skill</div>
|
|
124
123
|
) : (
|
|
125
|
-
<div className=
|
|
124
|
+
<div className={DOC}>
|
|
126
125
|
{/* AVAILABLE — the master switch. Which workflows actually carry this skill is
|
|
127
126
|
installed per workflow, in Canvas's Content Library. */}
|
|
128
127
|
<IdentityHeader
|
|
@@ -27,6 +27,7 @@ import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resol
|
|
|
27
27
|
import { NativeAppHost } from "./NativeAppHost";
|
|
28
28
|
import { inArrangement, viewOfSlice } from "./states";
|
|
29
29
|
import { client, authedFetch, hasAuth, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
|
|
30
|
+
import { NAV, NAV_SECTION, SCREEN, navItem, navSubItem } from "./chrome";
|
|
30
31
|
|
|
31
32
|
// Per-APP test conversation: each app chats in its own conversation id, so testing
|
|
32
33
|
// one client's app never inherits another's agent history/memory (the "BPP answers
|
|
@@ -652,10 +653,10 @@ export function TemplatesView({
|
|
|
652
653
|
}, [store, selected, components, templates, live, activeState, activeLayout]);
|
|
653
654
|
|
|
654
655
|
return (
|
|
655
|
-
<div className=
|
|
656
|
+
<div className={SCREEN}>
|
|
656
657
|
{/* NAVIGATOR */}
|
|
657
|
-
<nav className=
|
|
658
|
-
<div className=
|
|
658
|
+
<nav className={NAV}>
|
|
659
|
+
<div className={NAV_SECTION}>Apps</div>
|
|
659
660
|
{templates.length === 0 && (
|
|
660
661
|
<div className="p-2 text-xs text-gray-400">No apps yet — add one under design/<project>/templates/.</div>
|
|
661
662
|
)}
|
|
@@ -663,9 +664,7 @@ export function TemplatesView({
|
|
|
663
664
|
<div key={`${t.org ?? ""}/${t.name}`}>
|
|
664
665
|
<button
|
|
665
666
|
onClick={() => setSelected(t.name)}
|
|
666
|
-
className={`flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition ${
|
|
667
|
-
selected === t.name ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5"
|
|
668
|
-
}`}
|
|
667
|
+
className={`flex w-full items-center gap-2 rounded px-2 py-1 text-left text-sm transition ${navItem(selected === t.name)}`}
|
|
669
668
|
>
|
|
670
669
|
<span className="min-w-0 flex-1 truncate">{t.name}</span>
|
|
671
670
|
{/* Org badge only in the cross-org ("all") view — scoped, it's redundant */}
|
|
@@ -691,9 +690,7 @@ export function TemplatesView({
|
|
|
691
690
|
setSeedKey((k) => k + 1); // picking = jump + reseed that scenario
|
|
692
691
|
}}
|
|
693
692
|
title={`state: ${s.name}`}
|
|
694
|
-
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${
|
|
695
|
-
(liveStateName ?? activeState?.name) === s.name ? "bg-emerald-500/10 text-emerald-300" : "text-gray-400 hover:bg-white/5 hover:text-gray-200"
|
|
696
|
-
}`}
|
|
693
|
+
className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${navSubItem((liveStateName ?? activeState?.name) === s.name)}`}
|
|
697
694
|
>
|
|
698
695
|
{s.name}
|
|
699
696
|
</button>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio screen shell, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Every section (Apps, Components, Atoms, Styles, Skills, Prompt Blocks, Nodes) is the
|
|
5
|
+
* same anatomy: a dark navigator on the left, a white working pane on the right. That
|
|
6
|
+
* was true in spirit and false in the details — the navigators had drifted to four
|
|
7
|
+
* widths (190/220/240/330), three paddings and two completely different active states,
|
|
8
|
+
* and Styles had drifted furthest of all, painting its navigator from the THEME IT WAS
|
|
9
|
+
* PREVIEWING so a project with a white surface got a white sub-nav.
|
|
10
|
+
*
|
|
11
|
+
* These constants are the agreement. Import them; do not retype the class strings,
|
|
12
|
+
* because a retyped string is how the drift happened the first time.
|
|
13
|
+
*
|
|
14
|
+
* The reference screen is Skills: it is the one that read as finished, so its
|
|
15
|
+
* proportions (220px navigator, p-2, a readable measure on white) are the standard
|
|
16
|
+
* rather than an average of what everything else happened to be doing.
|
|
17
|
+
*
|
|
18
|
+
* This is CHROME ONLY. It says how a section frames its content, never what the content
|
|
19
|
+
* is. The dark ramp it uses (#0f141a shell → #1b2129 navigator → #232a34 panel) is
|
|
20
|
+
* Studio's own and is never derived from a project's tokens; see DesignSystemView for
|
|
21
|
+
* the one screen where that distinction actually bites.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The section itself: navigator + working pane, neither scrolling the page. */
|
|
25
|
+
export const SCREEN = "grid min-h-0 flex-1 grid-cols-[220px_1fr] overflow-hidden";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* WIDER NAVIGATOR, for a navigator that is a palette rather than a list. Nodes searches
|
|
29
|
+
* and renders two-line cards, which 220px genuinely cannot hold. An exception with a
|
|
30
|
+
* reason, spelled out here so the next person does not "fix" it back to SCREEN or, worse,
|
|
31
|
+
* invent a fifth width.
|
|
32
|
+
*/
|
|
33
|
+
export const SCREEN_WIDE_NAV = "grid min-h-0 flex-1 grid-cols-[330px_1fr] overflow-hidden";
|
|
34
|
+
|
|
35
|
+
/** The dark navigator column. */
|
|
36
|
+
export const NAV = "flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#1b2129] p-2";
|
|
37
|
+
|
|
38
|
+
/** A group heading inside the navigator ("PROMPTS", "PRIMARY", a category). */
|
|
39
|
+
export const NAV_SECTION = "px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* SELECTION STATE ONLY, never layout.
|
|
43
|
+
*
|
|
44
|
+
* The navigators legitimately differ in row geometry — a Skills row is two lines, a
|
|
45
|
+
* Templates row is a flex row with a chip, a Components row opens a state tree — and
|
|
46
|
+
* flattening that into one class string would change row heights across five screens to
|
|
47
|
+
* buy nothing. What must NOT differ is what "selected" looks like, and that is what these
|
|
48
|
+
* return. Call sites keep their own layout classes and append one of these.
|
|
49
|
+
*
|
|
50
|
+
* Emerald is Studio's selection colour and is deliberately NOT any project's action
|
|
51
|
+
* colour, so a loud brand cannot restyle the chrome around its own preview.
|
|
52
|
+
*/
|
|
53
|
+
export const navItem = (active: boolean) =>
|
|
54
|
+
active ? "bg-emerald-500/15 text-emerald-300" : "text-gray-300 hover:bg-white/5";
|
|
55
|
+
|
|
56
|
+
/** A nested navigator entry (a state under a component, a token group under a section). */
|
|
57
|
+
export const navSubItem = (active: boolean) =>
|
|
58
|
+
active ? "bg-emerald-500/10 text-emerald-300" : "text-gray-400 hover:bg-white/5 hover:text-gray-200";
|
|
59
|
+
|
|
60
|
+
/** The common one-line row. Compose with `navItem`; two-line rows add their own padding. */
|
|
61
|
+
export const NAV_ITEM = "block w-full rounded px-2 py-1 text-left text-sm transition";
|
|
62
|
+
|
|
63
|
+
/** The common nested row. Compose with `navSubItem`. */
|
|
64
|
+
export const NAV_SUBITEM = "block w-full truncate rounded px-2 py-0.5 text-left text-xs transition";
|
|
65
|
+
|
|
66
|
+
/** The working pane. White, and the only thing that scrolls. */
|
|
67
|
+
export const PANE = "min-h-0 min-w-0 overflow-auto bg-white";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reading content inside the pane — a skill's instructions, a prompt block, anything
|
|
71
|
+
* whose job is to be READ. Capped at a readable measure and centred.
|
|
72
|
+
*/
|
|
73
|
+
export const DOC = "mx-auto max-w-3xl p-8";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Browsing content inside the pane — swatch grids, galleries, anything laid out across
|
|
77
|
+
* the full width. Same padding as DOC so the two feel like one application; no measure
|
|
78
|
+
* cap, because capping a gallery just wastes the screen.
|
|
79
|
+
*/
|
|
80
|
+
export const GALLERY = "p-8";
|
|
81
|
+
|
|
82
|
+
/** The empty state, before anything is selected. Same words shape everywhere: "Select a …". */
|
|
83
|
+
export const EMPTY = "flex h-full items-center justify-center text-sm text-gray-500";
|
package/screens/index.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* Consumed as SOURCE via a bundler alias, the same convention `@unoverse/sdk`
|
|
13
13
|
* already uses. No build step, no publish cycle, no stale dist.
|
|
14
14
|
*/
|
|
15
|
+
// The screen shell (navigator + working pane) every section is built from. Exported so a
|
|
16
|
+
// host's own screens can sit beside these without re-deriving the chrome by eye, which is
|
|
17
|
+
// exactly how the navigators drifted to four widths and two selection colours.
|
|
18
|
+
export * from "./chrome";
|
|
15
19
|
export { ComponentsView } from "./ComponentsView";
|
|
16
20
|
export { AtomsView } from "./AtomsView";
|
|
17
21
|
export { TemplatesView } from "./TemplatesView";
|
package/src/NodesView.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
11
11
|
import { authedFetch, usePersistentState, useRegistryToggles, ToggleSwitch } from "./host";
|
|
12
12
|
import { ConfigForm, defaultsFromSchema } from "@unoverse/studio";
|
|
13
13
|
import { NodeKeys, type CredentialType } from "./NodeKeys";
|
|
14
|
+
import { NAV, SCREEN_WIDE_NAV } from "../screens/chrome";
|
|
14
15
|
|
|
15
16
|
interface NodeInput {
|
|
16
17
|
name: string;
|
|
@@ -294,9 +295,9 @@ export function NodesView() {
|
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
return (
|
|
297
|
-
<div className=
|
|
298
|
+
<div className={SCREEN_WIDE_NAV}>
|
|
298
299
|
{/* PALETTE — Canvas-style searchable node cards */}
|
|
299
|
-
<nav className=
|
|
300
|
+
<nav className={NAV}>
|
|
300
301
|
<div className="relative mb-3">
|
|
301
302
|
<input
|
|
302
303
|
value={query}
|
|
@@ -30,8 +30,21 @@ manifest, no folders. Structure is EARNED; start flat.
|
|
|
30
30
|
## The state model (v2): data + a state tree
|
|
31
31
|
|
|
32
32
|
A component is **data plus a state tree**; each state owns the layout(s) that draw it.
|
|
33
|
-
The root's `state:` block IS the design, readable top to bottom.
|
|
34
|
-
|
|
33
|
+
The root's `state:` block IS the design, readable top to bottom.
|
|
34
|
+
|
|
35
|
+
> **THE GATE — answer this before adding ANY state to `view`:**
|
|
36
|
+
> *"Would a hosting template rearrange the page to show this?"*
|
|
37
|
+
> **No → it's a private step**: nest it under `on:` inside an existing public state.
|
|
38
|
+
> **Yes → public.**
|
|
39
|
+
> ⚠ "The user asked for N states" never means N public states. In-place swaps
|
|
40
|
+
> (confirmation, success, empty, error, loading, a form's next step) are almost always
|
|
41
|
+
> PRIVATE. Privacy is the default; promotion to `view` is a deliberate act with a
|
|
42
|
+
> template consequence.
|
|
43
|
+
|
|
44
|
+
The canonical shape (a product-card component). Note where each state landed and why:
|
|
45
|
+
`products` and `detail` are PUBLIC because a template genuinely rearranges for each (a
|
|
46
|
+
grid rail vs a full page); `detail`'s inner `detail`/`apply` are PRIVATE steps because
|
|
47
|
+
applying swaps content in place — the page does not move:
|
|
35
48
|
|
|
36
49
|
```yaml
|
|
37
50
|
state:
|
|
@@ -65,9 +78,13 @@ The rules, in the order they bite:
|
|
|
65
78
|
- **Public states are the top level of the `view` axis** and they are the template's
|
|
66
79
|
WHOLE vocabulary: everything templates can react to, nothing more. Steps nest
|
|
67
80
|
privately (`on: <key>` + `initial` + `states`), invisible outside the component.
|
|
68
|
-
The placement rule: **if the template must rearrange to show it,
|
|
69
|
-
if the template wouldn't move, nest it privately.** Privacy is the
|
|
70
|
-
promotion is deliberate.
|
|
81
|
+
The placement rule is THE GATE above: **if the template must rearrange to show it,
|
|
82
|
+
make it public; if the template wouldn't move, nest it privately.** Privacy is the
|
|
83
|
+
default; promotion is deliberate.
|
|
84
|
+
**The tell in Studio:** non-initial public states preview FULL-BLEED (no stage
|
|
85
|
+
padding), precisely because they are template-liftable surfaces. If a state you
|
|
86
|
+
meant as an in-place swap suddenly renders edge-to-edge, you promoted a private
|
|
87
|
+
step to `view` — move it into the nest.
|
|
71
88
|
- **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
|
|
72
89
|
draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
|
|
73
90
|
when the filename differs. A layout is never addressable on its own; only its owning
|
|
@@ -280,5 +297,7 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
|
|
|
280
297
|
4. Publish from Studio; its lint must report 0 errors (the rules are v2-aware and cite
|
|
281
298
|
the doc for every rule).
|
|
282
299
|
5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
|
|
283
|
-
public states on top, steps beneath by state name), then live.
|
|
284
|
-
|
|
300
|
+
public states on top, steps beneath by state name), then live. Sanity-check the
|
|
301
|
+
switcher against THE GATE: every entry in its top row is a promise that a template
|
|
302
|
+
will rearrange for it, and a non-initial public state previews full-bleed. Debug:
|
|
303
|
+
stream log → state inspector → definition; never guess.
|