@unoverse-platform/studio 0.1.46 → 0.1.47

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 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 rx/. */
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 (existsSync(join(dir, "rx"))) return dir;
44
- if (existsSync(join(dir, "apps/unoverse/rx"))) return join(dir, "apps/unoverse");
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) => existsSync(join(cwd, e.name, "rx")))
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 (existsSync(join(dir, "rx")) || existsSync(join(dir, "apps/unoverse/rx"))) return;
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 rx/<project>/, and that name is the org they`);
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 rx/${name}/, prompts/ and nodes/ in ${cwd}\n`);
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 RX_HOME = paths.rx`), so
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: ${join(projectRoot, "rx")}\n`);
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
@@ -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/rx/`, and the editor lints unsaved text by handing that
6
- * library an overlay (`lintDefinitions(rxHome, { overlay })`), so the squiggles a developer
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, "rx/marketplace/atoms"), resolve(projectRoot, "rx/atoms")];
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 (existsSync(resolve(dir, "rx"))) return dir;
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 (existsSync(resolve(dir, "apps/unoverse/rx"))) return resolve(dir, "apps/unoverse");
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,2 @@
1
+ export function designHome(root: string): string;
2
+ export function hasDesignHome(root: string): boolean;
@@ -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, "rx/atoms", `${ref.toLowerCase()}.json`);
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, "rx/marketplace/components"), resolve(projectRoot, "rx/components")]) {
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: ${resolve(projectRoot, "rx")}\n`);
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 ["rx", "prompts"]) {
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: resolve(projectRoot, "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(rxHome: string, project: string): Promise<{ problems: unknown[]; errors: unknown[] }>;
280
- collectProject(rxHome: string, project: string): unknown;
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 rxHome = resolve(projectRoot, "rx");
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(rxHome, project);
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(rxHome, project);
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(rxHome: string): { problems: { level?: string; file?: string }[] } };
428
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
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
- (rxHome) => lintMod.lintDefinitions(rxHome),
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
- rxHome: string,
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/rx/index.mjs`)) as unknown as LintModule;
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(resolve(projectRoot, "rx"), { overlay: { [abs]: b.text as string } });
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/rx/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
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(rxHome: string): { problems: { level?: string; file?: string }[] } };
521
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
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, (rxHome) =>
524
- lintMod.lintDefinitions(rxHome),
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(rxHome: string): { problems: unknown[] } };
529
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
530
- return lintMod.lintDefinitions(resolve(projectRoot, "rx"));
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/rx/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
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 = [resolve(projectRoot, "rx"), resolve(projectRoot, "prompts")];
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(resolve(projectRoot, "rx"))) {
646
+ if (projectRoot && !existsSync(designHome(projectRoot))) {
646
647
  throw new Error(`No rx/ folder at ${projectRoot}`);
647
648
  }
648
- if (projectRoot) statSync(resolve(projectRoot, "rx"));
649
+ if (projectRoot) statSync(designHome(projectRoot));
649
650
  },
650
651
  };
651
652
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Creating a project, from either end.
3
3
  *
4
- * A project (an "org" in rx terms) is `rx/<name>/` holding components, styles and
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 rx/ are never a
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 `rx/<name>/` under `home`, with its three authoring folders and one component.
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, "rx", name);
69
- if (existsSync(root)) throw new Error(`rx/${name} already exists`);
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 rx/ rather than inside it.
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: (rxHome: string) => Promise<{ problems: LintFinding[] }> | { problems: LintFinding[] },
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 rxHome = resolve(projectRoot, "rx");
187
+ const designRoot = designHome(projectRoot);
187
188
  let problems: LintFinding[] = [];
188
189
  try {
189
- problems = (await lint(rxHome)).problems ?? [];
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.46",
3
+ "version": "0.1.47",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -321,7 +321,7 @@ export function DesignSystemView({
321
321
  {foundation ? (
322
322
  <>Tokens with an <OverrideMark /> are <b style={{ color: ink }}>{org}</b>&rsquo;s own; the rest are inherited from the design-system foundation.</>
323
323
  ) : (
324
- <>Live tokens from the design-system foundation (<code>rx/design-system/styles</code>). Toggle the theme to restyle everything.</>
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 (rx/). If a template seems to need code
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 rx name,
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 rx/orgs/&lt;client&gt;/templates/.</div>
660
+ <div className="p-2 text-xs text-gray-400">No apps yet — add one under design/&lt;project&gt;/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 rx/ folder. Flat, per RX_ORG_MODEL. Re-read after
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")
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Creating a project from inside Studio.
3
3
  *
4
- * A project is `rx/<name>/`: the folder a developer authors in, and the unit that
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 rx/&lt;name&gt;/ with components, styles and templates, and one component to
54
+ Creates design/&lt;name&gt;/ with components, styles and templates, and one component to
55
55
  open on.
56
56
  </p>
57
57
 
@@ -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(/^.*\/rx\//, "")}
270
+ {p.file?.replace(/^.*\/design\//, "")}
271
271
  {p.line ? `:${p.line}` : ""}
272
272
  </span>
273
273
  <br />
@@ -68,7 +68,7 @@ export function AppEngine(cfg: AppConfig) {
68
68
  setBoundFor(templateId);
69
69
  setBinding({});
70
70
  }
71
- // STATE-OWNED WIDTH (rx data): the layout root's `appWidth` = the core surface's
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 (rx/styles); the SDK only fetches — it bundles no tokens. Cached.
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 rx) that stays HIDDEN while
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
- // rx/styles) live and serves this shape at `unoverse://theme/{name}`. This is the
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 rx/styles is therefore refresh-only,
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 ← rx/styles/base/spacing.json) — used for BOTH spacing
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 ← rx/styles/semantic/app-sizes.json) — the named
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 ← rx/styles/semantic/grid) — `stackBelow`, the width
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 ← rx/styles/semantic/layout) — resolved by `dim()`
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 ← rx/styles/semantic/keyframes.json), injected
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 ← rx/styles/semantic/root.json) — a CSS
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 rx can't author per-node (pseudo-
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 `rx/` (the conversation state box).
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 `rx/`) and, using an IntersectionObserver on the bar's PREVIOUS sibling as
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 `rx/`.
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 ← rx/styles/semantic/skeleton.json). The SDK
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` ← rx/styles/semantic/prose.json). The SDK
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 rx/.
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 rx/styles/semantic/prose.
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 (rx/ definitions + the served theme) — an author creates
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 rx/ first.
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";
@@ -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 rx/styles and is served. See
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 ← rx/styles/semantic/keyframes.json)
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 rx/ + a `style.animation` ref in a definition. No
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 rx chrome, so it needs those facts as template state to bind against. Same
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 (rx/templates/chatlayout/*.json). The assistant sub-tree's ComponentSlot
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 (
@@ -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
- * rx/styles, resolved live) and serves it at `unoverse://theme/{name}`; this hook
6
- * fetches it (cached in the client). A brand change in rx/styles is refresh-only
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 (rx/), 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").
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
- | `rx/` | **Design** (data, not code): per-project components, templates + styles; each project is `rx/<project>/` |
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 `rx/<project>/`.** Every design artifact belongs to a project (an
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 `rx/marketplace/`.)
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 `rx/` directly) |
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 rx/ definitions at boot.
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
- `rx/marketplace/styles` is the **foundation**: the shared token contract every org
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 (`rx/marketplace/`) | `border` `color` `motion` `radius` `shadow` `spacing` `typography` |
49
- | A brand (`rx/bpp/`) | `color` `typography` **only** |
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
- rx/<neworg>/
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 (`rx/<org>/styles/`)
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, `rx/bpp/styles/`). Every file in a healthy
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.** `rx/<source>/` → `rx/<neworg>/`, keeping `styles/`, `components/`,
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>" rx/<neworg>/` returns **nothing**. This is the check that catches
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 (rx data)
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: `rx/sab/components/product-card`.)
12
+ is on disk: `design/sab/components/product-card`.)
13
13
 
14
14
  ## The anatomy
15
15
 
16
16
  ```
17
- rx/<project>/components/<name>/
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 `rx/marketplace/atoms/` in the
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 `rx/`. It's for DATA, not styling or logic.
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
- `rx/`** (a shared design folder) and never reaches the client.
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: `rx/sab/components/product-card`); copy its folder shape.
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: `rx/sab/templates/sab-chat-layout`.)
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
- rx/<project>/templates/<name>/
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
- `rx/<project>/` = `templates/` + optional `components/` (project-private microapps;
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 rx/. 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.
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 `rx/` component whose bound nodes carry `brief` tags.
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 rx lint
33
- (`node -e "import('./packages/base/src/lint/rx/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))})"`).
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