@unoverse-platform/studio 0.1.46 → 0.1.48
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/bin/studio.mjs +11 -9
- package/local/authoring.ts +4 -3
- package/local/catalog.ts +3 -2
- package/local/design-home.d.mts +2 -0
- package/local/design-home.mjs +31 -0
- package/local/plugin.ts +29 -28
- package/local/scaffold.mjs +7 -6
- package/local/source.ts +4 -3
- package/package.json +2 -2
- package/screens/DesignSystemView.tsx +1 -1
- package/screens/TemplatesView.tsx +3 -3
- package/src/App.tsx +1 -1
- package/src/NewProject.tsx +2 -2
- package/src/PublishDialog.tsx +1 -1
- package/vendor/sdk/appEngine.tsx +1 -1
- package/vendor/sdk/lib/core/client.ts +1 -1
- package/vendor/sdk/lib/core/types.ts +9 -9
- package/vendor/sdk/lib/isolate.tsx +1 -1
- package/vendor/sdk/lib/primitives.tsx +7 -7
- package/vendor/sdk/lib/render.tsx +2 -2
- package/vendor/sdk/lib/style.ts +3 -3
- package/vendor/sdk/lib/template.tsx +2 -2
- package/vendor/sdk/lib/theme.ts +2 -2
- package/vendor/skills/unoverse-create/SKILL.md +6 -6
- package/vendor/skills/unoverse-create/references/brand.md +8 -8
- package/vendor/skills/unoverse-create/references/component.md +7 -7
- package/vendor/skills/unoverse-create/references/template.md +3 -3
- package/vendor/skills/unoverse-prompt-component/SKILL.md +4 -4
package/bin/studio.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { dirname, resolve, join, basename } from "path";
|
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
22
|
import { existsSync, readdirSync } from "fs";
|
|
23
23
|
import { createInterface } from "readline/promises";
|
|
24
|
+
import { designHome, hasDesignHome } from "../local/design-home.mjs";
|
|
24
25
|
import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
|
|
25
26
|
|
|
26
27
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -37,11 +38,12 @@ const appRoot = resolve(here, "..");
|
|
|
37
38
|
* instruction; the folder says where and its name says what. The only question left is the
|
|
38
39
|
* one it cannot answer for you: a folder whose name no project can carry.
|
|
39
40
|
*/
|
|
40
|
-
/** The same walk `catalog.ts` does: the nearest folder above us holding
|
|
41
|
+
/** The same walk `catalog.ts` does: the nearest folder above us holding a design tree
|
|
42
|
+
* (design/, or the legacy rx/). */
|
|
41
43
|
function findProjectRootFromCwd() {
|
|
42
44
|
for (let dir = process.cwd(); ; ) {
|
|
43
|
-
if (
|
|
44
|
-
if (
|
|
45
|
+
if (hasDesignHome(dir)) return dir;
|
|
46
|
+
if (hasDesignHome(join(dir, "apps/unoverse"))) return join(dir, "apps/unoverse");
|
|
45
47
|
const parent = resolve(dir, "..");
|
|
46
48
|
if (parent === dir) return process.cwd();
|
|
47
49
|
dir = parent;
|
|
@@ -64,7 +66,7 @@ function findProjectBelowCwd() {
|
|
|
64
66
|
const cwd = process.cwd();
|
|
65
67
|
const found = readdirSync(cwd, { withFileTypes: true })
|
|
66
68
|
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
|
|
67
|
-
.filter((e) =>
|
|
69
|
+
.filter((e) => hasDesignHome(join(cwd, e.name)))
|
|
68
70
|
.map((e) => e.name);
|
|
69
71
|
|
|
70
72
|
if (found.length === 1) return join(cwd, found[0]);
|
|
@@ -80,7 +82,7 @@ async function scaffoldIfEmpty() {
|
|
|
80
82
|
// The same walk findProjectRoot does. A project ABOVE us is a project; do not offer to
|
|
81
83
|
// make a second one inside it.
|
|
82
84
|
for (let dir = cwd; ; ) {
|
|
83
|
-
if (
|
|
85
|
+
if (hasDesignHome(dir) || hasDesignHome(join(dir, "apps/unoverse"))) return;
|
|
84
86
|
const parent = resolve(dir, "..");
|
|
85
87
|
if (parent === dir) break;
|
|
86
88
|
dir = parent;
|
|
@@ -122,7 +124,7 @@ async function scaffoldIfEmpty() {
|
|
|
122
124
|
// being named or why one was needed. The name is the folder under rx/ AND the org
|
|
123
125
|
// column on everything that project publishes, which is why the tool cannot pick it.
|
|
124
126
|
console.log(`\n Setting up Studio here.\n\n ${cwd}\n`);
|
|
125
|
-
console.log(` Your components live in
|
|
127
|
+
console.log(` Your components live in design/<project>/, and that name is the org they`);
|
|
126
128
|
console.log(` publish under. This folder is "${derived}", which cannot be one.\n`);
|
|
127
129
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
128
130
|
for (;;) {
|
|
@@ -140,7 +142,7 @@ async function scaffoldIfEmpty() {
|
|
|
140
142
|
// publishes, so a flat rx/ could hold exactly one project and would read `components`
|
|
141
143
|
// and `templates` as orgs.
|
|
142
144
|
createFirstProject(home, name);
|
|
143
|
-
console.log(`\n Created
|
|
145
|
+
console.log(`\n Created design/${name}/, prompts/ and nodes/ in ${cwd}\n`);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
await scaffoldIfEmpty();
|
|
@@ -148,7 +150,7 @@ await scaffoldIfEmpty();
|
|
|
148
150
|
/**
|
|
149
151
|
* POINT `base` AT THIS DEVELOPER'S PROJECT, before anything imports it.
|
|
150
152
|
*
|
|
151
|
-
* `paths.ts` snapshots its exports at import time (`export const
|
|
153
|
+
* `paths.ts` snapshots its exports at import time (`export const DESIGN_HOME = paths.rx`), so
|
|
152
154
|
* `setPaths()` called later updates an object nobody reads any more. The only hook that
|
|
153
155
|
* lands is the environment, read by `findHome()` on first import.
|
|
154
156
|
*
|
|
@@ -159,7 +161,7 @@ await scaffoldIfEmpty();
|
|
|
159
161
|
*/
|
|
160
162
|
const projectRoot = findProjectRootFromCwd();
|
|
161
163
|
process.env.UNOVERSE_HOME = projectRoot;
|
|
162
|
-
console.log(`\n Studio reading: ${
|
|
164
|
+
console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
|
|
163
165
|
|
|
164
166
|
const server = await createServer({
|
|
165
167
|
// The APP is the vite root; the developer's project is found by walking up from the
|
package/local/authoring.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* COMPLETION VOCABULARY for the source pane.
|
|
3
3
|
*
|
|
4
4
|
* THERE IS ONE LINTER, and it is not here. The rules live in
|
|
5
|
-
* `@unoverse-platform/base/src/lint/
|
|
6
|
-
* library an overlay (`lintDefinitions(
|
|
5
|
+
* `@unoverse-platform/base/src/lint/design/`, and the editor lints unsaved text by handing that
|
|
6
|
+
* library an overlay (`lintDefinitions(designRoot, { overlay })`), so the squiggles a developer
|
|
7
7
|
* sees are the same rules publish and CI apply. An approximation of those rules in the
|
|
8
8
|
* editor would be a second linter, which is exactly what the shared library exists to
|
|
9
9
|
* prevent. This file adds no rule and judges nothing.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
20
20
|
*/
|
|
21
|
+
import { designHome } from "./design-home.mjs";
|
|
21
22
|
import { resolve } from "path";
|
|
22
23
|
import { existsSync, readdirSync } from "fs";
|
|
23
24
|
|
|
@@ -88,7 +89,7 @@ export function vocabulary(
|
|
|
88
89
|
|
|
89
90
|
/** Atom names off disk, for completion of `Ref.ref`. */
|
|
90
91
|
export function atomNames(projectRoot: string): { name: string }[] {
|
|
91
|
-
const dirs = [resolve(projectRoot, "
|
|
92
|
+
const dirs = [resolve(designHome(projectRoot), "marketplace/atoms"), resolve(designHome(projectRoot), "atoms")];
|
|
92
93
|
const names = new Set<string>();
|
|
93
94
|
for (const d of dirs) {
|
|
94
95
|
if (!existsSync(d)) continue;
|
package/local/catalog.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* See docs/architecture/LOCAL_STUDIO.md.
|
|
12
12
|
*/
|
|
13
|
+
import { hasDesignHome } from "./design-home.mjs";
|
|
13
14
|
import { resolve } from "path";
|
|
14
15
|
import { existsSync } from "fs";
|
|
15
16
|
|
|
@@ -41,10 +42,10 @@ export function findProjectRoot(from: string = process.cwd()): string {
|
|
|
41
42
|
let dir = resolve(from);
|
|
42
43
|
for (;;) {
|
|
43
44
|
// A developer's project (starter kit): rx/ sits at the root.
|
|
44
|
-
if (
|
|
45
|
+
if (hasDesignHome(dir)) return dir;
|
|
45
46
|
// This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
|
|
46
47
|
// shape underneath (rx/ + prompts/), just nested, so return that as the root.
|
|
47
|
-
if (
|
|
48
|
+
if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
|
|
48
49
|
const parent = resolve(dir, "..");
|
|
49
50
|
if (parent === dir) break;
|
|
50
51
|
dir = parent;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The design folder for a root, renamed `rx` → `design` (2026-08-20).
|
|
2
|
+
//
|
|
3
|
+
// MIGRATE-ONCE, not dual-read: a root still holding the legacy `rx/` is renamed to
|
|
4
|
+
// `design/` on first resolution, and the platform knows ONE name from then on. The
|
|
5
|
+
// canonical rule lives in @unoverse-platform/base `paths.ts` (`designDir`); this copy
|
|
6
|
+
// exists because the plain-node entrypoints (bin/studio.mjs, scaffold.mjs) cannot
|
|
7
|
+
// import base's TypeScript source. Keep the two in step.
|
|
8
|
+
import { existsSync, renameSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
export function designHome(root) {
|
|
12
|
+
const design = join(root, "design");
|
|
13
|
+
const legacy = join(root, "rx");
|
|
14
|
+
if (!existsSync(design) && existsSync(legacy)) {
|
|
15
|
+
try {
|
|
16
|
+
renameSync(legacy, design);
|
|
17
|
+
console.warn(`[studio] migrated ${legacy} → ${design} (rx/ is now design/)`);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.warn(`[studio] could not rename ${legacy} → ${design} (${e.message}). Rename it by hand; Studio reads design/ only.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return design;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Does this folder hold a design tree under either name? DETECTION ONLY — the walk in
|
|
26
|
+
* bin/studio.mjs probes arbitrary ancestor folders, and probing must never rename a
|
|
27
|
+
* stranger's `rx/`. Migration happens in `designHome`, called only on the root a
|
|
28
|
+
* caller actually adopts. */
|
|
29
|
+
export function hasDesignHome(root) {
|
|
30
|
+
return existsSync(join(root, "design")) || existsSync(join(root, "rx"));
|
|
31
|
+
}
|
package/local/plugin.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* See docs/architecture/LOCAL_STUDIO.md.
|
|
17
17
|
*/
|
|
18
|
+
import { designHome } from "./design-home.mjs";
|
|
18
19
|
import { createRequire } from "node:module";
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
20
21
|
import { resolve } from "path";
|
|
@@ -51,13 +52,13 @@ function definitionHome(
|
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
54
|
if (/\/atoms\//.test(uri)) {
|
|
54
|
-
const flat = resolve(projectRoot, "
|
|
55
|
+
const flat = resolve(designHome(projectRoot), "atoms", `${ref.toLowerCase()}.json`);
|
|
55
56
|
return existsSync(flat) ? flat : null;
|
|
56
57
|
}
|
|
57
58
|
const folder = c.componentFolder(ref);
|
|
58
59
|
if (folder) return folder;
|
|
59
60
|
// A flat, folderless component: `rx/components/<name>.json` in either tier.
|
|
60
|
-
for (const dir of [resolve(projectRoot, "
|
|
61
|
+
for (const dir of [resolve(designHome(projectRoot), "marketplace/components"), resolve(designHome(projectRoot), "components")]) {
|
|
61
62
|
const flat = resolve(dir, `${ref.toLowerCase()}.json`);
|
|
62
63
|
if (existsSync(flat)) return flat;
|
|
63
64
|
}
|
|
@@ -195,7 +196,7 @@ export function localStudioHost(): Plugin {
|
|
|
195
196
|
let nodesOnce: Promise<LocalNodes | null> | null = null;
|
|
196
197
|
const nodes = () => (nodesOnce ??= loadLocalNodes(baseSrc, ssr, projectRoot));
|
|
197
198
|
|
|
198
|
-
server.config.logger.info(`\n Studio reading: ${
|
|
199
|
+
server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
202
|
* WATCH THE PROJECT EXPLICITLY. Vite watches its own root, which is this package,
|
|
@@ -208,7 +209,7 @@ export function localStudioHost(): Plugin {
|
|
|
208
209
|
*
|
|
209
210
|
* Adding the paths puts them in the same watcher, so the existing hook fires.
|
|
210
211
|
*/
|
|
211
|
-
for (const dir of [
|
|
212
|
+
for (const dir of [designHome(projectRoot), resolve(projectRoot, "prompts")]) {
|
|
212
213
|
const full = resolve(projectRoot, dir);
|
|
213
214
|
if (existsSync(full)) server.watcher.add(full);
|
|
214
215
|
}
|
|
@@ -242,7 +243,7 @@ export function localStudioHost(): Plugin {
|
|
|
242
243
|
case path === "/local/info":
|
|
243
244
|
return json(res, {
|
|
244
245
|
projectRoot,
|
|
245
|
-
rx:
|
|
246
|
+
rx: designHome(projectRoot),
|
|
246
247
|
nodes: resolve(projectRoot, "nodes"),
|
|
247
248
|
envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
|
|
248
249
|
});
|
|
@@ -276,20 +277,20 @@ export function localStudioHost(): Plugin {
|
|
|
276
277
|
// mirrors the module's real exports — tsc can't follow an ssrLoadModule path.
|
|
277
278
|
type PublishItem = { kind: string; name: string; why?: string; mode?: string };
|
|
278
279
|
type PublishModule = {
|
|
279
|
-
lintForPublish(
|
|
280
|
-
collectProject(
|
|
280
|
+
lintForPublish(designRoot: string, project: string): Promise<{ problems: unknown[]; errors: unknown[] }>;
|
|
281
|
+
collectProject(designRoot: string, project: string): unknown;
|
|
281
282
|
planPublish(collected: unknown, universe: string, token: string): Promise<Record<string, PublishItem[]>>;
|
|
282
283
|
sendPublish(plan: unknown, universe: string, token: string): Promise<{ sent: PublishItem[]; failed?: PublishItem[] }>;
|
|
283
284
|
};
|
|
284
285
|
const items = (await ssr(`${baseSrc}/items/publish.ts`)) as PublishModule;
|
|
285
|
-
const
|
|
286
|
+
const designRoot = designHome(projectRoot);
|
|
286
287
|
|
|
287
288
|
// LINT FIRST, ALWAYS. Nothing leaves the machine until the rules pass: a
|
|
288
289
|
// developer should learn about a raw hex value here, not from an HTTP error.
|
|
289
|
-
const { problems, errors } = await items.lintForPublish(
|
|
290
|
+
const { problems, errors } = await items.lintForPublish(designRoot, project);
|
|
290
291
|
if (errors.length) return json(res, { blocked: true, problems, errors });
|
|
291
292
|
|
|
292
|
-
const collected = items.collectProject(
|
|
293
|
+
const collected = items.collectProject(designRoot, project);
|
|
293
294
|
const plan = await items.planPublish(collected, universe, token);
|
|
294
295
|
|
|
295
296
|
// The screen renders kinds and names. The plan carries every file's full
|
|
@@ -424,8 +425,8 @@ export function localStudioHost(): Plugin {
|
|
|
424
425
|
const p = String(b.path ?? "");
|
|
425
426
|
if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
|
|
426
427
|
|
|
427
|
-
type LintModule = { lintDefinitions(
|
|
428
|
-
const lintMod = (await ssr(`${baseSrc}/lint/
|
|
428
|
+
type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
|
|
429
|
+
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
429
430
|
|
|
430
431
|
// Marked BEFORE the write, so the filesystem event can never beat the mark.
|
|
431
432
|
recentSelfWrites.set(resolve(p), Date.now());
|
|
@@ -434,7 +435,7 @@ export function localStudioHost(): Plugin {
|
|
|
434
435
|
b.text as string,
|
|
435
436
|
writableRoots(projectRoot, c),
|
|
436
437
|
projectRoot,
|
|
437
|
-
(
|
|
438
|
+
(designRoot) => lintMod.lintDefinitions(designRoot),
|
|
438
439
|
);
|
|
439
440
|
return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
|
|
440
441
|
}
|
|
@@ -454,13 +455,13 @@ export function localStudioHost(): Plugin {
|
|
|
454
455
|
|
|
455
456
|
type LintModule = {
|
|
456
457
|
lintDefinitions(
|
|
457
|
-
|
|
458
|
+
designRoot: string,
|
|
458
459
|
options?: { overlay?: Record<string, string> },
|
|
459
460
|
): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
|
|
460
461
|
};
|
|
461
|
-
const lintMod = (await ssr(`${baseSrc}/lint/
|
|
462
|
+
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
462
463
|
const abs = resolve(p);
|
|
463
|
-
const { problems } = lintMod.lintDefinitions(
|
|
464
|
+
const { problems } = lintMod.lintDefinitions(designHome(projectRoot), { overlay: { [abs]: b.text as string } });
|
|
464
465
|
return json(res, {
|
|
465
466
|
findings: problems.filter((f) => f.file && resolve(f.file) === abs),
|
|
466
467
|
elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
|
|
@@ -472,7 +473,7 @@ export function localStudioHost(): Plugin {
|
|
|
472
473
|
* linter enforces, plus this project's own token, atom and component names.
|
|
473
474
|
*/
|
|
474
475
|
case path === "/local/vocabulary": {
|
|
475
|
-
const vocabMod = (await ssr(`${baseSrc}/lint/
|
|
476
|
+
const vocabMod = (await ssr(`${baseSrc}/lint/design/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
|
|
476
477
|
const theme = c.resolveTheme(url.searchParams.get("theme") ?? "light");
|
|
477
478
|
return json(
|
|
478
479
|
res,
|
|
@@ -517,21 +518,21 @@ export function localStudioHost(): Plugin {
|
|
|
517
518
|
},
|
|
518
519
|
readSource: (p) => readSource(p, writableRoots(projectRoot, c), projectRoot),
|
|
519
520
|
saveSource: async (p, text) => {
|
|
520
|
-
type LintModule = { lintDefinitions(
|
|
521
|
-
const lintMod = (await ssr(`${baseSrc}/lint/
|
|
521
|
+
type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
|
|
522
|
+
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
522
523
|
recentSelfWrites.set(resolve(p), Date.now());
|
|
523
|
-
return saveSource(p, text, writableRoots(projectRoot, c), projectRoot, (
|
|
524
|
-
lintMod.lintDefinitions(
|
|
524
|
+
return saveSource(p, text, writableRoots(projectRoot, c), projectRoot, (designRoot) =>
|
|
525
|
+
lintMod.lintDefinitions(designRoot),
|
|
525
526
|
);
|
|
526
527
|
},
|
|
527
528
|
lintProject: async () => {
|
|
528
|
-
type LintModule = { lintDefinitions(
|
|
529
|
-
const lintMod = (await ssr(`${baseSrc}/lint/
|
|
530
|
-
return lintMod.lintDefinitions(
|
|
529
|
+
type LintModule = { lintDefinitions(designRoot: string): { problems: unknown[] } };
|
|
530
|
+
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
531
|
+
return lintMod.lintDefinitions(designHome(projectRoot));
|
|
531
532
|
},
|
|
532
533
|
selection: () => selection,
|
|
533
534
|
vocabulary: async () => {
|
|
534
|
-
const vocabMod = (await ssr(`${baseSrc}/lint/
|
|
535
|
+
const vocabMod = (await ssr(`${baseSrc}/lint/design/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
|
|
535
536
|
return vocabulary(vocabMod, c.resolveTheme("light"), atomNames(projectRoot), c.listDefinitions("component") as { name?: string }[]);
|
|
536
537
|
},
|
|
537
538
|
},
|
|
@@ -632,7 +633,7 @@ export function localStudioHost(): Plugin {
|
|
|
632
633
|
* else, which means the developer's other editor, still reloads.
|
|
633
634
|
*/
|
|
634
635
|
handleHotUpdate({ server, file }) {
|
|
635
|
-
const watched = [
|
|
636
|
+
const watched = [designHome(projectRoot), resolve(projectRoot, "prompts")];
|
|
636
637
|
if (!watched.some((w) => file.startsWith(w))) return;
|
|
637
638
|
const self = recentSelfWrites.get(resolve(file));
|
|
638
639
|
if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
|
|
@@ -642,10 +643,10 @@ export function localStudioHost(): Plugin {
|
|
|
642
643
|
|
|
643
644
|
configResolved() {
|
|
644
645
|
// Surface a clear failure at startup rather than an empty catalog later.
|
|
645
|
-
if (projectRoot && !existsSync(
|
|
646
|
+
if (projectRoot && !existsSync(designHome(projectRoot))) {
|
|
646
647
|
throw new Error(`No rx/ folder at ${projectRoot}`);
|
|
647
648
|
}
|
|
648
|
-
if (projectRoot) statSync(
|
|
649
|
+
if (projectRoot) statSync(designHome(projectRoot));
|
|
649
650
|
},
|
|
650
651
|
};
|
|
651
652
|
}
|
package/local/scaffold.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creating a project, from either end.
|
|
3
3
|
*
|
|
4
|
-
* A project (an "org"
|
|
4
|
+
* A project (an "org") is `design/<name>/` holding components, styles and
|
|
5
5
|
* templates. It is the unit a developer authors in and the unit that publishes: every row
|
|
6
6
|
* that lands in a universe carries its name in the `org` column.
|
|
7
7
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See docs/architecture/LOCAL_STUDIO.md.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync } from "node:fs";
|
|
17
|
+
import { designHome } from "./design-home.mjs";
|
|
17
18
|
import { join, dirname, resolve } from "node:path";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
|
|
@@ -27,7 +28,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
28
|
export function checkProjectName(name) {
|
|
28
29
|
const n = String(name ?? "").trim();
|
|
29
30
|
if (!n) return "A name is required";
|
|
30
|
-
// RESERVED FIRST, so the message is the true reason. These folders under
|
|
31
|
+
// RESERVED FIRST, so the message is the true reason. These folders under design/ are never a
|
|
31
32
|
// developer's own project (collect.ts NOT_A_PROJECT), and a project named `marketplace`
|
|
32
33
|
// would shadow the design system in every loader that resolves one. Checked before the
|
|
33
34
|
// format rule because `_schema` fails that too, and "use lowercase letters" would send a
|
|
@@ -56,7 +57,7 @@ const WELCOME = [
|
|
|
56
57
|
].join("\n");
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Create `
|
|
60
|
+
* Create `design/<name>/` under `home`, with its three authoring folders and one component.
|
|
60
61
|
*
|
|
61
62
|
* Refuses an existing name rather than merging into it: "create" that silently adopts a
|
|
62
63
|
* folder is how a developer ends up publishing someone else's work under their own project.
|
|
@@ -65,8 +66,8 @@ export function createProject(home, name) {
|
|
|
65
66
|
const bad = checkProjectName(name);
|
|
66
67
|
if (bad) throw new Error(bad);
|
|
67
68
|
|
|
68
|
-
const root = join(home,
|
|
69
|
-
if (existsSync(root)) throw new Error(
|
|
69
|
+
const root = join(designHome(home), name);
|
|
70
|
+
if (existsSync(root)) throw new Error(`${name} already exists in this workspace`);
|
|
70
71
|
|
|
71
72
|
for (const d of ["components/welcome", "templates", "styles"]) mkdirSync(join(root, d), { recursive: true });
|
|
72
73
|
writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
|
|
@@ -74,7 +75,7 @@ export function createProject(home, name) {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* The FIRST project, plus the folders that live beside
|
|
78
|
+
* The FIRST project, plus the folders that live beside the design tree rather than inside it.
|
|
78
79
|
*
|
|
79
80
|
* Skills, prompt blocks and nodes are per-developer and not per-project, so they are made
|
|
80
81
|
* once here and never by `createProject`.
|
package/local/source.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
24
24
|
*/
|
|
25
|
+
import { designHome } from "./design-home.mjs";
|
|
25
26
|
import { resolve, sep, relative } from "path";
|
|
26
27
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
27
28
|
|
|
@@ -163,7 +164,7 @@ export async function saveSource(
|
|
|
163
164
|
text: string,
|
|
164
165
|
roots: string[],
|
|
165
166
|
projectRoot: string,
|
|
166
|
-
lint: (
|
|
167
|
+
lint: (designRoot: string) => Promise<{ problems: LintFinding[] }> | { problems: LintFinding[] },
|
|
167
168
|
): Promise<SaveResult> {
|
|
168
169
|
const abs = resolve(path);
|
|
169
170
|
const w = writability(abs, roots, projectRoot);
|
|
@@ -183,10 +184,10 @@ export async function saveSource(
|
|
|
183
184
|
|
|
184
185
|
writeFileSync(abs, text, "utf8");
|
|
185
186
|
|
|
186
|
-
const
|
|
187
|
+
const designRoot = designHome(projectRoot);
|
|
187
188
|
let problems: LintFinding[] = [];
|
|
188
189
|
try {
|
|
189
|
-
problems = (await lint(
|
|
190
|
+
problems = (await lint(designRoot)).problems ?? [];
|
|
190
191
|
} catch (e) {
|
|
191
192
|
// A linter that throws must not silently accept the write.
|
|
192
193
|
if (previous === null) rmSync(abs, { force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@codemirror/view": "^6.37.1",
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
28
28
|
"@tailwindcss/vite": "^4.0.0",
|
|
29
|
-
"@unoverse-platform/base": "^0.3.
|
|
29
|
+
"@unoverse-platform/base": "^0.3.36",
|
|
30
30
|
"@unoverse-platform/marketplace": "*",
|
|
31
31
|
"@vitejs/plugin-react": "^4.3.4",
|
|
32
32
|
"markdown-to-jsx": "^7.7.17",
|
|
@@ -321,7 +321,7 @@ export function DesignSystemView({
|
|
|
321
321
|
{foundation ? (
|
|
322
322
|
<>Tokens with an <OverrideMark /> are <b style={{ color: ink }}>{org}</b>’s own; the rest are inherited from the design-system foundation.</>
|
|
323
323
|
) : (
|
|
324
|
-
<>Live tokens from the design-system foundation (<code>
|
|
324
|
+
<>Live tokens from the design-system foundation (<code>design/marketplace/styles</code>). Toggle the theme to restyle everything.</>
|
|
325
325
|
)}
|
|
326
326
|
</p>
|
|
327
327
|
{current.body}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Gavin first. No exceptions: not bug fixes, not one-liners, not reverts.
|
|
8
8
|
* State the intended diff and wait for an explicit yes.
|
|
9
9
|
*
|
|
10
|
-
* Components and templates are DATA (
|
|
10
|
+
* Components and templates are DATA (design/). If a template seems to need code
|
|
11
11
|
* here, the answer is almost always to express it as data — or to ask.
|
|
12
12
|
* ============================================================================
|
|
13
13
|
*
|
|
@@ -532,7 +532,7 @@ export function TemplatesView({
|
|
|
532
532
|
const seedSet = (tmpls: Def[], comps: Def[], sel: string | null): Def[] => {
|
|
533
533
|
const t = tmpls.find((t) => t.name === sel);
|
|
534
534
|
// The declaration lives in the app manifest (app.previewComponents); the def-level
|
|
535
|
-
// field is the legacy envelope home. Names are case-insensitive like every
|
|
535
|
+
// field is the legacy envelope home. Names are case-insensitive like every design name,
|
|
536
536
|
// and each ENTRY seeds one instance — a repeated name previews several (a card rail).
|
|
537
537
|
const pc = t?.app?.previewComponents ?? t?.previewComponents ?? [];
|
|
538
538
|
return pc
|
|
@@ -657,7 +657,7 @@ export function TemplatesView({
|
|
|
657
657
|
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#2c2c2e] p-2">
|
|
658
658
|
<div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Apps</div>
|
|
659
659
|
{templates.length === 0 && (
|
|
660
|
-
<div className="p-2 text-xs text-gray-400">No apps yet — add one under
|
|
660
|
+
<div className="p-2 text-xs text-gray-400">No apps yet — add one under design/<project>/templates/.</div>
|
|
661
661
|
)}
|
|
662
662
|
{templates.map((t) => (
|
|
663
663
|
<div key={`${t.org ?? ""}/${t.name}`}>
|
package/src/App.tsx
CHANGED
|
@@ -81,7 +81,7 @@ function Shell({
|
|
|
81
81
|
const [foundation, setFoundation] = useState<ResolvedTheme | null>(null);
|
|
82
82
|
const [error, setError] = useState<string | null>(null);
|
|
83
83
|
|
|
84
|
-
// The projects in this developer's
|
|
84
|
+
// The projects in this developer's design/ folder. Flat, per RX_ORG_MODEL. Re-read after
|
|
85
85
|
// creating one, so a new project appears without a reload.
|
|
86
86
|
const loadProjects = () =>
|
|
87
87
|
fetch("/dev/orgs")
|
package/src/NewProject.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Creating a project from inside Studio.
|
|
3
3
|
*
|
|
4
|
-
* A project is `
|
|
4
|
+
* A project is `design/<name>/`: the folder a developer authors in, and the unit that
|
|
5
5
|
* publishes. Until this existed, Studio could create your FIRST one (bin/studio.mjs, at
|
|
6
6
|
* launch, only when the folder was empty) and never your second. The dropdown listed what
|
|
7
7
|
* was on disk and offered no way to add to it, so the answer to "start another project"
|
|
@@ -51,7 +51,7 @@ export function NewProject({
|
|
|
51
51
|
>
|
|
52
52
|
<h2 className="text-base font-semibold text-gray-100">New project</h2>
|
|
53
53
|
<p className="mt-1 text-xs text-gray-500">
|
|
54
|
-
Creates
|
|
54
|
+
Creates design/<name>/ with components, styles and templates, and one component to
|
|
55
55
|
open on.
|
|
56
56
|
</p>
|
|
57
57
|
|
package/src/PublishDialog.tsx
CHANGED
|
@@ -267,7 +267,7 @@ export function PublishDialog({
|
|
|
267
267
|
.map((p, i) => (
|
|
268
268
|
<li key={i} className="leading-relaxed">
|
|
269
269
|
<span className="text-gray-500">
|
|
270
|
-
{p.file?.replace(/^.*\/
|
|
270
|
+
{p.file?.replace(/^.*\/design\//, "")}
|
|
271
271
|
{p.line ? `:${p.line}` : ""}
|
|
272
272
|
</span>
|
|
273
273
|
<br />
|
package/vendor/sdk/appEngine.tsx
CHANGED
|
@@ -68,7 +68,7 @@ export function AppEngine(cfg: AppConfig) {
|
|
|
68
68
|
setBoundFor(templateId);
|
|
69
69
|
setBinding({});
|
|
70
70
|
}
|
|
71
|
-
// STATE-OWNED WIDTH (
|
|
71
|
+
// STATE-OWNED WIDTH (design data): the layout root's `appWidth` = the core surface's
|
|
72
72
|
// constant width; an occupied panel slot's `appWidth` adds to it (computeAppWidth).
|
|
73
73
|
// No declared width → the host keeps its own default. There is no other path.
|
|
74
74
|
// THE WHOLE DEFINITION, not a chosen four fields. This used to keep `{root, org, layouts,
|
|
@@ -163,7 +163,7 @@ export class UnoverseClient {
|
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
165
|
* resources/read the RESOLVED theme by name (e.g. "light"). The server owns the
|
|
166
|
-
* values (
|
|
166
|
+
* values (design/styles); the SDK only fetches — it bundles no tokens. Cached.
|
|
167
167
|
*/
|
|
168
168
|
async readTheme(name = "light"): Promise<ResolvedTheme> {
|
|
169
169
|
const cached = this.themeCache.get(name);
|
|
@@ -42,7 +42,7 @@ export interface UnoverseNode {
|
|
|
42
42
|
* content grows (new turn or streaming text), unless the user has scrolled up. Generic
|
|
43
43
|
* scroll-container behavior, opt-in via data (e.g. the conversation state's scroll box). */
|
|
44
44
|
autoScroll?: boolean;
|
|
45
|
-
/** Box: a sticky bar (its own `position: sticky` + `top` in
|
|
45
|
+
/** Box: a sticky bar (its own `position: sticky` + `top` in the design definition) that stays HIDDEN while
|
|
46
46
|
* the element before it (its previous sibling — e.g. a hero image) is on screen, then
|
|
47
47
|
* fades in once that sibling scrolls away. Overlays the content (no flow gap). Generic
|
|
48
48
|
* scroll-reveal behavior, opt-in via data. */
|
|
@@ -203,16 +203,16 @@ export type StyleVariant = Condition & { apply: Record<string, unknown> };
|
|
|
203
203
|
// ---- The resolved theme (SHAPE only — values live on the server) ----
|
|
204
204
|
//
|
|
205
205
|
// The MCP server OWNS THE VISUAL: it resolves the design tokens (apps/unoverse/
|
|
206
|
-
//
|
|
206
|
+
// design/styles) live and serves this shape at `unoverse://theme/{name}`. This is the
|
|
207
207
|
// token SHAPE, never the token VALUES — the SDK fetches the values (readTheme),
|
|
208
|
-
// it does NOT bundle them. A brand change in
|
|
208
|
+
// it does NOT bundle them. A brand change in design/styles is therefore refresh-only,
|
|
209
209
|
// exactly like a definition change (UNOVERSE_SPEC §2d-1).
|
|
210
210
|
export interface ResolvedTheme {
|
|
211
211
|
color: Record<string, string>;
|
|
212
212
|
/** Webfont stylesheet URLs (served `fonts.stylesheets` data) — injected into
|
|
213
213
|
* document.head by the SDK (shadow roots can't register @font-face). */
|
|
214
214
|
fonts?: string[];
|
|
215
|
-
/** Dimension scale (theme.space ←
|
|
215
|
+
/** Dimension scale (theme.space ← design/styles/base/spacing.json) — used for BOTH spacing
|
|
216
216
|
* (gap/padding/margin) AND element sizes (width/height), Tailwind-style. */
|
|
217
217
|
space: Record<string, string>;
|
|
218
218
|
radius: Record<string, string>;
|
|
@@ -227,17 +227,17 @@ export interface ResolvedTheme {
|
|
|
227
227
|
text: Record<string, Record<string, string | number>>;
|
|
228
228
|
skeleton: Record<string, unknown>;
|
|
229
229
|
prose: Record<string, unknown>;
|
|
230
|
-
/** STANDARD APP SIZES (theme.appSize ←
|
|
230
|
+
/** STANDARD APP SIZES (theme.appSize ← design/styles/semantic/app-sizes.json) — the named
|
|
231
231
|
* width blocks `appWidth` references ("chat"/"rail"/"panel" → raw host-facing CSS).
|
|
232
232
|
* Resolved by the SDK like every other token — one pipeline. */
|
|
233
233
|
appSize?: Record<string, string>;
|
|
234
|
-
/** Grid behaviour (theme.grid ←
|
|
234
|
+
/** Grid behaviour (theme.grid ← design/styles/semantic/grid) — `stackBelow`, the width
|
|
235
235
|
* at which a columns grid stacks. Served: the SDK holds no threshold of its own. */
|
|
236
236
|
grid?: Record<string, string>;
|
|
237
|
-
/** PAGE WIDTHS by name (theme.layout ←
|
|
237
|
+
/** PAGE WIDTHS by name (theme.layout ← design/styles/semantic/layout) — resolved by `dim()`
|
|
238
238
|
* alongside the space scale, so `maxWidth: "reading"` works like any dimension. */
|
|
239
239
|
layout?: Record<string, string>;
|
|
240
|
-
/** Named keyframes (theme.keyframes ←
|
|
240
|
+
/** Named keyframes (theme.keyframes ← design/styles/semantic/keyframes.json), injected
|
|
241
241
|
* once per render root. Lets any definition animate via `style.animation` — no SDK edit. */
|
|
242
242
|
keyframes: Record<string, Record<string, Record<string, string>>>;
|
|
243
243
|
/** Named icon glyphs as SERVED DATA. The SERVER sources these from an icon pack
|
|
@@ -245,7 +245,7 @@ export interface ResolvedTheme {
|
|
|
245
245
|
* elements `[tag, attrs]`. The generic `Icon` leaf renders them, sized/coloured by
|
|
246
246
|
* node.style. The SDK never bundles the pack — icons are served like any token. */
|
|
247
247
|
icons: Record<string, { viewBox?: string; attrs?: Record<string, unknown>; children?: [string, Record<string, unknown>][] }>;
|
|
248
|
-
/** Base render-root settings (theme.root ←
|
|
248
|
+
/** Base render-root settings (theme.root ← design/styles/semantic/root.json) — a CSS
|
|
249
249
|
* object the SDK spreads onto its outermost wrapper (e.g. font-smoothing). Served,
|
|
250
250
|
* not authored: a whole-tree rendering normalization, like legacy styles/base.css. */
|
|
251
251
|
root: Record<string, unknown>;
|
|
@@ -31,7 +31,7 @@ const RESET_CSS = `
|
|
|
31
31
|
font: inherit; letter-spacing: inherit; color: inherit; margin: 0;
|
|
32
32
|
}
|
|
33
33
|
/* Scrollbars hidden by default across every scroll container — scrolling still works,
|
|
34
|
-
the bar is just not painted. Generic chrome that
|
|
34
|
+
the bar is just not painted. Generic chrome that design definitions can't author per-node (pseudo-
|
|
35
35
|
elements), so it lives with the structural reset. */
|
|
36
36
|
.uno-isolate-root * { scrollbar-width: none; }
|
|
37
37
|
.uno-isolate-root *::-webkit-scrollbar { width: 0; height: 0; display: none; }
|
|
@@ -247,7 +247,7 @@ function useTextSelection(enabled: boolean) {
|
|
|
247
247
|
* (childList change) OR streaming text (a child resizing) — but only while the user is
|
|
248
248
|
* already near the bottom, so scrolling up to read history is never yanked back down.
|
|
249
249
|
* NOT chat-specific: any `overflow:auto` Box can opt in. The SDK owns behaviour, never
|
|
250
|
-
* styles — the scroll container itself is defined in `
|
|
250
|
+
* styles — the scroll container itself is defined in `design/` (the conversation state box).
|
|
251
251
|
*/
|
|
252
252
|
function useAutoScrollToEnd(enabled: boolean) {
|
|
253
253
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
@@ -281,11 +281,11 @@ function useAutoScrollToEnd(enabled: boolean) {
|
|
|
281
281
|
/**
|
|
282
282
|
* Generic "sticky bar that reveals only once you've scrolled past the element before it"
|
|
283
283
|
* (opt-in via `node.revealOnStick`). Pins the bar at the top (its own `position: sticky`
|
|
284
|
-
* + `top` in `
|
|
284
|
+
* + `top` in `design/`) and, using an IntersectionObserver on the bar's PREVIOUS sibling as
|
|
285
285
|
* the sentinel (e.g. a hero image), keeps it hidden while that sibling is on screen and
|
|
286
286
|
* fades it in once the sibling scrolls away. A negative margin removes the bar's flow
|
|
287
287
|
* footprint so it OVERLAYS the content (no gap at the top). NOT card-specific: any sticky
|
|
288
|
-
* bar can opt in. The SDK owns behaviour, never styles — the bar's look lives in `
|
|
288
|
+
* bar can opt in. The SDK owns behaviour, never styles — the bar's look lives in `design/`.
|
|
289
289
|
*/
|
|
290
290
|
function useRevealWhenStuck(enabled: boolean) {
|
|
291
291
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
@@ -415,7 +415,7 @@ export function IconView({ name, style, theme }: { name?: string; style?: Record
|
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
417
|
* `Skeleton` — built-in loading placeholder. ALL dimensions are DATA, fetched
|
|
418
|
-
* from the theme (theme.skeleton ←
|
|
418
|
+
* from the theme (theme.skeleton ← design/styles/semantic/skeleton.json). The SDK
|
|
419
419
|
* authors nothing here but structure (flex/full). `bars` are 'width height' pairs.
|
|
420
420
|
*/
|
|
421
421
|
export function SkeletonView({ variant = "text", theme }: { variant?: string; theme: ResolvedTheme }): ReactNode {
|
|
@@ -680,9 +680,9 @@ export function InputView({ node, data, theme, onAction }: { node: UnoverseNode;
|
|
|
680
680
|
* markdown-to-jsx overrides for the `Markdown` primitive.
|
|
681
681
|
*
|
|
682
682
|
* ⛔ AUTHORS ZERO STYLE VALUES. Every element style is read verbatim from the
|
|
683
|
-
* SERVED prose recipe (`theme.prose` ←
|
|
683
|
+
* SERVED prose recipe (`theme.prose` ← design/styles/semantic/prose.json). The SDK
|
|
684
684
|
* only maps element → recipe key and supplies BEHAVIOUR (target=_blank, image
|
|
685
|
-
* error-fallback). No hex, no px/rem, no weights here — they live in
|
|
685
|
+
* error-fallback). No hex, no px/rem, no weights here — they live in design/.
|
|
686
686
|
*
|
|
687
687
|
* Built once per theme (stable component identities so streaming re-renders don't
|
|
688
688
|
* remount images and drop their broken-state).
|
|
@@ -734,7 +734,7 @@ function buildMarkdownOverrides(theme: ResolvedTheme, inheritLinkColor = false)
|
|
|
734
734
|
// INLINE MARKS and BLOCKS. Authored copy uses all of these (a bolded access code, a
|
|
735
735
|
// rule between sections, a quoted term), and an element absent from this map renders
|
|
736
736
|
// at the browser's default — the one place a definition's look escapes the token
|
|
737
|
-
// layer. Each maps to a served recipe key; the VALUES live in
|
|
737
|
+
// layer. Each maps to a served recipe key; the VALUES live in design/styles/semantic/prose.
|
|
738
738
|
strong: { props: { style: s("strong") } },
|
|
739
739
|
em: { props: { style: s("emphasis") } },
|
|
740
740
|
del: { props: { style: s("strikethrough") } },
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `Button` (and its styling logic) kept leaking back into the dispatcher.
|
|
13
13
|
*
|
|
14
14
|
* THE FRAMEWORK IN ONE SENTENCE: this SDK is a FIXED, GENERIC interpreter. ALL
|
|
15
|
-
* UX is built in DATA (
|
|
15
|
+
* UX is built in DATA (design/ definitions + the served theme) — an author creates
|
|
16
16
|
* ANY interface WITHOUT editing this SDK. (See FRAMEWORK.md.)
|
|
17
17
|
*
|
|
18
18
|
* WHERE THINGS LIVE:
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* Need something the vocab can't express? Prefer extending the generic interpreter
|
|
32
32
|
* ONCE (a new style-vocab key in ./style, a new model projection) over a primitive.
|
|
33
|
-
* You almost never need to touch this file — build it in
|
|
33
|
+
* You almost never need to touch this file — build it in design/ first.
|
|
34
34
|
* ════════════════════════════════════════════════════════════════════════════
|
|
35
35
|
*/
|
|
36
36
|
import type { ReactNode } from "react";
|
package/vendor/sdk/lib/style.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The style + animation INTERPRETER — neutral vocab → CSS, resolving token NAMES
|
|
3
3
|
* against a theme FETCHED from the server. ⛔ OWNS ZERO STYLE VALUES (no hex, no
|
|
4
|
-
* px/rem/em, no recipes); every value lives in
|
|
4
|
+
* px/rem/em, no recipes); every value lives in design/styles and is served. See
|
|
5
5
|
* FRAMEWORK.md and render.tsx's header. Enforced by test/golden-rule.test.mjs.
|
|
6
6
|
*/
|
|
7
7
|
import type { CSSProperties } from "react";
|
|
@@ -227,10 +227,10 @@ export function cssDecls(css: CSSProperties, important = false): string {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
* Serialize the SERVED keyframes (theme.keyframes ←
|
|
230
|
+
* Serialize the SERVED keyframes (theme.keyframes ← design/styles/semantic/keyframes.json)
|
|
231
231
|
* into a CSS string, injected ONCE per render root (<style>). This authors zero values —
|
|
232
232
|
* it only structures DATA into `@keyframes uno-<name>{ <stop>{ <prop>:<val> } }`. Adding a
|
|
233
|
-
* new animation = adding a keyframe to
|
|
233
|
+
* new animation = adding a keyframe to design/ + a `style.animation` ref in a definition. No
|
|
234
234
|
* SDK edit, no new primitive. Keyframe decls use real CSS prop names, KEBAB-CASE
|
|
235
235
|
* (`box-shadow`, never `boxShadow`) — that is the canonical authored form, lint-enforced.
|
|
236
236
|
* Property keys are still kebab-normalized here, exactly as Studio's gallery serializer
|
|
@@ -179,7 +179,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
|
|
|
179
179
|
* THE LATCH, PUBLISHED FOR CHROME (docs/MCP_COMPLETE_GUIDE.md §The Component Latch).
|
|
180
180
|
*
|
|
181
181
|
* `resolveLatch` derives WHICH instance the conversation addresses; the composer's pill
|
|
182
|
-
* is ordinary
|
|
182
|
+
* is ordinary design chrome, so it needs those facts as template state to bind against. Same
|
|
183
183
|
* shape as the voice service (§4 "services feed the state"): a producer writes derived
|
|
184
184
|
* values into the normal bucket, and `core` learns no UI concept — nothing here knows a
|
|
185
185
|
* pill exists.
|
|
@@ -328,7 +328,7 @@ export function StreamedUnoverseTemplate({ client, store, uri, onAction, theme:
|
|
|
328
328
|
|
|
329
329
|
// Timeline — GENERIC iterator. Walks the timeline and renders the `user` / `assistant`
|
|
330
330
|
// DATA sub-tree per turn. Zero chat UX in the SDK: bubbles, alignment, structure all live
|
|
331
|
-
// in those sub-trees (
|
|
331
|
+
// in those sub-trees (design/templates/chatlayout/*.json). The assistant sub-tree's ComponentSlot
|
|
332
332
|
// is scoped to THAT turn's components.
|
|
333
333
|
const resolveTimeline = (node: UnoverseNode, key?: React.Key): ReactNode => {
|
|
334
334
|
return (
|
package/vendor/sdk/lib/theme.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* useUnoverseTheme — fetch the RESOLVED theme from the MCP server by name.
|
|
3
3
|
*
|
|
4
4
|
* The SDK owns ZERO token values. The server owns the visual (apps/unoverse/
|
|
5
|
-
*
|
|
6
|
-
* fetches it (cached in the client). A brand change in
|
|
5
|
+
* design/styles, resolved live) and serves it at `unoverse://theme/{name}`; this hook
|
|
6
|
+
* fetches it (cached in the client). A brand change in design/styles is refresh-only
|
|
7
7
|
* — there is no baked theme in this bundle to rebuild (UNOVERSE_SPEC §2d-1).
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useState } from "react";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unoverse-create
|
|
3
|
-
description: Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), templates, atoms and styles (
|
|
3
|
+
description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), templates, atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, template, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Creating Unoverse Artifacts
|
|
@@ -10,7 +10,7 @@ the platform reads.
|
|
|
10
10
|
|
|
11
11
|
| Folder | What lives there |
|
|
12
12
|
|---|---|
|
|
13
|
-
| `
|
|
13
|
+
| `design/` | **Design** (data, not code): per-project components, templates + styles; each project is `design/<project>/` |
|
|
14
14
|
| `prompts/` | **Behavior**: agent skills (`skills/`) + prompt blocks (`blocks/`) |
|
|
15
15
|
| `nodes/` | **Logic**: custom workflow nodes, authored as YAML |
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ that is the workspace folder itself; in the platform monorepo it is `apps/unover
|
|
|
19
19
|
|
|
20
20
|
## Step 0: Anchor in the project before writing
|
|
21
21
|
|
|
22
|
-
1. **Pick the right `
|
|
22
|
+
1. **Pick the right `design/<project>/`.** Every design artifact belongs to a project (an
|
|
23
23
|
"org"). If the workspace has more than one, or the only one looks like a placeholder,
|
|
24
24
|
ask which project the artifact belongs to. Never invent a new project folder to hold
|
|
25
25
|
one artifact.
|
|
@@ -29,7 +29,7 @@ that is the workspace folder itself; in the platform monorepo it is `apps/unover
|
|
|
29
29
|
authoring MCP: `listDefinitions` (kind `atom` / `component`, org `marketplace`),
|
|
30
30
|
`readDefinition` for a full definition, `getVocabulary` for the closed node/style
|
|
31
31
|
vocabulary. Compose those atoms; never hand-roll a shape the design system already
|
|
32
|
-
ships. (In the platform monorepo the same material is on disk at `
|
|
32
|
+
ships. (In the platform monorepo the same material is on disk at `design/marketplace/`.)
|
|
33
33
|
|
|
34
34
|
## Step 1: Identify the artifact, read its playbook
|
|
35
35
|
|
|
@@ -108,7 +108,7 @@ messages; justify any warning), and the conformance checklist is
|
|
|
108
108
|
| Artifact | To see it live |
|
|
109
109
|
|---|---|
|
|
110
110
|
| Component / atom / template / style | Preview in Studio (`unoverse studio`), then publish from Studio: the publish lint gates it |
|
|
111
|
-
| Existing component restyle/edit only | takes effect live (the SDK reads `
|
|
111
|
+
| Existing component restyle/edit only | takes effect live (the SDK reads `design/` directly) |
|
|
112
112
|
| Agent skill / prompt block | publish from Studio, same gate |
|
|
113
113
|
| Node | Studio's Nodes screen: Load sample → Run. A node is YAML; running it is the only proof (no build) |
|
|
114
114
|
|
|
@@ -120,4 +120,4 @@ Verify the platform with `unoverse check` (services, schema, environment).
|
|
|
120
120
|
|
|
121
121
|
> In the platform monorepo (not a developer workspace), the project root is
|
|
122
122
|
> `apps/unoverse/`, the docs source is `packages/docs/`, and the dev loop is
|
|
123
|
-
> `npm run dev`. Component nodes synthesize from
|
|
123
|
+
> `npm run dev`. Component nodes synthesize from design/ definitions at boot.
|
|
@@ -13,7 +13,7 @@ client. They share one discipline and one failure mode.
|
|
|
13
13
|
|
|
14
14
|
## 1. How overriding actually works: name a token to override, omit it to inherit
|
|
15
15
|
|
|
16
|
-
`
|
|
16
|
+
`design/marketplace/styles` is the **foundation**: the shared token contract every org
|
|
17
17
|
inherits. Your org pack is read **on top** of it.
|
|
18
18
|
|
|
19
19
|
The server builds a theme in three passes (`buildTheme`, in the platform's theme
|
|
@@ -45,8 +45,8 @@ re-copying anything.
|
|
|
45
45
|
|
|
46
46
|
| Pack | `styles/base/` holds |
|
|
47
47
|
|---|---|
|
|
48
|
-
| Foundation (`
|
|
49
|
-
| A brand (`
|
|
48
|
+
| Foundation (`design/marketplace/`) | `border` `color` `motion` `radius` `shadow` `spacing` `typography` |
|
|
49
|
+
| A brand (`design/bpp/`) | `color` `typography` **only** |
|
|
50
50
|
|
|
51
51
|
BPP names two files and inherits the other five. That ratio is the health check: a pack
|
|
52
52
|
redefining `spacing` or `radius` is not branding, it is forking. The neutral core is
|
|
@@ -57,7 +57,7 @@ redefining `spacing` or `radius` is not branding, it is forking. The neutral cor
|
|
|
57
57
|
An org is a folder. There is no registry to edit and nothing to declare.
|
|
58
58
|
|
|
59
59
|
```
|
|
60
|
-
|
|
60
|
+
design/<neworg>/
|
|
61
61
|
styles/
|
|
62
62
|
base/color.yaml brand INGREDIENTS (color.brand.*)
|
|
63
63
|
base/typography.yaml font families only
|
|
@@ -85,10 +85,10 @@ you genuinely differ.
|
|
|
85
85
|
|
|
86
86
|
Its theme is then served at `unoverse://theme/<neworg>/light`, and a template points at it.
|
|
87
87
|
|
|
88
|
-
## 4. Anatomy of an org pack (`
|
|
88
|
+
## 4. Anatomy of an org pack (`design/<org>/styles/`)
|
|
89
89
|
|
|
90
90
|
Read an installed org pack as the worked example (Studio MCP `listOrgs` →
|
|
91
|
-
`listDefinitions`; in the platform monorepo, `
|
|
91
|
+
`listDefinitions`; in the platform monorepo, `design/bpp/styles/`). Every file in a healthy
|
|
92
92
|
pack carries a `$description` explaining its own job, and those descriptions are the spec.
|
|
93
93
|
|
|
94
94
|
| File | Holds | Rule |
|
|
@@ -128,7 +128,7 @@ naming your situation is the intended answer.
|
|
|
128
128
|
Copying an org pack is mechanical, and every gotcha is a name that exists in more than one
|
|
129
129
|
place. Work through them in order.
|
|
130
130
|
|
|
131
|
-
1. **Copy the folder.** `
|
|
131
|
+
1. **Copy the folder.** `design/<source>/` → `design/<neworg>/`, keeping `styles/`, `components/`,
|
|
132
132
|
`templates/`.
|
|
133
133
|
2. **Rename the templates.** Folder AND the id inside the manifest. `bpp-chat-layout`
|
|
134
134
|
becomes `lyceum-chat-layout` in both places. Template ids are org-qualified and they
|
|
@@ -145,7 +145,7 @@ place. Work through them in order.
|
|
|
145
145
|
|
|
146
146
|
## 7. Done means all four
|
|
147
147
|
|
|
148
|
-
- `grep -ri "<oldorg>"
|
|
148
|
+
- `grep -ri "<oldorg>" design/<neworg>/` returns **nothing**. This is the check that catches
|
|
149
149
|
the id someone missed at midnight, and it is the one people skip.
|
|
150
150
|
- The new org appears in **Studio's universe dropdown** and its theme renders.
|
|
151
151
|
- **Studio's publish lint: 0 errors.** Justify any warning out loud.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Playbook: Components & Atoms (
|
|
1
|
+
# Playbook: Components & Atoms (design data)
|
|
2
2
|
|
|
3
3
|
**Read first:** [design/components](https://docs.unoverse.ai/design/components.md) (the
|
|
4
4
|
microapp anatomy) + [design/state](https://docs.unoverse.ai/design/state.md) (the state
|
|
@@ -9,12 +9,12 @@ model). Deep law: [UNOVERSE_AUTHORING](https://docs.unoverse.ai/unoverse/UNOVERS
|
|
|
9
9
|
below; for a full worked component, read an installed one through Studio's MCP
|
|
10
10
|
(`listDefinitions` kind `component`, then `readDefinition`), and prefer a rich
|
|
11
11
|
multi-state one over a starter stub. (In the platform monorepo the canonical exemplar
|
|
12
|
-
is on disk: `
|
|
12
|
+
is on disk: `design/sab/components/product-card`.)
|
|
13
13
|
|
|
14
14
|
## The anatomy
|
|
15
15
|
|
|
16
16
|
```
|
|
17
|
-
|
|
17
|
+
design/<project>/components/<name>/
|
|
18
18
|
<name>.yaml # ROOT: envelope (name/category/nodeSize/outputs/props) + the STATE TREE + root
|
|
19
19
|
manifest.yaml # RENDER CONTRACT + discovery meta (title / description ≤120 /
|
|
20
20
|
# whenToUse utterance-shaped). Presence = spatially discoverable;
|
|
@@ -142,7 +142,7 @@ The rules, in the order they bite:
|
|
|
142
142
|
questions sharing one shape are one `question` state whose data changes, never seven
|
|
143
143
|
files (LAYERS §4, §7).
|
|
144
144
|
5. **Atoms** (the installed design system's shared shapes, used via `Ref`; list them with
|
|
145
|
-
Studio's MCP `listDefinitions` kind `atom`, or on disk at `
|
|
145
|
+
Studio's MCP `listDefinitions` kind `atom`, or on disk at `design/marketplace/atoms/` in the
|
|
146
146
|
platform monorepo) are for shapes shared across components, authoring-time
|
|
147
147
|
only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
|
|
148
148
|
**`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
|
|
@@ -176,7 +176,7 @@ The rules, in the order they bite:
|
|
|
176
176
|
[UNOVERSE_AUTHORING](https://docs.unoverse.ai/unoverse/UNOVERSE_AUTHORING.md) §3c). When a component needs data the
|
|
177
177
|
workflow did not send (not from an AI = brief, not from the workflow = `input` props),
|
|
178
178
|
it declares CALLS that the platform runs at a platform-owned fire point. **No code**:
|
|
179
|
-
a hook is data like everything else in `
|
|
179
|
+
a hook is data like everything else in `design/`. It's for DATA, not styling or logic.
|
|
180
180
|
- **Two phases**: `onStart` (the instance is created: data every copy needs) and
|
|
181
181
|
`onEnterView` (the instance entered a named STATE: data only that state needs). A
|
|
182
182
|
detail view's content is `onEnterView`, NEVER `onStart` — hydrating at creation
|
|
@@ -210,7 +210,7 @@ The rules, in the order they bite:
|
|
|
210
210
|
- **`allowedHosts` in the manifest is NOT optional**: a hook that makes requests
|
|
211
211
|
without it is a lint ERROR (deny by default). Credentials are named by type in the
|
|
212
212
|
manifest and resolved server-side from encrypted storage; **a key never sits in
|
|
213
|
-
`
|
|
213
|
+
`design/`** (a shared design folder) and never reaches the client.
|
|
214
214
|
- A phase fires once per instance; enrichment streams in (no blocked render); errors
|
|
215
215
|
fail quietly, leaving whatever the card already had.
|
|
216
216
|
|
|
@@ -271,7 +271,7 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
|
|
|
271
271
|
## Workflow
|
|
272
272
|
|
|
273
273
|
1. Study the closest installed exemplar (Studio MCP `listDefinitions` → `readDefinition`;
|
|
274
|
-
monorepo: `
|
|
274
|
+
monorepo: `design/sab/components/product-card`); copy its folder shape.
|
|
275
275
|
2. Declare the state tree first: public states on top, steps nested, each state owning
|
|
276
276
|
its layout (same-name default, `layout:` only when the filename differs).
|
|
277
277
|
3. Write the envelope + manifest + layouts; root = `Switch on view`; every shown thing
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Everything in the component playbook (closed vocabulary, tokens, scale steps) applies
|
|
9
9
|
here too. **Study an exemplar before writing**: read the closest installed template
|
|
10
10
|
through Studio's MCP (`listDefinitions` kind `template` → `readDefinition`); the
|
|
11
|
-
canonical manifest tree is inlined below. (Monorepo: `
|
|
11
|
+
canonical manifest tree is inlined below. (Monorepo: `design/sab/templates/sab-chat-layout`.)
|
|
12
12
|
|
|
13
13
|
## THE model: the component drives; the template reacts by name
|
|
14
14
|
|
|
@@ -68,7 +68,7 @@ states:
|
|
|
68
68
|
## The anatomy: manifest-only (NO `<name>.yaml` envelope)
|
|
69
69
|
|
|
70
70
|
```
|
|
71
|
-
|
|
71
|
+
design/<project>/templates/<name>/
|
|
72
72
|
manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
|
|
73
73
|
layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
|
|
74
74
|
main.yaml # the base: the core alone
|
|
@@ -159,7 +159,7 @@ template states by name exactly as in chat. Audio is never wired in a definition
|
|
|
159
159
|
|
|
160
160
|
## New project?
|
|
161
161
|
|
|
162
|
-
`
|
|
162
|
+
`design/<project>/` = `templates/` + optional `components/` (project-private microapps;
|
|
163
163
|
names unique within the project and never shadowing a marketplace name; two projects
|
|
164
164
|
may share a name, each addressed `<org>/<name>`) + `styles/` holding ONLY the org's
|
|
165
165
|
overrides (the foundation cascades per token; the theme file is the one you copy
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unoverse-prompt-component
|
|
3
|
-
description: Build or audit a PROMPT component (a briefed, AI-filled Unoverse component) in
|
|
3
|
+
description: Build or audit a PROMPT component (a briefed, AI-filled Unoverse component) in design/. Use when adding brief tags, deciding which props an agent may fill, debugging a part the agent never fills or fills wrongly, or checking what a component's MCP tool schema actually contains. Covers the traps that silently discard an authored brief.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Prompt components: the parts an agent fills
|
|
7
7
|
|
|
8
|
-
A prompt component is an ordinary `
|
|
8
|
+
A prompt component is an ordinary `design/` component whose bound nodes carry `brief` tags.
|
|
9
9
|
The platform compiles those tags into the component's **MCP tool schema**, and that schema
|
|
10
10
|
is the only instruction channel: no side-channel prompt, no per-component agent skill.
|
|
11
11
|
`unoverse-create` covers authoring components in general; this skill is about the briefs.
|
|
@@ -29,8 +29,8 @@ rm -f verify.tmp.ts
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
It prints every part the agent can fill, its description, and its limits. Read that
|
|
32
|
-
output, not the YAML. Then run the
|
|
33
|
-
(`node -e "import('./packages/base/src/lint/
|
|
32
|
+
output, not the YAML. Then run the design lint
|
|
33
|
+
(`node -e "import('./packages/base/src/lint/design/index.mjs').then(m=>{const r=m.lintDefinitions();console.log(r.problems.length);r.problems.forEach(p=>console.log(p.level,p.file,p.msg))})"`).
|
|
34
34
|
|
|
35
35
|
In a **Studio developer workspace**, the same linter runs as Studio's publish lint (or
|
|
36
36
|
the authoring MCP's `lintProject` tool); it catches two of the five traps below by
|