@zohofinance/commerce 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/package.json +2 -2
- package/ts-dist/mocks/init-theme/.claude/CLAUDE.md +6 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/SKILL.md +242 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/inputs/figma.md +81 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/inputs/screenshot.md +54 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/inputs/url.md +88 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/spec-table.md +102 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/verification/accessibility.md +110 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/verification/performance.md +127 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/verification/tokens.md +87 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-section-from-design.20260528/verification/visual-diff.md +123 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/SKILL.md +605 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/design-tokens.md +105 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/page-json-contract.md +58 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/qa-accessibility.md +35 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/reference-lookup.md +94 -0
- package/ts-dist/mocks/init-theme/.claude/skills/.archive/zcommerce-theme-builder.20260528/section-anatomy.md +109 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/SKILL.md +313 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/agent-hierarchy.md +142 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/ask-developer.md +113 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/bootstrap.md +260 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/build-metrics.md +89 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/code-quality.md +186 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/common-pages-agent.md +97 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/dev-server-and-urls.md +127 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/exemplars.md +79 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/figma-discovery.md +138 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/figma-extraction.md +132 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/image-sources.md +84 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/inputs/guidelines-frame.md +178 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/page-builder.md +322 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/page-orchestrator.md +199 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/parallel-fanout.md +152 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/a11y.mjs +129 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/lint-section.mjs +502 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/perf.mjs +134 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/pixel-diff.mjs +206 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/record-build-metrics.mjs +118 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/section-builder.md +131 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/subagent-escalation.md +75 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/transcription-rules.md +98 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/verification.md +227 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/visual-verify-loop.md +117 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/_index.md +4 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/assets.md +105 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/best-practices.md +212 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/custom-scripts.md +294 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/i18n.md +155 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/introduction.md +91 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/layouts.md +158 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/pages.md +156 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/reference/_index.md +4 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/reference/filters.md +419 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/reference/global-objects.md +415 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/reference/schema-types.md +926 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/reference/tags.md +259 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/sections.md +203 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/setup-guide.md +424 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/snippets.md +70 -0
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/zcommerce-docs/theme-structure.md +252 -0
- package/ts-dist/mocks/init-theme/.cursor/PLAYBOOK.md +144 -0
- package/ts-dist/mocks/init-theme/.cursor/README.md +6 -0
- package/ts-dist/mocks/init-theme/.cursor/agents/figma-desktop-mobile-parallel.md +48 -0
- package/ts-dist/mocks/init-theme/.cursor/agents/visual-compare.md +221 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/add-field.md +83 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/add-theme-setting.md +200 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/assemble-page.md +72 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/audit-page.md +57 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/build-page-sections.md +110 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/build-section.md +418 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/build-theme.md +307 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/compare-section.md +358 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/create-theme.md +77 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/extract-tokens.md +43 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/fix-section.md +92 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/impact.md +50 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-a11y.md +66 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-i18n.md +110 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-layout.md +127 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-performance.md +72 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-section.md +84 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-sections.md +48 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/qa-theme.md +85 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/redesign-section.md +177 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/refactor-section.md +185 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/sync-theme-settings.md +132 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/validate-page.md +60 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/validate-section-group.md +88 -0
- package/ts-dist/mocks/init-theme/.cursor/commands/validate-theme-settings.md +119 -0
- package/ts-dist/mocks/init-theme/.cursor/examples/design-instructions-template.md +151 -0
- package/ts-dist/mocks/init-theme/.cursor/mcp.json +18 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/README.md +160 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/accessibility.mdc +24 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/add-field.mdc +222 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/add-theme-setting.mdc +39 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/assemble-page.mdc +130 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/audit-page.mdc +92 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/build-section.mdc +178 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/build-theme.mdc +47 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/compare-section.mdc +94 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/create-theme.mdc +78 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/css.mdc +137 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/design-spec-extraction.mdc +255 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/extract-tokens.mdc +43 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/figma-mcp-resolution.mdc +40 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/fix-section.mdc +154 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/general.mdc +30 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/i18n.mdc +85 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/javascript.mdc +85 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/liquid-no-hardcoded-strings.mdc +42 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/liquid.mdc +171 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/mobile-desktop-setting-meta.mdc +34 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/naming-convention.mdc +64 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/pages.mdc +120 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/performance.mdc +51 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/qa-section.mdc +134 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/redesign-section.mdc +147 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/schema-sync.mdc +110 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/schema.mdc +360 -0
- package/ts-dist/mocks/init-theme/.cursor/rules/share-urls.mdc +68 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/browser-verify.md +128 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/design-tokens.md +105 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/page-json-contract.md +87 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/qa-accessibility.md +35 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/reference-lookup.md +88 -0
- package/ts-dist/mocks/init-theme/.cursor/skills/section-anatomy.md +173 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pixel-diff.mjs — render a URL in headless Chrome, compare against a Figma reference image.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node pixel-diff.mjs \
|
|
7
|
+
* --rendered-url http://localhost:3000/home \
|
|
8
|
+
* --figma-image /path/to/figma-desktop.png \
|
|
9
|
+
* --viewport 1440x900 \
|
|
10
|
+
* --selector "#section-hero" (optional — crop to this element)
|
|
11
|
+
* --threshold 0.1 (pixelmatch threshold, default 0.1)
|
|
12
|
+
* --out /path/to/pixel-diff.json
|
|
13
|
+
*
|
|
14
|
+
* Output JSON shape:
|
|
15
|
+
* {
|
|
16
|
+
* "rendered_url": "...",
|
|
17
|
+
* "viewport": { "width": 1440, "height": 900 },
|
|
18
|
+
* "selector": "#section-hero",
|
|
19
|
+
* "metrics": {
|
|
20
|
+
* "mismatched_pixels": 12345,
|
|
21
|
+
* "total_pixels": 1296000,
|
|
22
|
+
* "mismatch_ratio": 0.0095,
|
|
23
|
+
* "mean_delta_e": 1.4,
|
|
24
|
+
* "max_delta_e": 4.2
|
|
25
|
+
* },
|
|
26
|
+
* "regions": [ { "x":..., "y":..., "w":..., "h":..., "delta_e":... }, ... ],
|
|
27
|
+
* "verdict": "PASS" | "FAIL",
|
|
28
|
+
* "rendered_screenshot": "/path/written.png",
|
|
29
|
+
* "diff_image": "/path/diff.png"
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Pass thresholds (default):
|
|
33
|
+
* mean_delta_e < 2.0
|
|
34
|
+
* max_delta_e < 5.0
|
|
35
|
+
* mismatch_ratio < 0.03
|
|
36
|
+
*
|
|
37
|
+
* Dependencies (install in this theme if missing):
|
|
38
|
+
* puppeteer
|
|
39
|
+
* pixelmatch
|
|
40
|
+
* pngjs
|
|
41
|
+
*
|
|
42
|
+
* If they are missing, run:
|
|
43
|
+
* npm install --no-save puppeteer pixelmatch pngjs
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import puppeteer from 'puppeteer';
|
|
47
|
+
import pixelmatch from 'pixelmatch';
|
|
48
|
+
import { PNG } from 'pngjs';
|
|
49
|
+
import fs from 'node:fs';
|
|
50
|
+
import path from 'node:path';
|
|
51
|
+
|
|
52
|
+
const args = parseArgs(process.argv.slice(2));
|
|
53
|
+
const renderedUrl = required(args, 'rendered-url');
|
|
54
|
+
const figmaImagePath = required(args, 'figma-image');
|
|
55
|
+
const viewport = parseViewport(required(args, 'viewport'));
|
|
56
|
+
const selector = args.selector || null;
|
|
57
|
+
const threshold = parseFloat(args.threshold ?? '0.1');
|
|
58
|
+
const outPath = required(args, 'out');
|
|
59
|
+
|
|
60
|
+
const PASS = { meanDeltaE: 2.0, maxDeltaE: 5.0, mismatchRatio: 0.03 };
|
|
61
|
+
|
|
62
|
+
(async () => {
|
|
63
|
+
if (!fs.existsSync(figmaImagePath)) {
|
|
64
|
+
fail(`Figma reference image not found: ${figmaImagePath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const browser = await puppeteer.launch({
|
|
68
|
+
headless: 'new',
|
|
69
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const page = await browser.newPage();
|
|
73
|
+
await page.setViewport({ width: viewport.width, height: viewport.height, deviceScaleFactor: 1 });
|
|
74
|
+
await page.goto(renderedUrl, { waitUntil: 'networkidle2', timeout: 60_000 });
|
|
75
|
+
|
|
76
|
+
// Optional small extra wait for late-loading images / fonts
|
|
77
|
+
await page.evaluate(() => document.fonts && document.fonts.ready);
|
|
78
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
79
|
+
|
|
80
|
+
const renderedPath = outPath.replace(/\.json$/, '-rendered.png');
|
|
81
|
+
if (selector) {
|
|
82
|
+
const el = await page.$(selector);
|
|
83
|
+
if (!el) fail(`Selector not found in DOM: ${selector}`);
|
|
84
|
+
await el.screenshot({ path: renderedPath });
|
|
85
|
+
} else {
|
|
86
|
+
await page.screenshot({ path: renderedPath, fullPage: false });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rendered = readPng(renderedPath);
|
|
90
|
+
const figma = readPng(figmaImagePath);
|
|
91
|
+
|
|
92
|
+
// Resize figma to match rendered if dimensions differ. pixelmatch needs identical sizes.
|
|
93
|
+
// Simple approach: write canvas with sharp-like manual scaling? We avoid extra deps —
|
|
94
|
+
// require the caller to provide a Figma image at the right dimensions. If they don't match,
|
|
95
|
+
// we crop or pad and warn.
|
|
96
|
+
let figmaForCompare = figma;
|
|
97
|
+
if (figma.width !== rendered.width || figma.height !== rendered.height) {
|
|
98
|
+
figmaForCompare = resizeOrPad(figma, rendered.width, rendered.height);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const diff = new PNG({ width: rendered.width, height: rendered.height });
|
|
102
|
+
const mismatchedPixels = pixelmatch(
|
|
103
|
+
rendered.data, figmaForCompare.data, diff.data,
|
|
104
|
+
rendered.width, rendered.height,
|
|
105
|
+
{ threshold, diffColor: [255, 0, 0], includeAA: false }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const diffPath = outPath.replace(/\.json$/, '-diff.png');
|
|
109
|
+
fs.writeFileSync(diffPath, PNG.sync.write(diff));
|
|
110
|
+
|
|
111
|
+
const totalPixels = rendered.width * rendered.height;
|
|
112
|
+
const mismatchRatio = mismatchedPixels / totalPixels;
|
|
113
|
+
|
|
114
|
+
// Approximate ΔE: pixelmatch doesn't compute it directly. We sample the diff regions
|
|
115
|
+
// and compute mean / max ΔE between corresponding pixels using a basic Lab approximation.
|
|
116
|
+
const { meanDeltaE, maxDeltaE } = computeDeltaE(rendered, figmaForCompare);
|
|
117
|
+
|
|
118
|
+
const verdict = (
|
|
119
|
+
meanDeltaE < PASS.meanDeltaE &&
|
|
120
|
+
maxDeltaE < PASS.maxDeltaE &&
|
|
121
|
+
mismatchRatio < PASS.mismatchRatio
|
|
122
|
+
) ? 'PASS' : 'FAIL';
|
|
123
|
+
|
|
124
|
+
const report = {
|
|
125
|
+
rendered_url: renderedUrl,
|
|
126
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
127
|
+
selector,
|
|
128
|
+
metrics: {
|
|
129
|
+
mismatched_pixels: mismatchedPixels,
|
|
130
|
+
total_pixels: totalPixels,
|
|
131
|
+
mismatch_ratio: round(mismatchRatio, 4),
|
|
132
|
+
mean_delta_e: round(meanDeltaE, 2),
|
|
133
|
+
max_delta_e: round(maxDeltaE, 2),
|
|
134
|
+
},
|
|
135
|
+
verdict,
|
|
136
|
+
rendered_screenshot: renderedPath,
|
|
137
|
+
diff_image: diffPath,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
141
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
142
|
+
console.log(JSON.stringify(report, null, 2));
|
|
143
|
+
} finally {
|
|
144
|
+
await browser.close();
|
|
145
|
+
}
|
|
146
|
+
})().catch((e) => fail(e.message ?? String(e)));
|
|
147
|
+
|
|
148
|
+
// ─── helpers ───────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function parseArgs(argv) {
|
|
151
|
+
const out = {};
|
|
152
|
+
for (let i = 0; i < argv.length; i++) {
|
|
153
|
+
if (argv[i].startsWith('--')) {
|
|
154
|
+
const key = argv[i].slice(2);
|
|
155
|
+
const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : 'true';
|
|
156
|
+
out[key] = val;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function required(args, k) { if (!args[k]) fail(`Missing --${k}`); return args[k]; }
|
|
162
|
+
function parseViewport(s) {
|
|
163
|
+
const m = String(s).match(/^(\d+)x(\d+)$/);
|
|
164
|
+
if (!m) fail(`Invalid --viewport "${s}", expected WxH e.g. 1440x900`);
|
|
165
|
+
return { width: +m[1], height: +m[2] };
|
|
166
|
+
}
|
|
167
|
+
function readPng(p) { return PNG.sync.read(fs.readFileSync(p)); }
|
|
168
|
+
function round(n, d) { return Math.round(n * 10 ** d) / 10 ** d; }
|
|
169
|
+
function fail(msg) { console.error('pixel-diff error:', msg); process.exit(2); }
|
|
170
|
+
|
|
171
|
+
function resizeOrPad(src, w, h) {
|
|
172
|
+
// Minimal no-deps resize: nearest-neighbor. For high-fidelity comparison this is OK because
|
|
173
|
+
// we're looking at large regions, not single pixels. For better results, install `sharp`
|
|
174
|
+
// and replace this function.
|
|
175
|
+
const dst = new PNG({ width: w, height: h });
|
|
176
|
+
const sx = src.width / w, sy = src.height / h;
|
|
177
|
+
for (let y = 0; y < h; y++) {
|
|
178
|
+
for (let x = 0; x < w; x++) {
|
|
179
|
+
const ix = Math.min(src.width - 1, Math.floor(x * sx));
|
|
180
|
+
const iy = Math.min(src.height - 1, Math.floor(y * sy));
|
|
181
|
+
const di = (y * w + x) * 4;
|
|
182
|
+
const si = (iy * src.width + ix) * 4;
|
|
183
|
+
dst.data[di] = src.data[si];
|
|
184
|
+
dst.data[di + 1] = src.data[si + 1];
|
|
185
|
+
dst.data[di + 2] = src.data[si + 2];
|
|
186
|
+
dst.data[di + 3] = src.data[si + 3];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return dst;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function computeDeltaE(a, b) {
|
|
193
|
+
// Approximate ΔE via RGB Euclidean (faster, no Lab conversion). Calibrated empirically.
|
|
194
|
+
let sum = 0, max = 0, count = 0;
|
|
195
|
+
for (let i = 0; i < a.data.length; i += 4) {
|
|
196
|
+
// Skip transparent pixels in figma reference
|
|
197
|
+
if (a.data[i + 3] < 10 || b.data[i + 3] < 10) continue;
|
|
198
|
+
const dr = a.data[i] - b.data[i];
|
|
199
|
+
const dg = a.data[i + 1] - b.data[i + 1];
|
|
200
|
+
const db = a.data[i + 2] - b.data[i + 2];
|
|
201
|
+
const d = Math.sqrt(dr * dr + dg * dg + db * db) / 4.42; // crude scale to ~ΔE units
|
|
202
|
+
sum += d; count++;
|
|
203
|
+
if (d > max) max = d;
|
|
204
|
+
}
|
|
205
|
+
return { meanDeltaE: count ? sum / count : 0, maxDeltaE: max };
|
|
206
|
+
}
|
package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/scripts/record-build-metrics.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* record-build-metrics.mjs — aggregate build stats for end-of-run report.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node record-build-metrics.mjs templates/<theme_name>
|
|
7
|
+
*
|
|
8
|
+
* Reads scratchpad artefacts and theme folder. Outputs JSON summary.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
const themePath = process.argv[2];
|
|
15
|
+
if (!themePath) {
|
|
16
|
+
console.error('Usage: node record-build-metrics.mjs templates/<theme_name>');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const themeName = path.basename(themePath);
|
|
21
|
+
const repoRoot = path.resolve(themePath, '../..');
|
|
22
|
+
const scratchpad = path.join(repoRoot, '.claude', 'scratchpad', themeName);
|
|
23
|
+
|
|
24
|
+
function countFiles(dir, ext) {
|
|
25
|
+
if (!fs.existsSync(dir)) return 0;
|
|
26
|
+
return fs.readdirSync(dir).filter(f => f.endsWith(ext)).length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walkVisualDiffs(root) {
|
|
30
|
+
const results = [];
|
|
31
|
+
if (!fs.existsSync(root)) return results;
|
|
32
|
+
function walk(dir) {
|
|
33
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) walk(full);
|
|
36
|
+
else if (entry.name === 'visual-diff.md') {
|
|
37
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
38
|
+
const critical = (content.match(/CRITICAL/gi) || []).length;
|
|
39
|
+
const hasZeroCritical = /0 CRITICAL|CRITICAL.*0 issues|no CRITICAL/i.test(content);
|
|
40
|
+
results.push({
|
|
41
|
+
path: path.relative(scratchpad, full),
|
|
42
|
+
critical_mentions: critical,
|
|
43
|
+
pass: hasZeroCritical || !/CRITICAL — [1-9]/i.test(content),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(root);
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parsePageReports(root) {
|
|
53
|
+
const pages = [];
|
|
54
|
+
const pagesDir = path.join(root, 'pages');
|
|
55
|
+
if (!fs.existsSync(pagesDir)) return pages;
|
|
56
|
+
for (const page of fs.readdirSync(pagesDir)) {
|
|
57
|
+
const reportPath = path.join(pagesDir, page, 'report.md');
|
|
58
|
+
if (!fs.existsSync(reportPath)) continue;
|
|
59
|
+
const content = fs.readFileSync(reportPath, 'utf-8');
|
|
60
|
+
const sectionsMatch = content.match(/SECTIONS BUILT:\s*(\d+)/i);
|
|
61
|
+
pages.push({
|
|
62
|
+
page,
|
|
63
|
+
sections_built: sectionsMatch ? Number(sectionsMatch[1]) : null,
|
|
64
|
+
has_report: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return pages;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const startedPath = path.join(scratchpad, 'build-started.json');
|
|
71
|
+
let startedAt = null;
|
|
72
|
+
let durationMs = null;
|
|
73
|
+
if (fs.existsSync(startedPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const s = JSON.parse(fs.readFileSync(startedPath, 'utf-8'));
|
|
76
|
+
startedAt = s.started_at;
|
|
77
|
+
if (startedAt) durationMs = Date.now() - new Date(startedAt).getTime();
|
|
78
|
+
} catch { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sections = countFiles(path.join(themePath, 'sections'), '.liquid');
|
|
82
|
+
const snippets = countFiles(path.join(themePath, 'snippets'), '.liquid');
|
|
83
|
+
const pages = countFiles(path.join(themePath, 'pages'), '.json');
|
|
84
|
+
const visualDiffs = walkVisualDiffs(scratchpad);
|
|
85
|
+
const pageReports = parsePageReports(scratchpad);
|
|
86
|
+
|
|
87
|
+
const summary = {
|
|
88
|
+
theme: themeName,
|
|
89
|
+
started_at: startedAt,
|
|
90
|
+
finished_at: new Date().toISOString(),
|
|
91
|
+
duration_ms: durationMs,
|
|
92
|
+
duration_human: durationMs
|
|
93
|
+
? `${Math.floor(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`
|
|
94
|
+
: null,
|
|
95
|
+
files: { sections, snippets, pages },
|
|
96
|
+
visual_verify: {
|
|
97
|
+
sections_with_diff: visualDiffs.length,
|
|
98
|
+
sections_passing: visualDiffs.filter(d => d.pass).length,
|
|
99
|
+
sections_failing: visualDiffs.filter(d => !d.pass).length,
|
|
100
|
+
},
|
|
101
|
+
page_reports: pageReports,
|
|
102
|
+
token_estimate: {
|
|
103
|
+
note: 'Approximate — Cursor does not expose per-run token counts to agents.',
|
|
104
|
+
profiles: {
|
|
105
|
+
small: '800K–1.5M tokens (≤15 sections, ~1 verify iteration each)',
|
|
106
|
+
medium: '1.5M–3M tokens (20–30 sections, 1–2 iterations)',
|
|
107
|
+
large: '3M–6M+ tokens (30+ sections, multiple visual re-loops)',
|
|
108
|
+
},
|
|
109
|
+
suggested_profile: sections <= 15 ? 'small' : sections <= 30 ? 'medium' : 'large',
|
|
110
|
+
},
|
|
111
|
+
time_estimate: {
|
|
112
|
+
note: 'Wall-clock varies with parallel agents and visual re-loop count.',
|
|
113
|
+
typical_first_build: '35–60 min for a 5-page theme',
|
|
114
|
+
typical_single_page_rerun: '5–15 min',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Level B — Section builder (one section only)
|
|
2
|
+
|
|
3
|
+
Spawned by **Level A (page orchestrator)**. Builds **one section**. Does not see other sections or the full page Figma tree for coding purposes.
|
|
4
|
+
|
|
5
|
+
**Scope rule:** `get_design_context` is called on the **section's node ID only**. Never on the parent page frame. See `agent-hierarchy.md`.
|
|
6
|
+
|
|
7
|
+
**The single most important rule, replacing everything from earlier versions:** the agent does NOT look at a section screenshot and improvise. It extracts a **Figma Spec** (per-element measurements, colors, fonts, positions, assets) from `get_design_context` and **transcribes** that into the section file. After building, it runs the visual verify loop until the rendered output matches the Figma in the agent's vision.
|
|
8
|
+
|
|
9
|
+
## Read first (in this order)
|
|
10
|
+
|
|
11
|
+
1. `agent-hierarchy.md` — Level B scope (section node only).
|
|
12
|
+
2. `figma-extraction.md` — Figma Spec from section node only.
|
|
13
|
+
3. `transcription-rules.md` — border-radius, colors, product card heights.
|
|
14
|
+
4. `visual-verify-loop.md` — Chrome DevTools verify vs **section crop**.
|
|
15
|
+
5. `image-sources.md` — `asset_url` for theme assets.
|
|
16
|
+
6. `ask-developer.md` — when to ask instead of guessing.
|
|
17
|
+
7. `zcommerce-docs/sections.md` — section structure rules.
|
|
18
|
+
8. `zcommerce-docs/reference/schema-types.md` — schema field types (READ every time, don't guess).
|
|
19
|
+
9. `zcommerce-docs/reference/filters.md` — valid Liquid filters.
|
|
20
|
+
10. `zcommerce-docs/reference/tags.md` — valid Liquid tags (only 13 — no Shopify-only tags).
|
|
21
|
+
11. `code-quality.md` — code budgets.
|
|
22
|
+
|
|
23
|
+
Reading just the screenshot was what produced 5% accuracy themes. The Figma Spec is the contract.
|
|
24
|
+
|
|
25
|
+
## Inputs you receive
|
|
26
|
+
|
|
27
|
+
From the page orchestrator:
|
|
28
|
+
- **`section_name`** — snake-case filename.
|
|
29
|
+
- **`archetype`** — hero, product-grid, PDP, etc. (used only for sibling-theme logic reference — never for design/asset reference).
|
|
30
|
+
- **`figma_node_id`** — the section's specific Figma node. If missing, ASK the developer (see `ask-developer.md`).
|
|
31
|
+
- **`theme_folder`** — `<theme>/` (the theme root folder).
|
|
32
|
+
- **`scratchpad`** — `.claude/scratchpad/<theme>/pages/<page>/sections/<section_name>/`.
|
|
33
|
+
- **`snippet_registry`** — path to pre-built snippets you can render via `{% render %}`.
|
|
34
|
+
- **`page_url`** — full URL from `dev-config.json` (e.g. `http://localhost:3000/home`). If missing or unconfirmed, write `needs-developer.md` and escalate — do not guess.
|
|
35
|
+
|
|
36
|
+
## Workflow (8 phases — every phase is mandatory)
|
|
37
|
+
|
|
38
|
+
### Phase 1 — Confirm the Figma node (~10s)
|
|
39
|
+
|
|
40
|
+
If `figma_node_id` is missing or marked uncertain, ASK the developer immediately. Don't try to infer from the parent page screenshot — that's how earlier versions hallucinated the wrong section boundaries.
|
|
41
|
+
|
|
42
|
+
### Phase 2 — Extract the Figma Spec (~2-3min)
|
|
43
|
+
|
|
44
|
+
Follow `figma-extraction.md` + `transcription-rules.md`:
|
|
45
|
+
- Call `get_design_context` on **`figma_node_id` (section frame only)** — NOT the parent page node.
|
|
46
|
+
- Record `border_radius` per element — default 0 means no `rounded-*` in code.
|
|
47
|
+
- Record exact colors → token map; no guessing from sibling themes.
|
|
48
|
+
- For product grids: record fixed media box `w`×`h` and `object_fit` per card.
|
|
49
|
+
- Walk the tree, extract every element's measurements/colors/fonts/positions.
|
|
50
|
+
- Extract image assets via `get_screenshot` on image nodes, save base64 PNGs to `assets/<section>-<purpose>.png`.
|
|
51
|
+
- Map colors → theme tokens. Log unmapped to `unmapped-colors.md`.
|
|
52
|
+
- Save the spec to `<scratchpad>/figma-spec.json`.
|
|
53
|
+
|
|
54
|
+
**Hard rule:** if Phase 2 produces nothing (Figma MCP unavailable, node not accessible), STOP and ask the developer. Do not proceed to Phase 3 without a spec — that's the no-imagination rule.
|
|
55
|
+
|
|
56
|
+
### Phase 3 — Read 3+ sibling implementations for LOGIC only (~30s)
|
|
57
|
+
|
|
58
|
+
Per `exemplars.md`, read siblings for **logic only** (loops, schema types, web components, i18n).
|
|
59
|
+
|
|
60
|
+
**Forbidden from siblings:** colors, border-radius, spacing, typography, shadows, card dimensions. All visual values come from `figma-spec.json` only (`transcription-rules.md`).
|
|
61
|
+
|
|
62
|
+
### Phase 4 — Plan the markup (~1min)
|
|
63
|
+
|
|
64
|
+
For every element in the Figma Spec, map to a Liquid element:
|
|
65
|
+
- Frame/Container → `<section>` / `<div>` with `{% style %}` block holding exact CSS values from the spec.
|
|
66
|
+
- Text → `<h1>` / `<h2>` / `<p>` with content from spec.text.content AND a schema setting so the admin can override.
|
|
67
|
+
- Image → theme asset: `{{ '<section>-<purpose>.png' | asset_url }}`; schema setting: `{{ section.settings.image.url | asset_url }}` (see `image-sources.md`).
|
|
68
|
+
- Layout → CSS grid/flex with exact gap, padding, alignment from spec.layout.
|
|
69
|
+
|
|
70
|
+
Plan the schema: every **heading/title/label** becomes a `text` setting; every **description/body** becomes a `richtext` setting; every image becomes an `image_picker` setting with the extracted asset as default, every URL becomes a `url` setting. **Never use `richtext` for heading blocks or fields rendered in `<h1>`–`<h6>`.** Schema field IDs follow the spec element IDs.
|
|
71
|
+
|
|
72
|
+
Plan inter-section structure: respect the section anatomy (script tags → style block → liquid assigns → conditional → wrapper → schema).
|
|
73
|
+
|
|
74
|
+
### Phase 5 — Write the section file (~1-2min)
|
|
75
|
+
|
|
76
|
+
Write `sections/<section_name>.liquid` by transcription from the Figma Spec. No improvising. Every measurement is the value from the spec — either a theme token (preferred when mapped) or an exact px/hex (otherwise).
|
|
77
|
+
|
|
78
|
+
After writing, also append to `i18n/en.json` (runtime keys for `| t` references) and `i18n/en.schema.json` (schema label keys). Re-read `zcommerce-docs/i18n.md` if unsure which file a key belongs in.
|
|
79
|
+
|
|
80
|
+
### Phase 6 — Lint (mandatory, blocking)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node .claude/skills/theme-from-figma/scripts/lint-section.mjs \
|
|
84
|
+
sections/<section_name>.liquid
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Must return PASS. If FAIL, fix and re-run before continuing.
|
|
88
|
+
|
|
89
|
+
### Phase 7 — 3-gate verify via Chrome DevTools MCP (mandatory, blocking)
|
|
90
|
+
|
|
91
|
+
Follow `visual-verify-loop.md`:
|
|
92
|
+
|
|
93
|
+
1. **Visual** — desktop + mobile `take_screenshot` vs Figma `get_screenshot`; write `visual-diff.md` with CRITICAL/MINOR/SUGGESTION.
|
|
94
|
+
2. **A11y** — `lighthouse_audit` mode `navigation` device `mobile` on the page route.
|
|
95
|
+
3. **Perf** — `lighthouse_audit` mobile baseline (flag if score < 85 on heavy pages).
|
|
96
|
+
4. **Network** — `list_network_requests` for image 404s; fix with `asset_url` per `image-sources.md`.
|
|
97
|
+
5. Fix CRITICAL items only. Re-loop. **Max 3 iterations.**
|
|
98
|
+
|
|
99
|
+
**If 3 iterations don't converge:** write `<scratchpad>/needs-developer.md` and exit. Page orchestrator surfaces in batch.
|
|
100
|
+
|
|
101
|
+
**PASS = `visual-diff.md` with 0 CRITICAL + no section-related console errors.**
|
|
102
|
+
|
|
103
|
+
### Phase 8 — Final report
|
|
104
|
+
|
|
105
|
+
Write `<scratchpad>/report.md`:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
SECTION: hero-banner
|
|
109
|
+
NODE: 2613:147
|
|
110
|
+
|
|
111
|
+
FIGMA SPEC: <scratchpad>/figma-spec.json (47 elements, 4 image assets extracted)
|
|
112
|
+
LINT: PASS
|
|
113
|
+
VISUAL DIFF (final iteration): <scratchpad>/visual-diff.md
|
|
114
|
+
CRITICAL: 0
|
|
115
|
+
MINOR: 2 (View All chevron gap, panel border-radius 1px off)
|
|
116
|
+
SUGGESTION: 1
|
|
117
|
+
ITERATIONS: 3
|
|
118
|
+
ASSETS WRITTEN: 4 (hero-banner-panel-left-penguin.png, hero-banner-panel-middle-cars.png, hero-banner-panel-right-robot.png, hero-banner-decorative-stripe.png)
|
|
119
|
+
SNIPPETS RENDERED: (none — section is self-contained)
|
|
120
|
+
UNRESOLVED (for orchestrator to batch):
|
|
121
|
+
(none)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Things to NOT do
|
|
125
|
+
|
|
126
|
+
- Don't skip the Figma Spec. No spec = no build.
|
|
127
|
+
- Don't write from a screenshot impression. Use the spec.
|
|
128
|
+
- Don't loop on visual diff without writing the structured `visual-diff.md`. The diff is the contract; vague "still doesn't look right" loops produce nothing.
|
|
129
|
+
- Don't substitute a sibling theme's asset. If extraction fails, ask the developer.
|
|
130
|
+
- Don't claim "verified pass" without the visual-diff.md and Figma Spec in scratchpad. `verify-theme.mjs` checks for both.
|
|
131
|
+
- Don't decide on hover/focus/animation states that aren't in Figma. Flag to the developer.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Subagent → main agent escalation
|
|
2
|
+
|
|
3
|
+
Every question, blocker, or notification from a **page orchestrator** or **section subagent** MUST appear on the **main agent screen**. Subagents must not silently fail or defer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What must escalate to the main agent
|
|
8
|
+
|
|
9
|
+
| Event | Main agent action |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `needs-developer.md` written | Surface immediately in chat with section/page name |
|
|
12
|
+
| `unresolved.md` with `blocking: true` | Batch into `AskUserQuestion` before continuing |
|
|
13
|
+
| Dev server not running | Ask developer to start OR start with confirmed `storeUrl` |
|
|
14
|
+
| Page URLs not confirmed | Pause build; run URL confirmation flow (`dev-server-and-urls.md`) |
|
|
15
|
+
| Visual verify stuck after 3 iterations | Show CRITICAL items from `visual-diff.md` |
|
|
16
|
+
| Subagent aborted / timed out | Report which page/section; parent takes over or re-spawns |
|
|
17
|
+
| Figma MCP unavailable | Ask developer to select frame or provide URL |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Subagent report format (required)
|
|
22
|
+
|
|
23
|
+
Each subagent ends with a short **ESCALATION** block in its return message:
|
|
24
|
+
|
|
25
|
+
```markdown
|
|
26
|
+
## ESCALATION (for main agent)
|
|
27
|
+
|
|
28
|
+
**Page/Section:** product / product-details
|
|
29
|
+
**Verdict:** PASS | NEEDS_REVIEW | BLOCKED
|
|
30
|
+
|
|
31
|
+
**Developer action needed:** yes | no
|
|
32
|
+
|
|
33
|
+
**Questions (if any):**
|
|
34
|
+
1. [blocking] Collection URL — used /collections/all, 404. Confirm correct handle?
|
|
35
|
+
|
|
36
|
+
**Artifacts:**
|
|
37
|
+
- scratchpad/.../visual-diff.md (2 CRITICAL remaining)
|
|
38
|
+
- scratchpad/.../needs-developer.md
|
|
39
|
+
|
|
40
|
+
**Notify main agent:** YES — do not proceed to Stage 2 until resolved.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Main orchestrator duties
|
|
46
|
+
|
|
47
|
+
1. **At Stage 0** — collect `storeUrl` + confirm page URLs → write `dev-config.json`
|
|
48
|
+
2. **After each page agent completes** — read its ESCALATION block; post summary to developer chat
|
|
49
|
+
3. **Before Stage 2 consolidation** — aggregate all `needs-developer.md` files into one developer-facing list
|
|
50
|
+
4. **Never hide subagent failures** — if collection agent aborted, say so explicitly
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Page orchestrator batching
|
|
55
|
+
|
|
56
|
+
Page orchestrators collect section `unresolved.md` files and ask the developer **one batch per page** (max 4 questions via `AskUserQuestion`).
|
|
57
|
+
|
|
58
|
+
But the **main orchestrator still gets a copy** of every batch in the consolidated report — even if the page agent already asked.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Example main-agent message to developer
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Subagent updates:
|
|
66
|
+
|
|
67
|
+
✅ index — 8 sections built, visual verify PASS
|
|
68
|
+
✅ product — 2 sections, 0 CRITICAL after 1 iteration
|
|
69
|
+
⚠️ collection — BLOCKED: dev server returned 404 for /collections/all
|
|
70
|
+
→ Please confirm collection URL (I guessed /collections/all-products)
|
|
71
|
+
|
|
72
|
+
❌ blog_list subagent aborted — parent will retry
|
|
73
|
+
|
|
74
|
+
Action needed: confirm collection + blog list URLs (see dev-server-and-urls.md)
|
|
75
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Transcription rules — Figma spec → code (Level B only)
|
|
2
|
+
|
|
3
|
+
Level B agents transcribe `figma-spec.json` into Liquid/CSS. **Zero improvisation.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Border radius
|
|
8
|
+
|
|
9
|
+
| Figma spec | Code |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `border_radius` absent or all `0` | **No** `rounded-*`, **no** `border-radius` in CSS |
|
|
12
|
+
| `border_radius: 20` (uniform) | `border-radius: 20px` in scoped `{% style %}` OR token if matches `--radius-card` |
|
|
13
|
+
| `border_radius` per corner | Exact px per corner in `{% style %}` |
|
|
14
|
+
|
|
15
|
+
**Never** apply `rounded-lg`, `rounded-card`, or theme `--radius-card` unless the Figma spec row explicitly has radius > 0 for that element.
|
|
16
|
+
|
|
17
|
+
If Figma shows square buttons (`border_radius: 0`) — use `border-radius: 0`, not `rounded-full`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Colors
|
|
22
|
+
|
|
23
|
+
1. Extract exact hex from Figma element fill/stroke.
|
|
24
|
+
2. Map to theme token via `figma-variables.json` (ΔE < 1.5).
|
|
25
|
+
3. Use `var(--color-*)` in code — never raw hex in markup.
|
|
26
|
+
4. If unmapped → log in `unmapped-colors.md` and ask developer — **do not pick a "close" token silently**.
|
|
27
|
+
|
|
28
|
+
| Wrong | Right |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `var(--color-primary)` on body text because "primary is brand" | Use spec text color → `--color-heading` or `--color-body` |
|
|
31
|
+
| Sibling theme's peach background | Spec background → `--section-bg-accent` or mapped token |
|
|
32
|
+
|
|
33
|
+
Each element in `figma-spec.json` must have a `token_used` field after mapping.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Product / image cards (equal heights)
|
|
38
|
+
|
|
39
|
+
When Figma shows a **grid of product cards** with uniform card frames:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"id": "product-card-media",
|
|
44
|
+
"position": { "w": 280, "h": 330 },
|
|
45
|
+
"image": { "object_fit": "contain" }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Level B must enforce in CSS:
|
|
50
|
+
|
|
51
|
+
```liquid
|
|
52
|
+
{% style %}
|
|
53
|
+
#section-{{ section.id }} .product-card-media {
|
|
54
|
+
aspect-ratio: 280 / 330; /* from spec w/h */
|
|
55
|
+
min-height: 330px;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
#section-{{ section.id }} .product-card-media img {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
object-fit: contain; /* from spec — usually contain for product shots */
|
|
62
|
+
}
|
|
63
|
+
#section-{{ section.id }} .product-card {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
height: 100%;
|
|
67
|
+
}
|
|
68
|
+
{% endstyle %}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Never** let `<img>` natural height drive card height in a grid — that causes the uneven heights you saw.
|
|
72
|
+
|
|
73
|
+
For `object-fit`: use what Figma spec says (`cover` for hero banners, `contain` for product packshots on neutral bg).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Typography
|
|
78
|
+
|
|
79
|
+
Transcribe exact `font_size`, `font_weight`, `line_height` from spec into scoped CSS or Tailwind only when class matches **exact px** from `tailwind.config.js` theme extension.
|
|
80
|
+
|
|
81
|
+
Do not round 44px → `text-4xl` if `text-4xl` resolves to 36px in this theme.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Spacing
|
|
86
|
+
|
|
87
|
+
`gap`, `padding`, `margin` — exact px from spec. One Tailwind step off = MINOR in visual diff; fix in re-loop.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## What Level B checks before claiming PASS
|
|
92
|
+
|
|
93
|
+
- [ ] Every visible element in Figma has a row in `figma-spec.json`
|
|
94
|
+
- [ ] No `rounded-*` on elements where spec `border_radius` is 0
|
|
95
|
+
- [ ] All colors trace to spec → token map
|
|
96
|
+
- [ ] Product grids have fixed media box dimensions from spec
|
|
97
|
+
- [ ] `lint-section.mjs` PASS
|
|
98
|
+
- [ ] `visual-diff.md` 0 CRITICAL (section crop only)
|