velloo 0.0.0 → 0.1.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 +91 -0
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +262 -2
- package/THIRD-PARTY-NOTICES.md +467 -0
- package/assets/elsewhere-alps-9fqhv029.jpg +0 -0
- package/assets/elsewhere-bali-vp60g115.jpg +0 -0
- package/assets/elsewhere-forest-xgn23zrj.jpg +0 -0
- package/assets/elsewhere-japan-qs7qkbr2.jpg +0 -0
- package/assets/elsewhere-kyoto-map-n2bvkrya.svg +1 -0
- package/assets/elsewhere-kyoto-qemtn6nk.jpg +0 -0
- package/assets/elsewhere-room-g1gaw7cs.jpg +0 -0
- package/canvas/apple-touch-icon.png +0 -0
- package/canvas/assets/index-BCvDZiBo.css +2 -0
- package/canvas/assets/index-BcBJ11d2.js +28 -0
- package/canvas/assets/lucide-all-BdEyryfF.js +1 -0
- package/canvas/assets/radix-C3HaOB-d.js +41 -0
- package/canvas/assets/react-D0Ec-185.js +9 -0
- package/canvas/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/canvas/assets/vendor-CKmQ-Zst.js +53 -0
- package/canvas/favicon.ico +0 -0
- package/canvas/favicon.svg +12 -0
- package/canvas/fonts/OFL-1.1.txt +93 -0
- package/canvas/fonts/poppins-700.woff2 +0 -0
- package/canvas/icon-192.png +0 -0
- package/canvas/icon-512.png +0 -0
- package/canvas/index.html +22 -0
- package/canvas/site.webmanifest +11 -0
- package/chunk-0t68szgr.js +3 -0
- package/chunk-1bywxgc5.js +4 -0
- package/chunk-1gx45384.js +41 -0
- package/chunk-1y50g9vp.js +2 -0
- package/chunk-1yhc252b.js +3 -0
- package/chunk-2mv65qxs.js +5 -0
- package/chunk-2smsg2ct.js +7 -0
- package/chunk-2vx420g2.js +442 -0
- package/chunk-2z0k59hz.js +3 -0
- package/chunk-329e3phg.js +451 -0
- package/chunk-40657ggd.js +10 -0
- package/chunk-56vqqr8s.js +3 -0
- package/chunk-582y60bv.js +2 -0
- package/chunk-5js0qw32.js +2 -0
- package/chunk-5x173xay.js +2 -0
- package/chunk-65wb0g7d.js +3 -0
- package/chunk-6tx68d90.js +3 -0
- package/chunk-74skmg76.js +2 -0
- package/chunk-75wbh438.js +2 -0
- package/chunk-79ykzywe.js +4 -0
- package/chunk-7dggmqwq.js +53 -0
- package/chunk-7e39xe21.js +18 -0
- package/chunk-7p7ha0qk.js +3 -0
- package/chunk-8ssr8a8w.js +89 -0
- package/chunk-946pcnt5.js +5 -0
- package/chunk-9vg3yafz.js +3 -0
- package/chunk-aapybh30.js +6 -0
- package/chunk-b2ebm1t3.js +303 -0
- package/chunk-b7j86xq7.js +4 -0
- package/chunk-bjnhhsak.js +2 -0
- package/chunk-bk8b7f9a.js +62 -0
- package/chunk-cep3ktr3.js +4 -0
- package/chunk-cxe6jjwj.js +3 -0
- package/chunk-d385435m.js +8 -0
- package/chunk-dt03awr1.js +2 -0
- package/chunk-e2ag33vd.js +226 -0
- package/chunk-erwfk47h.js +3 -0
- package/chunk-fgw3hp2v.js +3 -0
- package/chunk-fjbzk4sw.js +3 -0
- package/chunk-g02nd9hn.js +4 -0
- package/chunk-h0c36khx.js +2 -0
- package/chunk-h6gchare.js +3 -0
- package/chunk-hggap8fq.js +3 -0
- package/chunk-hs2j6s1v.js +75 -0
- package/chunk-jnsfgtn9.js +3 -0
- package/chunk-jrsknzjt.js +3 -0
- package/chunk-k6ferbam.js +4 -0
- package/chunk-k784090q.js +6 -0
- package/chunk-kxk5x7g6.js +3 -0
- package/chunk-mq4znqv2.js +2 -0
- package/chunk-mzhdrawd.js +3 -0
- package/chunk-n5007jcz.js +2 -0
- package/chunk-nfr3q7mg.js +5 -0
- package/chunk-nsqs1bqf.js +2 -0
- package/chunk-nwx5700b.js +3 -0
- package/chunk-pphgrsxa.js +2 -0
- package/chunk-qpvj6kzq.js +6 -0
- package/chunk-qwv7xkms.js +2 -0
- package/chunk-rksq3w72.js +244 -0
- package/chunk-s23gtejf.js +3 -0
- package/chunk-t2ttkxe9.js +85 -0
- package/chunk-tmtzxcs5.js +2 -0
- package/chunk-vbk1qhdz.js +3 -0
- package/chunk-w5rzk6ms.js +1229 -0
- package/chunk-xr6f94rj.js +2 -0
- package/chunk-y0zxzefg.js +5 -0
- package/chunk-zf3xc1wz.js +2 -0
- package/cli.js +3 -0
- package/launcher.cjs +97 -0
- package/package.json +58 -5
- package/pkgs/helpers/src/box.tsx +21 -0
- package/pkgs/helpers/src/cn.ts +32 -0
- package/pkgs/helpers/src/descriptors.ts +270 -0
- package/pkgs/helpers/src/divider.tsx +123 -0
- package/pkgs/helpers/src/gradient.tsx +90 -0
- package/pkgs/helpers/src/heading.tsx +20 -0
- package/pkgs/helpers/src/icon-data.ts +8009 -0
- package/pkgs/helpers/src/icon.tsx +53 -0
- package/pkgs/helpers/src/image.tsx +111 -0
- package/pkgs/helpers/src/index.ts +29 -0
- package/pkgs/helpers/src/layer.tsx +61 -0
- package/pkgs/helpers/src/lowering.ts +53 -0
- package/pkgs/helpers/src/paths.ts +47 -0
- package/pkgs/helpers/src/placeholder.tsx +88 -0
- package/pkgs/helpers/src/prose.tsx +42 -0
- package/pkgs/helpers/src/registry.ts +41 -0
- package/pkgs/helpers/src/svg.tsx +62 -0
- package/pkgs/helpers/src/text.tsx +16 -0
- package/pkgs/provider-antd/src/index.ts +78 -0
- package/pkgs/provider-antd/src/intro.ts +14 -0
- package/pkgs/provider-antd/src/manifest.ts +439 -0
- package/pkgs/provider-antd/src/overlays.ts +176 -0
- package/pkgs/provider-antd/src/registry.ts +104 -0
- package/pkgs/provider-antd/src/render-pass.ts +43 -0
- package/pkgs/provider-antd/src/tailwind-entry.css +21 -0
- package/pkgs/provider-antd/src/theme.ts +132 -0
- package/pkgs/provider-antd/src/version.ts +8 -0
- package/pkgs/provider-chakra/src/index.ts +80 -0
- package/pkgs/provider-chakra/src/intro.ts +14 -0
- package/pkgs/provider-chakra/src/manifest.ts +421 -0
- package/pkgs/provider-chakra/src/overlays.ts +246 -0
- package/pkgs/provider-chakra/src/registry.ts +227 -0
- package/pkgs/provider-chakra/src/render-pass.ts +55 -0
- package/pkgs/provider-chakra/src/tailwind-entry.css +21 -0
- package/pkgs/provider-chakra/src/theme.ts +180 -0
- package/pkgs/provider-chakra/src/version.ts +8 -0
- package/pkgs/provider-mui/src/index.ts +117 -0
- package/pkgs/provider-mui/src/intro.ts +12 -0
- package/pkgs/provider-mui/src/manifest.ts +263 -0
- package/pkgs/provider-mui/src/overlays.ts +124 -0
- package/pkgs/provider-mui/src/registry.ts +120 -0
- package/pkgs/provider-mui/src/render-pass.ts +36 -0
- package/pkgs/provider-mui/src/tailwind-entry.css +21 -0
- package/pkgs/provider-mui/src/theme.ts +129 -0
- package/pkgs/provider-mui/src/version.ts +8 -0
- package/pkgs/provider-none/src/components-inline.tsx +204 -0
- package/pkgs/provider-none/src/components.tsx +155 -0
- package/pkgs/provider-none/src/index.ts +57 -0
- package/pkgs/provider-none/src/intro.ts +18 -0
- package/pkgs/provider-none/src/manifest.ts +163 -0
- package/pkgs/provider-none/src/registry-inline.ts +36 -0
- package/pkgs/provider-none/src/registry.ts +39 -0
- package/pkgs/provider-none/src/tailwind-entry.css +57 -0
- package/pkgs/provider-none/src/version.ts +10 -0
- package/pkgs/schema/src/annotation.ts +120 -0
- package/pkgs/schema/src/asset.ts +66 -0
- package/pkgs/schema/src/board.ts +71 -0
- package/pkgs/schema/src/comment.ts +107 -0
- package/pkgs/schema/src/config.ts +178 -0
- package/pkgs/schema/src/css-sanitize.ts +56 -0
- package/pkgs/schema/src/extension.ts +105 -0
- package/pkgs/schema/src/fonts.ts +690 -0
- package/pkgs/schema/src/frame.ts +40 -0
- package/pkgs/schema/src/icon-name.ts +21 -0
- package/pkgs/schema/src/ids.ts +14 -0
- package/pkgs/schema/src/index.ts +161 -0
- package/pkgs/schema/src/migrate.ts +244 -0
- package/pkgs/schema/src/node.ts +229 -0
- package/pkgs/schema/src/paths.ts +28 -0
- package/pkgs/schema/src/repo.ts +52 -0
- package/pkgs/schema/src/screen.ts +30 -0
- package/pkgs/schema/src/snippet-resolve.ts +193 -0
- package/pkgs/schema/src/snippet.ts +104 -0
- package/pkgs/schema/src/svg-sanitize.ts +74 -0
- package/pkgs/schema/src/theme.ts +218 -0
- package/pkgs/schema/src/typeset.ts +693 -0
- package/pkgs/schema/src/validate-ids.ts +46 -0
- package/pkgs/schema/src/viewport.ts +8 -0
- package/pkgs/shadcn-snapshot/dist/manifest.json +8109 -0
- package/pkgs/shadcn-snapshot/src/components/canvas-portal.tsx +55 -0
- package/pkgs/shadcn-snapshot/src/components/ui/accordion.tsx +122 -0
- package/pkgs/shadcn-snapshot/src/components/ui/alert-dialog.tsx +164 -0
- package/pkgs/shadcn-snapshot/src/components/ui/alert.tsx +74 -0
- package/pkgs/shadcn-snapshot/src/components/ui/aspect-ratio.tsx +13 -0
- package/pkgs/shadcn-snapshot/src/components/ui/attachment.tsx +197 -0
- package/pkgs/shadcn-snapshot/src/components/ui/avatar.tsx +100 -0
- package/pkgs/shadcn-snapshot/src/components/ui/badge.tsx +50 -0
- package/pkgs/shadcn-snapshot/src/components/ui/breadcrumb.tsx +109 -0
- package/pkgs/shadcn-snapshot/src/components/ui/bubble.tsx +130 -0
- package/pkgs/shadcn-snapshot/src/components/ui/button-group.tsx +82 -0
- package/pkgs/shadcn-snapshot/src/components/ui/button.tsx +72 -0
- package/pkgs/shadcn-snapshot/src/components/ui/calendar.tsx +229 -0
- package/pkgs/shadcn-snapshot/src/components/ui/card.tsx +92 -0
- package/pkgs/shadcn-snapshot/src/components/ui/carousel.tsx +235 -0
- package/pkgs/shadcn-snapshot/src/components/ui/chart-option.ts +250 -0
- package/pkgs/shadcn-snapshot/src/components/ui/chart.tsx +177 -0
- package/pkgs/shadcn-snapshot/src/components/ui/checkbox.tsx +41 -0
- package/pkgs/shadcn-snapshot/src/components/ui/collapsible.tsx +32 -0
- package/pkgs/shadcn-snapshot/src/components/ui/combobox.tsx +196 -0
- package/pkgs/shadcn-snapshot/src/components/ui/context-menu.tsx +227 -0
- package/pkgs/shadcn-snapshot/src/components/ui/dialog.tsx +131 -0
- package/pkgs/shadcn-snapshot/src/components/ui/direction.tsx +24 -0
- package/pkgs/shadcn-snapshot/src/components/ui/drawer.tsx +112 -0
- package/pkgs/shadcn-snapshot/src/components/ui/dropdown-menu.tsx +205 -0
- package/pkgs/shadcn-snapshot/src/components/ui/empty.tsx +98 -0
- package/pkgs/shadcn-snapshot/src/components/ui/field.tsx +228 -0
- package/pkgs/shadcn-snapshot/src/components/ui/hover-card.tsx +51 -0
- package/pkgs/shadcn-snapshot/src/components/ui/input-group.tsx +148 -0
- package/pkgs/shadcn-snapshot/src/components/ui/input.tsx +43 -0
- package/pkgs/shadcn-snapshot/src/components/ui/item.tsx +187 -0
- package/pkgs/shadcn-snapshot/src/components/ui/kbd.tsx +30 -0
- package/pkgs/shadcn-snapshot/src/components/ui/label.tsx +25 -0
- package/pkgs/shadcn-snapshot/src/components/ui/marker.tsx +71 -0
- package/pkgs/shadcn-snapshot/src/components/ui/menubar.tsx +243 -0
- package/pkgs/shadcn-snapshot/src/components/ui/message.tsx +89 -0
- package/pkgs/shadcn-snapshot/src/components/ui/native-select.tsx +60 -0
- package/pkgs/shadcn-snapshot/src/components/ui/navigation-menu.tsx +165 -0
- package/pkgs/shadcn-snapshot/src/components/ui/pagination.tsx +117 -0
- package/pkgs/shadcn-snapshot/src/components/ui/popover.tsx +66 -0
- package/pkgs/shadcn-snapshot/src/components/ui/progress.tsx +35 -0
- package/pkgs/shadcn-snapshot/src/components/ui/radio-group.tsx +48 -0
- package/pkgs/shadcn-snapshot/src/components/ui/scroll-area.tsx +59 -0
- package/pkgs/shadcn-snapshot/src/components/ui/select.tsx +180 -0
- package/pkgs/shadcn-snapshot/src/components/ui/separator.tsx +32 -0
- package/pkgs/shadcn-snapshot/src/components/ui/sheet.tsx +107 -0
- package/pkgs/shadcn-snapshot/src/components/ui/skeleton.tsx +17 -0
- package/pkgs/shadcn-snapshot/src/components/ui/slider.tsx +68 -0
- package/pkgs/shadcn-snapshot/src/components/ui/sonner.tsx +91 -0
- package/pkgs/shadcn-snapshot/src/components/ui/spinner.tsx +21 -0
- package/pkgs/shadcn-snapshot/src/components/ui/switch.tsx +41 -0
- package/pkgs/shadcn-snapshot/src/components/ui/table.tsx +93 -0
- package/pkgs/shadcn-snapshot/src/components/ui/tabs.tsx +84 -0
- package/pkgs/shadcn-snapshot/src/components/ui/textarea.tsx +24 -0
- package/pkgs/shadcn-snapshot/src/components/ui/toggle-group.tsx +94 -0
- package/pkgs/shadcn-snapshot/src/components/ui/toggle.tsx +50 -0
- package/pkgs/shadcn-snapshot/src/components/ui/tooltip.tsx +47 -0
- package/pkgs/shadcn-snapshot/src/examples.ts +36 -0
- package/pkgs/shadcn-snapshot/src/groups.ts +87 -0
- package/pkgs/shadcn-snapshot/src/index.ts +43 -0
- package/pkgs/shadcn-snapshot/src/lib/utils.ts +4 -0
- package/pkgs/shadcn-snapshot/src/manifest.ts +12 -0
- package/pkgs/shadcn-snapshot/src/notes.ts +97 -0
- package/pkgs/shadcn-snapshot/src/paths.ts +47 -0
- package/pkgs/shadcn-snapshot/src/registry-files.ts +293 -0
- package/pkgs/shadcn-snapshot/src/registry.ts +631 -0
- package/pkgs/shadcn-snapshot/src/shadcn-tailwind.css +632 -0
- package/pkgs/shadcn-snapshot/src/tailwind-entry.css +159 -0
- package/pkgs/shadcn-snapshot/src/version.ts +8 -0
- package/plugins/claude/velloo/.claude-plugin/plugin.json +6 -0
- package/plugins/claude/velloo/agents/velloo-design-reviewer.md +33 -0
- package/plugins/claude/velloo/agents/velloo-designer.md +39 -0
- package/plugins/claude/velloo/commands/design.md +11 -0
- package/plugins/claude/velloo/commands/implement.md +10 -0
- package/plugins/claude/velloo/commands/review.md +8 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/commands/velloo/design.toml +10 -0
- package/plugins/gemini/commands/velloo/implement.toml +9 -0
- package/plugins/gemini/gemini-extension.json +9 -0
- package/skills/velloo-brand/SKILL.md +124 -0
- package/skills/velloo-design/SKILL.md +144 -0
- package/skills/velloo-design-system/SKILL.md +90 -0
- package/skills/velloo-implement/SKILL.md +101 -0
- package/skills/velloo-logo/SKILL.md +93 -0
- package/skills/velloo-setup/SKILL.md +156 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A node tree is a discriminated union over three shapes, distinguished by
|
|
5
|
+
* which `$`-prefixed key is present:
|
|
6
|
+
*
|
|
7
|
+
* - `ComponentNode` — a real shadcn / velloo component (`{ $ref }`).
|
|
8
|
+
* - `SnippetInstance` — an instantiation of a snippet defined in `snippets/`
|
|
9
|
+
* (`{ $snippet, args }`). From the page's POV the instance is opaque —
|
|
10
|
+
* path navigation stops at the instance; you can edit its args but not
|
|
11
|
+
* descend into the snippet body.
|
|
12
|
+
* - `ParamRef` — `{ $param: name }`. Valid only inside a snippet body;
|
|
13
|
+
* substituted with the corresponding instance arg at render time. May
|
|
14
|
+
* appear as a child node OR anywhere inside a `props` value via the
|
|
15
|
+
* normal JSON walk during substitution.
|
|
16
|
+
*
|
|
17
|
+
* Optional fields are spelled `?: T | undefined` rather than `?: T`. Under
|
|
18
|
+
* `exactOptionalPropertyTypes` those differ, but not for a shape that round-
|
|
19
|
+
* trips through JSON: `JSON.stringify` drops an explicitly-undefined key, so
|
|
20
|
+
* "absent" and "present but undefined" are the same node on disk. Writing it
|
|
21
|
+
* this way lets the zod schemas below (whose `.optional()` yields
|
|
22
|
+
* `T | undefined`) describe these types exactly.
|
|
23
|
+
*
|
|
24
|
+
* `ComponentNode` and `SnippetInstance` may carry an optional `$id` —
|
|
25
|
+
* a stable anchor that survives sibling insertions and deletions. Ids
|
|
26
|
+
* are unique within a single screen tree (validated at persist time).
|
|
27
|
+
* Agents address `$id`-bearing nodes via the locator form `"@id"` in
|
|
28
|
+
* place of a path array.
|
|
29
|
+
*
|
|
30
|
+
* Schema-level validation accepts all three at every Node position. Screen
|
|
31
|
+
* trees are expected to contain only `ComponentNode | SnippetInstance`;
|
|
32
|
+
* `ParamRef`s in a screen tree fail at substitution time rather than at
|
|
33
|
+
* parse time so the schema stays simple.
|
|
34
|
+
*/
|
|
35
|
+
export type ComponentNode = {
|
|
36
|
+
$ref: string;
|
|
37
|
+
$id?: string | undefined;
|
|
38
|
+
props?: Record<string, unknown> | undefined;
|
|
39
|
+
children?: Node[] | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Host-component facade (scan/import of a host-app component). When
|
|
42
|
+
* set, the canvas renders this node's real
|
|
43
|
+
* velloo subtree (the design agent's faithful approximation of a scanned app
|
|
44
|
+
* component it can't map to a primitive — SSR-only, data-bound, bespoke), but
|
|
45
|
+
* `emit_code` emits `<name />` imported from `importPath` *instead* of the
|
|
46
|
+
* subtree — preserving the app's real component identity through
|
|
47
|
+
* capture → design → emit. Absent ⇒ the node emits as itself.
|
|
48
|
+
*/
|
|
49
|
+
$emitAs?: { name: string; importPath: string } | undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SnippetInstance = {
|
|
53
|
+
$snippet: string;
|
|
54
|
+
$id?: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Extra Tailwind classes merged into the snippet body's root element at
|
|
57
|
+
* render time. Lets one-off instances tweak styling (e.g. wider, accent
|
|
58
|
+
* border) without forking the snippet definition. Pass it via
|
|
59
|
+
* a snippet tag's `className` in `compose`, or `update_snippet_instance`.
|
|
60
|
+
*/
|
|
61
|
+
$extraClassName?: string | undefined;
|
|
62
|
+
args?: Record<string, unknown> | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Per-instance interior prop patches, keyed by dotted path into the
|
|
65
|
+
* *resolved* snippet body ("" = root, "0.2" = third child of root's
|
|
66
|
+
* first child). Each patch shallow-merges over the body node's props
|
|
67
|
+
* at render time. The escape hatch for "this one instance needs its
|
|
68
|
+
* badge red" without forking the snippet — instances carrying
|
|
69
|
+
* overrides are inlined (not emitted as the shared component) by
|
|
70
|
+
* emit_code, since a shared React component can't express them.
|
|
71
|
+
*/
|
|
72
|
+
$overrides?: Record<string, { props: Record<string, unknown> }> | undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ParamRef = {
|
|
76
|
+
$param: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type Node = ComponentNode | SnippetInstance | ParamRef;
|
|
80
|
+
|
|
81
|
+
export function isComponentNode(n: Node): n is ComponentNode {
|
|
82
|
+
return typeof (n as { $ref?: unknown }).$ref === "string";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isSnippetInstance(n: Node): n is SnippetInstance {
|
|
86
|
+
return typeof (n as { $snippet?: unknown }).$snippet === "string";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isParamRef(n: Node): n is ParamRef {
|
|
90
|
+
return typeof (n as { $param?: unknown }).$param === "string";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format constraint for `$id` values. Must start with a letter; the rest
|
|
95
|
+
* is letters / digits / dashes / underscores. Matches typical anchor
|
|
96
|
+
* naming (`hero-cta`, `feature_card_3`, `nav`). The leading-letter rule
|
|
97
|
+
* keeps ids from colliding with numeric path indices in the locator
|
|
98
|
+
* parser if we ever support compound locators.
|
|
99
|
+
*/
|
|
100
|
+
export const NodeIdSchema = z
|
|
101
|
+
.string()
|
|
102
|
+
.min(1)
|
|
103
|
+
.max(64)
|
|
104
|
+
.regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, {
|
|
105
|
+
message: "node id must match /^[a-zA-Z][a-zA-Z0-9_-]*$/",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A bare string/number in a `children` array is a common first-try shape
|
|
110
|
+
* ("just put the label here"). Rather than reject it — `children` renders
|
|
111
|
+
* nodes only — auto-wrap it into an inline `Box as="span"` so it renders as
|
|
112
|
+
* text without stacking. Wrapping (vs. allowing scalars through) keeps
|
|
113
|
+
* `children` typed `Node[]` for every downstream consumer. The styled-run
|
|
114
|
+
* idiom (a mixed array in the `children` *prop*) is still preferred for rich
|
|
115
|
+
* text; this just removes a needless failure.
|
|
116
|
+
*/
|
|
117
|
+
function wrapScalarChild(item: string | number | Node): Node {
|
|
118
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
119
|
+
return { $ref: "Box", props: { as: "span", children: item } };
|
|
120
|
+
}
|
|
121
|
+
return item;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const EmitAsSchema = z.object({
|
|
125
|
+
name: z.string().min(1),
|
|
126
|
+
importPath: z.string().min(1),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const ComponentNodeSchema: z.ZodType<ComponentNode> = z.lazy(() =>
|
|
130
|
+
z.object({
|
|
131
|
+
$ref: z.string().min(1),
|
|
132
|
+
$id: NodeIdSchema.optional(),
|
|
133
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
134
|
+
children: z
|
|
135
|
+
.array(z.union([z.string(), z.number(), NodeSchema]))
|
|
136
|
+
.transform((items) => items.map(wrapScalarChild))
|
|
137
|
+
.optional(),
|
|
138
|
+
$emitAs: EmitAsSchema.optional(),
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const SnippetInstanceSchema: z.ZodType<SnippetInstance> = z.object({
|
|
143
|
+
$snippet: z.string().min(1),
|
|
144
|
+
$id: NodeIdSchema.optional(),
|
|
145
|
+
$extraClassName: z.string().optional(),
|
|
146
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
147
|
+
$overrides: z
|
|
148
|
+
.record(
|
|
149
|
+
z.string().regex(/^$|^\d+(\.\d+)*$|^@[a-zA-Z][a-zA-Z0-9_-]*$/, {
|
|
150
|
+
message:
|
|
151
|
+
'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',
|
|
152
|
+
}),
|
|
153
|
+
z.object({ props: z.record(z.string(), z.unknown()) }),
|
|
154
|
+
)
|
|
155
|
+
.optional(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const ParamRefSchema: z.ZodType<ParamRef> = z.object({
|
|
159
|
+
$param: z.string().min(1),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Key-routed parse instead of `z.union`: a malformed node yields the
|
|
164
|
+
* issues of the *one* branch its `$`-key selects (with a full path),
|
|
165
|
+
* not a three-branch union explosion. The MCP layer surfaces these
|
|
166
|
+
* issues verbatim to agents, so error shape is part of the tool UX.
|
|
167
|
+
*/
|
|
168
|
+
export const NodeSchema: z.ZodType<Node> = z
|
|
169
|
+
.unknown()
|
|
170
|
+
.transform((val, ctx): Node => {
|
|
171
|
+
if (val === null || typeof val !== "object" || Array.isArray(val)) {
|
|
172
|
+
ctx.addIssue({
|
|
173
|
+
code: "custom",
|
|
174
|
+
message:
|
|
175
|
+
'node must be an object with one of "$ref" (component), "$snippet" (instance), or "$param" (param ref)',
|
|
176
|
+
});
|
|
177
|
+
return z.NEVER;
|
|
178
|
+
}
|
|
179
|
+
const v = val as Record<string, unknown>;
|
|
180
|
+
const branch =
|
|
181
|
+
typeof v.$ref === "string"
|
|
182
|
+
? ComponentNodeSchema
|
|
183
|
+
: typeof v.$snippet === "string"
|
|
184
|
+
? SnippetInstanceSchema
|
|
185
|
+
: typeof v.$param === "string"
|
|
186
|
+
? ParamRefSchema
|
|
187
|
+
: null;
|
|
188
|
+
if (!branch) {
|
|
189
|
+
ctx.addIssue({
|
|
190
|
+
code: "custom",
|
|
191
|
+
message:
|
|
192
|
+
'node needs exactly one of "$ref" (component), "$snippet" (snippet instance), or "$param" (param ref, snippet bodies only)',
|
|
193
|
+
});
|
|
194
|
+
return z.NEVER;
|
|
195
|
+
}
|
|
196
|
+
const parsed = branch.safeParse(val);
|
|
197
|
+
if (!parsed.success) {
|
|
198
|
+
for (const issue of parsed.error.issues) {
|
|
199
|
+
ctx.addIssue({ ...issue });
|
|
200
|
+
}
|
|
201
|
+
return z.NEVER;
|
|
202
|
+
}
|
|
203
|
+
return parsed.data;
|
|
204
|
+
})
|
|
205
|
+
// Kept short on purpose: this string is inlined into five tools' schemas, so
|
|
206
|
+
// every character is paid five times by every session. The full shapes —
|
|
207
|
+
// $overrides, $extraClassName, $if, param placement — are in
|
|
208
|
+
// velloo://guide/components and velloo://guide/snippets.
|
|
209
|
+
.describe(
|
|
210
|
+
'{"$ref":"Button","$id?":"cta","props?":{...},"children?":[Node]}, or {"$snippet":"<id>","args?":{...}}. Guide: velloo://guide/components',
|
|
211
|
+
)
|
|
212
|
+
// `z.unknown()` emits a JSON Schema with no `type`, so strict MCP clients
|
|
213
|
+
// can't tell `tree`/`children` params are objects and serialize them as
|
|
214
|
+
// strings — the server then rejects a valid `{"$ref":"Box"}`. The runtime
|
|
215
|
+
// transform above still does the real validation; this only annotates the
|
|
216
|
+
// emitted schema so clients send objects. Keep it as `additionalProperties:
|
|
217
|
+
// true` (any object) rather than the full union — minimal and permissive.
|
|
218
|
+
// Unavoidable cast: `.transform().meta()` erases the recursive output type
|
|
219
|
+
// (ZodPipe of unknown), so reassert the declared `z.ZodType<Node>`.
|
|
220
|
+
.meta({ type: "object", additionalProperties: true }) as unknown as z.ZodType<Node>;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read the `$id` of a node, if any. Convenience wrapper so callers don't
|
|
224
|
+
* have to narrow by node kind first.
|
|
225
|
+
*/
|
|
226
|
+
export function nodeId(n: Node): string | undefined {
|
|
227
|
+
if (isParamRef(n)) return undefined;
|
|
228
|
+
return (n as { $id?: string }).$id;
|
|
229
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Node-only module — exported as `@velloo/schema/paths`, deliberately NOT from
|
|
2
|
+
// the package index: the index is compiled into browser bundles where `node:*`
|
|
3
|
+
// imports fail. Mirrors `@velloo/helpers/paths`.
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Absolute path to the schema sources. The canvas bundler resolves the
|
|
12
|
+
* zero-dependency leaf modules the velloo helper components import
|
|
13
|
+
* (`typeset`, `icon-name`, `svg-sanitize`) from here, because the installed
|
|
14
|
+
* binary inlines `@velloo/*` into `cli.js` — a runtime `Bun.build` of the
|
|
15
|
+
* shipped `.tsx` under `dist/pkgs` has no node_modules to walk up into.
|
|
16
|
+
* Resolves from source (`here` = this `src/` dir) and from the bundled CLI,
|
|
17
|
+
* where the CLI's `build.ts` copies these sources to `<dist>/pkgs/schema/src`.
|
|
18
|
+
*/
|
|
19
|
+
function resolveDir(): string {
|
|
20
|
+
const candidates = [
|
|
21
|
+
process.env.VELLOO_SCHEMA_SRC,
|
|
22
|
+
join(here, "pkgs", "schema", "src"),
|
|
23
|
+
here,
|
|
24
|
+
].filter((p): p is string => Boolean(p));
|
|
25
|
+
return candidates.find((d) => existsSync(join(d, "typeset.ts"))) ?? here;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const schemaSrcDir: string = resolveDir();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Consent for the `send_feedback` tool.
|
|
5
|
+
*
|
|
6
|
+
* `enabled` is a repo decision (committed in `velloo.json`) — whether the tool
|
|
7
|
+
* exists for this project at all. `contactOk` records consent to be *contacted*
|
|
8
|
+
* about what was sent, which is personal: it is read from and written to the
|
|
9
|
+
* machine's `~/.velloo/prefs.json`, never committed, so cloning a repo can't
|
|
10
|
+
* opt a different person into being emailed. It stays in this shape because
|
|
11
|
+
* every reader wants the pair, and because folders written before the split
|
|
12
|
+
* still carry it on disk (where it is now ignored).
|
|
13
|
+
*/
|
|
14
|
+
export const FeedbackPrefsSchema = z.object({
|
|
15
|
+
enabled: z.boolean(),
|
|
16
|
+
contactOk: z.boolean().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type FeedbackPrefs = z.infer<typeof FeedbackPrefsSchema>;
|
|
20
|
+
|
|
21
|
+
const PROJECT_NAME = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The repo-root `velloo.json`. It names a repo's design folders so a monorepo
|
|
25
|
+
* can hold several, and carries the handful of preferences that belong to the
|
|
26
|
+
* repo rather than to any one design folder — feedback consent is answered
|
|
27
|
+
* once per person, not once per canvas.
|
|
28
|
+
*
|
|
29
|
+
* Design settings still live in each folder's `.design/config.json`; nothing
|
|
30
|
+
* here duplicates that contract.
|
|
31
|
+
*
|
|
32
|
+
* Every project is a path to a design folder inside the repository. A design
|
|
33
|
+
* kept outside it is recorded only on the machine that has it, never here.
|
|
34
|
+
*/
|
|
35
|
+
export const RepoManifestSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
$schema: z.string().optional(),
|
|
38
|
+
projects: z.record(
|
|
39
|
+
z.string().regex(PROJECT_NAME, "project names are letters/digits plus . _ -"),
|
|
40
|
+
z.string().min(1),
|
|
41
|
+
),
|
|
42
|
+
defaultProject: z.string().optional(),
|
|
43
|
+
feedback: FeedbackPrefsSchema.optional(),
|
|
44
|
+
})
|
|
45
|
+
.refine((m) => !m.defaultProject || m.defaultProject in m.projects, {
|
|
46
|
+
message: "defaultProject must name an entry in projects",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type RepoManifest = z.infer<typeof RepoManifestSchema>;
|
|
50
|
+
|
|
51
|
+
/** Filename of the repo-root manifest, resolved by walking up from a folder. */
|
|
52
|
+
export const REPO_MANIFEST_FILE = "velloo.json";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ResourceIdSchema } from "./ids.ts";
|
|
3
|
+
import { NodeSchema } from "./node.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A Screen is one composition: a single responsive React tree built from
|
|
7
|
+
* the design folder's library. Lives at `screens/<id>.json`.
|
|
8
|
+
*
|
|
9
|
+
* One screen → one tree. Different viewport renderings are not separate
|
|
10
|
+
* screens — they're separate Frames on the Board pointing at the same
|
|
11
|
+
* screen. Edits to the tree propagate to every frame of that screen.
|
|
12
|
+
*
|
|
13
|
+
* If a layout truly diverges per breakpoint (e.g. drawer vs sidebar nav),
|
|
14
|
+
* the user/agent creates a *second* screen and a second frame. Explicit
|
|
15
|
+
* fork, no background sync.
|
|
16
|
+
*/
|
|
17
|
+
export const ScreenSchema = z.object({
|
|
18
|
+
id: ResourceIdSchema,
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
/**
|
|
21
|
+
* Library id (key in `Config.libraries`) this screen renders against.
|
|
22
|
+
* Optional — when absent the folder's `defaultLibrary` is used.
|
|
23
|
+
* Multi-library per folder; one library per screen. A screen's
|
|
24
|
+
* components and extensions resolve against this library's registry.
|
|
25
|
+
*/
|
|
26
|
+
library: z.string().min(1).optional(),
|
|
27
|
+
tree: NodeSchema,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type Screen = z.infer<typeof ScreenSchema>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { isComponentNode, isParamRef, isSnippetInstance, type Node } from "./node.ts";
|
|
2
|
+
import type { Snippet } from "./snippet.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure snippet-body resolution helpers shared by the renderer (render
|
|
6
|
+
* time) and codegen (inlining instances that carry `$overrides`). No
|
|
7
|
+
* throwing — callers wrap missing-param reports in their own error
|
|
8
|
+
* vocabulary (renderer throws SnippetParamError, codegen returns a
|
|
9
|
+
* CodegenError).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An optional param supplied no value and no default — resolves to "nothing".
|
|
14
|
+
* A node slot bearing it is dropped (renders/emits nothing); a prop bearing it
|
|
15
|
+
* is omitted. Internal sentinel: never appears in a persisted tree.
|
|
16
|
+
*/
|
|
17
|
+
const OMITTED: unique symbol = Symbol("velloo.omitted-optional-param");
|
|
18
|
+
|
|
19
|
+
/** Substitution result for an OMITTED value — pruned from arrays and prop objects. */
|
|
20
|
+
const DROP: unique symbol = Symbol("velloo.drop");
|
|
21
|
+
|
|
22
|
+
/** Resolve declared params against passed args, applying defaults. */
|
|
23
|
+
export function resolveSnippetArgs(
|
|
24
|
+
snippet: Snippet,
|
|
25
|
+
passed: Record<string, unknown>,
|
|
26
|
+
): { args: Record<string, unknown>; missing: string[] } {
|
|
27
|
+
const args: Record<string, unknown> = {};
|
|
28
|
+
const missing: string[] = [];
|
|
29
|
+
for (const param of snippet.params) {
|
|
30
|
+
if (param.name in passed) {
|
|
31
|
+
args[param.name] = passed[param.name];
|
|
32
|
+
} else if (param.default !== undefined) {
|
|
33
|
+
args[param.name] = param.default;
|
|
34
|
+
} else if (param.optional) {
|
|
35
|
+
args[param.name] = OMITTED;
|
|
36
|
+
} else {
|
|
37
|
+
missing.push(param.name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { args, missing };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTruthy(v: unknown): boolean {
|
|
44
|
+
if (v === undefined || v === null || v === OMITTED) return false;
|
|
45
|
+
if (typeof v === "boolean") return v;
|
|
46
|
+
if (typeof v === "number") return v !== 0 && !Number.isNaN(v);
|
|
47
|
+
if (typeof v === "string") return v !== "";
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A `$param` that substituted to a scalar where a child node was expected. */
|
|
52
|
+
export interface InvalidParamPlacement {
|
|
53
|
+
param: string;
|
|
54
|
+
/** typeof the resolved value (or "null") — for a precise error message. */
|
|
55
|
+
valueType: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively rewrite snippet body values:
|
|
60
|
+
* - `{ $param: "name" }` → `args.name`
|
|
61
|
+
* - `{ $if: "name", then, else }` → branch by truthiness
|
|
62
|
+
* - `{ $if: "name", eq, then, else }` → branch by strict equality
|
|
63
|
+
* Unknown param names are collected in `missing` (the offending
|
|
64
|
+
* substitution resolves to undefined) rather than thrown.
|
|
65
|
+
*
|
|
66
|
+
* `invalid` collects scalar params dropped into a *node* position — a
|
|
67
|
+
* component node's own `children` array, where only subtrees render. This
|
|
68
|
+
* is the classic mis-wire (a `string` param used as a child instead of as
|
|
69
|
+
* a prop value); flagging it lets the renderer name the param and point at
|
|
70
|
+
* the fix instead of failing opaquely deep in React. A `children` *prop*
|
|
71
|
+
* value (`props.children`) is plain content, not a node position, so a
|
|
72
|
+
* scalar param there is correct and never flagged.
|
|
73
|
+
*/
|
|
74
|
+
export function substituteSnippetParams(
|
|
75
|
+
value: unknown,
|
|
76
|
+
args: Record<string, unknown>,
|
|
77
|
+
): { value: unknown; missing: string[]; invalid: InvalidParamPlacement[] } {
|
|
78
|
+
const missing: string[] = [];
|
|
79
|
+
const invalid: InvalidParamPlacement[] = [];
|
|
80
|
+
function walk(v: unknown, inNodePosition: boolean): unknown {
|
|
81
|
+
if (v === null || typeof v !== "object") return v;
|
|
82
|
+
if (Array.isArray(v)) {
|
|
83
|
+
// Prune DROP holes so an omitted optional `node` slot leaves no gap.
|
|
84
|
+
return v.map((item) => walk(item, inNodePosition)).filter((item) => item !== DROP);
|
|
85
|
+
}
|
|
86
|
+
if (typeof (v as { $param?: unknown }).$param === "string") {
|
|
87
|
+
const name = (v as { $param: string }).$param;
|
|
88
|
+
if (!(name in args)) {
|
|
89
|
+
missing.push(name);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const resolved = args[name];
|
|
93
|
+
// An omitted optional param resolves to nothing — never a mis-wire.
|
|
94
|
+
if (resolved === OMITTED) return DROP;
|
|
95
|
+
if (inNodePosition && (resolved === null || typeof resolved !== "object")) {
|
|
96
|
+
invalid.push({ param: name, valueType: resolved === null ? "null" : typeof resolved });
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
if (typeof (v as { $if?: unknown }).$if === "string") {
|
|
102
|
+
const cond = v as { $if: string; eq?: unknown; then?: unknown; else?: unknown };
|
|
103
|
+
if (!(cond.$if in args)) {
|
|
104
|
+
missing.push(cond.$if);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const matched = "eq" in cond ? args[cond.$if] === cond.eq : isTruthy(args[cond.$if]);
|
|
108
|
+
return walk(matched ? cond.then : cond.else, inNodePosition);
|
|
109
|
+
}
|
|
110
|
+
// Only a component node's own `children` array carries node positions.
|
|
111
|
+
const isComponentNodeObj = typeof (v as { $ref?: unknown }).$ref === "string";
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
for (const [k, val] of Object.entries(v)) {
|
|
114
|
+
const w = walk(val, isComponentNodeObj && k === "children");
|
|
115
|
+
// A prop resolving to an omitted optional param is left off entirely.
|
|
116
|
+
if (w !== DROP) out[k] = w;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
return { value: walk(value, false), missing, invalid };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Depth-first search for a `$id`-bearing component node inside a body. */
|
|
124
|
+
function findNodeById(root: Node, id: string): Node | undefined {
|
|
125
|
+
if (!isComponentNode(root)) return undefined;
|
|
126
|
+
if (root.$id === id) return root;
|
|
127
|
+
for (const child of root.children ?? []) {
|
|
128
|
+
const hit = findNodeById(child, id);
|
|
129
|
+
if (hit) return hit;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply per-instance interior prop patches (`SnippetInstance.$overrides`)
|
|
136
|
+
* to a resolved body. Keys address the substituted tree either as dotted
|
|
137
|
+
* index paths ("0.2") or as "@id" references to `$id`-bearing body nodes
|
|
138
|
+
* — ids survive definition restructures, so prefer them when the body
|
|
139
|
+
* declares ids. A key that no longer resolves (definition changed since
|
|
140
|
+
* the override was written) is skipped silently — stale overrides
|
|
141
|
+
* degrade to "no effect".
|
|
142
|
+
*/
|
|
143
|
+
export function applySnippetOverrides(
|
|
144
|
+
body: Node,
|
|
145
|
+
overrides: Record<string, { props: Record<string, unknown> }>,
|
|
146
|
+
): Node {
|
|
147
|
+
const clone = structuredClone(body);
|
|
148
|
+
for (const [key, patch] of Object.entries(overrides)) {
|
|
149
|
+
let target: Node | undefined;
|
|
150
|
+
if (key.startsWith("@")) {
|
|
151
|
+
target = findNodeById(clone, key.slice(1));
|
|
152
|
+
} else {
|
|
153
|
+
const segments = key === "" ? [] : key.split(".").map(Number);
|
|
154
|
+
target = clone;
|
|
155
|
+
for (const i of segments) {
|
|
156
|
+
if (!target || !isComponentNode(target) || !target.children) {
|
|
157
|
+
target = undefined;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
target = target.children[i];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!target || !isComponentNode(target)) continue;
|
|
164
|
+
const props = { ...(target.props ?? {}) };
|
|
165
|
+
for (const [k, v] of Object.entries(patch.props)) {
|
|
166
|
+
if (v === null) delete props[k];
|
|
167
|
+
else props[k] = v;
|
|
168
|
+
}
|
|
169
|
+
target.props = props;
|
|
170
|
+
}
|
|
171
|
+
return clone;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Merge an instance's `$extraClassName` onto a resolved body's root.
|
|
176
|
+
* Snippet-of-snippet roots forward the extra to the inner instance;
|
|
177
|
+
* param-ref roots can't carry a className and pass through unchanged.
|
|
178
|
+
*/
|
|
179
|
+
export function applySnippetExtraClassName(node: Node, extra: string): Node {
|
|
180
|
+
if (isParamRef(node)) return node;
|
|
181
|
+
if (isSnippetInstance(node)) {
|
|
182
|
+
const existing = node.$extraClassName ? `${node.$extraClassName} ${extra}` : extra;
|
|
183
|
+
return { ...node, $extraClassName: existing };
|
|
184
|
+
}
|
|
185
|
+
if (!isComponentNode(node)) return node;
|
|
186
|
+
const existingClass =
|
|
187
|
+
typeof node.props?.className === "string" ? (node.props.className as string) : "";
|
|
188
|
+
const merged = existingClass ? `${existingClass} ${extra}` : extra;
|
|
189
|
+
return {
|
|
190
|
+
...node,
|
|
191
|
+
props: { ...(node.props ?? {}), className: merged },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ResourceIdSchema } from "./ids.ts";
|
|
3
|
+
import { NodeSchema } from "./node.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A parameter declared by a snippet. The agent passes a matching arg value
|
|
7
|
+
* at every instantiation; `default` is used when omitted.
|
|
8
|
+
*
|
|
9
|
+
* `type` drives both validation and the inspector's UI:
|
|
10
|
+
* - "string" → text input
|
|
11
|
+
* - "number" → numeric input with step/min/max
|
|
12
|
+
* - "boolean" → checkbox
|
|
13
|
+
* - "icon" → lucide icon picker (string-valued)
|
|
14
|
+
* - "color" → color swatch + token-aware picker (string-valued)
|
|
15
|
+
* - "enum" → select with the declared `enum` values
|
|
16
|
+
* - "node" → subtree slot
|
|
17
|
+
*
|
|
18
|
+
* For `enum`, declare the allowed strings in `enum`; for `number`,
|
|
19
|
+
* `min`/`max`/`step` shape the input.
|
|
20
|
+
*
|
|
21
|
+
* `icon` is for ONE icon chosen at design time — `emit_code` bakes it into
|
|
22
|
+
* the JSX as a literal `<Sparkles/>`. For an icon that varies per instance
|
|
23
|
+
* (by status, priority, …), use a `node` param instead: it emits as a
|
|
24
|
+
* `{slot}` the caller fills, where an `icon` param would collapse every
|
|
25
|
+
* instance to the same glyph (a lucide name must be a literal JSX tag).
|
|
26
|
+
*/
|
|
27
|
+
export const SnippetParamSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
name: z.string().min(1),
|
|
30
|
+
type: z.enum(["string", "number", "boolean", "node", "icon", "color", "enum"]),
|
|
31
|
+
default: z.unknown().optional(),
|
|
32
|
+
/**
|
|
33
|
+
* When true, the param may be omitted at instantiation with no `default`:
|
|
34
|
+
* it resolves to nothing (an omitted `node` slot renders/emits nothing; an
|
|
35
|
+
* omitted prop is absent) instead of erroring. Use it for optional slots —
|
|
36
|
+
* a header `action`, a card `badge` — so callers don't pass a throwaway
|
|
37
|
+
* placeholder. Distinct from `default`, which supplies a concrete value.
|
|
38
|
+
*/
|
|
39
|
+
optional: z.boolean().optional(),
|
|
40
|
+
/** Allowed values for `type: "enum"`. */
|
|
41
|
+
enum: z.array(z.string()).optional(),
|
|
42
|
+
/** Numeric constraints for `type: "number"`. */
|
|
43
|
+
min: z.number().optional(),
|
|
44
|
+
max: z.number().optional(),
|
|
45
|
+
step: z.number().optional(),
|
|
46
|
+
/** Free-form description for the inspector tooltip. */
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.superRefine((param, ctx) => {
|
|
50
|
+
// Catch type/default mismatches at parse time — otherwise the canvas
|
|
51
|
+
// inspector and render-time substitution each have to coerce bad data.
|
|
52
|
+
if (param.type === "enum") {
|
|
53
|
+
if (!param.enum || param.enum.length === 0) {
|
|
54
|
+
ctx.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: ["enum"],
|
|
57
|
+
message: `param "${param.name}": type "enum" requires a non-empty \`enum\` array`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (param.default !== undefined && !param.enum.includes(param.default as string)) {
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["default"],
|
|
65
|
+
message: `param "${param.name}": default ${JSON.stringify(param.default)} is not one of the declared enum values`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (param.default === undefined || param.type === "node") return;
|
|
71
|
+
const expected =
|
|
72
|
+
param.type === "number" ? "number" : param.type === "boolean" ? "boolean" : "string";
|
|
73
|
+
if (typeof param.default !== expected) {
|
|
74
|
+
ctx.addIssue({
|
|
75
|
+
code: "custom",
|
|
76
|
+
path: ["default"],
|
|
77
|
+
message: `param "${param.name}": default ${JSON.stringify(param.default)} does not match declared type "${param.type}"`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export type SnippetParam = z.infer<typeof SnippetParamSchema>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A reusable subtree living in `design/snippets/<id>.json`. Parameter
|
|
86
|
+
* placeholders inside `tree` are encoded as `{ $param: name }` and
|
|
87
|
+
* substituted with the corresponding arg at render time.
|
|
88
|
+
*/
|
|
89
|
+
export const SnippetSchema = z.object({
|
|
90
|
+
id: ResourceIdSchema,
|
|
91
|
+
name: z.string().min(1),
|
|
92
|
+
params: z.array(SnippetParamSchema),
|
|
93
|
+
/**
|
|
94
|
+
* Library id (key in `Config.libraries`) this snippet's body
|
|
95
|
+
* resolves against. Optional — when absent the folder's
|
|
96
|
+
* `defaultLibrary` is used. Snippets are pinned to one library; an
|
|
97
|
+
* instance placed in a screen using a different library renders
|
|
98
|
+
* against the snippet's library, not the embedding screen's.
|
|
99
|
+
*/
|
|
100
|
+
library: z.string().min(1).optional(),
|
|
101
|
+
tree: NodeSchema,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export type Snippet = z.infer<typeof SnippetSchema>;
|