velloo 0.7.0 → 0.9.0

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 (95) hide show
  1. package/BUN-LICENSE.md +4 -3
  2. package/README.md +8 -1
  3. package/THIRD-PARTY-NOTICES.md +3 -3
  4. package/canvas/assets/index-CkVteRZ9.css +2 -0
  5. package/canvas/assets/index-DFOlwAKW.js +31 -0
  6. package/canvas/assets/lucide-all-CezGNjGW.js +1 -0
  7. package/canvas/index.html +2 -2
  8. package/{chunk-qrnkrkkt.js → chunk-00n2d68z.js} +1 -1
  9. package/{chunk-mdgwdqnq.js → chunk-02gpgqj4.js} +8 -8
  10. package/{chunk-qp3wahg4.js → chunk-0ztxyzac.js} +1 -1
  11. package/{chunk-4y2xy1kf.js → chunk-2x5rw009.js} +3 -3
  12. package/{chunk-abr2hgay.js → chunk-3f2p94wa.js} +1 -1
  13. package/{chunk-srp5gyr1.js → chunk-3jqbybmp.js} +1 -1
  14. package/{chunk-0vaff40g.js → chunk-5tatkt2j.js} +1 -1
  15. package/{chunk-asfgvzse.js → chunk-640swnrm.js} +1 -1
  16. package/{chunk-bms1sg4c.js → chunk-6600wkmq.js} +1 -1
  17. package/{chunk-7gyzh0vq.js → chunk-6adv9eke.js} +1 -1
  18. package/{chunk-fvezyap4.js → chunk-6wfvb07c.js} +1 -1
  19. package/chunk-8a2agf28.js +22 -0
  20. package/{chunk-82x2jkfq.js → chunk-8kn6tp4b.js} +2 -2
  21. package/{chunk-fmcm06xb.js → chunk-9b4akcfp.js} +1 -1
  22. package/{chunk-0pxm5v0b.js → chunk-9kvbe53e.js} +3 -3
  23. package/{chunk-gfppsgvv.js → chunk-a5mxgvcz.js} +1 -1
  24. package/{chunk-g7d065ek.js → chunk-as4rn21e.js} +1 -1
  25. package/{chunk-xg71h7nb.js → chunk-avewjans.js} +4 -4
  26. package/chunk-bpb97rsk.js +17 -0
  27. package/{chunk-j306gr0c.js → chunk-bsn761jv.js} +1 -1
  28. package/{chunk-kkvf0xk2.js → chunk-c11n1tve.js} +2 -2
  29. package/{chunk-sme6hv6f.js → chunk-cfksd3zh.js} +1 -1
  30. package/{chunk-zv17t1n7.js → chunk-ck3bg8rf.js} +1 -1
  31. package/{chunk-d93kpv7s.js → chunk-cy1nmr0f.js} +1 -1
  32. package/{chunk-qqrac4va.js → chunk-dqy6a75x.js} +1 -1
  33. package/{chunk-xzf52wvn.js → chunk-f8jchnwe.js} +1 -1
  34. package/{chunk-s84vxcdv.js → chunk-fejhav0p.js} +1 -1
  35. package/{chunk-ykpctq15.js → chunk-fzqt4rsz.js} +1 -1
  36. package/{chunk-3m28pvzr.js → chunk-g7pbfad5.js} +1 -1
  37. package/{chunk-b8vcbbrg.js → chunk-gv209d5j.js} +1 -1
  38. package/chunk-j4rt31qb.js +2 -0
  39. package/{chunk-d0j8dg1e.js → chunk-kmsbcb3q.js} +1 -1
  40. package/{chunk-5s1wjhtj.js → chunk-ktps9v0k.js} +1 -1
  41. package/{chunk-667ag8ts.js → chunk-ktsdzzgx.js} +1 -1
  42. package/{chunk-0e5j6nqs.js → chunk-kvxn1cyx.js} +1 -1
  43. package/{chunk-ex36z3b3.js → chunk-n70zfv0m.js} +906 -906
  44. package/{chunk-rqd4swxx.js → chunk-n8pr0kc8.js} +1 -1
  45. package/{chunk-eh2xp4v6.js → chunk-pasvz8xw.js} +1 -1
  46. package/{chunk-j4fn2a2b.js → chunk-qewarcqf.js} +1 -1
  47. package/{chunk-dvh0b2r4.js → chunk-qf1c6jxh.js} +1 -1
  48. package/chunk-qn05hn7a.js +5 -0
  49. package/{chunk-gcce587w.js → chunk-qvdvxqs0.js} +1 -1
  50. package/{chunk-pjkkp7xa.js → chunk-r014f76h.js} +1 -1
  51. package/{chunk-acsv6m3n.js → chunk-seaar5na.js} +1 -1
  52. package/{chunk-84n18xgx.js → chunk-tax7fxav.js} +2 -2
  53. package/{chunk-gbq5wv7x.js → chunk-tmjs6xas.js} +1 -1
  54. package/{chunk-vs8j3x6t.js → chunk-v6w8akph.js} +1 -1
  55. package/{chunk-5x4nw0zz.js → chunk-w40pqmyz.js} +4 -4
  56. package/{chunk-vdbr81a3.js → chunk-x05grjd9.js} +1 -1
  57. package/{chunk-md4mpvqc.js → chunk-x61f42fj.js} +2 -2
  58. package/{chunk-531xzfq5.js → chunk-xc6mats8.js} +1 -1
  59. package/chunk-yp39f93e.js +815 -0
  60. package/{chunk-5yhnnhkd.js → chunk-ysf9388d.js} +1 -1
  61. package/{chunk-64pex3mw.js → chunk-z3r8px3d.js} +1 -1
  62. package/{chunk-ejppcfa5.js → chunk-z9kcdgj8.js} +1 -1
  63. package/chunk-zexzy6zv.js +2 -0
  64. package/chunk-zjbbd87h.js +2 -0
  65. package/cli.js +1 -1
  66. package/package.json +9 -9
  67. package/pkgs/provider-antd/src/index.ts +1 -0
  68. package/pkgs/provider-html/src/index.ts +1 -0
  69. package/pkgs/provider-mui/src/index.ts +10 -0
  70. package/pkgs/provider-none/src/index.ts +1 -0
  71. package/pkgs/schema/src/board.ts +13 -1
  72. package/pkgs/schema/src/config.ts +34 -0
  73. package/pkgs/schema/src/flow.ts +230 -0
  74. package/pkgs/schema/src/frame.ts +7 -0
  75. package/pkgs/schema/src/index.ts +22 -0
  76. package/pkgs/schema/src/node-identity.ts +48 -0
  77. package/pkgs/schema/src/node.ts +80 -9
  78. package/pkgs/schema/src/snippet-resolve.ts +43 -0
  79. package/pkgs/schema/src/theme.ts +56 -0
  80. package/plugins/claude/velloo/agents/velloo-designer.md +7 -4
  81. package/plugins/claude/velloo/commands/setup.md +13 -0
  82. package/plugins/gemini/GEMINI.md +9 -5
  83. package/plugins/gemini/commands/velloo/setup.toml +11 -0
  84. package/skills/velloo-design/SKILL.md +11 -6
  85. package/skills/velloo-setup/SKILL.md +179 -36
  86. package/canvas/assets/index-BQ5710lu.js +0 -29
  87. package/canvas/assets/index-GNLd0pCG.css +0 -2
  88. package/canvas/assets/lucide-all-DIRZdJPK.js +0 -1
  89. package/chunk-brnkprt5.js +0 -723
  90. package/chunk-bys7pqdb.js +0 -2
  91. package/chunk-g8xrwjvq.js +0 -22
  92. package/chunk-jbgde2v9.js +0 -2
  93. package/chunk-rxkvjyst.js +0 -18
  94. package/chunk-sjjnm4a1.js +0 -2
  95. package/chunk-snak5zpr.js +0 -5
@@ -54,6 +54,8 @@ export {
54
54
  type Library,
55
55
  LibrarySchema,
56
56
  StylingSchema,
57
+ type ThemeSource,
58
+ ThemeSourceSchema,
57
59
  type ViewportPreset,
58
60
  ViewportPresetSchema,
59
61
  } from "./config.ts";
@@ -74,6 +76,17 @@ export {
74
76
  type RepoComponentsManifest,
75
77
  RepoComponentsManifestSchema,
76
78
  } from "./extension.ts";
79
+ export {
80
+ DEFAULT_FLOW_STATE,
81
+ deriveFlowSteps,
82
+ FLOW_STATES,
83
+ type FlowEdge,
84
+ FlowEdgeSchema,
85
+ type FlowStep,
86
+ flowEdgeProblem,
87
+ liveFlowEdges,
88
+ transitionsDrawnAsStates,
89
+ } from "./flow.ts";
77
90
  export {
78
91
  type Frame,
79
92
  FrameSchema,
@@ -95,8 +108,11 @@ export {
95
108
  } from "./migrate.ts";
96
109
  export {
97
110
  type ComponentNode,
111
+ type ComponentType,
112
+ type ComponentTypeRef,
98
113
  type EmitAsRef,
99
114
  isComponentNode,
115
+ isComponentType,
100
116
  isParamRef,
101
117
  isRepoNode,
102
118
  isSnippetInstance,
@@ -115,9 +131,11 @@ export {
115
131
  type TextNode,
116
132
  } from "./node.ts";
117
133
  export {
134
+ componentTypesIn,
118
135
  type EmitAsNode,
119
136
  isNode,
120
137
  isSyntheticRef,
138
+ mapComponentTypes,
121
139
  type NodeShape,
122
140
  nodeShape,
123
141
  PARAM_TAG_REF,
@@ -145,6 +163,7 @@ export {
145
163
  applySnippetOverrides,
146
164
  type InvalidParamPlacement,
147
165
  resolveSnippetArgs,
166
+ snippetChildSlots,
148
167
  substituteSnippetParams,
149
168
  } from "./snippet-resolve.ts";
150
169
  export { sanitizeSvgMarkup, svgLooksActive } from "./svg-sanitize.ts";
@@ -154,12 +173,15 @@ export {
154
173
  type ColorsOverride,
155
174
  ColorsSchema,
156
175
  type LooseTokenGroup,
176
+ namedPaletteRoles,
177
+ normalizeThemeName,
157
178
  resolveColors,
158
179
  spacingCssTokens,
159
180
  TAILWIND_SIZE_NAMES,
160
181
  type Theme,
161
182
  ThemeSchema,
162
183
  TypographySchema,
184
+ themeNameProblem,
163
185
  } from "./theme.ts";
164
186
  export {
165
187
  DEFAULT_TYPESET_NAME,
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  type ComponentNode,
3
+ type ComponentTypeRef,
3
4
  type EmitAsRef,
4
5
  isComponentNode,
6
+ isComponentType,
5
7
  isParamRef,
6
8
  isSnippetInstance,
7
9
  isTextNode,
@@ -95,3 +97,49 @@ export function nodeShape(value: unknown): NodeShape {
95
97
  export function isNode(value: unknown): value is Node {
96
98
  return nodeShape(value).kind !== "invalid";
97
99
  }
100
+
101
+ /**
102
+ * Replace every component type in a prop value — the value itself, or one
103
+ * nested in the arrays and plain objects it holds — with what `resolve` makes
104
+ * of it. A node inside the value is left as it is: its own props are resolved
105
+ * when that node is walked. Returns `value` itself when nothing changed, so a
106
+ * props bag with no type in it keeps its identity.
107
+ *
108
+ * A component type is not a node (`nodeShape` calls one `invalid`, so no node
109
+ * position ever takes it), which is why the walkers that read prop values ask
110
+ * this rather than switching on the node union.
111
+ */
112
+ export function mapComponentTypes(
113
+ value: unknown,
114
+ resolve: (type: ComponentTypeRef) => unknown,
115
+ ): unknown {
116
+ if (isComponentType(value)) return resolve(value.$type);
117
+ if (Array.isArray(value)) {
118
+ let changed = false;
119
+ const out = value.map((item) => {
120
+ const next = mapComponentTypes(item, resolve);
121
+ if (next !== item) changed = true;
122
+ return next;
123
+ });
124
+ return changed ? out : value;
125
+ }
126
+ if (value === null || typeof value !== "object" || isNode(value)) return value;
127
+ let changed = false;
128
+ const out: Record<string, unknown> = {};
129
+ for (const [key, nested] of Object.entries(value)) {
130
+ const next = mapComponentTypes(nested, resolve);
131
+ if (next !== nested) changed = true;
132
+ out[key] = next;
133
+ }
134
+ return changed ? out : value;
135
+ }
136
+
137
+ /** Every component type in a prop value, by the same reach as {@link mapComponentTypes}. */
138
+ export function componentTypesIn(value: unknown): ComponentTypeRef[] {
139
+ const out: ComponentTypeRef[] = [];
140
+ mapComponentTypes(value, (type) => {
141
+ out.push(type);
142
+ return type;
143
+ });
144
+ return out;
145
+ }
@@ -127,20 +127,48 @@ export type TextNode = {
127
127
 
128
128
  export type Node = ComponentNode | SnippetInstance | ParamRef | TextNode;
129
129
 
130
+ /**
131
+ * A component *type* held as a prop value: `component={ScrollArea}`,
132
+ * `as={Link}`, `icon={IconBolt}` — the component itself, not an element of it.
133
+ * The reference is a component node with no props, children or `$id`, so the
134
+ * name resolves exactly as a `$ref` of that name would (the app's component by
135
+ * its `$repo`, else an extension, else the screen's library). It is a value,
136
+ * not a node: it sits anywhere a prop value can, nested in arrays and objects
137
+ * too, and never in a tree's children.
138
+ */
139
+ export type ComponentTypeRef = {
140
+ $ref: string;
141
+ $repo?: RepoComponentRef | undefined;
142
+ };
143
+
144
+ export type ComponentType = { $type: ComponentTypeRef };
145
+
146
+ // Walkers ask these of raw prop values too (`rightSection: null`), so each one
147
+ // has to answer for a value that is not an object at all.
130
148
  export function isComponentNode(n: Node): n is ComponentNode {
131
- return typeof (n as { $ref?: unknown }).$ref === "string";
149
+ return typeof (n as { $ref?: unknown } | null | undefined)?.$ref === "string";
132
150
  }
133
151
 
134
152
  export function isSnippetInstance(n: Node): n is SnippetInstance {
135
- return typeof (n as { $snippet?: unknown }).$snippet === "string";
153
+ return typeof (n as { $snippet?: unknown } | null | undefined)?.$snippet === "string";
136
154
  }
137
155
 
138
156
  export function isParamRef(n: Node): n is ParamRef {
139
- return typeof (n as { $param?: unknown }).$param === "string";
157
+ return typeof (n as { $param?: unknown } | null | undefined)?.$param === "string";
140
158
  }
141
159
 
142
160
  export function isTextNode(n: Node): n is TextNode {
143
- return typeof (n as { $text?: unknown }).$text === "string";
161
+ return typeof (n as { $text?: unknown } | null | undefined)?.$text === "string";
162
+ }
163
+
164
+ export function isComponentType(value: unknown): value is ComponentType {
165
+ const inner = (value as { $type?: unknown } | null | undefined)?.$type;
166
+ return (
167
+ !Array.isArray(value) &&
168
+ typeof inner === "object" &&
169
+ inner !== null &&
170
+ typeof (inner as { $ref?: unknown }).$ref === "string"
171
+ );
144
172
  }
145
173
 
146
174
  /**
@@ -213,11 +241,53 @@ export const RepoComponentRefSchema = z.object({
213
241
  proxy: z.string().min(1).optional(),
214
242
  });
215
243
 
244
+ const ComponentTypeSchema: z.ZodType<ComponentType> = z.strictObject({
245
+ $type: z.strictObject({
246
+ // Printed bare as a JS expression (`component={AppShell.Section}`), so it
247
+ // has to be an identifier path — which also keeps a synthetic `velloo:` ref out.
248
+ $ref: z.string().regex(/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/, {
249
+ message: 'a component type\'s $ref must be a component name like "ScrollArea"',
250
+ }),
251
+ $repo: RepoComponentRefSchema.optional(),
252
+ }),
253
+ });
254
+
255
+ /**
256
+ * Prop values are free-form JSON with one exception: an object carrying
257
+ * `$type` must be a well-formed component type, wherever it sits. Its `$repo`
258
+ * reaches codegen's imports and the canvas bundler exactly as a node's does, so
259
+ * it is held to the same checks.
260
+ */
261
+ function checkComponentTypes(value: unknown, ctx: z.RefinementCtx, path: PropertyKey[]): void {
262
+ if (Array.isArray(value)) {
263
+ value.forEach((item, i) => {
264
+ checkComponentTypes(item, ctx, [...path, i]);
265
+ });
266
+ return;
267
+ }
268
+ if (value === null || typeof value !== "object") return;
269
+ if (Object.hasOwn(value, "$type")) {
270
+ const parsed = ComponentTypeSchema.safeParse(value);
271
+ if (!parsed.success) {
272
+ for (const issue of parsed.error.issues) {
273
+ ctx.addIssue({ code: "custom", message: issue.message, path: [...path, ...issue.path] });
274
+ }
275
+ }
276
+ return;
277
+ }
278
+ for (const [key, nested] of Object.entries(value))
279
+ checkComponentTypes(nested, ctx, [...path, key]);
280
+ }
281
+
282
+ const PropsSchema = z.record(z.string(), z.unknown()).superRefine((props, ctx) => {
283
+ checkComponentTypes(props, ctx, []);
284
+ });
285
+
216
286
  const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
217
287
  z.object({
218
288
  $ref: z.string().min(1),
219
289
  $id: NodeIdSchema.optional(),
220
- props: z.record(z.string(), z.unknown()).optional(),
290
+ props: PropsSchema.optional(),
221
291
  children: z
222
292
  .array(z.union([z.string(), z.number(), NodeSchema]))
223
293
  .transform(scalarChildren)
@@ -262,14 +332,14 @@ const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
262
332
  $snippet: z.string().min(1),
263
333
  $id: NodeIdSchema.optional(),
264
334
  $extraClassName: z.string().optional(),
265
- args: z.record(z.string(), z.unknown()).optional(),
335
+ args: PropsSchema.optional(),
266
336
  $overrides: z
267
337
  .record(
268
338
  z.string().regex(/^$|^\d+(\.\d+)*$|^@[a-zA-Z][a-zA-Z0-9_-]*$/, {
269
339
  message:
270
340
  'override key must be a dotted index path like "0.2", an "@id" reference with a literal leading @ (e.g. "@row-active" to target the body node whose $id is "row-active"), or "" for the body root',
271
341
  }),
272
- z.object({ props: z.record(z.string(), z.unknown()) }),
342
+ z.object({ props: PropsSchema }),
273
343
  )
274
344
  .optional(),
275
345
  });
@@ -313,8 +383,9 @@ export const NodeSchema: z.ZodType<Node> = z
313
383
  if (!branch) {
314
384
  ctx.addIssue({
315
385
  code: "custom",
316
- message:
317
- 'node needs exactly one of "$ref" (component), "$snippet" (snippet instance), "$param" (param ref, snippet bodies only), or "$text" (text beside elements)',
386
+ message: isComponentType(v)
387
+ ? `a component type ({"$type": …}) is a prop value, not a node — to render one here, write {"$ref": ${JSON.stringify(v.$type.$ref)}}`
388
+ : 'node needs exactly one of "$ref" (component), "$snippet" (snippet instance), "$param" (param ref, snippet bodies only), or "$text" (text beside elements)',
318
389
  });
319
390
  return z.NEVER;
320
391
  }
@@ -19,6 +19,49 @@ const OMITTED: unique symbol = Symbol("velloo.omitted-optional-param");
19
19
  /** Substitution result for an OMITTED value — pruned from arrays and prop objects. */
20
20
  const DROP: unique symbol = Symbol("velloo.drop");
21
21
 
22
+ /**
23
+ * The params a snippet body places only as children — the `node` params whose
24
+ * args are nodes. One that also fills a prop, or is forwarded into another
25
+ * instance's args, carries data: what it holds there is the body's business,
26
+ * not a node's. The server reads only these args as nodes, and the canvas
27
+ * inspector edits only these as nodes.
28
+ */
29
+ export function snippetChildSlots(snippet: Pick<Snippet, "tree">): Set<string> {
30
+ const asChild = new Set<string>();
31
+ const elsewhere = new Set<string>();
32
+ const paramsIn = (v: unknown, into: Set<string>): void => {
33
+ if (v === null || typeof v !== "object") return;
34
+ if (Array.isArray(v)) {
35
+ for (const item of v) paramsIn(item, into);
36
+ return;
37
+ }
38
+ const record = v as Record<string, unknown>;
39
+ if (typeof record.$param === "string") into.add(record.$param);
40
+ for (const nested of Object.values(record)) paramsIn(nested, into);
41
+ };
42
+ const child = (v: unknown): void => {
43
+ if (Array.isArray(v)) {
44
+ for (const item of v) child(item);
45
+ return;
46
+ }
47
+ if (v === null || typeof v !== "object") return;
48
+ const record = v as Record<string, unknown>;
49
+ if (typeof record.$param === "string") asChild.add(record.$param);
50
+ else if (typeof record.$if === "string") {
51
+ child(record.then);
52
+ child(record.else);
53
+ } else node(record);
54
+ };
55
+ const node = (record: Record<string, unknown>): void => {
56
+ paramsIn(record.props, elsewhere);
57
+ paramsIn(record.args, elsewhere);
58
+ child(record.children);
59
+ };
60
+ child(snippet.tree);
61
+ for (const name of elsewhere) asChild.delete(name);
62
+ return asChild;
63
+ }
64
+
22
65
  /** Resolve declared params against passed args, applying defaults. */
23
66
  export function resolveSnippetArgs(
24
67
  snippet: Snippet,
@@ -140,6 +140,20 @@ const PaletteSchema = z.record(
140
140
  z.string().min(1),
141
141
  );
142
142
 
143
+ /**
144
+ * The palette entries that are roles in their own right — `success`,
145
+ * `tertiary`, `brand-soft` — as opposed to steps of a scale (`primary-600`),
146
+ * which an import can add by the hundred. A design language lists the first
147
+ * kind next to the semantic slots; the second kind is an app's raw material.
148
+ */
149
+ export function namedPaletteRoles(theme: Pick<Theme, "palette" | "paletteDark">): string[] {
150
+ const names = new Set([
151
+ ...Object.keys(theme.palette ?? {}),
152
+ ...Object.keys(theme.paletteDark ?? {}),
153
+ ]);
154
+ return [...names].filter((name) => !/(?:^|-)\d+$/.test(name)).sort();
155
+ }
156
+
143
157
  /**
144
158
  * A typeset: three rhythm controls plus font roles. Everything visible —
145
159
  * the h1..h6 ladder, body/lead/small copy, the space under a heading — derives
@@ -261,3 +275,45 @@ export const ThemeSchema = z.object({
261
275
  });
262
276
 
263
277
  export type Theme = z.infer<typeof ThemeSchema>;
278
+
279
+ /** Longer than any name a person would give a theme, short enough for a file name anywhere. */
280
+ const THEME_NAME_MAX = 48;
281
+
282
+ /**
283
+ * A theme's name is the stem of its file (`theme/<name>.json`), the value a
284
+ * board pins and the argument an agent passes, so there is one name and it is
285
+ * the one a person typed: "Night", "Brand B", "high-contrast".
286
+ *
287
+ * What it may contain is what a file name carries on every platform without
288
+ * quoting surprises — ASCII letters of either case, digits, spaces, dashes and
289
+ * underscores, beginning and ending on a letter or digit (Windows drops a
290
+ * trailing space or dot). Accented letters are left out because macOS and
291
+ * Linux disagree about how to spell them on disk, which git then reports as a
292
+ * renamed file.
293
+ */
294
+ const THEME_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9 _-]*[A-Za-z0-9])?$/;
295
+
296
+ /** A typed name as it will be stored: no outer space, single spaces within. */
297
+ export function normalizeThemeName(raw: string): string {
298
+ return raw.trim().replace(/\s+/g, " ");
299
+ }
300
+
301
+ /**
302
+ * Why `name` cannot be a new theme's name, as a sentence for whoever chose it;
303
+ * null when it can. `existing` is compared without case: macOS and Windows
304
+ * keep `Night.json` and `night.json` as one file, so the two cannot coexist in
305
+ * a design that is cloned onto either.
306
+ */
307
+ export function themeNameProblem(name: string, existing: Iterable<string> = []): string | null {
308
+ if (name === "") return "Give the theme a name.";
309
+ if (name.length > THEME_NAME_MAX) return `Keep the name to ${THEME_NAME_MAX} characters.`;
310
+ if (!THEME_NAME.test(name)) {
311
+ return "Use letters, digits, spaces, dashes and underscores, starting and ending with a letter or digit.";
312
+ }
313
+ const folded = name.toLowerCase();
314
+ if (folded === "default") return 'Every design already has a "Default" theme.';
315
+ for (const other of existing) {
316
+ if (other.toLowerCase() === folded) return `A theme named "${other}" already exists.`;
317
+ }
318
+ return null;
319
+ }
@@ -20,10 +20,13 @@ Workflow:
20
20
  file and read it: it is the folder's own design system in prose — house rules
21
21
  that outrank the defaults below, and what the reviewer will check this work
22
22
  against. Velloo points at the file rather than copying it, so it is current.
23
- 2. **Build in big strokes.** `compose` accepts a full subtree as restricted
24
- JSX — a whole section per call, not node-by-node; `batch` groups mutations
25
- atomically. Repeated structure (cards, rows, nav items) becomes a snippet
26
- with typed params (`add_snippet`, then placed in `compose` by its tag).
23
+ 2. **Build in big strokes.** `compose` accepts the JSX you would write for
24
+ the app — `const` data, `.map`, `cond && <X />`, small components, or a page
25
+ `file` to read — a whole screen per call, not node-by-node; `get_screen
26
+ mode: "jsx"` reads one back to edit and send again; `batch` groups mutations
27
+ atomically. Structure that should stay reusable on the canvas (cards, rows,
28
+ nav items) becomes a snippet with typed params (`add_snippet`, then placed
29
+ in `compose` by its tag).
27
30
  Set `vellooId` at creation and address nodes as `"@id"` afterwards.
28
31
  3. **Prefer semantic theme tokens** (`bg-background`, `text-foreground`,
29
32
  `border-border`, `bg-primary`, …) over raw palette colors; raw palette only
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Set Velloo up for this app — the first step after velloo init
3
+ argument-hint: (optional) the design to set up, when the repo has several
4
+ ---
5
+
6
+ Use the velloo-setup skill to set this design up for its app: $ARGUMENTS
7
+
8
+ Work through the velloo MCP tools — they own the design folder; never edit its
9
+ JSON by hand. If the tools aren't available, tell the user to run
10
+ `velloo connect` and restart this session. Give the user the canvas URL up
11
+ front, finish by telling them what is exact, what is adapted and what you
12
+ could not verify, and point them at the Design language stage on the canvas to
13
+ review what you proposed.
@@ -19,13 +19,17 @@ Ground rules:
19
19
  - **Prefer semantic theme tokens** (`bg-background`, `text-foreground`,
20
20
  `bg-primary`, …) over raw palette colors — they adapt to dark mode and
21
21
  theme changes.
22
- - **Build in big strokes:** `compose` takes full subtrees as restricted JSX;
23
- `batch` groups mutations; repeated structure becomes a snippet
24
- (`add_snippet`, then placed in `compose` by its tag).
22
+ - **Build in big strokes:** `compose` takes the JSX you would write for the
23
+ app (`const` data, `.map`, `cond && <X />`, small components, or a page
24
+ `file`) and writes it out as elements; `get_screen mode: "jsx"` reads a
25
+ screen back to edit; `batch` groups mutations; structure that should stay
26
+ reusable becomes a snippet (`add_snippet`, then placed in `compose` by its
27
+ tag).
25
28
  - **Verify:** `screenshot mode: "compare"` (light + dark), its `diagnostics`,
26
29
  `score_theme_contrast`, and `compare_to_url` against a running app.
27
30
  - The user can watch on the live canvas — `velloo run` prints its URL.
28
31
 
29
- This extension ships the Velloo skills (design, implement, brand, design
30
- system, logo) — reach for `velloo-design` when composing and
32
+ This extension ships the Velloo skills (setup, design, implement, brand,
33
+ design system, logo) — start a design that has an app to match with
34
+ `velloo-setup`, reach for `velloo-design` when composing and
31
35
  `velloo-implement` when turning a finished design into production code.
@@ -0,0 +1,11 @@
1
+ description = "Set Velloo up for this app — the first step after velloo init"
2
+
3
+ prompt = """
4
+ Use the velloo-setup skill (or the velloo MCP tools directly) to set this design up for its app: {{args}}
5
+
6
+ Work through the velloo MCP tools — they own the design folder; never edit its
7
+ JSON by hand. Import the app's theme and real fonts, get `preview_status` to
8
+ `valid`, look at what `check_components` returns, then propose the design
9
+ language with `set_design_language`. Finish by telling the user what is exact,
10
+ what is adapted and what you could not verify.
11
+ """
@@ -44,12 +44,17 @@ reference — read them; this skill is the workflow on top.
44
44
  dark mode and survive theme changes; raw palette colors render identically in
45
45
  both. Use raw palette only for an *intentional* accent that should not flip —
46
46
  and mark that node `data-accent: "ok"` so the raw-color diagnostic exempts it.
47
- 3. **Build whole subtrees, not node-by-node.** `compose` takes a full subtree
48
- as restricted JSX in one call (`mode: "append"` under a parent, or
49
- `"replace"` for the whole screen). For repeated structure (list rows, cards,
50
- nav items), define a **snippet** with typed params once (`add_snippet`), then
51
- place it in `compose` by its PascalCase tag, args as props. `batch` runs many
52
- mutations atomically in one round-trip.
47
+ 3. **Build whole subtrees, not node-by-node.** `compose` takes the JSX you
48
+ would write for the app in one call — `const` data above the markup, `.map`,
49
+ `cond && <X />`, small components — or reads it from a page file (`file`);
50
+ `mode: "append"` adds under a parent, `"replace"` swaps the whole screen
51
+ (creating it if it is new) or one node (`path`). It is read as data and
52
+ written out as elements; nothing runs. `get_screen mode: "jsx"` reads a
53
+ screen back in that form to edit and send again. For structure that should
54
+ stay reusable on the canvas (list rows, cards, nav items), define a
55
+ **snippet** with typed params once (`add_snippet`), then place it in
56
+ `compose` by its PascalCase tag, args as props. `batch` runs many mutations
57
+ atomically in one round-trip.
53
58
  4. **Think in ids.** Set `vellooId` at creation and address nodes as `"@id"` in
54
59
  later calls — number paths shift when siblings move.
55
60