velloo 0.0.0 → 0.2.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 (267) hide show
  1. package/BUN-LICENSE.md +91 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +9 -0
  4. package/README.md +288 -2
  5. package/THIRD-PARTY-NOTICES.md +467 -0
  6. package/assets/elsewhere-alps-9fqhv029.jpg +0 -0
  7. package/assets/elsewhere-bali-vp60g115.jpg +0 -0
  8. package/assets/elsewhere-forest-xgn23zrj.jpg +0 -0
  9. package/assets/elsewhere-japan-qs7qkbr2.jpg +0 -0
  10. package/assets/elsewhere-kyoto-map-n2bvkrya.svg +1 -0
  11. package/assets/elsewhere-kyoto-qemtn6nk.jpg +0 -0
  12. package/assets/elsewhere-room-g1gaw7cs.jpg +0 -0
  13. package/canvas/apple-touch-icon.png +0 -0
  14. package/canvas/assets/index-BCvDZiBo.css +2 -0
  15. package/canvas/assets/index-ClaBqcaS.js +28 -0
  16. package/canvas/assets/lucide-all-iIF-pmzN.js +1 -0
  17. package/canvas/assets/radix-C3HaOB-d.js +41 -0
  18. package/canvas/assets/react-D0Ec-185.js +9 -0
  19. package/canvas/assets/rolldown-runtime-hePW80VL.js +1 -0
  20. package/canvas/assets/vendor-CKmQ-Zst.js +53 -0
  21. package/canvas/favicon.ico +0 -0
  22. package/canvas/favicon.svg +12 -0
  23. package/canvas/fonts/OFL-1.1.txt +93 -0
  24. package/canvas/fonts/poppins-700.woff2 +0 -0
  25. package/canvas/icon-192.png +0 -0
  26. package/canvas/icon-512.png +0 -0
  27. package/canvas/index.html +22 -0
  28. package/canvas/site.webmanifest +11 -0
  29. package/chunk-08sv3k41.js +4 -0
  30. package/chunk-0fe83yzt.js +2 -0
  31. package/chunk-0t68szgr.js +3 -0
  32. package/chunk-1h55s5he.js +2 -0
  33. package/chunk-2c7jbx46.js +2 -0
  34. package/chunk-2smsg2ct.js +7 -0
  35. package/chunk-308ynsxd.js +2 -0
  36. package/chunk-3e8vez4d.js +1229 -0
  37. package/chunk-40xgcth8.js +63 -0
  38. package/chunk-4dn75m6t.js +3 -0
  39. package/chunk-4fj5tahv.js +3 -0
  40. package/chunk-6nqghjda.js +3 -0
  41. package/chunk-6tx68d90.js +3 -0
  42. package/chunk-6w6ch4ce.js +2 -0
  43. package/chunk-735ezz23.js +86 -0
  44. package/chunk-7eq7nx37.js +7 -0
  45. package/chunk-7eyb8yrf.js +6 -0
  46. package/chunk-80fjfyha.js +86 -0
  47. package/chunk-80r93t2e.js +4 -0
  48. package/chunk-94vx1yb0.js +3 -0
  49. package/chunk-9vg3yafz.js +3 -0
  50. package/chunk-a84n72m2.js +4 -0
  51. package/chunk-ampmdzz4.js +10 -0
  52. package/chunk-avns4dbf.js +2 -0
  53. package/chunk-ayrb3qnm.js +3 -0
  54. package/chunk-bwnvcncx.js +3 -0
  55. package/chunk-ccszg02q.js +5 -0
  56. package/chunk-cvn689d5.js +3 -0
  57. package/chunk-cxe6jjwj.js +3 -0
  58. package/chunk-dg6z4vdd.js +3 -0
  59. package/chunk-dgwjpbp9.js +3 -0
  60. package/chunk-dy41n60j.js +52 -0
  61. package/chunk-ek71z2fv.js +226 -0
  62. package/chunk-er1fxb7d.js +2 -0
  63. package/chunk-eyr8hrzf.js +2 -0
  64. package/chunk-fjbzk4sw.js +3 -0
  65. package/chunk-fsbsksga.js +3 -0
  66. package/chunk-h6gchare.js +3 -0
  67. package/chunk-hcb68zt7.js +85 -0
  68. package/chunk-hggap8fq.js +3 -0
  69. package/chunk-j11fyeqh.js +2 -0
  70. package/chunk-jb21jkp1.js +8 -0
  71. package/chunk-jfx9xjmr.js +18 -0
  72. package/chunk-jr0ss0mm.js +303 -0
  73. package/chunk-jrsknzjt.js +3 -0
  74. package/chunk-jv0xggh0.js +2 -0
  75. package/chunk-k17gfwq3.js +2 -0
  76. package/chunk-mh2pr7cq.js +5 -0
  77. package/chunk-mnapdp1y.js +2 -0
  78. package/chunk-mzhdrawd.js +3 -0
  79. package/chunk-n9gs6yx9.js +2 -0
  80. package/chunk-ngnymg2q.js +3 -0
  81. package/chunk-nwx5700b.js +3 -0
  82. package/chunk-pjtg3wnm.js +4 -0
  83. package/chunk-pp86v6m6.js +3 -0
  84. package/chunk-r22epxz2.js +3 -0
  85. package/chunk-rgasy862.js +5 -0
  86. package/chunk-rpwpeph5.js +3 -0
  87. package/chunk-s23gtejf.js +3 -0
  88. package/chunk-sjgqnykd.js +6 -0
  89. package/chunk-sn3x2m0m.js +6 -0
  90. package/chunk-t448rrfr.js +2 -0
  91. package/chunk-tdsk5wxa.js +451 -0
  92. package/chunk-v40fn08f.js +442 -0
  93. package/chunk-vbk1qhdz.js +3 -0
  94. package/chunk-vdkrbzh1.js +2 -0
  95. package/chunk-vgmr98cv.js +40 -0
  96. package/chunk-vsv8z729.js +6 -0
  97. package/chunk-x4f4k9pp.js +26 -0
  98. package/chunk-xt5ayg7g.js +4 -0
  99. package/chunk-ydhwv29b.js +244 -0
  100. package/chunk-zb6hc1y0.js +2 -0
  101. package/cli.js +3 -0
  102. package/launcher.cjs +97 -0
  103. package/package.json +58 -5
  104. package/pkgs/helpers/src/box.tsx +21 -0
  105. package/pkgs/helpers/src/cn.ts +32 -0
  106. package/pkgs/helpers/src/descriptors.ts +270 -0
  107. package/pkgs/helpers/src/divider.tsx +123 -0
  108. package/pkgs/helpers/src/gradient.tsx +90 -0
  109. package/pkgs/helpers/src/heading.tsx +20 -0
  110. package/pkgs/helpers/src/icon-data.ts +8009 -0
  111. package/pkgs/helpers/src/icon.tsx +53 -0
  112. package/pkgs/helpers/src/image.tsx +111 -0
  113. package/pkgs/helpers/src/index.ts +29 -0
  114. package/pkgs/helpers/src/layer.tsx +61 -0
  115. package/pkgs/helpers/src/lowering.ts +53 -0
  116. package/pkgs/helpers/src/paths.ts +47 -0
  117. package/pkgs/helpers/src/placeholder.tsx +88 -0
  118. package/pkgs/helpers/src/prose.tsx +42 -0
  119. package/pkgs/helpers/src/registry.ts +41 -0
  120. package/pkgs/helpers/src/svg.tsx +62 -0
  121. package/pkgs/helpers/src/text.tsx +16 -0
  122. package/pkgs/provider-antd/src/index.ts +78 -0
  123. package/pkgs/provider-antd/src/intro.ts +14 -0
  124. package/pkgs/provider-antd/src/manifest.ts +439 -0
  125. package/pkgs/provider-antd/src/overlays.ts +176 -0
  126. package/pkgs/provider-antd/src/registry.ts +104 -0
  127. package/pkgs/provider-antd/src/render-pass.ts +43 -0
  128. package/pkgs/provider-antd/src/tailwind-entry.css +21 -0
  129. package/pkgs/provider-antd/src/theme.ts +132 -0
  130. package/pkgs/provider-antd/src/version.ts +8 -0
  131. package/pkgs/provider-chakra/src/index.ts +80 -0
  132. package/pkgs/provider-chakra/src/intro.ts +14 -0
  133. package/pkgs/provider-chakra/src/manifest.ts +421 -0
  134. package/pkgs/provider-chakra/src/overlays.ts +246 -0
  135. package/pkgs/provider-chakra/src/registry.ts +227 -0
  136. package/pkgs/provider-chakra/src/render-pass.ts +55 -0
  137. package/pkgs/provider-chakra/src/tailwind-entry.css +21 -0
  138. package/pkgs/provider-chakra/src/theme.ts +180 -0
  139. package/pkgs/provider-chakra/src/version.ts +8 -0
  140. package/pkgs/provider-mui/src/index.ts +117 -0
  141. package/pkgs/provider-mui/src/intro.ts +12 -0
  142. package/pkgs/provider-mui/src/manifest.ts +263 -0
  143. package/pkgs/provider-mui/src/overlays.ts +124 -0
  144. package/pkgs/provider-mui/src/registry.ts +120 -0
  145. package/pkgs/provider-mui/src/render-pass.ts +36 -0
  146. package/pkgs/provider-mui/src/tailwind-entry.css +21 -0
  147. package/pkgs/provider-mui/src/theme.ts +129 -0
  148. package/pkgs/provider-mui/src/version.ts +8 -0
  149. package/pkgs/provider-none/src/components-inline.tsx +204 -0
  150. package/pkgs/provider-none/src/components.tsx +155 -0
  151. package/pkgs/provider-none/src/index.ts +57 -0
  152. package/pkgs/provider-none/src/intro.ts +18 -0
  153. package/pkgs/provider-none/src/manifest.ts +163 -0
  154. package/pkgs/provider-none/src/registry-inline.ts +36 -0
  155. package/pkgs/provider-none/src/registry.ts +39 -0
  156. package/pkgs/provider-none/src/tailwind-entry.css +57 -0
  157. package/pkgs/provider-none/src/version.ts +10 -0
  158. package/pkgs/schema/src/annotation.ts +120 -0
  159. package/pkgs/schema/src/asset.ts +66 -0
  160. package/pkgs/schema/src/board.ts +71 -0
  161. package/pkgs/schema/src/comment.ts +107 -0
  162. package/pkgs/schema/src/config.ts +185 -0
  163. package/pkgs/schema/src/css-sanitize.ts +56 -0
  164. package/pkgs/schema/src/extension.ts +105 -0
  165. package/pkgs/schema/src/fonts.ts +690 -0
  166. package/pkgs/schema/src/frame.ts +40 -0
  167. package/pkgs/schema/src/icon-name.ts +21 -0
  168. package/pkgs/schema/src/ids.ts +14 -0
  169. package/pkgs/schema/src/index.ts +169 -0
  170. package/pkgs/schema/src/migrate.ts +297 -0
  171. package/pkgs/schema/src/node.ts +229 -0
  172. package/pkgs/schema/src/paths.ts +28 -0
  173. package/pkgs/schema/src/repo.ts +134 -0
  174. package/pkgs/schema/src/screen.ts +30 -0
  175. package/pkgs/schema/src/snippet-resolve.ts +193 -0
  176. package/pkgs/schema/src/snippet.ts +104 -0
  177. package/pkgs/schema/src/svg-sanitize.ts +487 -0
  178. package/pkgs/schema/src/theme.ts +218 -0
  179. package/pkgs/schema/src/typeset.ts +693 -0
  180. package/pkgs/schema/src/validate-ids.ts +46 -0
  181. package/pkgs/schema/src/viewport.ts +8 -0
  182. package/pkgs/shadcn-snapshot/dist/manifest.json +8109 -0
  183. package/pkgs/shadcn-snapshot/src/components/canvas-portal.tsx +55 -0
  184. package/pkgs/shadcn-snapshot/src/components/ui/accordion.tsx +122 -0
  185. package/pkgs/shadcn-snapshot/src/components/ui/alert-dialog.tsx +164 -0
  186. package/pkgs/shadcn-snapshot/src/components/ui/alert.tsx +74 -0
  187. package/pkgs/shadcn-snapshot/src/components/ui/aspect-ratio.tsx +13 -0
  188. package/pkgs/shadcn-snapshot/src/components/ui/attachment.tsx +197 -0
  189. package/pkgs/shadcn-snapshot/src/components/ui/avatar.tsx +100 -0
  190. package/pkgs/shadcn-snapshot/src/components/ui/badge.tsx +50 -0
  191. package/pkgs/shadcn-snapshot/src/components/ui/breadcrumb.tsx +109 -0
  192. package/pkgs/shadcn-snapshot/src/components/ui/bubble.tsx +130 -0
  193. package/pkgs/shadcn-snapshot/src/components/ui/button-group.tsx +82 -0
  194. package/pkgs/shadcn-snapshot/src/components/ui/button.tsx +72 -0
  195. package/pkgs/shadcn-snapshot/src/components/ui/calendar.tsx +229 -0
  196. package/pkgs/shadcn-snapshot/src/components/ui/card.tsx +92 -0
  197. package/pkgs/shadcn-snapshot/src/components/ui/carousel.tsx +235 -0
  198. package/pkgs/shadcn-snapshot/src/components/ui/chart-option.ts +250 -0
  199. package/pkgs/shadcn-snapshot/src/components/ui/chart.tsx +177 -0
  200. package/pkgs/shadcn-snapshot/src/components/ui/checkbox.tsx +41 -0
  201. package/pkgs/shadcn-snapshot/src/components/ui/collapsible.tsx +32 -0
  202. package/pkgs/shadcn-snapshot/src/components/ui/combobox.tsx +196 -0
  203. package/pkgs/shadcn-snapshot/src/components/ui/context-menu.tsx +227 -0
  204. package/pkgs/shadcn-snapshot/src/components/ui/dialog.tsx +131 -0
  205. package/pkgs/shadcn-snapshot/src/components/ui/direction.tsx +24 -0
  206. package/pkgs/shadcn-snapshot/src/components/ui/drawer.tsx +112 -0
  207. package/pkgs/shadcn-snapshot/src/components/ui/dropdown-menu.tsx +205 -0
  208. package/pkgs/shadcn-snapshot/src/components/ui/empty.tsx +98 -0
  209. package/pkgs/shadcn-snapshot/src/components/ui/field.tsx +228 -0
  210. package/pkgs/shadcn-snapshot/src/components/ui/hover-card.tsx +51 -0
  211. package/pkgs/shadcn-snapshot/src/components/ui/input-group.tsx +148 -0
  212. package/pkgs/shadcn-snapshot/src/components/ui/input.tsx +43 -0
  213. package/pkgs/shadcn-snapshot/src/components/ui/item.tsx +187 -0
  214. package/pkgs/shadcn-snapshot/src/components/ui/kbd.tsx +30 -0
  215. package/pkgs/shadcn-snapshot/src/components/ui/label.tsx +25 -0
  216. package/pkgs/shadcn-snapshot/src/components/ui/marker.tsx +71 -0
  217. package/pkgs/shadcn-snapshot/src/components/ui/menubar.tsx +243 -0
  218. package/pkgs/shadcn-snapshot/src/components/ui/message.tsx +89 -0
  219. package/pkgs/shadcn-snapshot/src/components/ui/native-select.tsx +60 -0
  220. package/pkgs/shadcn-snapshot/src/components/ui/navigation-menu.tsx +165 -0
  221. package/pkgs/shadcn-snapshot/src/components/ui/pagination.tsx +117 -0
  222. package/pkgs/shadcn-snapshot/src/components/ui/popover.tsx +66 -0
  223. package/pkgs/shadcn-snapshot/src/components/ui/progress.tsx +35 -0
  224. package/pkgs/shadcn-snapshot/src/components/ui/radio-group.tsx +48 -0
  225. package/pkgs/shadcn-snapshot/src/components/ui/scroll-area.tsx +59 -0
  226. package/pkgs/shadcn-snapshot/src/components/ui/select.tsx +180 -0
  227. package/pkgs/shadcn-snapshot/src/components/ui/separator.tsx +32 -0
  228. package/pkgs/shadcn-snapshot/src/components/ui/sheet.tsx +107 -0
  229. package/pkgs/shadcn-snapshot/src/components/ui/skeleton.tsx +17 -0
  230. package/pkgs/shadcn-snapshot/src/components/ui/slider.tsx +68 -0
  231. package/pkgs/shadcn-snapshot/src/components/ui/sonner.tsx +91 -0
  232. package/pkgs/shadcn-snapshot/src/components/ui/spinner.tsx +21 -0
  233. package/pkgs/shadcn-snapshot/src/components/ui/switch.tsx +41 -0
  234. package/pkgs/shadcn-snapshot/src/components/ui/table.tsx +93 -0
  235. package/pkgs/shadcn-snapshot/src/components/ui/tabs.tsx +84 -0
  236. package/pkgs/shadcn-snapshot/src/components/ui/textarea.tsx +24 -0
  237. package/pkgs/shadcn-snapshot/src/components/ui/toggle-group.tsx +94 -0
  238. package/pkgs/shadcn-snapshot/src/components/ui/toggle.tsx +50 -0
  239. package/pkgs/shadcn-snapshot/src/components/ui/tooltip.tsx +47 -0
  240. package/pkgs/shadcn-snapshot/src/examples.ts +36 -0
  241. package/pkgs/shadcn-snapshot/src/groups.ts +87 -0
  242. package/pkgs/shadcn-snapshot/src/index.ts +43 -0
  243. package/pkgs/shadcn-snapshot/src/lib/utils.ts +4 -0
  244. package/pkgs/shadcn-snapshot/src/manifest.ts +12 -0
  245. package/pkgs/shadcn-snapshot/src/notes.ts +97 -0
  246. package/pkgs/shadcn-snapshot/src/paths.ts +47 -0
  247. package/pkgs/shadcn-snapshot/src/registry-files.ts +293 -0
  248. package/pkgs/shadcn-snapshot/src/registry.ts +631 -0
  249. package/pkgs/shadcn-snapshot/src/shadcn-tailwind.css +632 -0
  250. package/pkgs/shadcn-snapshot/src/tailwind-entry.css +159 -0
  251. package/pkgs/shadcn-snapshot/src/version.ts +8 -0
  252. package/plugins/claude/velloo/.claude-plugin/plugin.json +6 -0
  253. package/plugins/claude/velloo/agents/velloo-design-reviewer.md +33 -0
  254. package/plugins/claude/velloo/agents/velloo-designer.md +39 -0
  255. package/plugins/claude/velloo/commands/design.md +11 -0
  256. package/plugins/claude/velloo/commands/implement.md +10 -0
  257. package/plugins/claude/velloo/commands/review.md +8 -0
  258. package/plugins/gemini/GEMINI.md +31 -0
  259. package/plugins/gemini/commands/velloo/design.toml +10 -0
  260. package/plugins/gemini/commands/velloo/implement.toml +9 -0
  261. package/plugins/gemini/gemini-extension.json +9 -0
  262. package/skills/velloo-brand/SKILL.md +124 -0
  263. package/skills/velloo-design/SKILL.md +144 -0
  264. package/skills/velloo-design-system/SKILL.md +90 -0
  265. package/skills/velloo-implement/SKILL.md +101 -0
  266. package/skills/velloo-logo/SKILL.md +93 -0
  267. 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,134 @@
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 MAX_DESIGN_NAME = 80;
22
+
23
+ /**
24
+ * Why `name` can't name a design, or null when it can. Almost anything goes —
25
+ * spaces, emoji, any script. The limits are the ones a name typed as a command
26
+ * argument needs: no path separator (a bare argument with one is read as a
27
+ * path), no control characters, nothing that is only whitespace or a dot
28
+ * path, and no padding a shell would silently strip.
29
+ */
30
+ export function designNameIssue(name: string): string | null {
31
+ if (name.trim() === "") return "a design name can't be empty";
32
+ if (name !== name.trim()) return "a design name can't start or end with spaces";
33
+ if (name === "." || name === "..") return `"${name}" can't be a design name`;
34
+ if (/[/\\]/.test(name)) return "a design name can't contain / or \\";
35
+ if (/\p{Cc}/u.test(name)) return "a design name can't contain control characters";
36
+ if ([...name].length > MAX_DESIGN_NAME)
37
+ return `a design name can't be longer than ${MAX_DESIGN_NAME} characters`;
38
+ return null;
39
+ }
40
+
41
+ export function isDesignName(name: string): boolean {
42
+ return designNameIssue(name) === null;
43
+ }
44
+
45
+ /** A Zod string that must be a design name, with the specific reason when it isn't. */
46
+ export const DesignNameSchema = z.string().superRefine((name, ctx) => {
47
+ const issue = designNameIssue(name);
48
+ if (issue) ctx.addIssue({ code: "custom", message: issue });
49
+ });
50
+
51
+ /**
52
+ * Coerce free text (a directory name, an app name) into a valid design name,
53
+ * or null when nothing usable is left.
54
+ */
55
+ export function toDesignName(text: string): string | null {
56
+ const cleaned = [...text.replace(/[/\\\p{Cc}]+/gu, " ").trim()]
57
+ .slice(0, MAX_DESIGN_NAME)
58
+ .join("")
59
+ .trim();
60
+ return cleaned && isDesignName(cleaned) ? cleaned : null;
61
+ }
62
+
63
+ /**
64
+ * The repo-root `velloo.json`. It lists a repo's designs so a monorepo can
65
+ * hold several, and carries the handful of preferences that belong to the
66
+ * repo rather than to any one design — feedback consent is answered once per
67
+ * person, not once per canvas.
68
+ *
69
+ * Only locations live here: each design's name and settings are in its own
70
+ * `.design/config.json`, so nothing here can drift from the design itself.
71
+ * Every entry is a path to a design folder inside the repository. A design
72
+ * kept outside it is recorded only on the machine that has it, never here.
73
+ */
74
+ export const RepoManifestSchema = z
75
+ .object({
76
+ $schema: z.string().optional(),
77
+ designs: z.array(z.string().min(1)),
78
+ /** The name of the design a bare command resolves to when nothing else picks one. */
79
+ defaultDesign: DesignNameSchema.optional(),
80
+ feedback: FeedbackPrefsSchema.optional(),
81
+ })
82
+ .refine((m) => new Set(m.designs).size === m.designs.length, {
83
+ message: "designs lists the same path twice",
84
+ });
85
+
86
+ export type RepoManifest = z.infer<typeof RepoManifestSchema>;
87
+
88
+ /** Filename of the repo-root manifest, resolved by walking up from a folder. */
89
+ export const REPO_MANIFEST_FILE = "velloo.json";
90
+
91
+ export interface NormalizedRepoManifest {
92
+ /** The manifest in the current shape, ready for {@link RepoManifestSchema}. */
93
+ manifest: unknown;
94
+ /**
95
+ * Names the pre-v4 `projects` map gave each path. A folder not yet migrated
96
+ * has no `name` in its config, so these stand in until `velloo upgrade`
97
+ * writes them there.
98
+ */
99
+ legacyNames: Record<string, string>;
100
+ /** True when the file was in the pre-v4 shape and should be rewritten. */
101
+ legacy: boolean;
102
+ }
103
+
104
+ /**
105
+ * Read either manifest shape. Before designs carried their own names the file
106
+ * was `{ projects: { name: path }, defaultProject }`; the schema would silently
107
+ * drop those keys, so they are converted here, before parsing.
108
+ */
109
+ export function normalizeRepoManifest(raw: unknown): NormalizedRepoManifest {
110
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
111
+ return { manifest: raw, legacyNames: {}, legacy: false };
112
+ }
113
+ const { projects, defaultProject, ...rest } = raw as Record<string, unknown>;
114
+ if (projects === undefined || "designs" in rest) {
115
+ return { manifest: raw, legacyNames: {}, legacy: false };
116
+ }
117
+ const legacyNames: Record<string, string> = {};
118
+ const designs: unknown[] = [];
119
+ if (typeof projects === "object" && projects !== null && !Array.isArray(projects)) {
120
+ for (const [name, path] of Object.entries(projects)) {
121
+ designs.push(path);
122
+ if (typeof path === "string") legacyNames[path] = name;
123
+ }
124
+ }
125
+ return {
126
+ manifest: {
127
+ ...rest,
128
+ designs,
129
+ ...(defaultProject !== undefined ? { defaultDesign: defaultProject } : {}),
130
+ },
131
+ legacyNames,
132
+ legacy: true,
133
+ };
134
+ }
@@ -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
+ }