velloo 0.5.0 → 0.7.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 (161) hide show
  1. package/README.md +116 -227
  2. package/THIRD-PARTY-NOTICES.md +3 -3
  3. package/canvas/assets/index-BQ5710lu.js +29 -0
  4. package/canvas/assets/index-GNLd0pCG.css +2 -0
  5. package/canvas/assets/lucide-all-DIRZdJPK.js +1 -0
  6. package/canvas/index.html +2 -2
  7. package/{chunk-qpv28z8d.js → chunk-0e5j6nqs.js} +1 -1
  8. package/{chunk-vrvkvb0v.js → chunk-0pxm5v0b.js} +3 -3
  9. package/{chunk-a16mz9zf.js → chunk-0vaff40g.js} +1 -1
  10. package/{chunk-42bj6r30.js → chunk-3m28pvzr.js} +1 -1
  11. package/{chunk-33t0csxs.js → chunk-4y2xy1kf.js} +2 -2
  12. package/{chunk-d4r0myxr.js → chunk-531xzfq5.js} +1 -1
  13. package/{chunk-1h3gqy7w.js → chunk-5s1wjhtj.js} +1 -1
  14. package/{chunk-4pkw6d9n.js → chunk-5x4nw0zz.js} +5 -5
  15. package/{chunk-b3x80jc1.js → chunk-5yhnnhkd.js} +1 -1
  16. package/{chunk-675tr675.js → chunk-64pex3mw.js} +1 -1
  17. package/chunk-667ag8ts.js +2 -0
  18. package/chunk-7gyzh0vq.js +5 -0
  19. package/{chunk-4sjvdakg.js → chunk-82x2jkfq.js} +1 -1
  20. package/{chunk-5y9hem1j.js → chunk-84n18xgx.js} +257 -3
  21. package/{chunk-edgwx026.js → chunk-abr2hgay.js} +1 -1
  22. package/{chunk-cqt1dpxd.js → chunk-acsv6m3n.js} +1 -1
  23. package/{chunk-j7tzbt3d.js → chunk-asfgvzse.js} +1 -1
  24. package/{chunk-0wqxb8r9.js → chunk-b8vcbbrg.js} +1 -1
  25. package/chunk-bms1sg4c.js +2 -0
  26. package/chunk-brnkprt5.js +723 -0
  27. package/{chunk-afe25dpr.js → chunk-bys7pqdb.js} +1 -1
  28. package/chunk-d0j8dg1e.js +3 -0
  29. package/chunk-d93kpv7s.js +12 -0
  30. package/chunk-dvh0b2r4.js +3 -0
  31. package/{chunk-5ma8zbty.js → chunk-eh2xp4v6.js} +1 -1
  32. package/{chunk-z78c1kf8.js → chunk-ejppcfa5.js} +1 -1
  33. package/{chunk-ngbqd7kb.js → chunk-ex36z3b3.js} +317 -119
  34. package/{chunk-6ytv1v2r.js → chunk-fmcm06xb.js} +1 -1
  35. package/{chunk-3ffs39e8.js → chunk-fvezyap4.js} +1 -1
  36. package/chunk-g7d065ek.js +2 -0
  37. package/{chunk-tw4kw4ps.js → chunk-g8xrwjvq.js} +2 -2
  38. package/{chunk-df557bn4.js → chunk-gbq5wv7x.js} +1 -1
  39. package/chunk-gcce587w.js +3 -0
  40. package/{chunk-cdqah1hy.js → chunk-gfppsgvv.js} +1 -1
  41. package/{chunk-5f0xv80x.js → chunk-j306gr0c.js} +1 -1
  42. package/chunk-j4fn2a2b.js +2 -0
  43. package/chunk-jbgde2v9.js +2 -0
  44. package/{chunk-wkqrb2tz.js → chunk-kkvf0xk2.js} +1 -1
  45. package/{chunk-d9kx7v12.js → chunk-md4mpvqc.js} +1 -1
  46. package/{chunk-vryadew3.js → chunk-mdgwdqnq.js} +2 -2
  47. package/chunk-p93x4pn2.js +3 -0
  48. package/chunk-pjkkp7xa.js +2 -0
  49. package/chunk-qp3wahg4.js +2 -0
  50. package/{chunk-f45p6kcb.js → chunk-qqrac4va.js} +1 -1
  51. package/chunk-qrnkrkkt.js +2 -0
  52. package/chunk-rqd4swxx.js +2 -0
  53. package/{chunk-xzkczmgg.js → chunk-rxkvjyst.js} +1 -1
  54. package/chunk-s84vxcdv.js +4 -0
  55. package/chunk-sjjnm4a1.js +2 -0
  56. package/chunk-skk6yvnw.js +3 -0
  57. package/{chunk-ahm4zd56.js → chunk-sme6hv6f.js} +1 -1
  58. package/chunk-snak5zpr.js +5 -0
  59. package/{chunk-gn9wcc1b.js → chunk-srp5gyr1.js} +1 -1
  60. package/{chunk-rd65xm6r.js → chunk-vdbr81a3.js} +1 -1
  61. package/chunk-vs8j3x6t.js +3 -0
  62. package/{chunk-hr3gbak5.js → chunk-xg71h7nb.js} +1 -1
  63. package/chunk-xzf52wvn.js +6 -0
  64. package/chunk-ykpctq15.js +6 -0
  65. package/chunk-zv17t1n7.js +3 -0
  66. package/cli.js +1 -1
  67. package/launcher.cjs +3 -1
  68. package/package.json +1 -1
  69. package/pkgs/helpers/src/box.tsx +12 -6
  70. package/pkgs/helpers/src/descriptors.ts +7 -0
  71. package/pkgs/helpers/src/icon.tsx +5 -3
  72. package/pkgs/helpers/src/index.ts +9 -6
  73. package/pkgs/helpers/src/lowering.ts +0 -24
  74. package/pkgs/helpers/src/primitive-classes.tsx +43 -0
  75. package/pkgs/helpers/src/text.tsx +29 -5
  76. package/pkgs/provider-antd/src/intro.ts +3 -3
  77. package/pkgs/provider-antd/src/manifest.ts +479 -57
  78. package/pkgs/provider-antd/src/registry.ts +8 -5
  79. package/pkgs/provider-antd/src/render-pass.ts +10 -4
  80. package/pkgs/provider-antd/src/reset-css.d.ts +5 -0
  81. package/pkgs/provider-antd/src/theme.ts +54 -11
  82. package/pkgs/provider-chakra/src/manifest.ts +6 -1
  83. package/pkgs/provider-mui/src/index.ts +4 -2
  84. package/pkgs/provider-mui/src/manifest.ts +11 -1
  85. package/pkgs/provider-mui/src/render-pass.ts +15 -1
  86. package/pkgs/provider-mui/src/theme.ts +10 -4
  87. package/pkgs/provider-none/src/components-inline.tsx +20 -14
  88. package/pkgs/provider-none/src/components.tsx +17 -54
  89. package/pkgs/provider-none/src/manifest.ts +1 -0
  90. package/pkgs/schema/src/annotation.ts +6 -3
  91. package/pkgs/schema/src/comment.ts +10 -1
  92. package/pkgs/schema/src/config.ts +4 -1
  93. package/pkgs/schema/src/index.ts +16 -1
  94. package/pkgs/schema/src/node-identity.ts +97 -0
  95. package/pkgs/schema/src/node.ts +49 -23
  96. package/pkgs/schema/src/snippet-resolve.ts +18 -12
  97. package/pkgs/schema/src/svg-sanitize.ts +3 -0
  98. package/pkgs/schema/src/theme.ts +6 -1
  99. package/pkgs/schema/src/typeset.ts +45 -21
  100. package/pkgs/shadcn-snapshot/dist/manifest.json +7 -0
  101. package/pkgs/shadcn-snapshot/src/components/ui/alert.tsx +2 -4
  102. package/pkgs/shadcn-snapshot/src/components/ui/aspect-ratio.tsx +2 -3
  103. package/pkgs/shadcn-snapshot/src/components/ui/attachment.tsx +3 -4
  104. package/pkgs/shadcn-snapshot/src/components/ui/avatar.tsx +2 -4
  105. package/pkgs/shadcn-snapshot/src/components/ui/badge.tsx +2 -4
  106. package/pkgs/shadcn-snapshot/src/components/ui/breadcrumb.tsx +2 -4
  107. package/pkgs/shadcn-snapshot/src/components/ui/bubble.tsx +2 -4
  108. package/pkgs/shadcn-snapshot/src/components/ui/button-group.tsx +3 -4
  109. package/pkgs/shadcn-snapshot/src/components/ui/button.tsx +2 -4
  110. package/pkgs/shadcn-snapshot/src/components/ui/card.tsx +2 -4
  111. package/pkgs/shadcn-snapshot/src/components/ui/carousel.tsx +3 -4
  112. package/pkgs/shadcn-snapshot/src/components/ui/chart.tsx +1 -1
  113. package/pkgs/shadcn-snapshot/src/components/ui/direction.tsx +2 -3
  114. package/pkgs/shadcn-snapshot/src/components/ui/empty.tsx +2 -4
  115. package/pkgs/shadcn-snapshot/src/components/ui/field.tsx +3 -4
  116. package/pkgs/shadcn-snapshot/src/components/ui/input-group.tsx +3 -4
  117. package/pkgs/shadcn-snapshot/src/components/ui/item.tsx +3 -4
  118. package/pkgs/shadcn-snapshot/src/components/ui/kbd.tsx +2 -3
  119. package/pkgs/shadcn-snapshot/src/components/ui/label.tsx +2 -4
  120. package/pkgs/shadcn-snapshot/src/components/ui/marker.tsx +2 -4
  121. package/pkgs/shadcn-snapshot/src/components/ui/message.tsx +2 -4
  122. package/pkgs/shadcn-snapshot/src/components/ui/native-select.tsx +2 -4
  123. package/pkgs/shadcn-snapshot/src/components/ui/navigation-menu.tsx +2 -4
  124. package/pkgs/shadcn-snapshot/src/components/ui/pagination.tsx +3 -4
  125. package/pkgs/shadcn-snapshot/src/components/ui/progress.tsx +2 -4
  126. package/pkgs/shadcn-snapshot/src/components/ui/radio-group.tsx +2 -4
  127. package/pkgs/shadcn-snapshot/src/components/ui/scroll-area.tsx +2 -4
  128. package/pkgs/shadcn-snapshot/src/components/ui/separator.tsx +2 -4
  129. package/pkgs/shadcn-snapshot/src/components/ui/skeleton.tsx +2 -3
  130. package/pkgs/shadcn-snapshot/src/components/ui/spinner.tsx +2 -3
  131. package/pkgs/shadcn-snapshot/src/components/ui/table.tsx +2 -4
  132. package/pkgs/shadcn-snapshot/src/components/ui/tabs.tsx +2 -4
  133. package/pkgs/shadcn-snapshot/src/components/ui/toggle.tsx +2 -4
  134. package/pkgs/shadcn-snapshot/src/shadcn-tailwind.css +1 -1
  135. package/pkgs/shadcn-snapshot/src/tailwind-entry.css +1 -1
  136. package/canvas/assets/index-BDBBHdHH.css +0 -2
  137. package/canvas/assets/index-QdsLJNY9.js +0 -29
  138. package/canvas/assets/lucide-all-R8DUyY-B.js +0 -1
  139. package/chunk-0rdf6trc.js +0 -2
  140. package/chunk-0wafc9aw.js +0 -4
  141. package/chunk-10hap8kc.js +0 -6
  142. package/chunk-18eqh3s1.js +0 -2
  143. package/chunk-19xyzep8.js +0 -2
  144. package/chunk-49w26e94.js +0 -5
  145. package/chunk-66qgm91k.js +0 -708
  146. package/chunk-6cgvzwec.js +0 -3
  147. package/chunk-75sc3adm.js +0 -2
  148. package/chunk-7cexzg71.js +0 -2
  149. package/chunk-98rann8t.js +0 -2
  150. package/chunk-asvdqkdp.js +0 -6
  151. package/chunk-emv1j50t.js +0 -2
  152. package/chunk-ewgdcgrk.js +0 -2
  153. package/chunk-mm8wv3bg.js +0 -3
  154. package/chunk-nkqtnwzz.js +0 -2
  155. package/chunk-q3kf8243.js +0 -3
  156. package/chunk-ryga3ny1.js +0 -3
  157. package/chunk-sb5h27w9.js +0 -3
  158. package/chunk-vgx47anc.js +0 -3
  159. package/chunk-w6f40y2x.js +0 -11
  160. package/chunk-we8hvy6e.js +0 -2
  161. package/chunk-ycrddh47.js +0 -5
@@ -36,6 +36,7 @@ import {
36
36
  import { Drawer, Dropdown, Modal, Popover, Tooltip } from "./overlays.ts";
37
37
 
38
38
  const REUSED_HELPER_IDS = [
39
+ "Box",
39
40
  "Icon",
40
41
  "Image",
41
42
  "Placeholder",
@@ -49,8 +50,9 @@ const REUSED_HELPER_IDS = [
49
50
  * The runtime registry for antd-native folders: design `$ref` ids → real antd
50
51
  * components, SSR'd in-process via the adapter's cssinjs render pass. Dotted
51
52
  * antd subcomponents get manifest-friendly flat ids (`Typography.Title` ⇒
52
- * `TypographyTitle`, `List.Item` ⇒ `ListItem`) — the MCP intro tells the agent
53
- * how those destructure in emitted code. The overlay surface
53
+ * `TypographyTitle`, `List.Item` ⇒ `ListItem`) because neither a manifest id nor
54
+ * a `$ref` can carry a dot; the manifest records the dotted export as
55
+ * `nativeExport`, so emitted code names it directly. The overlay surface
54
56
  * (Modal/Drawer/Popover/Tooltip/Dropdown) is canvas-safe-wrapped in
55
57
  * `overlays.ts` — antd's own overlays portal and render NOTHING in SSR, so the
56
58
  * shims render them open + inline instead.
@@ -97,8 +99,9 @@ export const registry: ComponentRegistry = {
97
99
  TypographyText: Typography.Text,
98
100
  TypographyTitle: Typography.Title,
99
101
  // Framework-neutral velloo helpers antd has no equivalent for — chiefly
100
- // `Icon` (lucide; @ant-design/icons isn't surfaced) + imagery/composition
101
- // helpers. Reused verbatim from `@velloo/helpers`, like provider-mui. They
102
- // size via props (`Icon size`), not Tailwind — the JIT is off on an antd folder.
102
+ // `Icon` (lucide; @ant-design/icons isn't surfaced), `Box` (the plain element
103
+ // compose lowers `<div>` and stray text to) + imagery/composition helpers.
104
+ // Reused verbatim from `@velloo/helpers`, like provider-mui. They size via
105
+ // props (`Icon size`), not Tailwind — the JIT is off on an antd folder.
103
106
  ...helpersRegistry(REUSED_HELPER_IDS),
104
107
  };
@@ -2,8 +2,9 @@ import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";
2
2
  import type { RenderPass } from "@velloo/provider";
3
3
  import type { Theme as VellooTheme } from "@velloo/schema";
4
4
  import { ConfigProvider } from "antd";
5
+ import antdResetCss from "antd/dist/reset.css" with { type: "text" };
5
6
  import { createElement } from "react";
6
- import { antdThemeConfig } from "./theme.ts";
7
+ import { antdDocumentCss, antdThemeConfig } from "./theme.ts";
7
8
 
8
9
  /**
9
10
  * A fresh cssinjs + antd render pass for one SSR. `wrap` nests the tree under
@@ -11,8 +12,12 @@ import { antdThemeConfig } from "./theme.ts";
11
12
  * projected velloo tokens (with `cssVar: true`, so `--ant-*` variables reach
12
13
  * inline styles); `css()` extracts the style rules the render registered in
13
14
  * the cache. The renderer injects that CSS into a `data-velloo-adapter`
14
- * <style>. Unlike emotion, cssinjs extraction reads the cache directly — the
15
- * rendered HTML isn't needed.
15
+ * <style>, led by antd's own `reset.css` — the global sheet every antd app
16
+ * imports, which makes boxes border-box and zeroes body and heading margins,
17
+ * so a sized container measures what it does in the app — and the document
18
+ * baseline (`antdDocumentCss`) that gives plain elements the theme's type
19
+ * and color. Unlike emotion, cssinjs extraction
20
+ * reads the cache directly — the rendered HTML isn't needed.
16
21
  *
17
22
  * SSR works in-process because antd is a velloo dependency, so it links the
18
23
  * shared monorepo React — no dual-React hazard.
@@ -29,7 +34,8 @@ export function makeRenderPass(theme: VellooTheme, dark = false): RenderPass {
29
34
  ),
30
35
  // `plain: true` returns bare CSS text; strip <style> wrappers defensively
31
36
  // anyway — the RenderPass contract returns CSS, not markup.
32
- css: () => stripStyleTags(extractStyle(cache, { plain: true })),
37
+ css: () =>
38
+ `${antdResetCss}\n${antdDocumentCss(theme, dark)}\n${stripStyleTags(extractStyle(cache, { plain: true }))}`,
33
39
  };
34
40
  }
35
41
 
@@ -0,0 +1,5 @@
1
+ /** antd's global reset, imported as text so the render pass can inline it. */
2
+ declare module "antd/dist/reset.css" {
3
+ const css: string;
4
+ export default css;
5
+ }
@@ -3,6 +3,8 @@ import {
3
3
  type ColorPair,
4
4
  DEFAULT_TYPESET_NAME,
5
5
  resolveColors,
6
+ sanitizeCssTokenValue,
7
+ TYPESET_DEFAULT,
6
8
  typesetScale,
7
9
  type Theme as VellooTheme,
8
10
  } from "@velloo/schema";
@@ -18,7 +20,9 @@ import { formatRgb, parse } from "culori";
18
20
  * theme stays hex (and round-trips identically in codegen).
19
21
  */
20
22
  function antdColor(css: string): string {
21
- const s = css.trim();
23
+ // Theme strings are free text from the design JSON and end up inside
24
+ // cssinjs rules, so nothing that could close a declaration gets through.
25
+ const s = sanitizeCssTokenValue(css);
22
26
  if (/^(#|rgb|hsl)/i.test(s)) return s;
23
27
  return formatRgb(parse(s)) ?? s;
24
28
  }
@@ -74,17 +78,27 @@ function antdTokens(theme: VellooTheme, dark: boolean): NonNullable<ThemeConfig[
74
78
  * antd has only five heading tokens (h1–h5), so the velloo `h6` rung has no
75
79
  * antd equivalent and is intentionally dropped here; `Prose` regions still get
76
80
  * it from the typeset CSS.
81
+ *
82
+ * Line heights are left to antd unless the typeset chose its own leading.
83
+ * antd derives each one from its font size (14px reads 1.5714), and an antd
84
+ * app that sets only sizes gets those; velloo's default leading is a
85
+ * long-form reading rhythm, not something the app asked for, so projecting
86
+ * it would put every component on a rhythm the app does not have. A leading
87
+ * the user changed is a decision, and it reaches every antd line height.
77
88
  */
78
89
  function antdTypography(theme: VellooTheme): NonNullable<ThemeConfig["token"]> {
79
90
  const typography = theme.typography;
80
- const scale = typesetScale(typography.typesets?.[DEFAULT_TYPESET_NAME], {
91
+ const typeset = typography.typesets?.[DEFAULT_TYPESET_NAME];
92
+ const scale = typesetScale(typeset, {
81
93
  ...(typography.fontFamily ? { fontFamily: typography.fontFamily } : {}),
82
94
  });
95
+ const ownLeading = typeset?.leading !== undefined && typeset.leading !== TYPESET_DEFAULT.leading;
83
96
  return {
84
- fontFamily:
97
+ fontFamily: sanitizeCssTokenValue(
85
98
  scale.body.fontFamily ??
86
- typography.fontFamily?.sans ??
87
- "system-ui, -apple-system, sans-serif",
99
+ typography.fontFamily?.sans ??
100
+ "system-ui, -apple-system, sans-serif",
101
+ ),
88
102
  fontSize: scale.body.fontSize,
89
103
  fontSizeSM: scale.caption.fontSize,
90
104
  fontSizeLG: scale.lead.fontSize,
@@ -93,12 +107,16 @@ function antdTypography(theme: VellooTheme): NonNullable<ThemeConfig["token"]> {
93
107
  fontSizeHeading3: scale.h3.fontSize,
94
108
  fontSizeHeading4: scale.h4.fontSize,
95
109
  fontSizeHeading5: scale.h5.fontSize,
96
- lineHeight: scale.body.lineHeight,
97
- lineHeightHeading1: scale.h1.lineHeight,
98
- lineHeightHeading2: scale.h2.lineHeight,
99
- lineHeightHeading3: scale.h3.lineHeight,
100
- lineHeightHeading4: scale.h4.lineHeight,
101
- lineHeightHeading5: scale.h5.lineHeight,
110
+ ...(ownLeading
111
+ ? {
112
+ lineHeight: scale.body.lineHeight,
113
+ lineHeightHeading1: scale.h1.lineHeight,
114
+ lineHeightHeading2: scale.h2.lineHeight,
115
+ lineHeightHeading3: scale.h3.lineHeight,
116
+ lineHeightHeading4: scale.h4.lineHeight,
117
+ lineHeightHeading5: scale.h5.lineHeight,
118
+ }
119
+ : {}),
102
120
  };
103
121
  }
104
122
 
@@ -116,6 +134,31 @@ export function antdThemeConfig(theme: VellooTheme, dark = false): ThemeConfig {
116
134
  };
117
135
  }
118
136
 
137
+ /**
138
+ * The document rules an antd frame renders on, after antd's own `reset.css`
139
+ * (which a real antd app imports, and which sets box-sizing and margins but
140
+ * no type or color). antd v5 styles only its own components, so a plain
141
+ * element — a `Box`, a `Flex` container's own text — inherits from `<body>`,
142
+ * and the `style` channel skips the JIT that gives other providers a themed
143
+ * body. So `<body>` takes antd's resolved tokens for this theme, the values
144
+ * its components read, and the text between antd components matches them.
145
+ * The seed tokens are already sanitized, so they interpolate as-is.
146
+ */
147
+ export function antdDocumentCss(theme: VellooTheme, dark = false): string {
148
+ const token = antdTheme.getDesignToken(antdThemeConfig(theme, dark));
149
+ return (
150
+ "html,body{" +
151
+ [
152
+ `background-color:${token.colorBgBase}`,
153
+ `color:${token.colorTextBase}`,
154
+ `font-family:${token.fontFamily}`,
155
+ `font-size:${token.fontSize}px`,
156
+ `line-height:${token.lineHeight}`,
157
+ ].join(";") +
158
+ "}"
159
+ );
160
+ }
161
+
119
162
  /**
120
163
  * The codegen projection (`themeToNative`): the same token mapping, but the
121
164
  * algorithm emits as a bare identifier (`antdTheme.darkAlgorithm`) that the
@@ -96,7 +96,11 @@ function helper(
96
96
 
97
97
  export const CHAKRA_MANIFEST: Manifest = [
98
98
  // --- layout ---
99
- ui("Box", [children], "Generic layout primitive; style via sx."),
99
+ ui(
100
+ "Box",
101
+ [children, str("as")],
102
+ 'Generic layout primitive; style via sx. `as` renders another HTML tag ("span", "section").',
103
+ ),
100
104
  ui(
101
105
  "Flex",
102
106
  [children, json("gap", "number | string"), str("align"), str("justify"), str("direction")],
@@ -399,6 +403,7 @@ export const CHAKRA_MANIFEST: Manifest = [
399
403
  [
400
404
  { name: "content", type: "string", optional: false, control: "string" },
401
405
  { name: "viewBox", type: "string", optional: false, control: "string" },
406
+ { name: "preserveAspectRatio", type: "string", optional: true, control: "string" },
402
407
  ],
403
408
  "Raw inline SVG markup.",
404
409
  ),
@@ -82,8 +82,10 @@ export function createProvider(): FrameworkAdapter {
82
82
  {
83
83
  importPath: path,
84
84
  exportName: id,
85
- fidelity: "fallback" as const,
86
- note: "Velloo helper used alongside the host framework.",
85
+ // A helper is Velloo's own component, not a stand-in
86
+ // for an MUI one: this file is its real render.
87
+ fidelity: "exact" as const,
88
+ note: "Velloo helper, mounted beside the host framework's components.",
87
89
  },
88
90
  ],
89
91
  },
@@ -70,7 +70,16 @@ function helper(
70
70
  }
71
71
 
72
72
  export const MUI_MANIFEST: Manifest = [
73
- ui("Box", [children], "Generic layout primitive; style via sx."),
73
+ ui(
74
+ "Box",
75
+ [
76
+ children,
77
+ // Compose writes `<span>` as `as="span"` on every library's element; MUI
78
+ // types and documents the override as `component`, so it emits as that.
79
+ { name: "as", type: "string", optional: true, control: "string", nativeName: "component" },
80
+ ],
81
+ 'Generic layout primitive; style via sx. `as` renders another HTML tag ("span", "section") and emits as MUI\'s `component`.',
82
+ ),
74
83
  ui(
75
84
  "Stack",
76
85
  [children, enumProp("direction", ["row", "column"], "column")],
@@ -241,6 +250,7 @@ export const MUI_MANIFEST: Manifest = [
241
250
  [
242
251
  { name: "content", type: "string", optional: false, control: "string" },
243
252
  { name: "viewBox", type: "string", optional: false, control: "string" },
253
+ { name: "preserveAspectRatio", type: "string", optional: true, control: "string" },
244
254
  ],
245
255
  "Raw inline SVG markup.",
246
256
  ),
@@ -1,6 +1,7 @@
1
1
  import createCache from "@emotion/cache";
2
2
  import { CacheProvider } from "@emotion/react";
3
3
  import createEmotionServer from "@emotion/server/create-instance";
4
+ import CssBaseline from "@mui/material/CssBaseline";
4
5
  import { ThemeProvider } from "@mui/material/styles";
5
6
  import type { RenderPass } from "@velloo/provider";
6
7
  import type { Theme as VellooTheme } from "@velloo/schema";
@@ -13,6 +14,14 @@ import { muiThemeFrom } from "./theme.ts";
13
14
  * tokens; `css(html)` extracts exactly the emotion rules the rendered markup
14
15
  * uses. The renderer injects that CSS into a `data-velloo-adapter` <style>.
15
16
  *
17
+ * The tree renders beside a `CssBaseline`, as a real MUI app's root does: MUI
18
+ * styles only its own components, and the style channel skips the Tailwind
19
+ * JIT, so without it a plain element inherits a bare `<body>` (Times, 16px,
20
+ * black, an 8px margin). The baseline puts the theme's body1 type, text color
21
+ * and background on the body, from the same theme the components get, and
22
+ * `enableColorScheme` gives a dark frame dark native controls. Its rules are
23
+ * emotion globals, which `extractCriticalToChunks` always keeps.
24
+ *
16
25
  * SSR works in-process because @mui/material is a velloo dependency, so it links
17
26
  * the shared monorepo React — no dual-React hazard.
18
27
  */
@@ -25,7 +34,12 @@ export function makeRenderPass(theme: VellooTheme, dark = false): RenderPass {
25
34
  createElement(
26
35
  CacheProvider,
27
36
  { value: cache },
28
- createElement(ThemeProvider, { theme: muiTheme }, element),
37
+ createElement(
38
+ ThemeProvider,
39
+ { theme: muiTheme },
40
+ createElement(CssBaseline, { enableColorScheme: true }),
41
+ element,
42
+ ),
29
43
  ),
30
44
  css: (html) =>
31
45
  server
@@ -3,6 +3,7 @@ import {
3
3
  type ColorPair,
4
4
  DEFAULT_TYPESET_NAME,
5
5
  resolveColors,
6
+ sanitizeCssTokenValue,
6
7
  typesetScale,
7
8
  type Theme as VellooTheme,
8
9
  } from "@velloo/schema";
@@ -16,7 +17,9 @@ import { formatRgb, parse } from "culori";
16
17
  * theme stays hex (and round-trips identically in codegen).
17
18
  */
18
19
  function muiColor(css: string): string {
19
- const s = css.trim();
20
+ // Theme strings are free text from the design JSON and end up inside
21
+ // emotion rules, so nothing that could close a declaration gets through.
22
+ const s = sanitizeCssTokenValue(css);
20
23
  if (/^(#|rgb|hsl)/i.test(s)) return s;
21
24
  return formatRgb(parse(s)) ?? s;
22
25
  }
@@ -90,15 +93,18 @@ function muiTypography(theme: VellooTheme): NonNullable<ThemeOptions["typography
90
93
  ...(typography.fontFamily ? { fontFamily: typography.fontFamily } : {}),
91
94
  });
92
95
  // The body face, falling back to the `sans` role then the system stack.
93
- const bodyFamily =
94
- scale.body.fontFamily ?? typography.fontFamily?.sans ?? "system-ui, -apple-system, sans-serif";
96
+ const bodyFamily = sanitizeCssTokenValue(
97
+ scale.body.fontFamily ?? typography.fontFamily?.sans ?? "system-ui, -apple-system, sans-serif",
98
+ );
95
99
 
96
100
  const variant = (role: keyof typeof scale) => {
97
101
  const r = scale[role];
98
102
  return {
99
103
  // Only override the family when the typeset names a *different* face for
100
104
  // this role; otherwise inherit the theme's own fontFamily.
101
- ...(r.fontFamily && r.fontFamily !== bodyFamily ? { fontFamily: r.fontFamily } : {}),
105
+ ...(r.fontFamily && sanitizeCssTokenValue(r.fontFamily) !== bodyFamily
106
+ ? { fontFamily: sanitizeCssTokenValue(r.fontFamily) }
107
+ : {}),
102
108
  fontSize: `${r.fontSize}px`,
103
109
  lineHeight: r.lineHeight,
104
110
  letterSpacing: r.letterSpacing,
@@ -1,12 +1,7 @@
1
+ import { ELEMENT_TAG, TextRunContext, textTag } from "@velloo/helpers";
1
2
  import { headingInlineStyle, textInlineStyle } from "@velloo/schema/typeset";
2
3
  import * as React from "react";
3
- import {
4
- BOX_TAG,
5
- type ButtonProps,
6
- type ContainerProps,
7
- type InputProps,
8
- type StackProps,
9
- } from "./components.tsx";
4
+ import type { ButtonProps, ContainerProps, InputProps, StackProps } from "./components.tsx";
10
5
 
11
6
  /**
12
7
  * Inline-styled variants of the no-library primitives, for a `none`-CSS folder
@@ -38,7 +33,10 @@ export const Box = React.forwardRef<
38
33
  HTMLDivElement,
39
34
  React.HTMLAttributes<HTMLDivElement> & { as?: string }
40
35
  >(({ as, ...rest }, ref) =>
41
- React.createElement(typeof as === "string" && BOX_TAG.test(as) ? as : "div", { ref, ...rest }),
36
+ React.createElement(typeof as === "string" && ELEMENT_TAG.test(as) ? as : "div", {
37
+ ref,
38
+ ...rest,
39
+ }),
42
40
  );
43
41
  Box.displayName = "Box";
44
42
 
@@ -201,13 +199,21 @@ export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
201
199
  );
202
200
  Heading.displayName = "Heading";
203
201
 
204
- interface TextProps extends React.HTMLAttributes<HTMLParagraphElement> {
202
+ interface TextProps extends React.HTMLAttributes<HTMLElement> {
205
203
  variant?: "default" | "muted" | "small" | "lead";
206
204
  }
207
205
 
208
- export const Text = React.forwardRef<HTMLParagraphElement, TextProps>(
209
- ({ variant = "default", style, ...rest }, ref) => (
210
- <p ref={ref} style={merge(textInlineStyle(variant), style)} {...rest} />
211
- ),
212
- );
206
+ // A `Text` inside a `Text` is a run of it — see `TextRunContext`.
207
+ export const Text = React.forwardRef<HTMLElement, TextProps>(({ variant, style, ...rest }, ref) => {
208
+ const run = React.useContext(TextRunContext);
209
+ return (
210
+ <TextRunContext.Provider value={true}>
211
+ {React.createElement(textTag(run), {
212
+ ref,
213
+ style: merge(textInlineStyle(variant, run), style),
214
+ ...rest,
215
+ })}
216
+ </TextRunContext.Provider>
217
+ );
218
+ });
213
219
  Text.displayName = "Text";
@@ -1,3 +1,13 @@
1
+ import {
2
+ BUTTON_BASE_CLASS,
3
+ BUTTON_VARIANT_CLASS,
4
+ CARD_CLASS,
5
+ CONTAINER_WIDTH_CLASS,
6
+ ELEMENT_TAG,
7
+ INPUT_CLASS,
8
+ STACK_ALIGN_CLASS,
9
+ STACK_JUSTIFY_CLASS,
10
+ } from "@velloo/helpers";
1
11
  import { clsx } from "clsx";
2
12
  import * as React from "react";
3
13
 
@@ -14,12 +24,9 @@ import * as React from "react";
14
24
 
15
25
  interface DivProps extends React.HTMLAttributes<HTMLDivElement> {}
16
26
 
17
- /** `as` takes a lowercase HTML tag only — anything else renders the div. */
18
- export const BOX_TAG = /^[a-z][a-z0-9]*$/;
19
-
20
27
  export const Box = React.forwardRef<HTMLDivElement, DivProps & { as?: string }>(
21
28
  ({ as, className, ...rest }, ref) =>
22
- React.createElement(typeof as === "string" && BOX_TAG.test(as) ? as : "div", {
29
+ React.createElement(typeof as === "string" && ELEMENT_TAG.test(as) ? as : "div", {
23
30
  ref,
24
31
  className: clsx(className),
25
32
  ...rest,
@@ -38,20 +45,6 @@ export interface StackProps extends DivProps {
38
45
  justify?: "start" | "center" | "end" | "between" | "around";
39
46
  }
40
47
 
41
- const alignClass: Record<NonNullable<StackProps["align"]>, string> = {
42
- start: "items-start",
43
- center: "items-center",
44
- end: "items-end",
45
- stretch: "items-stretch",
46
- };
47
- const justifyClass: Record<NonNullable<StackProps["justify"]>, string> = {
48
- start: "justify-start",
49
- center: "justify-center",
50
- end: "justify-end",
51
- between: "justify-between",
52
- around: "justify-around",
53
- };
54
-
55
48
  export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
56
49
  ({ direction = "col", gap = 4, align, justify, className, ...rest }, ref) => (
57
50
  <div
@@ -60,8 +53,8 @@ export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
60
53
  "flex",
61
54
  direction === "row" ? "flex-row" : "flex-col",
62
55
  `gap-${gap}`,
63
- align ? alignClass[align] : undefined,
64
- justify ? justifyClass[justify] : undefined,
56
+ align ? STACK_ALIGN_CLASS[align] : undefined,
57
+ justify ? STACK_JUSTIFY_CLASS[justify] : undefined,
65
58
  className,
66
59
  )}
67
60
  {...rest}
@@ -75,19 +68,11 @@ export interface ContainerProps extends DivProps {
75
68
  size?: "sm" | "md" | "lg" | "xl" | "full";
76
69
  }
77
70
 
78
- const containerWidth: Record<NonNullable<ContainerProps["size"]>, string> = {
79
- sm: "max-w-screen-sm",
80
- md: "max-w-screen-md",
81
- lg: "max-w-screen-lg",
82
- xl: "max-w-screen-xl",
83
- full: "max-w-full",
84
- };
85
-
86
71
  export const Container = React.forwardRef<HTMLDivElement, ContainerProps>(
87
72
  ({ size = "md", className, ...rest }, ref) => (
88
73
  <div
89
74
  ref={ref}
90
- className={clsx("mx-auto w-full px-4", containerWidth[size], className)}
75
+ className={clsx("mx-auto w-full px-4", CONTAINER_WIDTH_CLASS[size], className)}
91
76
  {...rest}
92
77
  />
93
78
  ),
@@ -98,23 +83,12 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
98
83
  variant?: "default" | "ghost" | "outline";
99
84
  }
100
85
 
101
- const buttonVariant: Record<NonNullable<ButtonProps["variant"]>, string> = {
102
- default: "bg-foreground text-background hover:bg-foreground/90",
103
- ghost: "bg-transparent text-foreground hover:bg-muted",
104
- outline: "bg-transparent text-foreground border border-border hover:bg-muted",
105
- };
106
-
107
86
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
108
87
  ({ variant = "default", className, type, ...rest }, ref) => (
109
88
  <button
110
89
  ref={ref}
111
90
  type={type ?? "button"}
112
- className={clsx(
113
- "inline-flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium",
114
- "focus-visible:outline-2 focus-visible:outline-offset-2",
115
- buttonVariant[variant],
116
- className,
117
- )}
91
+ className={clsx(BUTTON_BASE_CLASS, BUTTON_VARIANT_CLASS[variant], className)}
118
92
  {...rest}
119
93
  />
120
94
  ),
@@ -136,11 +110,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
136
110
  value={value}
137
111
  onChange={onChange}
138
112
  readOnly={isStatic || rest.readOnly}
139
- className={clsx(
140
- "block w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
141
- "placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2",
142
- className,
143
- )}
113
+ className={clsx(INPUT_CLASS, className)}
144
114
  {...rest}
145
115
  />
146
116
  );
@@ -151,13 +121,6 @@ Input.displayName = "Input";
151
121
  export interface CardProps extends DivProps {}
152
122
 
153
123
  export const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, ...rest }, ref) => (
154
- <div
155
- ref={ref}
156
- className={clsx(
157
- "rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm",
158
- className,
159
- )}
160
- {...rest}
161
- />
124
+ <div ref={ref} className={clsx(CARD_CLASS, className)} {...rest} />
162
125
  ));
163
126
  Card.displayName = "Card";
@@ -14,6 +14,7 @@ const PRIMITIVE_DESCRIPTORS: ComponentDescriptor[] = [
14
14
  id: "Box",
15
15
  category: "ui",
16
16
  source: "velloo",
17
+ plainElement: true,
17
18
  props: [
18
19
  { name: "as", type: "string", optional: true, control: "string" },
19
20
  {
@@ -100,11 +100,14 @@ export const CanvasNoteSchema = z
100
100
  */
101
101
  x: z.number().optional(),
102
102
  y: z.number().optional(),
103
+ /** User-resizable width. */
104
+ width: z.number().positive(),
103
105
  /**
104
- * User-resizable width. Height is always derived from content (notes
105
- * never scroll; the box expands downward).
106
+ * The height the author drew or resized the note to. Absent, the note is
107
+ * exactly as tall as its text; set, it is this tall, and text that runs
108
+ * past it scrolls inside the note.
106
109
  */
107
- width: z.number().positive(),
110
+ height: z.number().positive().optional(),
108
111
  /** Markdown body. */
109
112
  body: z.string(),
110
113
  /** Present when the note is about a specific node rather than the board. */
@@ -116,4 +116,13 @@ export type CommentAnchorState =
116
116
  | { status: "attached"; resolvedPath: number[] }
117
117
  | { status: "stale" };
118
118
 
119
- export type CommentThreadView = CommentThread & { anchorState: CommentAnchorState };
119
+ export type CommentThreadView = CommentThread & {
120
+ anchorState: CommentAnchorState;
121
+ /**
122
+ * False when the viewer can read the thread but not resolve or delete it — a
123
+ * cloud thread on a link they no longer manage. Absent means they can.
124
+ */
125
+ manage?: false;
126
+ /** The git branch a cloud thread's link was last published from, where known. */
127
+ branch?: string;
128
+ };
@@ -106,6 +106,9 @@ export const HostAppSchema = z.object({
106
106
 
107
107
  export type HostApp = z.infer<typeof HostAppSchema>;
108
108
 
109
+ /** The CSS framework a folder styles with. See `ConfigSchema.styling`. */
110
+ export const StylingSchema = z.object({ framework: z.enum(["tailwind", "none"]) });
111
+
109
112
  /**
110
113
  * Folder config. Multi-library: a folder registers N libraries by id
111
114
  * and pins one as the default; each screen optionally declares which
@@ -186,7 +189,7 @@ export const ConfigSchema = z
186
189
  * server rejects a folder whose CSS framework no registered library
187
190
  * supports (e.g. shadcn + none).
188
191
  */
189
- styling: z.object({ framework: z.enum(["tailwind", "none"]) }).optional(),
192
+ styling: StylingSchema.optional(),
190
193
  /** Host app location for the live-island bundler. See `HostAppSchema`. */
191
194
  hostApp: HostAppSchema.optional(),
192
195
  /**
@@ -53,6 +53,7 @@ export {
53
53
  HostAppSchema,
54
54
  type Library,
55
55
  LibrarySchema,
56
+ StylingSchema,
56
57
  type ViewportPreset,
57
58
  ViewportPresetSchema,
58
59
  } from "./config.ts";
@@ -94,10 +95,12 @@ export {
94
95
  } from "./migrate.ts";
95
96
  export {
96
97
  type ComponentNode,
98
+ type EmitAsRef,
97
99
  isComponentNode,
98
100
  isParamRef,
99
101
  isRepoNode,
100
102
  isSnippetInstance,
103
+ isTextNode,
101
104
  type Node,
102
105
  NodeIdSchema,
103
106
  NodeSchema,
@@ -109,7 +112,19 @@ export {
109
112
  repoImportIssue,
110
113
  repoKey,
111
114
  type SnippetInstance,
115
+ type TextNode,
112
116
  } from "./node.ts";
117
+ export {
118
+ type EmitAsNode,
119
+ isNode,
120
+ isSyntheticRef,
121
+ type NodeShape,
122
+ nodeShape,
123
+ PARAM_TAG_REF,
124
+ type RepoNode,
125
+ STATIC_REF,
126
+ type SyntheticRef,
127
+ } from "./node-identity.ts";
113
128
  export {
114
129
  DesignNameSchema,
115
130
  designNameIssue,
@@ -176,7 +191,7 @@ export {
176
191
  typesetSizePx,
177
192
  typesetThemeTokens,
178
193
  typesetUtilityClasses,
179
- typesetV3FontSize,
194
+ typesetV3Theme,
180
195
  typesetVars,
181
196
  } from "./typeset.ts";
182
197
  export { type DuplicateId, findDuplicateIds } from "./validate-ids.ts";