velloo 0.2.0 → 0.3.1

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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/canvas/assets/index-DZFrkC4P.js +29 -0
  3. package/canvas/assets/index-gB0StCrx.css +2 -0
  4. package/canvas/assets/lucide-all-Bu8-9UDF.js +1 -0
  5. package/canvas/assets/{radix-C3HaOB-d.js → radix-gmpYzWRH.js} +1 -1
  6. package/canvas/index.html +3 -3
  7. package/chunk-0s6drs3n.js +2 -0
  8. package/chunk-0zen7bzb.js +3 -0
  9. package/{chunk-er1fxb7d.js → chunk-1xb88e07.js} +1 -1
  10. package/{chunk-jv0xggh0.js → chunk-2e3nd4tz.js} +1 -1
  11. package/{chunk-08sv3k41.js → chunk-2fgfex9b.js} +1 -1
  12. package/{chunk-a84n72m2.js → chunk-2p37rp3j.js} +1 -1
  13. package/{chunk-ydhwv29b.js → chunk-2zbn6641.js} +2 -2
  14. package/{chunk-6w6ch4ce.js → chunk-3px2d1j1.js} +1 -1
  15. package/{chunk-2c7jbx46.js → chunk-4e9jh482.js} +1 -1
  16. package/{chunk-dgwjpbp9.js → chunk-4g250157.js} +1 -1
  17. package/{chunk-sjgqnykd.js → chunk-4ma2wnqw.js} +1 -1
  18. package/{chunk-x4f4k9pp.js → chunk-59zas5y2.js} +7 -7
  19. package/{chunk-0fe83yzt.js → chunk-5wv5a52y.js} +1 -1
  20. package/{chunk-bwnvcncx.js → chunk-6zz9kfbd.js} +1 -1
  21. package/{chunk-mh2pr7cq.js → chunk-7cvhnp9h.js} +1 -1
  22. package/{chunk-1h55s5he.js → chunk-7s8frnag.js} +1 -1
  23. package/chunk-7t13w87m.js +2 -0
  24. package/{chunk-zb6hc1y0.js → chunk-7yaafw73.js} +1 -1
  25. package/chunk-88eqqdk1.js +688 -0
  26. package/{chunk-ampmdzz4.js → chunk-8z3ykvcs.js} +1 -1
  27. package/{chunk-r22epxz2.js → chunk-98vvrj0c.js} +1 -1
  28. package/{chunk-vdkrbzh1.js → chunk-9cdwhrkh.js} +1 -1
  29. package/{chunk-xt5ayg7g.js → chunk-9p1dk5cb.js} +1 -1
  30. package/{chunk-dg6z4vdd.js → chunk-9psr8atb.js} +1 -1
  31. package/{chunk-4fj5tahv.js → chunk-9ydc34wc.js} +1 -1
  32. package/{chunk-pp86v6m6.js → chunk-as08hzp3.js} +1 -1
  33. package/chunk-asbajsmd.js +5 -0
  34. package/{chunk-vgmr98cv.js → chunk-avkfsc3p.js} +1 -1
  35. package/{chunk-ngnymg2q.js → chunk-bhx8k09w.js} +1 -1
  36. package/{chunk-3e8vez4d.js → chunk-c4fym47a.js} +63 -19
  37. package/{chunk-jb21jkp1.js → chunk-c6hywdvq.js} +1 -1
  38. package/{chunk-rpwpeph5.js → chunk-f0vsjfpc.js} +1 -1
  39. package/{chunk-ek71z2fv.js → chunk-g71v35gd.js} +1 -1
  40. package/{chunk-80r93t2e.js → chunk-hfzg98c8.js} +1 -1
  41. package/{chunk-jfx9xjmr.js → chunk-js6mz7h1.js} +1 -1
  42. package/{chunk-4dn75m6t.js → chunk-mfbxfb9b.js} +1 -1
  43. package/{chunk-mnapdp1y.js → chunk-mt8ds8zj.js} +1 -1
  44. package/chunk-n9t36h0q.js +2 -0
  45. package/{chunk-fsbsksga.js → chunk-nev5mv85.js} +1 -1
  46. package/{chunk-40xgcth8.js → chunk-nte0t7v3.js} +2 -2
  47. package/{chunk-hcb68zt7.js → chunk-p62fygqz.js} +1 -1
  48. package/{chunk-ayrb3qnm.js → chunk-p6e1eg76.js} +1 -1
  49. package/{chunk-7eq7nx37.js → chunk-ph2jfg1x.js} +1 -1
  50. package/{chunk-n9gs6yx9.js → chunk-pve2mmnr.js} +1 -1
  51. package/{chunk-avns4dbf.js → chunk-sn9vg42q.js} +1 -1
  52. package/{chunk-dy41n60j.js → chunk-t2w8a591.js} +5 -5
  53. package/{chunk-735ezz23.js → chunk-t5m5mznb.js} +2 -2
  54. package/{chunk-6nqghjda.js → chunk-t947mxr1.js} +1 -1
  55. package/chunk-tt7fys2w.js +2 -0
  56. package/chunk-wafb2xm9.js +6 -0
  57. package/{chunk-sn3x2m0m.js → chunk-whw3epmr.js} +1 -1
  58. package/{chunk-j11fyeqh.js → chunk-wndntr7s.js} +1 -1
  59. package/{chunk-7eyb8yrf.js → chunk-wnwgqvtd.js} +1 -1
  60. package/{chunk-v40fn08f.js → chunk-wp123630.js} +2 -2
  61. package/{chunk-ccszg02q.js → chunk-wvs939t5.js} +1 -1
  62. package/{chunk-jr0ss0mm.js → chunk-z21hg6j3.js} +2 -2
  63. package/cli.js +1 -1
  64. package/package.json +1 -1
  65. package/pkgs/helpers/src/descriptors.ts +7 -1
  66. package/pkgs/provider-antd/src/index.ts +1 -0
  67. package/pkgs/provider-chakra/src/index.ts +1 -0
  68. package/pkgs/provider-mui/src/index.ts +1 -0
  69. package/pkgs/provider-none/src/index.ts +42 -1
  70. package/pkgs/provider-none/src/intro.ts +2 -2
  71. package/pkgs/schema/src/config.ts +19 -0
  72. package/pkgs/schema/src/extension.ts +26 -0
  73. package/pkgs/schema/src/index.ts +10 -0
  74. package/pkgs/schema/src/node.ts +99 -0
  75. package/pkgs/schema/src/screen.ts +7 -0
  76. package/pkgs/shadcn-snapshot/dist/manifest.json +9 -1
  77. package/skills/velloo-setup/SKILL.md +70 -48
  78. package/canvas/assets/index-BCvDZiBo.css +0 -2
  79. package/canvas/assets/index-ClaBqcaS.js +0 -28
  80. package/canvas/assets/lucide-all-iIF-pmzN.js +0 -1
  81. package/chunk-308ynsxd.js +0 -2
  82. package/chunk-9vg3yafz.js +0 -3
  83. package/chunk-eyr8hrzf.js +0 -2
  84. package/chunk-k17gfwq3.js +0 -2
  85. package/chunk-rgasy862.js +0 -5
  86. package/chunk-t448rrfr.js +0 -2
  87. package/chunk-tdsk5wxa.js +0 -451
  88. package/chunk-vsv8z729.js +0 -6
package/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- import"./chunk-s23gtejf.js";import{COMMANDS}from"./chunk-4fj5tahv.js";import{TOOL_VERSION}from"./chunk-dg6z4vdd.js";import{maybeNotifyAboutUpdate}from"./chunk-xt5ayg7g.js";import{defineCommand,runMain}from"./chunk-2smsg2ct.js";var main=defineCommand({meta:{name:"velloo",version:TOOL_VERSION,description:"Open-source, local-first canvas for agent-driven design."},subCommands:COMMANDS});await runMain(main);var command=process.argv[2];if(!["mcp","__daemon","__update_check","run","upgrade"].includes(command??""))await maybeNotifyAboutUpdate();
3
+ import"./chunk-s23gtejf.js";import{COMMANDS}from"./chunk-9ydc34wc.js";import{TOOL_VERSION}from"./chunk-9psr8atb.js";import{maybeNotifyAboutUpdate}from"./chunk-9p1dk5cb.js";import{defineCommand,runMain}from"./chunk-2smsg2ct.js";var main=defineCommand({meta:{name:"velloo",version:TOOL_VERSION,description:"Open-source, local-first canvas for agent-driven design."},subCommands:COMMANDS});await runMain(main);var command=process.argv[2];if(!["mcp","__daemon","__update_check","run","upgrade"].includes(command??""))await maybeNotifyAboutUpdate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "velloo",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Local-first, code-shaped design canvas — your AI agent designs with your real components, in your repo",
6
6
  "license": "Apache-2.0",
@@ -15,7 +15,13 @@ export const HELPER_DESCRIPTORS: readonly ComponentDescriptor[] = [
15
15
  group: "layout",
16
16
  family: "Box",
17
17
  source: "velloo",
18
- props: [],
18
+ props: [
19
+ // Undiscoverable before this: an agent reproducing inline markup
20
+ // concluded Box could only be a div and reached for classes instead.
21
+ { name: "as", type: "string", optional: true, control: "string" },
22
+ ],
23
+ designModeNotes:
24
+ 'A plain div by default; `as` renders a lowercase HTML tag instead — "span" for an inline run inside a sentence, "ul"/"li", "section". Anything else falls back to div.',
19
25
  example: { className: "flex flex-col gap-6 px-8 py-12" },
20
26
  },
21
27
  {
@@ -63,6 +63,7 @@ export function createProvider(): FrameworkAdapter {
63
63
  factory: null,
64
64
  defaultPath: "antd-theme.ts",
65
65
  },
66
+ ownedModules: { packages: ["antd"] },
66
67
  // canvasBundleSpec is deliberately absent in v1: the server's bundle-entry
67
68
  // builder only implements the emotion style runtime, so antd folders render
68
69
  // via the in-process cssinjs SSR path. Extending it means a new
@@ -63,6 +63,7 @@ export function createProvider(): FrameworkAdapter {
63
63
  factory: "extendTheme",
64
64
  defaultPath: "chakra-theme.ts",
65
65
  },
66
+ ownedModules: { packages: ["@chakra-ui/react"] },
66
67
  // canvasBundleSpec is deliberately absent in v1: the server's bundle-entry
67
68
  // builder implements only the MUI-shaped emotion style runtime (its entry
68
69
  // template expects `ThemeProvider` + `createTheme` from `stylesModule`);
@@ -66,6 +66,7 @@ export function createProvider(): FrameworkAdapter {
66
66
  factory: "createTheme",
67
67
  defaultPath: "theme.ts",
68
68
  },
69
+ ownedModules: { packages: ["@mui/material"] },
69
70
  canvasBundleSpec: {
70
71
  components: (ids) =>
71
72
  ids.flatMap((id): CanvasComponentSpec[] => {
@@ -1,12 +1,22 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { type FrameworkAdapter, type Manifest, TAILWIND_CLASSNAME } from "@velloo/provider";
3
+ import { helperSourcePath } from "@velloo/helpers/paths";
4
+ import {
5
+ type CanvasComponentSpec,
6
+ type FrameworkAdapter,
7
+ type Manifest,
8
+ TAILWIND_CLASSNAME,
9
+ } from "@velloo/provider";
4
10
  import { resolveProviderSrcDir } from "@velloo/provider/src-dir";
5
11
  import { NONE_INLINE_INTRO, NONE_INTRO } from "./intro.ts";
6
12
  import { NONE_MANIFEST } from "./manifest.ts";
7
13
  import { registry } from "./registry.ts";
8
14
  import { inlineRegistry } from "./registry-inline.ts";
9
15
 
16
+ const PRIMITIVES = new Set(["Box", "Stack", "Container", "Card", "Button", "Input"]);
17
+ /** Typography the inline channel restyles; the Tailwind channel reuses the helpers. */
18
+ const INLINE_TYPOGRAPHY = new Set(["Heading", "Text"]);
19
+
10
20
  export { Box, Button, Card, Container, Input, Stack } from "./components.tsx";
11
21
  export { NONE_MANIFEST } from "./manifest.ts";
12
22
  export { registry } from "./registry.ts";
@@ -53,5 +63,36 @@ export function createProvider(): FrameworkAdapter {
53
63
  // Inline-styled primitives for the `style` channel; Tailwind-classed for
54
64
  // every other channel. Lets a `none/none` folder paint with the JIT off.
55
65
  registryForChannel: (kind) => (kind === "style" ? inlineRegistry : registry),
66
+ // No framework to install, so this only matters beside repository
67
+ // components: the screen then client-mounts, and these primitives have to
68
+ // render in the same browser tree as the app's own components.
69
+ canvasBundleSpec: {
70
+ styleRuntime: { kind: "none" },
71
+ onlyWithRepository: true,
72
+ components: (ids, context) => {
73
+ const inline = context?.channel === "style";
74
+ return ids.flatMap((id): CanvasComponentSpec[] => {
75
+ const own =
76
+ PRIMITIVES.has(id) || (inline && INLINE_TYPOGRAPHY.has(id))
77
+ ? join(srcDir, inline ? "components-inline.tsx" : "components.tsx")
78
+ : helperSourcePath(id);
79
+ return own
80
+ ? [
81
+ {
82
+ id,
83
+ sources: [
84
+ {
85
+ importPath: own,
86
+ exportName: id,
87
+ fidelity: "exact" as const,
88
+ note: "Velloo primitive, mounted beside the app's components.",
89
+ },
90
+ ],
91
+ },
92
+ ]
93
+ : [];
94
+ });
95
+ },
96
+ },
56
97
  };
57
98
  }
@@ -6,12 +6,12 @@
6
6
  * vocabulary positively rather than correcting a shadcn claim.
7
7
  */
8
8
  export const NONE_INTRO: readonly string[] = [
9
- "**This folder has no component library** — only bare primitives wrapping plain HTML: `Box`/`Stack`/`Container` for layout, `Card`, `Button`, `Input`, plus the velloo helpers (`Heading`/`Text`, `Image`, `Gradient`, `Layer`, `SVG`, `Divider`, `Placeholder`, `Icon`). There is no `Badge`/`Avatar`/`Tabs`/`Dialog` — build those from primitives or define snippets. Call `list_components` for the exact set. Styling is **Tailwind classes**.",
9
+ "**This folder has no component library** — only bare primitives wrapping plain HTML: `Box`/`Stack`/`Container` for layout, `Card`, `Button`, `Input`, plus the velloo helpers (`Heading`/`Text`, `Image`, `Gradient`, `Layer`, `SVG`, `Divider`, `Placeholder`, `Icon`). The primitives stop there: for a `Badge`/`Tabs`/`Dialog`, use the app's own component when `list_components` shows one on a Repo shelf, else build it from primitives or a snippet. Call `list_components` for the exact set. Styling is **Tailwind classes**.",
10
10
  "",
11
11
  ];
12
12
 
13
13
  export const NONE_INLINE_INTRO: readonly string[] = [
14
- "**This folder has no component library and no CSS framework** — bare primitives wrapping plain HTML (`Box`/`Stack`/`Container`, `Card`, `Button`, `Input`) plus the velloo helpers (`Heading`/`Text`, `Image`, `Icon`, `SVG`, `Divider`, `Gradient`, `Layer`, `Placeholder`). There is no `Badge`/`Avatar`/`Tabs`/`Dialog` — build those from primitives or snippets. Call `list_components` for the exact set.",
14
+ "**This folder has no component library and no CSS framework** — bare primitives wrapping plain HTML (`Box`/`Stack`/`Container`, `Card`, `Button`, `Input`) plus the velloo helpers (`Heading`/`Text`, `Image`, `Icon`, `SVG`, `Divider`, `Gradient`, `Layer`, `Placeholder`). The primitives stop there: for a `Badge`/`Tabs`/`Dialog`, use the app's own component when `list_components` shows one on a Repo shelf, else build it from primitives or a snippet. Call `list_components` for the exact set.",
15
15
  "",
16
16
  '**Style through inline `style` objects.** `update_props { style: { display: "flex", gap: "16px", padding: "24px", borderRadius: "8px", color: "var(--color-foreground)" } }` — a plain React style object (merges shallowly; an inner `null` drops a key, `style: null` clears). Reference theme tokens as CSS variables (`var(--color-primary)`, `var(--radius)`) from `get_theme` so the design stays themable. `emit_code` emits `style={{…}}` on plain elements — no imports, no Tailwind. Tailwind utility diagnostics do not apply.',
17
17
  "",
@@ -65,6 +65,25 @@ export type CodegenConfig = z.infer<typeof CodegenConfigSchema>;
65
65
  export const HostAppSchema = z.object({
66
66
  root: z.string().min(1),
67
67
  aliases: z.record(z.string().min(1), z.string().min(1)).optional(),
68
+ /**
69
+ * The app's preview entry: a module whose default export wraps mounted
70
+ * repository components in the context they need (providers, global CSS).
71
+ * Resolved from the design folder root. Absent ⇒ `preview.{tsx,jsx,ts,js}`
72
+ * in the design folder (`preview.<appKey>.*` for a named host app), then a
73
+ * built-in framework recipe, then no wrapper.
74
+ */
75
+ preview: z.string().min(1).optional(),
76
+ /**
77
+ * Bounds repository-component discovery beyond what the app's entries and
78
+ * routes import: `include` adds component roots (files or directories,
79
+ * host-root relative), `exclude` drops matching specifiers or paths.
80
+ */
81
+ components: z
82
+ .object({
83
+ include: z.array(z.string().min(1)).optional(),
84
+ exclude: z.array(z.string().min(1)).optional(),
85
+ })
86
+ .optional(),
68
87
  });
69
88
 
70
89
  export type HostApp = z.infer<typeof HostAppSchema>;
@@ -103,3 +103,29 @@ export const ExtensionSchema = z.object({
103
103
  });
104
104
 
105
105
  export type Extension = z.infer<typeof ExtensionSchema>;
106
+
107
+ /**
108
+ * `repo-components.json` in a design folder: checked-in corrections to what
109
+ * repository-component discovery infers. Keyed `<importPath>#<export>` (or
110
+ * `…#<export>.<Member>` for a compound part). Overrides refine the inferred
111
+ * entry; they never replace discovery, and a key or prop that no longer
112
+ * matches anything discovered is reported as stale.
113
+ */
114
+ export const RepoComponentOverrideSchema = z.object({
115
+ description: z.string().optional(),
116
+ /** Leave this component out of the catalog. */
117
+ exclude: z.boolean().optional(),
118
+ /** Snippet drawn in the component's place when it can't render for real. */
119
+ proxy: z.string().min(1).optional(),
120
+ /** Replaces or adds prop descriptors by name. */
121
+ props: z.array(ExtensionPropDescriptorSchema).optional(),
122
+ /** Named preview states: state name → props. */
123
+ states: z.record(z.string().min(1), z.record(z.string(), z.unknown())).optional(),
124
+ });
125
+
126
+ export const RepoComponentsManifestSchema = z.object({
127
+ components: z.record(z.string().min(1), RepoComponentOverrideSchema),
128
+ });
129
+
130
+ export type RepoComponentOverride = z.infer<typeof RepoComponentOverrideSchema>;
131
+ export type RepoComponentsManifest = z.infer<typeof RepoComponentsManifestSchema>;
@@ -66,6 +66,10 @@ export {
66
66
  type ExtensionPropDescriptor,
67
67
  ExtensionPropDescriptorSchema,
68
68
  ExtensionSchema,
69
+ type RepoComponentOverride,
70
+ RepoComponentOverrideSchema,
71
+ type RepoComponentsManifest,
72
+ RepoComponentsManifestSchema,
69
73
  } from "./extension.ts";
70
74
  export {
71
75
  type Frame,
@@ -90,12 +94,18 @@ export {
90
94
  type ComponentNode,
91
95
  isComponentNode,
92
96
  isParamRef,
97
+ isRepoNode,
93
98
  isSnippetInstance,
94
99
  type Node,
95
100
  NodeIdSchema,
96
101
  NodeSchema,
97
102
  nodeId,
98
103
  type ParamRef,
104
+ parseRepoKey,
105
+ type RepoComponentRef,
106
+ RepoComponentRefSchema,
107
+ repoImportIssue,
108
+ repoKey,
99
109
  type SnippetInstance,
100
110
  } from "./node.ts";
101
111
  export {
@@ -47,6 +47,37 @@ export type ComponentNode = {
47
47
  * capture → design → emit. Absent ⇒ the node emits as itself.
48
48
  */
49
49
  $emitAs?: { name: string; importPath: string } | undefined;
50
+ /**
51
+ * Repository component identity: this node IS a component the host app
52
+ * imports (a package export such as Mantine's `Tabs`, or the app's own
53
+ * `StatCard`), not a library component. `$ref` stays the JSX name codegen
54
+ * prints; the identity decides rendering and imports. Takes precedence over
55
+ * any provider component or extension of the same name, so shadowing is
56
+ * explicit on the node rather than a registry-order accident.
57
+ */
58
+ $repo?: RepoComponentRef | undefined;
59
+ };
60
+
61
+ /**
62
+ * Where a repository component comes from. The import form is kept exactly as
63
+ * code generation must print it; the resolved file is runtime cache data and
64
+ * never persisted.
65
+ */
66
+ export type RepoComponentRef = {
67
+ /**
68
+ * Module specifier. Bare (`@mantine/core`) and aliased (`@/components/card`)
69
+ * specifiers are kept as the app writes them; a `./`-relative one is relative
70
+ * to the host app root, since the importing file varies per call site.
71
+ */
72
+ importPath: string;
73
+ /** The export binding, or `"default"` for a default export. */
74
+ exportName: string;
75
+ /** Static member path of a compound part: `"List"` for `Tabs.List`. */
76
+ member?: string | undefined;
77
+ /** `config.hostApps` key in a monorepo; absent ⇒ the default host app. */
78
+ app?: string | undefined;
79
+ /** Snippet id drawn in the component's place when it can't render for real. */
80
+ proxy?: string | undefined;
50
81
  };
51
82
 
52
83
  export type SnippetInstance = {
@@ -126,6 +157,42 @@ const EmitAsSchema = z.object({
126
157
  importPath: z.string().min(1),
127
158
  });
128
159
 
160
+ /**
161
+ * A specifier codegen may print verbatim inside `import … from "…"` and the
162
+ * bundler may resolve against the host app. Conservative charset (no quotes,
163
+ * spaces, semicolons), and no absolute or `..` paths: a design folder is data a
164
+ * cloned repo supplies, and it must not be able to reach outside the app.
165
+ */
166
+ export function repoImportIssue(importPath: string): string | null {
167
+ if (!/^[\w@./~-]+$/.test(importPath)) return "has characters an import specifier can't carry";
168
+ if (importPath.startsWith("/")) return "must not be an absolute path";
169
+ if (importPath.split("/").includes("..")) return "must not climb out of the host app with ..";
170
+ return null;
171
+ }
172
+
173
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
174
+
175
+ export const RepoComponentRefSchema = z.object({
176
+ importPath: z
177
+ .string()
178
+ .min(1)
179
+ .superRefine((value, ctx) => {
180
+ const issue = repoImportIssue(value);
181
+ if (issue) ctx.addIssue({ code: "custom", message: `importPath ${issue}` });
182
+ }),
183
+ exportName: z
184
+ .string()
185
+ .regex(IDENTIFIER, { message: 'exportName must be an identifier or "default"' }),
186
+ member: z
187
+ .string()
188
+ .regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/, {
189
+ message: 'member must be a dotted identifier path like "List"',
190
+ })
191
+ .optional(),
192
+ app: z.string().min(1).optional(),
193
+ proxy: z.string().min(1).optional(),
194
+ });
195
+
129
196
  const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
130
197
  z.object({
131
198
  $ref: z.string().min(1),
@@ -136,9 +203,41 @@ const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
136
203
  .transform((items) => items.map(wrapScalarChild))
137
204
  .optional(),
138
205
  $emitAs: EmitAsSchema.optional(),
206
+ $repo: RepoComponentRefSchema.optional(),
139
207
  }),
140
208
  );
141
209
 
210
+ /** True when the node is a repository component (see ComponentNode.$repo). */
211
+ export function isRepoNode(n: Node): n is ComponentNode & { $repo: RepoComponentRef } {
212
+ return isComponentNode(n) && n.$repo !== undefined;
213
+ }
214
+
215
+ /**
216
+ * The runtime key a repository component registers under — identity only, so
217
+ * two apps' `Button`s (or Mantine's vs the provider's) never collide.
218
+ */
219
+ export function repoKey(ref: RepoComponentRef): string {
220
+ const member = ref.member ? `.${ref.member}` : "";
221
+ return `repo:${encodeURIComponent(ref.app ?? "")}:${ref.importPath}#${ref.exportName}${member}`;
222
+ }
223
+
224
+ /**
225
+ * Invert {@link repoKey}. A bundle URL carries only keys, so the runtime can
226
+ * resolve a screen's components from identity alone — no catalog lookup, which
227
+ * keeps a design renderable while discovery is cold or the app has moved on.
228
+ */
229
+ export function parseRepoKey(key: string): RepoComponentRef | null {
230
+ const match = /^repo:([^:]*):([^#]+)#([A-Za-z_$][\w$]*)(?:\.(.+))?$/.exec(key);
231
+ if (!match) return null;
232
+ const parsed = RepoComponentRefSchema.safeParse({
233
+ importPath: match[2],
234
+ exportName: match[3],
235
+ ...(match[4] ? { member: match[4] } : {}),
236
+ ...(match[1] ? { app: decodeURIComponent(match[1]) } : {}),
237
+ });
238
+ return parsed.success ? parsed.data : null;
239
+ }
240
+
142
241
  const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
143
242
  $snippet: z.string().min(1),
144
243
  $id: NodeIdSchema.optional(),
@@ -24,6 +24,13 @@ export const ScreenSchema = z.object({
24
24
  * components and extensions resolve against this library's registry.
25
25
  */
26
26
  library: z.string().min(1).optional(),
27
+ /**
28
+ * The app route this screen stands for ("/", "/settings/account"). Set by
29
+ * the route scan `init` runs, and editable per screen. The canvas mount
30
+ * hands it to the host framework's router contexts, so a repository nav
31
+ * lights the item the real page would.
32
+ */
33
+ route: z.string().min(1).optional(),
27
34
  tree: NodeSchema,
28
35
  });
29
36
 
@@ -460,7 +460,15 @@
460
460
  "group": "layout",
461
461
  "family": "Box",
462
462
  "source": "velloo",
463
- "props": [],
463
+ "props": [
464
+ {
465
+ "name": "as",
466
+ "type": "string",
467
+ "optional": true,
468
+ "control": "string"
469
+ }
470
+ ],
471
+ "designModeNotes": "A plain div by default; `as` renders a lowercase HTML tag instead — \"span\" for an inline run inside a sentence, \"ul\"/\"li\", \"section\". Anything else falls back to div.",
464
472
  "example": {
465
473
  "className": "flex flex-col gap-6 px-8 py-12"
466
474
  }
@@ -11,18 +11,41 @@ description: >-
11
11
 
12
12
  # Calibrating Velloo to an existing app
13
13
 
14
- For shadcn folders, the canvas can import client-safe components directly from
15
- the app's configured `components/ui` directory. That is a bounded capability,
16
- not a promise that arbitrary application React can run inside a design iframe:
17
- portal/state-heavy families use named canvas-safe adaptations, and a missing or
18
- unbuildable file falls back independently. Other providers follow their own
19
- adapter contract. The fidelity is observable through `component_status`; never
20
- infer it from `installedInApp` or from a component merely appearing on screen.
14
+ The canvas renders the app's own components — whatever library they come from
15
+ (Mantine, a private design system, a hand-rolled `components/` directory) —
16
+ as real, nested, editable nodes. `list_components` lists them on **Repo**
17
+ shelves, found from what the app's routes actually render. For shadcn folders
18
+ the configured `components/ui` files additionally back the library itself.
19
+ None of that is a promise that arbitrary application React runs inside a
20
+ design iframe: the components need the context the app gives them (providers,
21
+ global CSS), portal-heavy families use adaptations, and a component that can't
22
+ build or render falls back on its own. Fidelity is observable through
23
+ `component_status`; never infer it from a component merely appearing on screen.
21
24
 
22
25
  Your job here is to close that gap **before** you start designing, and to say
23
26
  honestly what's left open. Do this once per design folder. Skip it only for a
24
27
  greenfield folder with no app to match.
25
28
 
29
+ ## 0. The preview entry — when the app has components
30
+
31
+ Call `preview_status` first. It reports `state` (`absent` / `valid` /
32
+ `failing`), the providers and stylesheets the app's own entry uses
33
+ (`appWrappers`, `appStylesheets`), and mounts one real component to prove the
34
+ setup works — a missing provider or an unstyled render shows up here, not
35
+ halfway through a design.
36
+
37
+ - **`valid` with a recipe** (Mantine and friends): a built-in wrapper is doing
38
+ the work, themed from Velloo's tokens. Good enough to design with; write your
39
+ own entry only when the app's theme, router or data providers matter.
40
+ - **`absent` / `failing`**: adapt `suggestedPreviewEntry` — it is lifted from
41
+ the app's entry — and call `set_preview_entry { source }`. Keep the app's
42
+ global CSS imports, swap network-backed providers for fixtures (a
43
+ `QueryClient` with seeded data, a memory router), never pass credentials. The
44
+ tool re-probes and answers with the new state; iterate until `valid`.
45
+
46
+ A snippet can't do this job: it is node data, so it can't import CSS, build a
47
+ router or wrap the whole screen. Snippets are for step 4 below.
48
+
26
49
  ## 1. Theme first — it buys the most
27
50
 
28
51
  `import_theme { cssPath, apply: true }` against the app's stylesheet. This is
@@ -78,26 +101,28 @@ live data legitimately differ.
78
101
 
79
102
  Only after theme parity, call `component_status { screen: "<id>" }` for each
80
103
  screen you're about to design or verify (or `{ ids: [...] }` before a screen
81
- exists). Check `mounted` first: the mount is all-or-nothing, so a single
82
- `unavailable` component keeps the whole screen — and every screenshot and
83
- `compare_to_url` of it — on Velloo's bundled components, whatever the other
84
- statuses say. Fix or replace the blocking component before trusting any
85
- `exact`. Treat the statuses as part of the design brief:
86
-
87
- - **`exact`** — Velloo compile-checked and selected the app's source file for
88
- the whole-screen canvas mount. Custom CVA variants and ordinary explicit
89
- props are also reflected into discovery when their syntax is recognizable.
90
- - **`adapted`** — the real family depends on portals, runtime state, browser
91
- layout, or another interaction that conflicts with a static selectable
92
- canvas. Velloo deliberately renders a canvas-safe counterpart. Preserve the
93
- component identity and props, but do not claim pixel-identical behavior.
94
- - **`fallback`** — the app file is absent or failed the browser preflight, so a
95
- bundled provider component or Velloo helper is rendering. Read the returned
96
- note/errors before deciding whether the visual difference matters.
97
- - **`unavailable`** — there is no usable canvas source, so a screen using it
98
- does not mount at all (`mounted: false`); read its errors — a resolution
99
- failure usually means the recorded app root is wrong
100
- (`velloo design set-app-root`).
104
+ exists — repo catalog ids work too). For library components, check `mounted`
105
+ first: that mount is all-or-nothing, so a single `unavailable` library
106
+ component keeps the whole screen on Velloo's bundled components. A screen with
107
+ the app's own components always mounts, and each of those falls back alone.
108
+ Treat the statuses as part of the design brief:
109
+
110
+ - **`exact`** — the app's own source (or package export) renders in the canvas
111
+ mount. Custom CVA variants and ordinary explicit props are also reflected
112
+ into discovery when their syntax is recognizable.
113
+ - **`adapted`** — the real component renders through a design-time wrapper
114
+ (an overlay kept inside the frame, focus trapping off). Preserve its identity
115
+ and props, but do not claim pixel-identical behavior.
116
+ - **`unstyled`** — it rendered, but its stylesheet never loaded: import the
117
+ library's CSS in the preview entry.
118
+ - **`fallback`** — a bundled provider component or Velloo helper is rendering
119
+ instead of the app's file. Read the returned note/errors.
120
+ - **`proxy`** — the app's component can't render here, and the node's proxy
121
+ snippet stands in for it; emitted code still imports the real one.
122
+ - **`unavailable`** — nothing renders it; read its `code` and `remedy`
123
+ (`missing-provider`, `resolve-failed`, `compile-failed`, `server-only`,
124
+ `render-threw`, `missing-export`). A resolution failure usually means the
125
+ recorded app root is wrong (`velloo design set-app-root`).
101
126
 
102
127
  Host-source edits invalidate the canvas bundle automatically. After changing a
103
128
  component, wait for the frame to reload and call `component_status` again; do
@@ -106,25 +131,22 @@ not restart the daemon merely to pick up a normal source edit.
106
131
  ## 4. Close only the important remaining gaps
107
132
 
108
133
  If an on-screen component is not `exact` and the difference is load-bearing,
109
- read its source and choose the smallest honest adaptation:
110
-
111
- - **A snippet** (`add_snippet`) that composes library primitives to match the
112
- component's appearance. It stays editable on the canvas and is the preferred
113
- answer for a bespoke compound component that is outside the shadcn library.
114
- Library compound components whose files are `exact` already preserve their
115
- children in the whole-screen mount and do not need a snippet. `render_snippet`
116
- immediately after defining one.
117
- - **`$emitAs { name, importPath }`** on the node when the preview can be an
118
- approximation but the generated code must import the real component. Design
119
- with primitives, emit `<DataTable />`. Use this for anything whose appearance
120
- you cannot reasonably rebuild but whose identity in the code matters.
121
- - **A live extension** (`add_extension` with `render: "live"`) only for the
122
- genuinely dynamic minority — charts above all. It bundles the real host file
123
- and client-mounts it, so it is the one path that renders the user's actual
124
- code. Know its limits before reaching for it: children are stripped, the
125
- mount is visual-only, and it breaks whenever the host file doesn't compile.
126
- Never the default for library components, and never use it to work around a
127
- compound component with children.
134
+ choose the smallest honest adaptation:
135
+
136
+ - **Fix the context** first: most `unavailable` app components are missing a
137
+ provider or fixture the preview entry can supply.
138
+ - **A proxy snippet** for a component that genuinely can't render in a static
139
+ canvas (it needs live data, a server, a browser API). `add_snippet` composing
140
+ primitives to match its appearance, then point the node at it — `update_props`
141
+ can't set identity, so place it with `add_node { repo: { importPath,
142
+ exportName }, … }` and a `proxy`, or record `"proxy": "<snippet-id>"` for it in
143
+ the design folder's `repo-components.json`. The snippet draws on the canvas;
144
+ `emit_code` still imports the real component with the design's props.
145
+ - **A snippet** (`add_snippet`) for a composition that isn't a component in the
146
+ app at all. `render_snippet` immediately after defining one.
147
+ - **A live extension** (`add_extension` with `render: "live"`) only for a
148
+ dynamic leaf the preview entry can't make render as a normal node. Its
149
+ children are stripped and the mount is visual-only.
128
150
 
129
151
  Do not replace an `adapted` overlay merely because its status is not `exact`;
130
152
  the adaptation is what keeps dialogs, menus, popovers, and similar components
@@ -138,8 +160,8 @@ session — and the user — knows where things stand. Three headings, honest:
138
160
 
139
161
  - **Exact** — theme imported from `<path>`, fonts, and the components reported
140
162
  `exact` by `component_status`.
141
- - **Adapted / fallback** — the status, reason, and any snippet / `$emitAs` / live
142
- decision made to close a load-bearing difference.
163
+ - **Adapted / fallback / proxy** — the status, reason, and any preview-entry,
164
+ proxy-snippet or live decision made to close a load-bearing difference.
143
165
  - **Unverified** — screens behind auth you couldn't reach, pages whose dev
144
166
  server wouldn't start, tokens the stylesheet didn't declare.
145
167