velloo 0.4.1 → 0.5.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 (101) hide show
  1. package/README.md +29 -7
  2. package/canvas/assets/index-BDBBHdHH.css +2 -0
  3. package/canvas/assets/index-QdsLJNY9.js +29 -0
  4. package/canvas/assets/{lucide-all-Bcm80WFX.js → lucide-all-R8DUyY-B.js} +1 -1
  5. package/canvas/index.html +2 -2
  6. package/chunk-0rdf6trc.js +2 -0
  7. package/{chunk-npsg7btt.js → chunk-0wafc9aw.js} +1 -1
  8. package/chunk-0wqxb8r9.js +3 -0
  9. package/{chunk-e6nhc3ay.js → chunk-10hap8kc.js} +1 -1
  10. package/chunk-18eqh3s1.js +2 -0
  11. package/chunk-19xyzep8.js +2 -0
  12. package/{chunk-7gtvstne.js → chunk-1h3gqy7w.js} +1 -1
  13. package/{chunk-vjrn10xr.js → chunk-33t0csxs.js} +2 -2
  14. package/{chunk-7xffbj5h.js → chunk-3ffs39e8.js} +1 -1
  15. package/chunk-42bj6r30.js +3 -0
  16. package/chunk-49w26e94.js +5 -0
  17. package/{chunk-7a45tn73.js → chunk-4pkw6d9n.js} +2 -2
  18. package/{chunk-ph94nrc8.js → chunk-4sjvdakg.js} +1 -1
  19. package/chunk-5f0xv80x.js +5 -0
  20. package/{chunk-2dm4x17z.js → chunk-5ma8zbty.js} +1 -1
  21. package/{chunk-t9ygewjx.js → chunk-5y9hem1j.js} +1 -1
  22. package/chunk-66qgm91k.js +708 -0
  23. package/{chunk-dj1pp4e3.js → chunk-675tr675.js} +1 -1
  24. package/{chunk-jwzhhhzn.js → chunk-6cgvzwec.js} +1 -1
  25. package/chunk-6k05kqnc.js +3 -0
  26. package/{chunk-py6sjyzk.js → chunk-6ytv1v2r.js} +1 -1
  27. package/{chunk-2h24va0j.js → chunk-75sc3adm.js} +1 -1
  28. package/chunk-7cexzg71.js +2 -0
  29. package/chunk-98rann8t.js +2 -0
  30. package/{chunk-b58pnj20.js → chunk-a16mz9zf.js} +1 -1
  31. package/{chunk-m1wp8cpt.js → chunk-afe25dpr.js} +1 -1
  32. package/{chunk-k3dpwjx4.js → chunk-ahm4zd56.js} +1 -1
  33. package/chunk-asvdqkdp.js +6 -0
  34. package/{chunk-rfrbnw8r.js → chunk-b3x80jc1.js} +1 -1
  35. package/{chunk-tr49kraa.js → chunk-cdqah1hy.js} +1 -1
  36. package/{chunk-r03aywsb.js → chunk-cqt1dpxd.js} +1 -1
  37. package/{chunk-xbhxzdat.js → chunk-d4r0myxr.js} +1 -1
  38. package/{chunk-km3z2czq.js → chunk-d9kx7v12.js} +1 -1
  39. package/{chunk-2y9nnzpr.js → chunk-df557bn4.js} +1 -1
  40. package/{chunk-6zr596r2.js → chunk-edgwx026.js} +1 -1
  41. package/chunk-emv1j50t.js +2 -0
  42. package/{chunk-jep31ffe.js → chunk-ewgdcgrk.js} +1 -1
  43. package/chunk-f45p6kcb.js +2 -0
  44. package/{chunk-b4jy4hcd.js → chunk-gn9wcc1b.js} +1 -1
  45. package/{chunk-cejmmx3r.js → chunk-hr3gbak5.js} +2 -2
  46. package/{chunk-91pajfnm.js → chunk-j7tzbt3d.js} +1 -1
  47. package/chunk-mm8wv3bg.js +3 -0
  48. package/chunk-nbbnb5e9.js +3 -0
  49. package/{chunk-a9mgvspm.js → chunk-ngbqd7kb.js} +32 -12
  50. package/chunk-nkqtnwzz.js +2 -0
  51. package/{chunk-2jbhv7vh.js → chunk-q3kf8243.js} +1 -1
  52. package/{chunk-8t5phg0s.js → chunk-qpv28z8d.js} +1 -1
  53. package/{chunk-hsrg23gd.js → chunk-rd65xm6r.js} +1 -1
  54. package/{chunk-aa3str7g.js → chunk-ryga3ny1.js} +1 -1
  55. package/{chunk-cvn689d5.js → chunk-sb5h27w9.js} +2 -2
  56. package/chunk-tw4kw4ps.js +22 -0
  57. package/chunk-v6c3h9ft.js +3 -0
  58. package/chunk-vgx47anc.js +3 -0
  59. package/chunk-vrvkvb0v.js +244 -0
  60. package/{chunk-db7mf114.js → chunk-vryadew3.js} +9 -7
  61. package/chunk-w6f40y2x.js +11 -0
  62. package/{chunk-0446t32z.js → chunk-we8hvy6e.js} +1 -1
  63. package/{chunk-ykdx12na.js → chunk-wkqrb2tz.js} +1 -1
  64. package/{chunk-r9ekd92e.js → chunk-xzkczmgg.js} +1 -1
  65. package/{chunk-yfttm20w.js → chunk-ycrddh47.js} +1 -1
  66. package/{chunk-j5axqyan.js → chunk-z78c1kf8.js} +1 -1
  67. package/cli.js +1 -1
  68. package/package.json +3 -1
  69. package/pkgs/provider-html/src/index.ts +68 -0
  70. package/pkgs/provider-html/src/registry.ts +121 -0
  71. package/pkgs/provider-html/src/version.ts +1 -0
  72. package/pkgs/provider-none/src/components-inline.tsx +12 -3
  73. package/pkgs/provider-none/src/components.tsx +11 -3
  74. package/pkgs/provider-none/src/manifest.ts +1 -0
  75. package/pkgs/schema/src/config.ts +33 -1
  76. package/pkgs/schema/src/css-declarations.ts +41 -0
  77. package/pkgs/schema/src/index.ts +3 -0
  78. package/pkgs/schema/src/theme.ts +40 -0
  79. package/plugins/claude/velloo/agents/velloo-design-reviewer.md +56 -13
  80. package/plugins/claude/velloo/agents/velloo-designer.md +4 -1
  81. package/skills/velloo-setup/SKILL.md +19 -55
  82. package/canvas/assets/index-B1OD0Yz5.css +0 -2
  83. package/canvas/assets/index-BWYFx2Rr.js +0 -29
  84. package/chunk-00t0bz7x.js +0 -244
  85. package/chunk-0zen7bzb.js +0 -3
  86. package/chunk-5ywkk7d6.js +0 -2
  87. package/chunk-b7famda5.js +0 -3
  88. package/chunk-f5g45xed.js +0 -2
  89. package/chunk-fjbzk4sw.js +0 -3
  90. package/chunk-hv52a6hf.js +0 -2
  91. package/chunk-mcymwefw.js +0 -5
  92. package/chunk-n0kx20s2.js +0 -2
  93. package/chunk-nwf2kn41.js +0 -26
  94. package/chunk-phz993p3.js +0 -2
  95. package/chunk-qan5dnjt.js +0 -688
  96. package/chunk-r5d0jsnd.js +0 -5
  97. package/chunk-shac6bat.js +0 -6
  98. package/chunk-t4zvcece.js +0 -10
  99. package/chunk-tarws372.js +0 -2
  100. package/chunk-yghypth2.js +0 -2
  101. package/chunk-zqtz23xk.js +0 -3
@@ -28,7 +28,7 @@ export const LibrarySchema = z.object({
28
28
  * Component provider id. The server's provider loader maps ids to
29
29
  * factories; `"shadcn-upstream"` is the default for new folders.
30
30
  */
31
- id: z.enum(["shadcn-upstream", "none", "mui", "antd", "chakra"]),
31
+ id: z.enum(["shadcn-upstream", "none", "html", "mui", "antd", "chakra"]),
32
32
  version: z.string().min(1),
33
33
  source: z.enum(["binary", "cache", "in-repo"]),
34
34
  /** Where the components live, relative to the design folder root. */
@@ -50,6 +50,19 @@ const CodegenConfigSchema = z.object({
50
50
 
51
51
  export type CodegenConfig = z.infer<typeof CodegenConfigSchema>;
52
52
 
53
+ /**
54
+ * A path inside the host app, relative to its root. The config is committed
55
+ * with the repo and the agents are told to open what it names, so a cloned
56
+ * repo must not be able to aim it anywhere else: absolute paths and `..`
57
+ * segments make the file invalid rather than being resolved.
58
+ */
59
+ const AppRelativePathSchema = z
60
+ .string()
61
+ .min(1)
62
+ .refine((path) => !/^(?:[\\/~]|[A-Za-z]:)/.test(path) && !path.split(/[\\/]/).includes(".."), {
63
+ message: "must be relative to the app root, with no absolute path or `..` segment",
64
+ });
65
+
53
66
  /**
54
67
  * Where the host app lives, used by the live-island bundler to resolve
55
68
  * a `render:"live"` extension's `importPath` (and its dependencies, e.g.
@@ -73,6 +86,11 @@ export const HostAppSchema = z.object({
73
86
  * built-in framework recipe, then no wrapper.
74
87
  */
75
88
  preview: z.string().min(1).optional(),
89
+ /**
90
+ * The app's stylesheets an HTML design is styled by, in cascade order. The
91
+ * design keeps copies of them under `assets/host/` (`store_host_files`).
92
+ */
93
+ stylesheets: z.array(z.string().regex(/^(?:\/(?!\/)|https:\/\/)/)).optional(),
76
94
  /**
77
95
  * Bounds repository-component discovery beyond what the app's entries and
78
96
  * routes import: `include` adds component roots (files or directories,
@@ -171,6 +189,20 @@ export const ConfigSchema = z
171
189
  styling: z.object({ framework: z.enum(["tailwind", "none"]) }).optional(),
172
190
  /** Host app location for the live-island bundler. See `HostAppSchema`. */
173
191
  hostApp: HostAppSchema.optional(),
192
+ /**
193
+ * A design-system document this folder follows — normally the repo's
194
+ * `DESIGN.md`.
195
+ *
196
+ * A path, never a copy. The file belongs to the repo and goes on being
197
+ * edited there; a copy inside the design folder would drift away from the
198
+ * rules it was supposed to state, which is the one thing a design system
199
+ * must not do. Relative to the host app root, so the usual value is
200
+ * `DESIGN.md`.
201
+ *
202
+ * Optional because the folder also finds one by convention — this records
203
+ * a file somewhere unconventional, or pins the choice when several exist.
204
+ */
205
+ designSystem: z.object({ path: AppRelativePathSchema }).optional(),
174
206
  /**
175
207
  * Named host apps for monorepos — the multi-app twin of `hostApp`, keyed
176
208
  * by a short app name (`"web"`, `"admin"`; `init`'s multi-app scan uses
@@ -0,0 +1,41 @@
1
+ const CSS_PROPERTY = /^(--[\w-]+|-?[a-z][a-z-]*)$/;
2
+
3
+ /**
4
+ * CSS declaration text as a React style object (`background-size` ⇒
5
+ * `backgroundSize`, custom properties kept), or null when the text is not a
6
+ * declaration list — a class string, say. Semicolons inside `url(…)` or quotes
7
+ * do not split.
8
+ */
9
+ export function styleObjectFromCss(text: string): Record<string, string> | null {
10
+ const declarations: string[] = [];
11
+ let depth = 0;
12
+ let quote: string | null = null;
13
+ let start = 0;
14
+ for (let i = 0; i < text.length; i++) {
15
+ const char = text[i];
16
+ if (quote) {
17
+ if (char === quote) quote = null;
18
+ } else if (char === '"' || char === "'") quote = char;
19
+ else if (char === "(") depth++;
20
+ else if (char === ")") depth = Math.max(0, depth - 1);
21
+ else if (char === ";" && depth === 0) {
22
+ declarations.push(text.slice(start, i));
23
+ start = i + 1;
24
+ }
25
+ }
26
+ declarations.push(text.slice(start));
27
+ const style: Record<string, string> = {};
28
+ for (const declaration of declarations) {
29
+ if (declaration.trim() === "") continue;
30
+ const colon = declaration.indexOf(":");
31
+ if (colon < 0) return null;
32
+ const property = declaration.slice(0, colon).trim().toLowerCase();
33
+ const value = declaration.slice(colon + 1).trim();
34
+ if (!CSS_PROPERTY.test(property) || value === "") return null;
35
+ const key = property.startsWith("--")
36
+ ? property
37
+ : property.replace(/^-(ms)-/, "$1-").replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
38
+ style[key] = value;
39
+ }
40
+ return Object.keys(style).length > 0 ? style : null;
41
+ }
@@ -56,6 +56,7 @@ export {
56
56
  type ViewportPreset,
57
57
  ViewportPresetSchema,
58
58
  } from "./config.ts";
59
+ export { styleObjectFromCss } from "./css-declarations.ts";
59
60
  export {
60
61
  isCssIdent,
61
62
  neutralizeCssText,
@@ -139,6 +140,8 @@ export {
139
140
  ColorsSchema,
140
141
  type LooseTokenGroup,
141
142
  resolveColors,
143
+ spacingCssTokens,
144
+ TAILWIND_SIZE_NAMES,
142
145
  type Theme,
143
146
  ThemeSchema,
144
147
  TypographySchema,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { isCssIdent } from "./css-sanitize.ts";
2
3
 
3
4
  const NumOrCssLen = z.union([z.number(), z.string().min(1)]);
4
5
 
@@ -56,6 +57,45 @@ export type Colors = z.infer<typeof ColorsSchema>;
56
57
  */
57
58
  export type ColorsOverride = { [K in keyof Colors]?: Colors[K] | undefined };
58
59
 
60
+ /**
61
+ * Spacing names Tailwind v4 already reads as a size scale. Its `max-w-*`,
62
+ * `w-*` and `min-w-*` utilities resolve a `--spacing-<name>` ahead of
63
+ * `--container-<name>`, so a design system's t-shirt spacing (`xl: 1.5rem`)
64
+ * emitted under one of these names turns every `max-w-xl` into 24px and a
65
+ * paragraph into one word per line.
66
+ */
67
+ export const TAILWIND_SIZE_NAMES: ReadonlySet<string> = new Set([
68
+ "3xs",
69
+ "2xs",
70
+ "xs",
71
+ "sm",
72
+ "md",
73
+ "lg",
74
+ "xl",
75
+ "2xl",
76
+ "3xl",
77
+ "4xl",
78
+ "5xl",
79
+ "6xl",
80
+ "7xl",
81
+ "prose",
82
+ ]);
83
+
84
+ /**
85
+ * The named spacing tokens that become `--spacing-<name>` in CSS. Numeric
86
+ * steps are Tailwind's built-in scale and names Tailwind reads as sizes stay
87
+ * in the theme only; the canvas stylesheet and the emitted globals.css both
88
+ * come from this list, so they cannot disagree.
89
+ */
90
+ export function spacingCssTokens(spacing: Theme["spacing"]): Array<[string, string | number]> {
91
+ const out: Array<[string, string | number]> = [];
92
+ for (const [name, value] of Object.entries(spacing ?? {})) {
93
+ if (!Number.isNaN(Number(name)) || !isCssIdent(name) || TAILWIND_SIZE_NAMES.has(name)) continue;
94
+ if (typeof value === "string" || typeof value === "number") out.push([name, value]);
95
+ }
96
+ return out;
97
+ }
98
+
59
99
  /**
60
100
  * The effective colors for a mode: `colors`, with `colorsDark` layered on when
61
101
  * rendering dark.
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: velloo-design-reviewer
3
3
  description: >-
4
- Reviews Velloo screens without modifying them — dark-mode adaptation, theme
5
- token discipline, contrast, layout at mobile width, and fidelity against a
6
- live app page. Use after a design pass for an independent findings list.
4
+ Reviews Velloo screens without modifying them — the folder's own stated design
5
+ rules, dark-mode adaptation, theme token discipline, contrast, layout at mobile
6
+ width, and fidelity against a live app page. Use after a design pass for an
7
+ independent findings list.
7
8
  ---
8
9
 
9
10
  You are a design reviewer for a Velloo design folder. You are read-only:
@@ -11,23 +12,65 @@ screenshots, diagnostics, and annotations only. Do NOT mutate screens, snippets,
11
12
  or the theme — the one exception is `add_annotation`, to pin a finding to the
12
13
  node it concerns.
13
14
 
15
+ Start with `get_theme`. Besides the tokens it may return `designSystem.path` —
16
+ the design system document this folder follows, normally the repo's
17
+ `DESIGN.md`. **Open that file and read it before looking at any screen.** Velloo
18
+ points at it rather than copying it, so the file you read is the current one.
19
+ Its "Do's and Don'ts" section is the house rules, and they outrank your taste.
20
+
14
21
  For each target screen (default: every screen on the default board):
15
22
 
16
- 1. `screenshot mode: "compare"` — does the design actually adapt to dark
23
+ 1. **The folder's stated rules** — check the screens against the Do's and
24
+ Don'ts in the design system document. See below; this dimension comes first
25
+ because it is the only one that is not generic.
26
+ 2. `screenshot mode: "compare"` — does the design actually adapt to dark
17
27
  mode, or do raw palette colors freeze it?
18
- 2. The screenshot's `diagnostics` — read the `theme/raw-color` entries;
28
+ 3. The screenshot's `diagnostics` — read the `theme/raw-color` entries;
19
29
  distinguish real token violations from intentional accents (`data-accent`
20
30
  nodes are exempt).
21
- 3. `score_theme_contrast` — flag failing pairs; dark is where contrast
31
+ 4. `score_theme_contrast` — flag failing pairs; dark is where contrast
22
32
  usually breaks.
23
- 4. `screenshot` at a mobile viewport (390 wide) — does the layout hold, or do
33
+ 5. `screenshot` at a mobile viewport (390 wide) — does the layout hold, or do
24
34
  grids overflow and type sizes collapse?
25
- 5. If given a live URL, `compare_to_url` at the same viewport — report the
35
+ 6. If given a live URL, `compare_to_url` at the same viewport — report the
26
36
  similarity score and what the per-region refs point at. An `unverified`
27
37
  result is itself a finding: say so instead of guessing.
28
- 6. `list_annotations` — surface unresolved designer/reviewer notes.
38
+ 7. `list_annotations` — surface unresolved designer/reviewer notes.
39
+
40
+ ## Reviewing against the folder's rules
41
+
42
+ Work rule by rule, not screen by screen — a rule like "one accent per screen"
43
+ is about the whole screen, and you will miss it if you are looking at nodes.
44
+
45
+ **Quote the rule verbatim in every finding it produces**, copied from the file.
46
+ The user wrote these; a finding that paraphrases them reads as your opinion, and
47
+ the point of this dimension is that it is not.
48
+
49
+ **Check only what you can actually see.** A rule is checkable when a screenshot
50
+ or a diagnostic settles it — how many accent colors a screen uses, whether a
51
+ flat surface carries a shadow, whether CTAs are pill-shaped, whether a heading
52
+ uses the display face. A rule is not checkable here when it is about behavior
53
+ (motion, transitions, hover, focus order), about content or tone, or about
54
+ things outside the screens you were given.
55
+
56
+ For an unverifiable rule, say so once in a short list at the end — "not checked:
57
+ rules 4, 7 (both about hover states)" — and move on. Do not guess, do not
58
+ soften it into a maybe, and do not pad the review by restating rules you did not
59
+ test. An honest "not checked" is worth more than a speculative finding.
60
+
61
+ When a rule and one of the generic dimensions disagree, report both and say
62
+ which is which. If the folder's rule is the reason something looks wrong to
63
+ you, the rule wins and there is no finding.
64
+
65
+ If `get_theme` returns no `designSystem`, or the document has no Do's and Don'ts
66
+ section, the folder states no rules. Say that once — it is not the same as the
67
+ screens passing — and review the other dimensions normally. Do not invent house
68
+ rules to fill the gap. If the path is there but you cannot open the file, say
69
+ that too rather than reviewing as if it were empty.
70
+
71
+ ## Reporting
29
72
 
30
- Report findings ranked by severity. Each finding: screen id, node ref
31
- (`"@id"` or path), what is wrong, and a concrete fix. Pin the top findings
32
- with `add_annotation` so they're addressable on the canvas. End with the one
33
- thing you'd fix first.
73
+ Report findings ranked by severity. Each finding: screen id, node ref (`"@id"`
74
+ or path), what is wrong, and a concrete fix. A rule-based finding also carries
75
+ the quoted rule. Pin the top findings with `add_annotation` so
76
+ they're addressable on the canvas. End with the one thing you'd fix first.
@@ -16,7 +16,10 @@ Workflow:
16
16
 
17
17
  1. **Discover before composing.** `list_components` (`mode: "summary"` first),
18
18
  `get_theme`, `list_components` (`kind: "snippet"`), `list_boards`. Reuse existing snippets before
19
- defining new ones.
19
+ defining new ones. When `get_theme` returns a `designSystem.path`, open that
20
+ file and read it: it is the folder's own design system in prose — house rules
21
+ that outrank the defaults below, and what the reviewer will check this work
22
+ against. Velloo points at the file rather than copying it, so it is current.
20
23
  2. **Build in big strokes.** `compose` accepts a full subtree as restricted
21
24
  JSX — a whole section per call, not node-by-node; `batch` groups mutations
22
25
  atomically. Repeated structure (cards, rows, nav items) becomes a snippet
@@ -6,7 +6,8 @@ description: >-
6
6
  verify the canvas against the running app. Use once per repo before the first
7
7
  design task, and again when the app's design language changes. Triggers:
8
8
  "set up velloo for this repo", "make the preview match my app", "why doesn't
9
- the canvas look like my app", or the setup step of an init handoff prompt.
9
+ the canvas look like my app", or before recreating an app's page from an
10
+ init handoff prompt.
10
11
  ---
11
12
 
12
13
  # Calibrating Velloo to an existing app
@@ -44,60 +45,23 @@ halfway through a design.
44
45
  tool re-probes and answers with the new state; iterate until `valid`.
45
46
 
46
47
  A snippet can't do this job: it is node data, so it can't import CSS, build a
47
- router or wrap the whole screen. Snippets are for step 4 below.
48
-
49
- ## 1. Theme first — it buys the most
50
-
51
- `import_theme { cssPath, apply: true }` against the app's stylesheet. This is
52
- not just colors: it ingests `--radius`, `--font-*` roles, the non-semantic
53
- palette (`--primary-600`, `--ink`), and — when it finds a `tailwind.config`
54
- nearby — that config's `theme.extend` spacing, shadows, fonts, keyframes, and
55
- `container` settings. Run it dry first (the default) and read the reported
56
- changes; `apply: true` when they look like the app.
57
-
58
- Init records the stylesheet it found in the folder config, so check there
59
- before hunting. If there's no stylesheet (a `none`-CSS folder, an MUI app),
60
- `import_theme` still takes raw `css` text, and an MUI app's `createTheme` call
61
- is the equivalent source.
62
-
63
- **Fonts are the highest-leverage single token.** A design in the wrong typeface
64
- reads as wrong no matter how correct the layout is. If `import_theme` didn't
65
- resolve real families, set them with `set_theme { fonts }` — read the app's font loading
66
- (next/font, a `@font-face`, a Google Fonts link) to get the actual names.
67
-
68
- Then `score_theme_contrast` to confirm the imported palette holds up in both
69
- modes; an app that only ever ships light mode often imports into a dark palette
70
- that fails.
71
-
72
- ## 2. Verify against the real thing, early
73
-
74
- Do not wait until a design is finished to discover the baseline was wrong.
75
- Recreate one representative screen — or use whatever init scaffolded — and run
76
- `compare_to_url` against the running app.
77
-
78
- - Get the app running first. Start its dev server yourself if the package
79
- scripts make it obvious; otherwise ask the user for the command, a running
80
- URL, or a deployed preview. Ask once, plainly, rather than guessing ports.
81
- - **Read `unverified` on every result.** `redirected` / `authWall` means you
82
- captured a login page, so the similarity number is meaningless.
83
- - **For a login wall, use a capture session.** `start_capture_session { url }`
84
- opens a real browser the *user* drives. It returns immediately — it does not
85
- wait — so say plainly what you need ("log in, then hit **Capture page** in the
86
- velloo toolbar on each page, then **Done**") and poll `list_captures` until
87
- their captures land. Then verify with `compare_to_url { captureId }` instead
88
- of `url`: the capture is already past the login and frozen, so it can't bounce
89
- to a login page or drift between runs. `storageStatePath` / `cookies` /
90
- `localStorage` stay available for when you already hold a session.
91
- - If you cannot get a real capture, **leave it unverified and say so.** Tuning a
92
- design toward a page you never saw is worse than admitting the gap.
93
- - For a dashboard or feed whose content shifts between loads, pass
94
- `cacheUrl: true` so you diff against one frozen capture instead of drifting
95
- content.
48
+ router or wrap the whole screen. Snippets are for step 3 below.
49
+
50
+ ## 1. Theme, fonts and the compare loop
96
51
 
97
- 0.85+ similarity is a faithful structural port. Don't chase 1.0 — fonts and
98
- live data legitimately differ.
52
+ The MCP instructions and `velloo://guide/porting` own this part: import the
53
+ app's stylesheet with `import_theme` (dry run, then `apply: true`), set its real
54
+ fonts with `set_theme { fonts }` — read how the app loads them; a wrong typeface
55
+ makes a correct layout read as wrong — then recreate one representative screen
56
+ and iterate `compare_to_url` against the running app, reading `unverified`
57
+ before trusting any number. Two additions that live only here:
58
+
59
+ - Run `score_theme_contrast` after the import. An app that only ever ships
60
+ light mode often imports into a dark palette that fails.
61
+ - For a dashboard or feed whose content shifts between loads, pass
62
+ `cacheUrl: true` so you diff against one frozen capture.
99
63
 
100
- ## 3. Establish component fidelity before adapting anything
64
+ ## 2. Establish component fidelity before adapting anything
101
65
 
102
66
  Only after theme parity, call `component_status { screen: "<id>" }` for each
103
67
  screen you're about to design or verify (or `{ ids: [...] }` before a screen
@@ -128,7 +92,7 @@ Host-source edits invalidate the canvas bundle automatically. After changing a
128
92
  component, wait for the frame to reload and call `component_status` again; do
129
93
  not restart the daemon merely to pick up a normal source edit.
130
94
 
131
- ## 4. Close only the important remaining gaps
95
+ ## 3. Close only the important remaining gaps
132
96
 
133
97
  If an on-screen component is not `exact` and the difference is load-bearing,
134
98
  choose the smallest honest adaptation:
@@ -153,7 +117,7 @@ the adaptation is what keeps dialogs, menus, popovers, and similar components
153
117
  visible and selectable on a static canvas. Adapt only when the visual contract
154
118
  the user cares about is materially different.
155
119
 
156
- ## 5. Leave a record
120
+ ## 4. Leave a record
157
121
 
158
122
  Write what you found as a note on the main board (`add_note`) so the next
159
123
  session — and the user — knows where things stand. Three headings, honest: