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.
- package/BUN-LICENSE.md +4 -3
- package/README.md +8 -1
- package/THIRD-PARTY-NOTICES.md +3 -3
- package/canvas/assets/index-CkVteRZ9.css +2 -0
- package/canvas/assets/index-DFOlwAKW.js +31 -0
- package/canvas/assets/lucide-all-CezGNjGW.js +1 -0
- package/canvas/index.html +2 -2
- package/{chunk-qrnkrkkt.js → chunk-00n2d68z.js} +1 -1
- package/{chunk-mdgwdqnq.js → chunk-02gpgqj4.js} +8 -8
- package/{chunk-qp3wahg4.js → chunk-0ztxyzac.js} +1 -1
- package/{chunk-4y2xy1kf.js → chunk-2x5rw009.js} +3 -3
- package/{chunk-abr2hgay.js → chunk-3f2p94wa.js} +1 -1
- package/{chunk-srp5gyr1.js → chunk-3jqbybmp.js} +1 -1
- package/{chunk-0vaff40g.js → chunk-5tatkt2j.js} +1 -1
- package/{chunk-asfgvzse.js → chunk-640swnrm.js} +1 -1
- package/{chunk-bms1sg4c.js → chunk-6600wkmq.js} +1 -1
- package/{chunk-7gyzh0vq.js → chunk-6adv9eke.js} +1 -1
- package/{chunk-fvezyap4.js → chunk-6wfvb07c.js} +1 -1
- package/chunk-8a2agf28.js +22 -0
- package/{chunk-82x2jkfq.js → chunk-8kn6tp4b.js} +2 -2
- package/{chunk-fmcm06xb.js → chunk-9b4akcfp.js} +1 -1
- package/{chunk-0pxm5v0b.js → chunk-9kvbe53e.js} +3 -3
- package/{chunk-gfppsgvv.js → chunk-a5mxgvcz.js} +1 -1
- package/{chunk-g7d065ek.js → chunk-as4rn21e.js} +1 -1
- package/{chunk-xg71h7nb.js → chunk-avewjans.js} +4 -4
- package/chunk-bpb97rsk.js +17 -0
- package/{chunk-j306gr0c.js → chunk-bsn761jv.js} +1 -1
- package/{chunk-kkvf0xk2.js → chunk-c11n1tve.js} +2 -2
- package/{chunk-sme6hv6f.js → chunk-cfksd3zh.js} +1 -1
- package/{chunk-zv17t1n7.js → chunk-ck3bg8rf.js} +1 -1
- package/{chunk-d93kpv7s.js → chunk-cy1nmr0f.js} +1 -1
- package/{chunk-qqrac4va.js → chunk-dqy6a75x.js} +1 -1
- package/{chunk-xzf52wvn.js → chunk-f8jchnwe.js} +1 -1
- package/{chunk-s84vxcdv.js → chunk-fejhav0p.js} +1 -1
- package/{chunk-ykpctq15.js → chunk-fzqt4rsz.js} +1 -1
- package/{chunk-3m28pvzr.js → chunk-g7pbfad5.js} +1 -1
- package/{chunk-b8vcbbrg.js → chunk-gv209d5j.js} +1 -1
- package/chunk-j4rt31qb.js +2 -0
- package/{chunk-d0j8dg1e.js → chunk-kmsbcb3q.js} +1 -1
- package/{chunk-5s1wjhtj.js → chunk-ktps9v0k.js} +1 -1
- package/{chunk-667ag8ts.js → chunk-ktsdzzgx.js} +1 -1
- package/{chunk-0e5j6nqs.js → chunk-kvxn1cyx.js} +1 -1
- package/{chunk-ex36z3b3.js → chunk-n70zfv0m.js} +906 -906
- package/{chunk-rqd4swxx.js → chunk-n8pr0kc8.js} +1 -1
- package/{chunk-eh2xp4v6.js → chunk-pasvz8xw.js} +1 -1
- package/{chunk-j4fn2a2b.js → chunk-qewarcqf.js} +1 -1
- package/{chunk-dvh0b2r4.js → chunk-qf1c6jxh.js} +1 -1
- package/chunk-qn05hn7a.js +5 -0
- package/{chunk-gcce587w.js → chunk-qvdvxqs0.js} +1 -1
- package/{chunk-pjkkp7xa.js → chunk-r014f76h.js} +1 -1
- package/{chunk-acsv6m3n.js → chunk-seaar5na.js} +1 -1
- package/{chunk-84n18xgx.js → chunk-tax7fxav.js} +2 -2
- package/{chunk-gbq5wv7x.js → chunk-tmjs6xas.js} +1 -1
- package/{chunk-vs8j3x6t.js → chunk-v6w8akph.js} +1 -1
- package/{chunk-5x4nw0zz.js → chunk-w40pqmyz.js} +4 -4
- package/{chunk-vdbr81a3.js → chunk-x05grjd9.js} +1 -1
- package/{chunk-md4mpvqc.js → chunk-x61f42fj.js} +2 -2
- package/{chunk-531xzfq5.js → chunk-xc6mats8.js} +1 -1
- package/chunk-yp39f93e.js +815 -0
- package/{chunk-5yhnnhkd.js → chunk-ysf9388d.js} +1 -1
- package/{chunk-64pex3mw.js → chunk-z3r8px3d.js} +1 -1
- package/{chunk-ejppcfa5.js → chunk-z9kcdgj8.js} +1 -1
- package/chunk-zexzy6zv.js +2 -0
- package/chunk-zjbbd87h.js +2 -0
- package/cli.js +1 -1
- package/package.json +9 -9
- package/pkgs/provider-antd/src/index.ts +1 -0
- package/pkgs/provider-html/src/index.ts +1 -0
- package/pkgs/provider-mui/src/index.ts +10 -0
- package/pkgs/provider-none/src/index.ts +1 -0
- package/pkgs/schema/src/board.ts +13 -1
- package/pkgs/schema/src/config.ts +34 -0
- package/pkgs/schema/src/flow.ts +230 -0
- package/pkgs/schema/src/frame.ts +7 -0
- package/pkgs/schema/src/index.ts +22 -0
- package/pkgs/schema/src/node-identity.ts +48 -0
- package/pkgs/schema/src/node.ts +80 -9
- package/pkgs/schema/src/snippet-resolve.ts +43 -0
- package/pkgs/schema/src/theme.ts +56 -0
- package/plugins/claude/velloo/agents/velloo-designer.md +7 -4
- package/plugins/claude/velloo/commands/setup.md +13 -0
- package/plugins/gemini/GEMINI.md +9 -5
- package/plugins/gemini/commands/velloo/setup.toml +11 -0
- package/skills/velloo-design/SKILL.md +11 -6
- package/skills/velloo-setup/SKILL.md +179 -36
- package/canvas/assets/index-BQ5710lu.js +0 -29
- package/canvas/assets/index-GNLd0pCG.css +0 -2
- package/canvas/assets/lucide-all-DIRZdJPK.js +0 -1
- package/chunk-brnkprt5.js +0 -723
- package/chunk-bys7pqdb.js +0 -2
- package/chunk-g8xrwjvq.js +0 -22
- package/chunk-jbgde2v9.js +0 -2
- package/chunk-rxkvjyst.js +0 -18
- package/chunk-sjjnm4a1.js +0 -2
- package/chunk-snak5zpr.js +0 -5
package/pkgs/schema/src/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/pkgs/schema/src/node.ts
CHANGED
|
@@ -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 })
|
|
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 })
|
|
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 })
|
|
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 })
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,
|
package/pkgs/schema/src/theme.ts
CHANGED
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
package/plugins/gemini/GEMINI.md
CHANGED
|
@@ -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
|
|
23
|
-
`
|
|
24
|
-
|
|
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,
|
|
30
|
-
system, logo) —
|
|
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
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|