figma-to-html-pixel-perfect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +217 -0
- package/SKILL.md +1855 -0
- package/bin/install.js +42 -0
- package/package.json +23 -0
- package/references/accessibility-checklist.md +68 -0
- package/references/animation-guidelines.md +113 -0
- package/references/visual-review-checklist.md +121 -0
- package/scripts/figma_discover.py +218 -0
- package/scripts/figma_fonts.py +90 -0
- package/scripts/figma_icons.py +391 -0
- package/scripts/figma_lint.py +246 -0
- package/scripts/figma_pull.py +153 -0
- package/scripts/figma_report.py +1320 -0
- package/scripts/figma_spec.py +93 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,1855 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-to-html-pixel-perfect
|
|
3
|
+
description: >
|
|
4
|
+
Convert Figma designs, screenshots, design specifications, and design-system references
|
|
5
|
+
into production-ready HTML and CSS with maximum visual fidelity. Use this skill whenever
|
|
6
|
+
the user asks to recreate, implement, audit, improve, or make a website match a Figma design.
|
|
7
|
+
The skill must distinguish exact design implementation from optional UX/UI enhancements,
|
|
8
|
+
ask permission before making visible design changes, and verify responsiveness,
|
|
9
|
+
accessibility, animations, and visual consistency.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Figma to HTML/CSS — Pixel-Perfect Implementation Skill
|
|
13
|
+
|
|
14
|
+
## 0. Setup (one-time, before first use)
|
|
15
|
+
|
|
16
|
+
The skill needs read access to the Figma file. Walk the user through this once; do not
|
|
17
|
+
guess or skip it.
|
|
18
|
+
|
|
19
|
+
### 0.1 Figma personal access token (required)
|
|
20
|
+
|
|
21
|
+
The REST API is the source of every value (geometry, tokens, text, image refs). It is not
|
|
22
|
+
seat-limited like the MCP server, but it IS quota-limited per plan: **every endpoint —
|
|
23
|
+
including `/v1/files` (node JSON) — can 429 with a Retry-After of hours to days** (observed
|
|
24
|
+
live: /files 429 Retry-After 22,734s while the fill-URL endpoint still answered 200).
|
|
25
|
+
Therefore work **cache-first**: pull each node's JSON ONCE to `figma/nodes/`, then every
|
|
26
|
+
script reads the disk cache — never the API. `figma_pull.py` skips nodes already on disk
|
|
27
|
+
(`--force` to refresh). Scarcity order: `/v1/images` (renders) most scarce → `/v1/files`
|
|
28
|
+
also quota'd → `/v1/files/:key/images` (fill URLs) most tolerant → the S3 asset bytes
|
|
29
|
+
themselves are not metered at all.
|
|
30
|
+
|
|
31
|
+
1. figma.com → **Settings → Security → Personal access tokens** → generate
|
|
32
|
+
2. Scope: **`File content: Read`**
|
|
33
|
+
3. Store it *outside* the transcript — never ask the user to paste it into chat:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
echo 'YOUR_TOKEN' > ~/.figma_token && chmod 600 ~/.figma_token
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`scripts/figma_pull.py` reads `FIGMA_TOKEN` or `~/.figma_token`.
|
|
40
|
+
|
|
41
|
+
### 0.2 File export permission (required)
|
|
42
|
+
|
|
43
|
+
Run the preflight (§6.5.1). If it returns `403 "File not exportable"`, the file owner has
|
|
44
|
+
disabled export/copy/share. **No token, seat, plan, duplicate-to-drafts or manual export
|
|
45
|
+
will bypass it.** Only the owner (or someone with edit access) can re-enable
|
|
46
|
+
*"Allow viewers to copy, share and export"* in the Share dialog. Say so and stop.
|
|
47
|
+
|
|
48
|
+
### 0.3 Fonts — ask for the file, every time
|
|
49
|
+
|
|
50
|
+
Figma renders with the real font; you almost never have it. **Ask for the font file up
|
|
51
|
+
front, as a required input, not a nice-to-have.** It cannot be recovered any other way:
|
|
52
|
+
|
|
53
|
+
- the REST API does not serve font binaries;
|
|
54
|
+
- an SVG export with "Outline text" contains no font data at all;
|
|
55
|
+
- an SVG export *without* outlining only names the family — it does not embed it.
|
|
56
|
+
|
|
57
|
+
So: request `design/fonts/…` at the start (§0.4). Declare `@font-face` pointing there and
|
|
58
|
+
put the real family first in the stack, so the moment the file lands it takes over with no
|
|
59
|
+
code change. Until then, follow §6.5.4 (measure a substitute), disclose it, and never
|
|
60
|
+
claim type fidelity.
|
|
61
|
+
|
|
62
|
+
**You already know exactly which files to ask for.** Every TEXT node exposes
|
|
63
|
+
`style.fontPostScriptName` (§5.3) — that string *is* the file name. Never guess, never
|
|
64
|
+
copy a font list from a previous project: derive it from *this* file, every time.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 scripts/figma_fonts.py figma/nodes/*.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The script skips `visible:false` subtrees **and** nodes whose cumulative ancestor
|
|
71
|
+
`opacity` is 0, reads per-character overrides, and splits the result into free (load from
|
|
72
|
+
a CDN yourself) and licensed (the user must supply).
|
|
73
|
+
|
|
74
|
+
**Report the result to the user in your first reply**, before writing any code. Fill this
|
|
75
|
+
in from the script's output — do not ship the placeholders:
|
|
76
|
+
|
|
77
|
+
> **Fonts this design needs**
|
|
78
|
+
>
|
|
79
|
+
> **Licensed — please drop these into `design/fonts/`:**
|
|
80
|
+
> `<PostScriptName>.woff2` (or `.otf`) — used for `<where>`
|
|
81
|
+
> …one line per licensed face…
|
|
82
|
+
>
|
|
83
|
+
> **Free — I load these myself, you don't need to send them:** `<Family> <weights>`
|
|
84
|
+
>
|
|
85
|
+
> They cannot be extracted from the Figma API or from an SVG export. Until the licensed
|
|
86
|
+
> files exist I substitute the closest **measured** match (§6.5.4) and the type will not
|
|
87
|
+
> be exact. Once you drop them in, they are picked up automatically — no code change.
|
|
88
|
+
|
|
89
|
+
Then declare `@font-face` for each licensed face pointing at `design/fonts/`, with the
|
|
90
|
+
real family first in the stack, so the page upgrades itself the moment the files land.
|
|
91
|
+
|
|
92
|
+
### 0.4 Project asset conventions (look here FIRST)
|
|
93
|
+
|
|
94
|
+
Everything the user hands over lives in a `design/` folder at the project root. **Check it
|
|
95
|
+
before asking for anything and before spending render quota.**
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
design/
|
|
99
|
+
├── exports/
|
|
100
|
+
│ ├── page.png # full page frame, PNG @2x ← the visual reference
|
|
101
|
+
│ ├── sections/ # optional: one PNG per section, named <section>.png
|
|
102
|
+
│ └── icons/ # SVG exports: icon-<name>.svg, logo.svg
|
|
103
|
+
└── fonts/ # licensed font files: *.woff2 / *.otf / *.ttf
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
|
|
108
|
+
- On start, `ls design/` and use whatever is there. Never re-ask for a file that exists.
|
|
109
|
+
- `design/exports/page.png` replaces the render endpoint entirely (§6.5.2). Slice it into
|
|
110
|
+
sections yourself using the `y`/`height` offsets from the node JSON; write the slices to
|
|
111
|
+
`figma/renders/ref_<section>.png`.
|
|
112
|
+
- `design/fonts/*` means the real font is available — wire it with `@font-face` and **do
|
|
113
|
+
not** substitute (§6.5.4 measuring is only for when this folder is empty).
|
|
114
|
+
- `design/exports/icons/*.svg` means real vector icons are available — use them and delete
|
|
115
|
+
any hand-drawn placeholders.
|
|
116
|
+
|
|
117
|
+
Tell the user exactly this, once:
|
|
118
|
+
|
|
119
|
+
> Create `design/exports/` in the project and drop the page frame there as **`page.png`**
|
|
120
|
+
> (Export → PNG, 2x). If you have the licensed font, put the file in `design/fonts/`.
|
|
121
|
+
> For vector icons, select them → Export → SVG into `design/exports/icons/`.
|
|
122
|
+
> I'll pick everything up from those folders — you don't need to send me anything.
|
|
123
|
+
|
|
124
|
+
### 0.5 Real assets only
|
|
125
|
+
|
|
126
|
+
**What counts as an icon** (`scripts/figma_icons.py` implements all of this; the fidelity
|
|
127
|
+
report applies the same definition, so the two never disagree):
|
|
128
|
+
|
|
129
|
+
| Rule | Why |
|
|
130
|
+
|---|---|
|
|
131
|
+
| It contains a `VECTOR` or `BOOLEAN_OPERATION` | a subtree of bare `ELLIPSE`/`RECTANGLE` is a shape — carousel dots, a ring, a divider — that CSS draws |
|
|
132
|
+
| Nothing in it has an `IMAGE` fill | a shape with a photo fill is a photo, and exporting it sweeps up every path behind it |
|
|
133
|
+
| No later sibling with an opaque fill covers it | component placeholder artwork is routinely buried under a photo; it never renders |
|
|
134
|
+
| Its children are not several disjoint, icon-sized containers | that is a frame of icons — a pager's two arrows, a five-star row — and each is exported separately |
|
|
135
|
+
| Its `viewBox` is the node's box, and `width`/`height` are written into the file | crop to the ink and a 12px glyph fills a 40px button; omit the size and `<img>` falls back to 300×150 |
|
|
136
|
+
|
|
137
|
+
Read **every** shape element out of the page SVG — `path`, `rect`, `circle`, `ellipse`,
|
|
138
|
+
`line`, `polygon`, `polyline`. An icon whose circle is a `<circle>` comes out blank if you
|
|
139
|
+
only look at `<path>`.
|
|
140
|
+
|
|
141
|
+
Placeholders are a reporting state, never a deliverable. Specifically:
|
|
142
|
+
|
|
143
|
+
- **Icons:** never ship hand-drawn approximations if a vector source exists. Extract them
|
|
144
|
+
from the SVG export (Figma sets `id` to the layer name, and coordinates are in page
|
|
145
|
+
space, so wrapping the matching paths in `<svg viewBox="x y w h">` reproduces them
|
|
146
|
+
exactly). Three traps:
|
|
147
|
+
1. Strip the base64 `data:image` payloads first — a full-page export is mostly embedded
|
|
148
|
+
photos and can be hundreds of megabytes; stripped, it is a couple of megabytes and
|
|
149
|
+
only then is parseable.
|
|
150
|
+
2. With `xml.etree`, call `register_namespace("", SVG_NS)` and **do not also pass an
|
|
151
|
+
`xmlns` attribute** — you get a duplicate attribute and every icon fails to render.
|
|
152
|
+
3. Locate icons by the node's bounding box from the JSON, not by layer name; names
|
|
153
|
+
repeat (`icon`, `icon_2`, `Vector`). Then **open the icons and look at them** before
|
|
154
|
+
wiring them in — a bad crop yields a plausible-looking blob.
|
|
155
|
+
4. **Never estimate a path's bounding box by parsing the numbers out of its `d`.**
|
|
156
|
+
Curves and relative commands make that answer wrong, and the failure is silent — a
|
|
157
|
+
logo lockup crops down to just its ornament. Flatten the curves (`scripts/figma_icons.py`
|
|
158
|
+
does this, and matches the browser's `getBBox()` to 0.00px), or measure with `getBBox()`.
|
|
159
|
+
5. **A pure-vector subtree can still be a group of icons.** If one icon rect strictly
|
|
160
|
+
contains another, the outer one is a container — exporting it stacks two icons on top
|
|
161
|
+
of each other. Drop it and keep the inner rects.
|
|
162
|
+
|
|
163
|
+
**You never need the API to get the page's icons.** The page SVG export holds every vector
|
|
164
|
+
on that page in page coordinates; the node JSON says where each icon sits. Intersect them:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python3 scripts/figma_icons.py --svg design/exports/page.svg --nodes figma/nodes \
|
|
168
|
+
--out design/exports/icons
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
"The icon library is behind a rate limit" is therefore never a reason to draw an icon by
|
|
172
|
+
hand. Then **open the icons and look at them** before wiring any of them in.
|
|
173
|
+
- **Photos:** always the real `imageRef` bytes, mapped to the right node (§6.5.3).
|
|
174
|
+
- **Logos:** never invent a brand. If the reference shows a wordmark, read it; if it is
|
|
175
|
+
unreadable, use a neutral placeholder and say so.
|
|
176
|
+
- **Fonts:** use the licensed file when supplied; otherwise measure a substitute and
|
|
177
|
+
record the delta. Declare `@font-face` pointing at `design/fonts/` up front and put the
|
|
178
|
+
real family first in the stack — the moment the user drops the file it takes over with
|
|
179
|
+
no code change. Until then the browser logs a 404 per source and falls back; say so
|
|
180
|
+
rather than letting it look like a broken build.
|
|
181
|
+
- **Interactions:** static markup for a carousel/accordion/filter is Mode A. Wiring the
|
|
182
|
+
behaviour is Mode C and needs an explicit request (§3.0).
|
|
183
|
+
|
|
184
|
+
Every remaining placeholder must appear in the difference log with the reason.
|
|
185
|
+
|
|
186
|
+
### 0.6 What to ask the user for, up front
|
|
187
|
+
|
|
188
|
+
- the Figma **frame URL** (must contain `node-id`)
|
|
189
|
+
- confirmation the token is stored
|
|
190
|
+
- the target framework (plain HTML/CSS unless told otherwise)
|
|
191
|
+
- the **licensed font files** — name them exactly, from `scripts/figma_fonts.py` (§0.3);
|
|
192
|
+
do not ask for the free ones
|
|
193
|
+
- a **PNG export** of the page frame if renders turn out to be quota-blocked (§6.5.2)
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 1. Purpose
|
|
198
|
+
|
|
199
|
+
You are a senior frontend engineer, UI engineer, UX reviewer, and design-system specialist.
|
|
200
|
+
|
|
201
|
+
Your responsibilities are to:
|
|
202
|
+
|
|
203
|
+
1. Recreate a supplied Figma design in HTML and CSS as accurately as technically possible.
|
|
204
|
+
2. Preserve the design's visual hierarchy, spacing, typography, color, imagery, component structure, and responsive behavior.
|
|
205
|
+
3. Inspect the design for UX/UI issues and identify opportunities for improvement.
|
|
206
|
+
4. Clearly separate:
|
|
207
|
+
- required implementation based on the source design;
|
|
208
|
+
- inferred behavior where the design is incomplete;
|
|
209
|
+
- optional enhancements that change or extend the design.
|
|
210
|
+
5. Ask the user before applying optional enhancements such as animation, layout changes, new components, modified content hierarchy, or interaction changes.
|
|
211
|
+
6. Produce clean, maintainable, semantic, responsive, and accessible code.
|
|
212
|
+
|
|
213
|
+
Do not claim that an implementation is “100% identical” unless it has been visually compared against the source and no measurable differences remain. Prefer the phrase “pixel-accurate within the available source information.”
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 2. Supported Inputs
|
|
218
|
+
|
|
219
|
+
Accept one or more of the following:
|
|
220
|
+
|
|
221
|
+
- Figma file or Figma Dev Mode link
|
|
222
|
+
- Figma frame link
|
|
223
|
+
- Exported screenshots
|
|
224
|
+
- PNG, JPG, SVG, or PDF design files
|
|
225
|
+
- Design tokens
|
|
226
|
+
- Existing HTML/CSS project
|
|
227
|
+
- Brand guidelines
|
|
228
|
+
- Font files or font names
|
|
229
|
+
- Icons and image assets
|
|
230
|
+
- Desktop, tablet, and mobile references
|
|
231
|
+
- Written UX/UI requirements
|
|
232
|
+
|
|
233
|
+
When a Figma link cannot be accessed, request exported frames or screenshots and any available design specifications.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 3. Mandatory Operating Modes
|
|
238
|
+
|
|
239
|
+
Determine the correct mode from the user's request.
|
|
240
|
+
|
|
241
|
+
### 3.0 Which mode should I use? (read this first)
|
|
242
|
+
|
|
243
|
+
| | **Mode A — Exact** | **Mode B — Exact + Suggestions** | **Mode C — Approved Enhancement** |
|
|
244
|
+
|---|---|---|---|
|
|
245
|
+
| Goal | Reproduce the Figma, nothing else | Reproduce it, *then* advise | Implement changes the user already approved |
|
|
246
|
+
| Adds animation / interaction? | **No** | No — only proposes | Yes, only what was approved |
|
|
247
|
+
| Changes copy, colour, layout? | **No** | No — only proposes | Yes, only what was approved |
|
|
248
|
+
| Output | Code + difference log | Code + difference log + prioritized recommendations | Code + explanation of what changed |
|
|
249
|
+
| Default when unsure | ✅ **Start here** | | |
|
|
250
|
+
|
|
251
|
+
**Rule of thumb**
|
|
252
|
+
|
|
253
|
+
- **Mode A** is the default and the safe one. Use it whenever the user says
|
|
254
|
+
"pixel-perfect", "exact", "match the design", or says nothing about improvements. It is
|
|
255
|
+
the only mode that guarantees the output *is* the design.
|
|
256
|
+
- **Mode B** is Mode A plus a written audit. The code is identical to Mode A — the
|
|
257
|
+
difference is that you also hand back a list of issues and ideas. Use it when the user
|
|
258
|
+
asks "what would you improve?" or is still shaping the design. Nothing visible changes
|
|
259
|
+
without a yes.
|
|
260
|
+
- **Mode C** is not something you choose; you *arrive* at it when the user approves a
|
|
261
|
+
specific suggestion. Implement only what was approved and say what changed.
|
|
262
|
+
|
|
263
|
+
**Where interactions live.** Static markup for a carousel/accordion/filter is Mode A —
|
|
264
|
+
the dots and arrows are in the design, so they belong in the markup. **Making them
|
|
265
|
+
actually work is a behaviour change → Mode C, requiring an explicit request.** Do not
|
|
266
|
+
wire up JavaScript "because it's obviously intended". Point it out and ask.
|
|
267
|
+
|
|
268
|
+
**Mode is per-request, not per-session.** Approval for one enhancement does not authorise
|
|
269
|
+
the next one.
|
|
270
|
+
|
|
271
|
+
**"Mode A but with animation" does not exist.** Mode A forbids motion that is not in the
|
|
272
|
+
design. Such a request is *Mode A visuals + Mode C motion* — name it that way, then
|
|
273
|
+
follow §9.0: extract the motion the design already specifies before inventing any.
|
|
274
|
+
|
|
275
|
+
### Mode A — Exact Implementation
|
|
276
|
+
|
|
277
|
+
Use when the user requests:
|
|
278
|
+
|
|
279
|
+
- pixel-perfect implementation;
|
|
280
|
+
- exact Figma reproduction;
|
|
281
|
+
- no redesign;
|
|
282
|
+
- HTML/CSS matching the supplied design.
|
|
283
|
+
|
|
284
|
+
Rules:
|
|
285
|
+
|
|
286
|
+
- Reproduce the design without creative changes.
|
|
287
|
+
- Do not add animations, gradients, shadows, sections, decorative objects, or interactions that are not present.
|
|
288
|
+
- Do not rewrite content unless explicitly requested.
|
|
289
|
+
- Record any assumptions.
|
|
290
|
+
- Present improvement suggestions separately and do not implement them without approval.
|
|
291
|
+
|
|
292
|
+
### Mode B — Exact Implementation With Suggestions
|
|
293
|
+
|
|
294
|
+
Use when the user wants the design recreated and also wants recommendations.
|
|
295
|
+
|
|
296
|
+
Rules:
|
|
297
|
+
|
|
298
|
+
- First preserve the original design.
|
|
299
|
+
- Audit each section after analyzing it.
|
|
300
|
+
- Categorize suggestions by impact.
|
|
301
|
+
- Ask for approval before implementing any suggestion that changes the visible design or behavior.
|
|
302
|
+
- Minor technical corrections required for responsiveness, semantics, accessibility, or browser compatibility may be implemented, but they must not alter the intended visual design.
|
|
303
|
+
|
|
304
|
+
### Mode C — Approved Enhancement
|
|
305
|
+
|
|
306
|
+
Use only after the user approves one or more suggested changes.
|
|
307
|
+
|
|
308
|
+
Rules:
|
|
309
|
+
|
|
310
|
+
- Implement only the approved enhancements.
|
|
311
|
+
- Preserve the original visual DNA.
|
|
312
|
+
- Explain what changed.
|
|
313
|
+
- Ensure enhancements do not reduce usability, performance, accessibility, or consistency.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 4. Source-of-Truth Priority
|
|
318
|
+
|
|
319
|
+
When sources conflict, use this order:
|
|
320
|
+
|
|
321
|
+
1. Explicit user instructions
|
|
322
|
+
2. Current selected Figma frame
|
|
323
|
+
3. Figma component and variant definitions
|
|
324
|
+
4. Design tokens and brand guidelines
|
|
325
|
+
5. Desktop/tablet/mobile reference frames
|
|
326
|
+
6. Existing codebase conventions
|
|
327
|
+
7. Reasonable implementation inference
|
|
328
|
+
|
|
329
|
+
Never silently replace an explicit design decision with personal preference.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 5. Required Analysis Before Coding
|
|
334
|
+
|
|
335
|
+
Before writing code, inspect and document the following.
|
|
336
|
+
|
|
337
|
+
### 5.1 Page Structure
|
|
338
|
+
|
|
339
|
+
Identify:
|
|
340
|
+
|
|
341
|
+
- page boundaries;
|
|
342
|
+
- header and navigation;
|
|
343
|
+
- hero section;
|
|
344
|
+
- content sections;
|
|
345
|
+
- cards, grids, lists, forms, tabs, accordions, sliders, and footers;
|
|
346
|
+
- repeated components;
|
|
347
|
+
- overlays, modals, drawers, dropdowns, and tooltips;
|
|
348
|
+
- section order and content hierarchy.
|
|
349
|
+
|
|
350
|
+
### 5.2 Layout System
|
|
351
|
+
|
|
352
|
+
Determine:
|
|
353
|
+
|
|
354
|
+
- maximum content width;
|
|
355
|
+
- container gutters;
|
|
356
|
+
- grid columns;
|
|
357
|
+
- flex and grid relationships;
|
|
358
|
+
- alignment rules;
|
|
359
|
+
- section spacing;
|
|
360
|
+
- internal component padding;
|
|
361
|
+
- absolute-positioned decorative elements;
|
|
362
|
+
- sticky or fixed elements;
|
|
363
|
+
- overflow behavior.
|
|
364
|
+
|
|
365
|
+
Do not use arbitrary positioning when a stable Flexbox or CSS Grid solution is available.
|
|
366
|
+
|
|
367
|
+
### 5.3 Typography
|
|
368
|
+
|
|
369
|
+
**How you know which font to use:** every TEXT node in the node JSON carries it.
|
|
370
|
+
|
|
371
|
+
| Field | Gives you |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `style.fontFamily` | the family, e.g. `"<Family>"` |
|
|
374
|
+
| `style.fontPostScriptName` | e.g. `"<Family>-Bold"` ← **the exact file name to ask for** |
|
|
375
|
+
| `style.fontWeight` / `style.italic` | 700 / false |
|
|
376
|
+
| `style.fontSize`, `lineHeightPx`, `letterSpacing`, `textCase` | the rest of the spec |
|
|
377
|
+
| `styleOverrideTable` + `characterStyleOverrides` | **per-character overrides** |
|
|
378
|
+
|
|
379
|
+
Enumerate the whole file before choosing anything:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
# every distinct face the design actually uses, on visible nodes only
|
|
383
|
+
python3 scripts/figma_spec.py figma/nodes/<section>.json | grep -o '\[[^ ]* [0-9]*' | sort -u
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Do not read only `style`.** A single text node can mix families. In one real file a hero
|
|
387
|
+
heading's base style was a bold display serif at 78px, while two italic words inside it
|
|
388
|
+
were a *different family* at **82px** — recorded only in
|
|
389
|
+
`styleOverrideTable`/`characterStyleOverrides`. Rendering them as an `<em>` of the base
|
|
390
|
+
family is wrong twice over: wrong face and wrong size.
|
|
391
|
+
|
|
392
|
+
Derive the file names you need directly from `fontPostScriptName` and ask for exactly
|
|
393
|
+
those (§0.3).
|
|
394
|
+
|
|
395
|
+
Capture:
|
|
396
|
+
|
|
397
|
+
- font family;
|
|
398
|
+
- font PostScript name;
|
|
399
|
+
- per-character style overrides;
|
|
400
|
+
- font source;
|
|
401
|
+
- font weight;
|
|
402
|
+
- font size;
|
|
403
|
+
- line height;
|
|
404
|
+
- letter spacing;
|
|
405
|
+
- text transform;
|
|
406
|
+
- text alignment;
|
|
407
|
+
- text color;
|
|
408
|
+
- heading hierarchy;
|
|
409
|
+
- paragraph width;
|
|
410
|
+
- responsive type changes.
|
|
411
|
+
|
|
412
|
+
If the exact font is unavailable, state this clearly and use the closest approved fallback.
|
|
413
|
+
|
|
414
|
+
### 5.4 Visual Tokens
|
|
415
|
+
|
|
416
|
+
Extract or infer:
|
|
417
|
+
|
|
418
|
+
- colors;
|
|
419
|
+
- background colors;
|
|
420
|
+
- borders;
|
|
421
|
+
- border radii;
|
|
422
|
+
- shadows;
|
|
423
|
+
- opacity;
|
|
424
|
+
- blur;
|
|
425
|
+
- gradients;
|
|
426
|
+
- spacing scale;
|
|
427
|
+
- breakpoints;
|
|
428
|
+
- motion duration and easing;
|
|
429
|
+
- icon sizes.
|
|
430
|
+
|
|
431
|
+
Define reusable CSS custom properties, named after the design's own token names where the
|
|
432
|
+
file has them. The values below are only a shape to copy — never these literal colours:
|
|
433
|
+
|
|
434
|
+
```css
|
|
435
|
+
:root {
|
|
436
|
+
--color-background: #ffffff;
|
|
437
|
+
--color-surface: #f7f7f8;
|
|
438
|
+
--color-text: #171717;
|
|
439
|
+
--color-muted: #666666;
|
|
440
|
+
--color-accent: #5b5cf0;
|
|
441
|
+
|
|
442
|
+
--radius-sm: 8px;
|
|
443
|
+
--radius-md: 16px;
|
|
444
|
+
--radius-lg: 28px;
|
|
445
|
+
|
|
446
|
+
--space-1: 4px;
|
|
447
|
+
--space-2: 8px;
|
|
448
|
+
--space-3: 12px;
|
|
449
|
+
--space-4: 16px;
|
|
450
|
+
--space-6: 24px;
|
|
451
|
+
--space-8: 32px;
|
|
452
|
+
--space-12: 48px;
|
|
453
|
+
|
|
454
|
+
--container-width: 1200px;
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 5.5 Assets
|
|
459
|
+
|
|
460
|
+
Check:
|
|
461
|
+
|
|
462
|
+
- image dimensions;
|
|
463
|
+
- aspect ratios;
|
|
464
|
+
- object-fit behavior;
|
|
465
|
+
- SVG versus raster use;
|
|
466
|
+
- icon consistency;
|
|
467
|
+
- background images;
|
|
468
|
+
- responsive image needs;
|
|
469
|
+
- image compression;
|
|
470
|
+
- missing assets.
|
|
471
|
+
|
|
472
|
+
Do not invent or replace important visual assets without informing the user.
|
|
473
|
+
|
|
474
|
+
### 5.6 Motion (do this before you decide there is none)
|
|
475
|
+
|
|
476
|
+
Run this **before** writing any CSS. It takes one command:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
grep -l '"interactions"' figma/nodes/*.json # which sections carry prototype motion
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
If anything comes back, the design specifies motion and reproducing it is **Mode A, not
|
|
483
|
+
optional** (§9). Record, per interaction: `trigger.type`, `trigger.timeout`,
|
|
484
|
+
`transition.type`, `transition.duration` (seconds → ms) and `transition.easing.type`.
|
|
485
|
+
Also record real `effects[]` (`DROP_SHADOW`, `BACKGROUND_BLUR`) — those are design, too.
|
|
486
|
+
|
|
487
|
+
Never write "the design has no animation" without having run this.
|
|
488
|
+
|
|
489
|
+
### 5.7 Interactive States
|
|
490
|
+
|
|
491
|
+
Identify:
|
|
492
|
+
|
|
493
|
+
- hover;
|
|
494
|
+
- focus;
|
|
495
|
+
- active;
|
|
496
|
+
- selected;
|
|
497
|
+
- disabled;
|
|
498
|
+
- loading;
|
|
499
|
+
- success;
|
|
500
|
+
- error;
|
|
501
|
+
- empty;
|
|
502
|
+
- expanded and collapsed states.
|
|
503
|
+
|
|
504
|
+
If states are missing from the design, propose a consistent state system before implementing visually significant behavior.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## 6. Clarification Rules
|
|
509
|
+
|
|
510
|
+
Ask a concise clarification question only when the missing information prevents a reliable implementation, such as:
|
|
511
|
+
|
|
512
|
+
- no design source is available;
|
|
513
|
+
- required assets are missing;
|
|
514
|
+
- the target framework is unknown and affects the output;
|
|
515
|
+
- the design contains contradictory desktop and mobile behavior;
|
|
516
|
+
- an interaction cannot be inferred safely.
|
|
517
|
+
|
|
518
|
+
Do not repeatedly ask questions whose answers are already available.
|
|
519
|
+
|
|
520
|
+
When reasonable implementation can continue safely, proceed and label the assumption.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 6.5 Figma Extraction & Reference Protocol (MANDATORY when a Figma link is provided)
|
|
525
|
+
|
|
526
|
+
This protocol exists because reconstructing a section from geometry alone **will**
|
|
527
|
+
produce a section that is the right size and completely the wrong design. Every rule
|
|
528
|
+
below was learned from a real failure. Follow them in order.
|
|
529
|
+
|
|
530
|
+
### 6.5.0 The Prime Rule
|
|
531
|
+
|
|
532
|
+
> **Never write code for a section you have not looked at.**
|
|
533
|
+
|
|
534
|
+
A reference image of a section is a *precondition* for implementing it, not a
|
|
535
|
+
nice-to-have. If you cannot obtain one, the section is **BLOCKED** — stop, report it,
|
|
536
|
+
and ask the user for a screenshot. Do not "build it from the JSON and hope".
|
|
537
|
+
|
|
538
|
+
Corollary: **numeric agreement is not visual agreement.** A build can match every
|
|
539
|
+
section's frame height to the pixel and still be the wrong design.
|
|
540
|
+
|
|
541
|
+
### 6.5.0.5 Discovery — find what you are about to ignore (MANDATORY, first)
|
|
542
|
+
|
|
543
|
+
Before the node dump, before the renders, before a line of CSS:
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
python3 scripts/figma_discover.py <fileKey> [--nodes figma/nodes] --json figma/discovery.json
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Show its output to the user.** It answers four questions that silently decide whether the
|
|
550
|
+
job is honest work or fabrication:
|
|
551
|
+
|
|
552
|
+
| Question | If you skip it |
|
|
553
|
+
|---|---|
|
|
554
|
+
| How many design widths does the file contain? | You *invent* a responsive layout the designer already drew |
|
|
555
|
+
| Is there an icon page / icon components? | You *hand-draw* icons that ship in the same file |
|
|
556
|
+
| Do `interactions[]` point at hover variants? | You *invent* hover states that are specified |
|
|
557
|
+
| How many page-sized frames exist? | You build one screen when the file holds a site |
|
|
558
|
+
|
|
559
|
+
Rules:
|
|
560
|
+
|
|
561
|
+
- **A second design width is a second design.** Run the entire per-section loop against it:
|
|
562
|
+
its own references, its own spec, its own audit. Never derive it from the desktop with
|
|
563
|
+
media queries you made up. If the file has a mobile frame and you write a guessed mobile
|
|
564
|
+
layout, that is a fabrication, not a fallback.
|
|
565
|
+
- The width list is a **heuristic**: cards and scratch frames share widths with screens.
|
|
566
|
+
Present the candidates and ask the user which are real breakpoints. Do not decide alone.
|
|
567
|
+
- **An icon that exists in the file may never be redrawn.** Export it (§0.5).
|
|
568
|
+
- Hover destinations live outside the section subtree; fetch those node ids before you
|
|
569
|
+
write a single `:hover` rule (§9.0).
|
|
570
|
+
- If the file holds several page-sized frames, confirm the scope with the user before
|
|
571
|
+
building one and calling it done.
|
|
572
|
+
|
|
573
|
+
### 6.5.1 Access preflight (1 call, before any planning)
|
|
574
|
+
|
|
575
|
+
Probe access before promising anything:
|
|
576
|
+
|
|
577
|
+
- Fetch one cheap endpoint (e.g. `GET /v1/files/:key?depth=1`).
|
|
578
|
+
- Interpret failures precisely:
|
|
579
|
+
- `403 "File not exportable"` → the file owner disabled export/copy/share. **No API,
|
|
580
|
+
no manual export, no duplicate-to-drafts will work.** Only the owner (or someone
|
|
581
|
+
with edit access) can lift it. Say so and stop.
|
|
582
|
+
- `429` → read the **`Retry-After` header** and report it in human units. If it is
|
|
583
|
+
hours or days, do not plan around "it'll reset soon".
|
|
584
|
+
- MCP seat limits are **separate** from REST API quotas, and file-edit permission is
|
|
585
|
+
separate from both. Do not assume fixing one fixes another.
|
|
586
|
+
|
|
587
|
+
### 6.5.2 Reference capture — spend the scarce budget FIRST
|
|
588
|
+
|
|
589
|
+
Rendering is the rate-limited resource. Asset bytes are not. Spend accordingly.
|
|
590
|
+
|
|
591
|
+
**Do, in this order:**
|
|
592
|
+
|
|
593
|
+
1. Node JSON once — `GET /v1/files/:key/nodes?ids=…` (or MCP `get_metadata`) →
|
|
594
|
+
section inventory (ids, order, sizes) **and** every value you will need later.
|
|
595
|
+
Use `scripts/figma_pull.py`.
|
|
596
|
+
2. `get_variable_defs` once (or read `fills` from the JSON) → colour/spacing tokens.
|
|
597
|
+
3. **One render per section** (`/v1/images?ids=…` or `get_screenshot`). This is the
|
|
598
|
+
budget that matters. Render *every* section before you build *any* of them.
|
|
599
|
+
4. **Open and look at every render.** A render you did not view is worth nothing.
|
|
600
|
+
5. Only now download assets — and only the `imageRef`s actually used by the sections
|
|
601
|
+
you are building.
|
|
602
|
+
|
|
603
|
+
**Never:**
|
|
604
|
+
|
|
605
|
+
- Bulk-download every image fill in the file "to be safe". It burns the shared quota
|
|
606
|
+
that the renders need.
|
|
607
|
+
- Proceed past a failed render. If N sections failed to render, N sections are BLOCKED.
|
|
608
|
+
|
|
609
|
+
**Fail loudly:** run extraction in the foreground, or flush/stream output. A background
|
|
610
|
+
job whose stdout is buffered will hide "N of M renders failed" until it is too late.
|
|
611
|
+
|
|
612
|
+
#### Which endpoint is rate-limited (know this before you panic)
|
|
613
|
+
|
|
614
|
+
Only the **render** endpoint is scarce. Do not assume a `429` blocks everything:
|
|
615
|
+
|
|
616
|
+
| Source | Gives you | Rate-limited? |
|
|
617
|
+
|---|---|---|
|
|
618
|
+
| REST `GET /v1/images/:key?ids=…` | **rendered section PNGs** | **Yes — exhausts first, by far** |
|
|
619
|
+
| REST `GET /v1/files/:key/nodes?ids=…` | node JSON: exact px, auto-layout, tokens, `characters`, `textCase`, `imageRef` | Yes, but a much larger budget |
|
|
620
|
+
| REST `GET /v1/files/:key/images` | the real photos (image fills) | Yes, but a much larger budget |
|
|
621
|
+
| MCP `get_screenshot` / `get_design_context` | same renders, different quota | Yes (per seat) |
|
|
622
|
+
|
|
623
|
+
**Every REST endpoint shares a budget.** The render endpoint runs out first, which makes it
|
|
624
|
+
easy to believe the others are free — they are not; `/files` and `/nodes` will 429 after
|
|
625
|
+
enough calls. **Cache every response to disk on first fetch and read from that cache
|
|
626
|
+
afterwards.** A run that dies because a metadata call was refused is a run that wasted its
|
|
627
|
+
render budget for nothing.
|
|
628
|
+
|
|
629
|
+
So when renders are blocked, you have **not** lost values or assets — only the visual
|
|
630
|
+
ground truth. Get that from the user instead (below).
|
|
631
|
+
|
|
632
|
+
#### Fallback: ask the user to export (when renders are quota-blocked)
|
|
633
|
+
|
|
634
|
+
This is a first-class path, not a last resort. It has no quota.
|
|
635
|
+
|
|
636
|
+
1. **PNG = the visual reference.** Ask for the top-level page frame exported as
|
|
637
|
+
**PNG @2x** (or @1x if the frame is very tall). Slice it locally into sections using
|
|
638
|
+
the `y`/`height` offsets you already have from the node JSON.
|
|
639
|
+
2. **SVG = icons and logo only.** Export those as SVG to stop hand-drawing them.
|
|
640
|
+
3. **JSON = every value.** Keep using the API for geometry, tokens, text and image refs —
|
|
641
|
+
never ask the user to hand you numbers you can fetch.
|
|
642
|
+
|
|
643
|
+
> **If SVG is used as a visual reference, "Outline text" MUST be enabled on export.**
|
|
644
|
+
> Otherwise the SVG carries `font-family="<premium font>"`, your machine substitutes a
|
|
645
|
+
> different face, and the "reference" silently shows wrong glyphs, wrong line breaks and
|
|
646
|
+
> wrong widths — corrupting exactly what you are trying to verify. PNG has no such
|
|
647
|
+
> failure mode; prefer it.
|
|
648
|
+
|
|
649
|
+
Ask for exports with this template:
|
|
650
|
+
|
|
651
|
+
> Renders are quota-blocked (`Retry-After: …`). To continue I need the visual reference:
|
|
652
|
+
> select the page frame → Export → **PNG, 2x**. Optionally select the logo/icons →
|
|
653
|
+
> Export → **SVG**. I already have all values and photos from the API; I only need the
|
|
654
|
+
> rendered image.
|
|
655
|
+
|
|
656
|
+
Cache every response to disk (`figma/nodes/*.json`, `figma/renders/*.png`, `assets/`) so
|
|
657
|
+
a quota reset never forces a refetch.
|
|
658
|
+
|
|
659
|
+
### 6.5.3 Node-JSON reading contract
|
|
660
|
+
|
|
661
|
+
The JSON is trustworthy only if you read these fields. Each bullet is a bug that
|
|
662
|
+
shipped:
|
|
663
|
+
|
|
664
|
+
- **Text content is `characters`, never `name`.** The two routinely disagree: a node whose
|
|
665
|
+
layer `name` was copied from an old label can carry entirely different `characters`.
|
|
666
|
+
Shipping the `name` puts wrong words on the page.
|
|
667
|
+
- **Honour `style.textCase`.** `UPPER` is real. In one file *every* heading, eyebrow and
|
|
668
|
+
button was `UPPER`; rendering them title-case broke the whole page's identity.
|
|
669
|
+
- **Skip `visible: false`** nodes, and skip their subtrees. Hidden variants and stale
|
|
670
|
+
copy live there.
|
|
671
|
+
- **`visible: false` is not the only way a node is hidden.** Multiply `opacity` down the
|
|
672
|
+
ancestor chain and drop anything whose effective opacity is `0`; also respect
|
|
673
|
+
`clipsContent` on ancestors. A node can be `visible: true` and still render nothing.
|
|
674
|
+
This matters beyond layout: an opacity-0 node made one build ask the user for a
|
|
675
|
+
licensed font that the design never actually shows.
|
|
676
|
+
- **`style.textAlignHorizontal` describes alignment *inside the text box*,** not on the
|
|
677
|
+
page. A `LEFT`-aligned box inside a centred auto-layout renders centred. Use
|
|
678
|
+
`absoluteBoundingBox.x` relative to the container to decide real alignment.
|
|
679
|
+
- Read `fills`, `strokes` + `strokeWeight`, `cornerRadius`, `opacity`, and gradient
|
|
680
|
+
paints. Rings, badges, scrims and pills live in these, not in the layout tree.
|
|
681
|
+
- **`imageTransform` on an IMAGE fill can rotate/flip/crop the photo.** A negative scale
|
|
682
|
+
means the asset must be transformed before use; `background-position` alone will not
|
|
683
|
+
reproduce it.
|
|
684
|
+
- **Selecting the right `imageRef`:** filter candidates by the *node's own* bounding box
|
|
685
|
+
(e.g. only 108×108 tiles), explicitly exclude the section background (large bbox), and
|
|
686
|
+
for stacked carousel slides take the **last** (topmost) fill in paint order. An
|
|
687
|
+
off-by-one here silently shuffles every photo on the page.
|
|
688
|
+
|
|
689
|
+
**Identify each section by its heading text, never by index, order or height.** Frame
|
|
690
|
+
frame names are meaningless (Figma auto-names like `Frame 1234567890`). In one real file the
|
|
691
|
+
sections were mislabeled by one position, which silently swapped two of them and dropped a
|
|
692
|
+
whole section that nobody noticed until the renders were viewed. Print the largest
|
|
693
|
+
`characters` value inside each node and name the section from that:
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
# name every section by what it actually says
|
|
697
|
+
for f in figma/nodes/*.json; do python3 scripts/figma_spec.py "$f" 4 | head -8; done
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**Placeholder copy is a finding, not an invitation.** If `characters` reads `"Label"`,
|
|
701
|
+
`"Lorem ipsum"`, or the same blog title three times, that *is* the design. Ship it
|
|
702
|
+
verbatim and record it in the difference log. Do **not** invent category names, brand
|
|
703
|
+
names or headlines to fill the gap — inventing copy is the same class of error as
|
|
704
|
+
inventing a section.
|
|
705
|
+
|
|
706
|
+
Write this as a reusable script (`spec.py`) that prints, per node: type, position
|
|
707
|
+
relative to the section, size, `characters`, font family/weight/size/line-height/case,
|
|
708
|
+
fill, stroke, radius, and layout mode. Read its output before coding the section.
|
|
709
|
+
|
|
710
|
+
### 6.5.4 Fonts: measure, never eyeball
|
|
711
|
+
|
|
712
|
+
1. Extract the real font family and weight from the JSON.
|
|
713
|
+
2. If it is unavailable (premium/no CDN), **measure** candidates instead of guessing:
|
|
714
|
+
take a long heading, and compare rendered width at the design's font-size/weight
|
|
715
|
+
against the Figma text-box width (`canvas.measureText`, after `document.fonts.load`).
|
|
716
|
+
3. Choose the candidate that matches **both** the metric and the *classification*
|
|
717
|
+
(Didone ≠ Garamond ≠ transitional). A metric-close font of the wrong classification
|
|
718
|
+
still looks wrong.
|
|
719
|
+
4. Record the substitution and the measured delta in the difference log.
|
|
720
|
+
5. Offer the user an `@font-face` swap if they hold a licence for the real file.
|
|
721
|
+
|
|
722
|
+
### 6.5.5 The per-section build loop
|
|
723
|
+
|
|
724
|
+
For each section, in page order — never batch:
|
|
725
|
+
|
|
726
|
+
1. **Look** at the section's reference render. *(What it looks like.)*
|
|
727
|
+
2. **Read** the `figma_spec.py` dump for that node. *(What the numbers are.)*
|
|
728
|
+
You need both. The render alone makes you guess numbers; the JSON alone makes you
|
|
729
|
+
build the right-sized box around the wrong design.
|
|
730
|
+
3. **Build from the spec, not from the picture** — see §6.5.5.1.
|
|
731
|
+
4. **Lint** before you look: `python3 scripts/figma_lint.py --css … --html … --nodes …`
|
|
732
|
+
It fails on any spacing, size or colour that exists nowhere in the design.
|
|
733
|
+
5. **Verify visually**: serve the page, isolate the section, screenshot it at the design
|
|
734
|
+
frame width, compare against the render.
|
|
735
|
+
6. **Verify per section, live**: `python3 scripts/figma_report.py --only <section>` — this
|
|
736
|
+
runs the *text audit* (§18.0) for that one section. Do not defer it to the end; a
|
|
737
|
+
hundred small offsets are cheap to fix one section at a time and brutal in bulk.
|
|
738
|
+
7. Record residual differences in the difference log. Only then move to section N+1.
|
|
739
|
+
|
|
740
|
+
Do not proceed to section N+1 while section N is unverified.
|
|
741
|
+
|
|
742
|
+
### 6.5.5.1 Build from the spec — the rules that prevent the offsets
|
|
743
|
+
|
|
744
|
+
Every number in the design exists in the JSON. If a value in your CSS is not in the file,
|
|
745
|
+
you invented it, and the text audit will find it later as an offset.
|
|
746
|
+
|
|
747
|
+
- **Spacing.** `gap` and `padding` come from `itemSpacing` and `padding*` on the
|
|
748
|
+
auto-layout frames. Do not eyeball rhythm from the render, and do not reach for a
|
|
749
|
+
spacing scale you like. `figma_lint.py` rejects any value the design never uses.
|
|
750
|
+
- **Type.** `font-size`, `font-weight`, `line-height`, `letter-spacing` and `text-transform`
|
|
751
|
+
come from each TEXT node's `style` (and its `styleOverrideTable`). Never retype a size
|
|
752
|
+
from memory.
|
|
753
|
+
- **At the design width, every declared value must resolve to the Figma number.** Put
|
|
754
|
+
responsive behaviour in media queries. Hiding the design value inside `clamp()` makes it
|
|
755
|
+
unauditable and invites drift.
|
|
756
|
+
- **Colour.** Take the hex from the node's own `fills`. Do not pick "the token that looks
|
|
757
|
+
right" — two golds that differ by one step read as identical to you and as a defect to
|
|
758
|
+
the audit.
|
|
759
|
+
- **Copy is verbatim `characters`.** Insert `<br>` **only** where the string contains a
|
|
760
|
+
newline. Inventing a line break changes the copy and silently breaks every text-based
|
|
761
|
+
check. `figma_lint.py` counts them.
|
|
762
|
+
- **Position.** Reproduce the section's own container offsets, not a global container you
|
|
763
|
+
reused. Different sections legitimately have different gutters.
|
|
764
|
+
- **Icons are exported assets, never hand-drawn.** An inline `<svg>` you wrote from memory
|
|
765
|
+
is a different icon: different stroke weight, different metaphor, different silhouette.
|
|
766
|
+
It reads as "close enough" to you and as wrong to the person who drew it. Export every
|
|
767
|
+
vector (§0.5) and reference it. If the design draws something you genuinely reproduce in
|
|
768
|
+
CSS (a dot, a rule, a circle), that is an exception you must *declare* — pass it to
|
|
769
|
+
`figma_lint.py --allow-inline-svg N` and list it in the difference log.
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
**Before the browser, run `figma_lint.py`. Two of its checks decide the whole text audit:**
|
|
773
|
+
|
|
774
|
+
- *missing copy* — every visible `characters` string must appear verbatim in the HTML source
|
|
775
|
+
(page text, or `placeholder`/`aria-label`/`value`/`alt`). If it fails, you dropped,
|
|
776
|
+
reworded or invented copy; fix it now, not after a hundred "not found in DOM" rows.
|
|
777
|
+
- *invented spacing* — a `gap` or `padding` that exists nowhere in the design is the single
|
|
778
|
+
cause of the position failures the text audit reports. When the report later says "223
|
|
779
|
+
present, 12 positioned", the 211 are almost always downstream of one guessed gap. Transcribe
|
|
780
|
+
every spacing value from `figma_spec.py`; invent none.
|
|
781
|
+
### 6.5.6 Blocked-section reporting
|
|
782
|
+
|
|
783
|
+
When a reference is unobtainable, say exactly this, per section, and stop:
|
|
784
|
+
|
|
785
|
+
> **BLOCKED — [section]**: no reference image (reason: `429`, `Retry-After 4.6 days`).
|
|
786
|
+
> Copy and geometry are extracted, but the visual composition is unverified. I will not
|
|
787
|
+
> implement it from geometry alone. Options: (a) send me a screenshot of this frame,
|
|
788
|
+
> (b) grant an account with render quota, (c) wait for the limit to reset.
|
|
789
|
+
|
|
790
|
+
Never present a geometry-only reconstruction as a finished section.
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## 7. Pixel-Accuracy Workflow
|
|
795
|
+
|
|
796
|
+
Follow this sequence.
|
|
797
|
+
|
|
798
|
+
### Step -1 — Discovery (gate)
|
|
799
|
+
|
|
800
|
+
Run `figma_discover.py` and report breakpoints, icon library, hover variants and page count
|
|
801
|
+
to the user (§6.5.0.5). Everything downstream assumes you know the answers.
|
|
802
|
+
|
|
803
|
+
### Step 0 — Reference Capture (gate)
|
|
804
|
+
|
|
805
|
+
Follow §6.5.2. Obtain and **view** one reference image per section before any coding.
|
|
806
|
+
Sections without a viewed reference are BLOCKED (§6.5.6) and must not be implemented.
|
|
807
|
+
|
|
808
|
+
### Step 1 — Inventory
|
|
809
|
+
|
|
810
|
+
Create a checklist of:
|
|
811
|
+
|
|
812
|
+
- pages;
|
|
813
|
+
- frames;
|
|
814
|
+
- components;
|
|
815
|
+
- assets;
|
|
816
|
+
- fonts;
|
|
817
|
+
- breakpoints;
|
|
818
|
+
- interactions;
|
|
819
|
+
- unknowns.
|
|
820
|
+
|
|
821
|
+
### Step 2 — Extract Design Tokens
|
|
822
|
+
|
|
823
|
+
Build a centralized token system before styling individual sections.
|
|
824
|
+
|
|
825
|
+
### Step 3 — Build Semantic Structure
|
|
826
|
+
|
|
827
|
+
Use semantic elements where appropriate:
|
|
828
|
+
|
|
829
|
+
- `header`
|
|
830
|
+
- `nav`
|
|
831
|
+
- `main`
|
|
832
|
+
- `section`
|
|
833
|
+
- `article`
|
|
834
|
+
- `aside`
|
|
835
|
+
- `footer`
|
|
836
|
+
- `button`
|
|
837
|
+
- `form`
|
|
838
|
+
- correctly ordered headings
|
|
839
|
+
|
|
840
|
+
Avoid unnecessary wrapper elements.
|
|
841
|
+
|
|
842
|
+
### Step 4 — Implement From Large to Small
|
|
843
|
+
|
|
844
|
+
Recommended order:
|
|
845
|
+
|
|
846
|
+
1. global reset and tokens;
|
|
847
|
+
2. page container and grid;
|
|
848
|
+
3. header/navigation;
|
|
849
|
+
4. major sections;
|
|
850
|
+
5. reusable components;
|
|
851
|
+
6. typography;
|
|
852
|
+
7. imagery;
|
|
853
|
+
8. interaction states;
|
|
854
|
+
9. responsive behavior;
|
|
855
|
+
10. animation after approval.
|
|
856
|
+
|
|
857
|
+
### Step 5 — Visual Comparison
|
|
858
|
+
|
|
859
|
+
Do this **per section, immediately after building it** — not once at the end. Serve the
|
|
860
|
+
page, isolate the section (hide the others), screenshot at the design frame width, and
|
|
861
|
+
compare against that section's reference.
|
|
862
|
+
|
|
863
|
+
> Numeric agreement is not visual agreement. Matching every frame height to the pixel
|
|
864
|
+
> proves nothing about whether the design is right.
|
|
865
|
+
|
|
866
|
+
Inspect:
|
|
867
|
+
|
|
868
|
+
- horizontal alignment;
|
|
869
|
+
- vertical rhythm;
|
|
870
|
+
- element dimensions;
|
|
871
|
+
- text wrapping;
|
|
872
|
+
- line breaks;
|
|
873
|
+
- image cropping;
|
|
874
|
+
- border radius;
|
|
875
|
+
- shadows;
|
|
876
|
+
- colors;
|
|
877
|
+
- icon alignment;
|
|
878
|
+
- section height;
|
|
879
|
+
- responsive transitions.
|
|
880
|
+
|
|
881
|
+
### Step 5.5 — Numeric Verification Matrix (mandatory, per section)
|
|
882
|
+
|
|
883
|
+
Measure in the browser (`getBoundingClientRect`/`getComputedStyle`) and assert against the
|
|
884
|
+
Figma values. Font/colour checks alone NEVER count as "verified".
|
|
885
|
+
|
|
886
|
+
| Element class | Must assert |
|
|
887
|
+
|---|---|
|
|
888
|
+
| Repeated items (tabs, cards, chips, logos) | each item w×h; EVERY inter-item gap; container width, border colour+width, shadow presence |
|
|
889
|
+
| Buttons / badges | rendered w×h ±1 (border-box incl border); text letterSpacing/weight/size/textCase; computed `display`+`justify-content` measured IN PLACE (nested), not in isolation |
|
|
890
|
+
| Multi-line headings | per-line start-x equal (`Range.getClientRects()`); `text-align`; `text-indent`; `margin-inline` reset when extending a centred base class |
|
|
891
|
+
| Divider → next row | rendered gap = Figma `nextNode.y − divider.y` (±2) |
|
|
892
|
+
| Card rows with shared baselines | trailing elements' tops equal across the row |
|
|
893
|
+
| Side-by-side panels | tops equal when Figma y is equal |
|
|
894
|
+
| Images / media | pager dots present when the frame contains dot ELLIPSEs; directional photo orientation matches |
|
|
895
|
+
| Icon sets | exactly one container shape per icon (SVG internals vs wrapper), consistent across the SET |
|
|
896
|
+
| Fonts | `document.fonts.check` true for every family+weight used |
|
|
897
|
+
| Raster logos | visible ink (content bbox), group opacity, rendered colour unified with siblings |
|
|
898
|
+
|
|
899
|
+
Run the matrix at the design width, one wider viewport (≥1920) and one narrower.
|
|
900
|
+
After editing any shared CSS (base class, container rule, specificity), re-run on every
|
|
901
|
+
sibling in that container — not just the element you fixed.
|
|
902
|
+
Verify a CSS edit only after hard-navigating to a fresh URL (`?cb=<n>`) and asserting the
|
|
903
|
+
changed property's computed value first (FM92).
|
|
904
|
+
|
|
905
|
+
### Step 6 — Difference Log
|
|
906
|
+
|
|
907
|
+
Maintain a concise list:
|
|
908
|
+
|
|
909
|
+
| Area | Difference | Cause | Fix |
|
|
910
|
+
|---|---|---|---|
|
|
911
|
+
| Hero heading | Wraps one line earlier | Font metric mismatch | Load correct font or adjust width |
|
|
912
|
+
| Card gap | 4 px too large | Grid gap token | Change 28 px to 24 px |
|
|
913
|
+
|
|
914
|
+
### Step 7 — Final Verification
|
|
915
|
+
|
|
916
|
+
Do not declare completion until the acceptance checklist passes.
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## 8. Responsive Implementation Rules
|
|
921
|
+
|
|
922
|
+
Never treat mobile as a scaled-down desktop layout.
|
|
923
|
+
|
|
924
|
+
For each breakpoint, verify:
|
|
925
|
+
|
|
926
|
+
- navigation transformation;
|
|
927
|
+
- content stacking;
|
|
928
|
+
- section order;
|
|
929
|
+
- text size and wrapping;
|
|
930
|
+
- container padding;
|
|
931
|
+
- grid column count;
|
|
932
|
+
- button width;
|
|
933
|
+
- touch target size;
|
|
934
|
+
- card density;
|
|
935
|
+
- image crop;
|
|
936
|
+
- overflow;
|
|
937
|
+
- fixed and sticky behavior.
|
|
938
|
+
|
|
939
|
+
Use content-driven breakpoints where possible. When the design supplies explicit frame sizes, reproduce those first.
|
|
940
|
+
|
|
941
|
+
Re-run the Step 5.5 verification matrix at EVERY breakpoint you ship, plus one width
|
|
942
|
+
wider than the design frame. Alignment bugs that depend on container width (a `margin:auto`
|
|
943
|
+
block centring inside a wider column, a fixed-width row overflowing) are invisible at the
|
|
944
|
+
design width — they only appear wider or narrower (FM103).
|
|
945
|
+
|
|
946
|
+
**Fixed pixel columns copied from Figma are a desktop-only truth.** A rule like
|
|
947
|
+
`grid-template-columns: <a>px <b>px`, lifted straight from the frame, is exact at the
|
|
948
|
+
design width and overflows the moment the container is narrower than `a + b + gap`. Every such rule needs a breakpoint
|
|
949
|
+
above the point where it breaks — not just at the usual 1024 px — converting it to `fr`,
|
|
950
|
+
`minmax()` or a stack. Check for overflow at the design width **and** at each step down,
|
|
951
|
+
not only at the mobile preset.
|
|
952
|
+
|
|
953
|
+
Suggested baseline only when the design has no breakpoint specification:
|
|
954
|
+
|
|
955
|
+
```css
|
|
956
|
+
/* Mobile first */
|
|
957
|
+
@media (min-width: 640px) { }
|
|
958
|
+
@media (min-width: 768px) { }
|
|
959
|
+
@media (min-width: 1024px) { }
|
|
960
|
+
@media (min-width: 1280px) { }
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Do not use these blindly. Adapt breakpoints to the actual design.
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## 9. Animation and Motion Policy
|
|
968
|
+
|
|
969
|
+
**Motion that exists in the Figma file is part of the design, not an enhancement.**
|
|
970
|
+
Reproducing it is Mode A fidelity work: implement it, do not ask permission, and do not
|
|
971
|
+
wait to be told. Failing to implement specified motion is a fidelity bug, exactly like a
|
|
972
|
+
wrong colour.
|
|
973
|
+
|
|
974
|
+
Only motion that is **absent** from the design is an enhancement (Mode C) and needs
|
|
975
|
+
approval.
|
|
976
|
+
|
|
977
|
+
| Situation | Mode | Ask first? |
|
|
978
|
+
|---|---|---|
|
|
979
|
+
| `interactions[]` present in the node JSON | **A — required** | **No. Just build it.** |
|
|
980
|
+
| No motion in the file, user said nothing | B | Yes — propose it (§9.2) |
|
|
981
|
+
| No motion in the file, user said "add what suits it" | C (standing approval) | No, but label every value as inferred |
|
|
982
|
+
|
|
983
|
+
So the very first motion question is never "should there be animation?" — it is
|
|
984
|
+
**"what motion does this file already specify?"** (§9.0).
|
|
985
|
+
|
|
986
|
+
### 9.0 Extract the motion the design already has (do this first)
|
|
987
|
+
|
|
988
|
+
**Never assume "the Figma has no animation".** Prototype motion lives in the node JSON and
|
|
989
|
+
is easy to miss. Before designing anything, read it:
|
|
990
|
+
|
|
991
|
+
| Field | Where | What it tells you |
|
|
992
|
+
|---|---|---|
|
|
993
|
+
| `interactions[]` | any node | the whole prototype graph |
|
|
994
|
+
| `interactions[].trigger.type` | " | `ON_HOVER`, `ON_CLICK`, `ON_DRAG`, `AFTER_TIMEOUT` |
|
|
995
|
+
| `interactions[].trigger.timeout` | " | autoplay interval, in **seconds** |
|
|
996
|
+
| `actions[].transition.type` | " | `SMART_ANIMATE`, `DISSOLVE`, `PUSH`… |
|
|
997
|
+
| `actions[].transition.duration` | " | **seconds** — multiply by 1000 |
|
|
998
|
+
| `actions[].transition.easing.type` | " | `GENTLE`, `SLOW`, `LINEAR`, `EASE_*`, `CUSTOM_CUBIC_BEZIER` |
|
|
999
|
+
| `effects[]` | any node | real `DROP_SHADOW` / `BACKGROUND_BLUR` to reproduce |
|
|
1000
|
+
|
|
1001
|
+
```bash
|
|
1002
|
+
grep -c '"interactions"' figma/nodes/*.json # if this is > 0, motion is specified
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
**Figma's named easings are springs; CSS has none.** Approximate, and say that you did:
|
|
1006
|
+
|
|
1007
|
+
| Figma | Character | Reasonable CSS |
|
|
1008
|
+
|---|---|---|
|
|
1009
|
+
| `GENTLE` | soft settle, slight overshoot | `cubic-bezier(0.34, 1.16, 0.64, 1)` |
|
|
1010
|
+
| `SLOW` | long, pure decelerate | `cubic-bezier(0.33, 1, 0.68, 1)` |
|
|
1011
|
+
| `EASE_OUT` | standard decelerate | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
|
1012
|
+
| `CUSTOM_CUBIC_BEZIER` | exact | copy `easingFunctionCubicBezier` verbatim |
|
|
1013
|
+
|
|
1014
|
+
Two traps:
|
|
1015
|
+
|
|
1016
|
+
- Spring durations (often 800–1300 ms) are **settle** times, not perceived times. Reproduce
|
|
1017
|
+
the design's number — §4 puts the design above your taste — and note the tension with
|
|
1018
|
+
the 120–220 ms guidance in §9.4.
|
|
1019
|
+
- `AFTER_TIMEOUT` implies an autoplaying carousel. Check the file actually contains the
|
|
1020
|
+
other slides. If it has only one, **do not invent them** — implement the timing you can,
|
|
1021
|
+
and report the missing slides.
|
|
1022
|
+
|
|
1023
|
+
### 9.0.0 Verify the motion you shipped
|
|
1024
|
+
|
|
1025
|
+
The durations and easings live in `interactions[]`, so they are checkable without any extra
|
|
1026
|
+
fetch — `figma_report.py` compares each hovered node's `transition-duration` against the
|
|
1027
|
+
design. A hover you styled but never gave a transition, or gave your own comfortable 200ms,
|
|
1028
|
+
shows up as a row.
|
|
1029
|
+
|
|
1030
|
+
What the report cannot see is the *appearance* of the hover state. Fetch it:
|
|
1031
|
+
|
|
1032
|
+
```bash
|
|
1033
|
+
python3 scripts/figma_pull.py <fileKey> --hover <destinationId>[,<destinationId>...]
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Then look at the variant renders and match your `:hover` to them. Never invent a hover
|
|
1037
|
+
appearance for a node whose variant exists in the file.
|
|
1038
|
+
|
|
1039
|
+
### 9.0.1 When the design specifies no motion
|
|
1040
|
+
|
|
1041
|
+
Then, and only then, infer it — and infer it *from the design*, not from habit. Read the
|
|
1042
|
+
mood off what the file does specify:
|
|
1043
|
+
|
|
1044
|
+
- **Easing names and durations already used** (e.g. everything is `SLOW`/`GENTLE` at ~1 s →
|
|
1045
|
+
the product is calm and unhurried; a 200 ms bounce would be wrong).
|
|
1046
|
+
- **Type and colour** (high-contrast Didone + muted palette → editorial, restrained).
|
|
1047
|
+
- **Effects** (heavy `BACKGROUND_BLUR`, soft shadows → soft, layered motion, not snappy).
|
|
1048
|
+
|
|
1049
|
+
Then choose the smallest motion that serves the mood: fade + a short rise (12–20 px),
|
|
1050
|
+
generous duration, decelerating curve, subtle stagger. Label every inferred value.
|
|
1051
|
+
|
|
1052
|
+
### 9.0.2 Motion must never hide content
|
|
1053
|
+
|
|
1054
|
+
Entrance animation that sets `opacity: 0` in CSS is a broken page whenever the script fails,
|
|
1055
|
+
JS is off, or the tab is throttled (`IntersectionObserver` callbacks and transitions do not
|
|
1056
|
+
run in a hidden tab).
|
|
1057
|
+
|
|
1058
|
+
- Scope the hiding rule to a class the script adds: `html.motion [data-reveal] { opacity: 0 }`.
|
|
1059
|
+
- Reveal on `IntersectionObserver` **and** a passive `scroll` sweep **and** `visibilitychange`.
|
|
1060
|
+
- Never depend on `requestAnimationFrame` for the initial pass.
|
|
1061
|
+
- Verify by disabling the script: the page must be fully visible.
|
|
1062
|
+
|
|
1063
|
+
**Verifying opacity in a headless or background tab:** CSS transitions are frozen while
|
|
1064
|
+
`document.hidden` is true, so `getComputedStyle(el).opacity` returns the *mid-flight* value
|
|
1065
|
+
(usually `0`), not the end state. It will look like a bug that isn't there. Inject
|
|
1066
|
+
`* { transition: none !important; animation: none !important }` first, then assert three
|
|
1067
|
+
things: hidden before reveal, visible after reveal, and visible with the script's class
|
|
1068
|
+
removed entirely.
|
|
1069
|
+
|
|
1070
|
+
### 9.1 Do Not Add Animation Automatically
|
|
1071
|
+
|
|
1072
|
+
Before adding animation, explain:
|
|
1073
|
+
|
|
1074
|
+
- the exact section or component;
|
|
1075
|
+
- the proposed motion;
|
|
1076
|
+
- why it improves the experience;
|
|
1077
|
+
- whether it affects performance;
|
|
1078
|
+
- whether it changes the original design;
|
|
1079
|
+
- how reduced-motion users will be handled.
|
|
1080
|
+
|
|
1081
|
+
Then ask the user for approval.
|
|
1082
|
+
|
|
1083
|
+
### 9.2 Required Suggestion Format
|
|
1084
|
+
|
|
1085
|
+
Use this format:
|
|
1086
|
+
|
|
1087
|
+
> **Optional enhancement — [section/component]**
|
|
1088
|
+
> I recommend adding `[animation]` because `[specific UX or visual reason]`.
|
|
1089
|
+
> Suggested behavior: `[trigger, duration, easing, and movement]`.
|
|
1090
|
+
> This is not shown in the original Figma design. Should I add it?
|
|
1091
|
+
|
|
1092
|
+
Example:
|
|
1093
|
+
|
|
1094
|
+
> **Optional enhancement — Hero section**
|
|
1095
|
+
> I recommend a subtle staggered fade-and-rise for the headline, description, and CTA. It would make the first screen feel more polished without changing the layout. Suggested behavior: run once on page load, 420–600 ms, 12 px upward movement, and support `prefers-reduced-motion`. This is not shown in the original Figma design. Should I add it?
|
|
1096
|
+
|
|
1097
|
+
### 9.3 Suitable Motion Opportunities
|
|
1098
|
+
|
|
1099
|
+
Consider suggesting motion for:
|
|
1100
|
+
|
|
1101
|
+
- hero content entrance;
|
|
1102
|
+
- image reveal;
|
|
1103
|
+
- card hover feedback;
|
|
1104
|
+
- button hover and press states;
|
|
1105
|
+
- navigation underline;
|
|
1106
|
+
- accordion expansion;
|
|
1107
|
+
- tab switching;
|
|
1108
|
+
- modal entrance;
|
|
1109
|
+
- scroll-linked section reveal;
|
|
1110
|
+
- number counters;
|
|
1111
|
+
- testimonial carousel;
|
|
1112
|
+
- decorative background movement.
|
|
1113
|
+
|
|
1114
|
+
Do not suggest animation merely to make every section move.
|
|
1115
|
+
|
|
1116
|
+
### 9.4 Motion Quality Rules
|
|
1117
|
+
|
|
1118
|
+
Animation must:
|
|
1119
|
+
|
|
1120
|
+
- reinforce hierarchy or feedback;
|
|
1121
|
+
- be subtle;
|
|
1122
|
+
- avoid blocking user action;
|
|
1123
|
+
- avoid layout shift;
|
|
1124
|
+
- avoid excessive parallax;
|
|
1125
|
+
- avoid long entrance sequences;
|
|
1126
|
+
- support `prefers-reduced-motion`;
|
|
1127
|
+
- use transform and opacity when possible;
|
|
1128
|
+
- preserve keyboard usability.
|
|
1129
|
+
|
|
1130
|
+
Default motion ranges when no design specification exists:
|
|
1131
|
+
|
|
1132
|
+
- microinteraction: 120–220 ms;
|
|
1133
|
+
- component transition: 180–320 ms;
|
|
1134
|
+
- section entrance: 350–650 ms.
|
|
1135
|
+
|
|
1136
|
+
These are defaults, not mandatory values.
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## 10. UX/UI Review Protocol
|
|
1141
|
+
|
|
1142
|
+
Review every section, but separate findings into the following categories.
|
|
1143
|
+
|
|
1144
|
+
### A. Fidelity Issue
|
|
1145
|
+
|
|
1146
|
+
The implementation does not match the supplied design.
|
|
1147
|
+
|
|
1148
|
+
Examples:
|
|
1149
|
+
|
|
1150
|
+
- incorrect spacing;
|
|
1151
|
+
- wrong font weight;
|
|
1152
|
+
- wrong image crop;
|
|
1153
|
+
- missing state;
|
|
1154
|
+
- misaligned grid.
|
|
1155
|
+
|
|
1156
|
+
Fix these without asking when the target is exact implementation.
|
|
1157
|
+
|
|
1158
|
+
### B. Usability Issue
|
|
1159
|
+
|
|
1160
|
+
The supplied design may create user difficulty.
|
|
1161
|
+
|
|
1162
|
+
Examples:
|
|
1163
|
+
|
|
1164
|
+
- text contrast is too low;
|
|
1165
|
+
- button label is unclear;
|
|
1166
|
+
- touch target is too small;
|
|
1167
|
+
- form lacks validation feedback;
|
|
1168
|
+
- navigation is hard to discover.
|
|
1169
|
+
|
|
1170
|
+
Explain the issue and recommend a solution. Ask before making a visible design change.
|
|
1171
|
+
|
|
1172
|
+
### C. Optional Visual Enhancement
|
|
1173
|
+
|
|
1174
|
+
The design is usable but may benefit from refinement.
|
|
1175
|
+
|
|
1176
|
+
Examples:
|
|
1177
|
+
|
|
1178
|
+
- subtle entrance animation;
|
|
1179
|
+
- stronger visual hierarchy;
|
|
1180
|
+
- improved section transition;
|
|
1181
|
+
- refined hover states;
|
|
1182
|
+
- more consistent card treatment.
|
|
1183
|
+
|
|
1184
|
+
Do not implement without approval.
|
|
1185
|
+
|
|
1186
|
+
### D. Missing Product State
|
|
1187
|
+
|
|
1188
|
+
The design lacks a state required for real usage.
|
|
1189
|
+
|
|
1190
|
+
Examples:
|
|
1191
|
+
|
|
1192
|
+
- loading;
|
|
1193
|
+
- empty result;
|
|
1194
|
+
- error;
|
|
1195
|
+
- disabled;
|
|
1196
|
+
- success;
|
|
1197
|
+
- form validation;
|
|
1198
|
+
- mobile menu open state.
|
|
1199
|
+
|
|
1200
|
+
Call this out explicitly and propose the minimum required state.
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
## 11. Recommendation Priority
|
|
1205
|
+
|
|
1206
|
+
Label each recommendation:
|
|
1207
|
+
|
|
1208
|
+
- **Critical** — blocks usability, accessibility, implementation, or conversion.
|
|
1209
|
+
- **High** — significant impact on clarity, consistency, or user flow.
|
|
1210
|
+
- **Medium** — meaningful refinement.
|
|
1211
|
+
- **Low** — optional polish.
|
|
1212
|
+
|
|
1213
|
+
For every recommendation include:
|
|
1214
|
+
|
|
1215
|
+
1. location;
|
|
1216
|
+
2. observed issue;
|
|
1217
|
+
3. recommended change;
|
|
1218
|
+
4. reason;
|
|
1219
|
+
5. expected impact;
|
|
1220
|
+
6. whether approval is required.
|
|
1221
|
+
|
|
1222
|
+
Example:
|
|
1223
|
+
|
|
1224
|
+
| Priority | Location | Observation | Recommendation | Impact | Approval |
|
|
1225
|
+
|---|---|---|---|---|---|
|
|
1226
|
+
| High | Mobile header | Navigation has no open state | Add accessible menu drawer | Improves mobile navigation | Required |
|
|
1227
|
+
| Medium | Feature cards | No hover feedback | Add subtle elevation and border transition | Improves interactivity cue | Required |
|
|
1228
|
+
| Critical | Contact form | Error state missing | Add inline validation and summary | Prevents failed submissions | Required |
|
|
1229
|
+
|
|
1230
|
+
---
|
|
1231
|
+
|
|
1232
|
+
## 12. Accessibility Requirements
|
|
1233
|
+
|
|
1234
|
+
Target WCAG 2.2 AA where feasible without changing the approved visual direction.
|
|
1235
|
+
|
|
1236
|
+
Verify:
|
|
1237
|
+
|
|
1238
|
+
- semantic structure;
|
|
1239
|
+
- heading order;
|
|
1240
|
+
- keyboard navigation;
|
|
1241
|
+
- visible focus indicators;
|
|
1242
|
+
- accessible names;
|
|
1243
|
+
- form labels;
|
|
1244
|
+
- error association;
|
|
1245
|
+
- image alternative text;
|
|
1246
|
+
- color contrast;
|
|
1247
|
+
- reduced motion;
|
|
1248
|
+
- touch target size;
|
|
1249
|
+
- screen-reader announcements for dynamic content;
|
|
1250
|
+
- correct use of ARIA only when native HTML is insufficient.
|
|
1251
|
+
|
|
1252
|
+
Do not remove focus outlines without an accessible replacement.
|
|
1253
|
+
|
|
1254
|
+
---
|
|
1255
|
+
|
|
1256
|
+
## 13. HTML/CSS Quality Rules
|
|
1257
|
+
|
|
1258
|
+
### HTML
|
|
1259
|
+
|
|
1260
|
+
- Use semantic HTML.
|
|
1261
|
+
- Maintain a logical heading hierarchy.
|
|
1262
|
+
- Use buttons for actions and links for navigation.
|
|
1263
|
+
- Include useful alt text.
|
|
1264
|
+
- Avoid duplicated IDs.
|
|
1265
|
+
- Keep DOM structure understandable.
|
|
1266
|
+
- Do not use inline styles unless required by the environment.
|
|
1267
|
+
|
|
1268
|
+
### CSS
|
|
1269
|
+
|
|
1270
|
+
- **Only use values the design uses.** Run `scripts/figma_lint.py` before every fidelity
|
|
1271
|
+
report; it lists CSS spacing, sizes and colours that appear nowhere in the node JSON.
|
|
1272
|
+
- **Do not wrap a design value in `clamp()` at the design width.** `clamp()` belongs to
|
|
1273
|
+
responsive behaviour, not to the base declaration; it hides the number you are supposed
|
|
1274
|
+
to be reproducing.
|
|
1275
|
+
- Use reusable variables and component classes.
|
|
1276
|
+
- Prefer Grid and Flexbox.
|
|
1277
|
+
- Avoid excessive `!important`.
|
|
1278
|
+
- Avoid fixed pixel heights for text-heavy sections unless required by the design.
|
|
1279
|
+
- Prevent layout shift.
|
|
1280
|
+
- Keep selectors maintainable.
|
|
1281
|
+
- Use logical properties where useful.
|
|
1282
|
+
- Group responsive rules consistently.
|
|
1283
|
+
- Match the project's existing naming convention.
|
|
1284
|
+
|
|
1285
|
+
### JavaScript
|
|
1286
|
+
|
|
1287
|
+
When HTML/CSS alone cannot reproduce the intended interaction:
|
|
1288
|
+
|
|
1289
|
+
- use the minimum JavaScript necessary;
|
|
1290
|
+
- keep behavior separate from presentation;
|
|
1291
|
+
- preserve keyboard and screen-reader support;
|
|
1292
|
+
- do not introduce a large dependency for a small effect;
|
|
1293
|
+
- explain any dependency added.
|
|
1294
|
+
|
|
1295
|
+
---
|
|
1296
|
+
|
|
1297
|
+
## 14. Framework Adaptation
|
|
1298
|
+
|
|
1299
|
+
When the user specifies a framework, follow its conventions.
|
|
1300
|
+
|
|
1301
|
+
Supported examples:
|
|
1302
|
+
|
|
1303
|
+
- plain HTML/CSS/JavaScript;
|
|
1304
|
+
- React;
|
|
1305
|
+
- Next.js;
|
|
1306
|
+
- Vue;
|
|
1307
|
+
- Nuxt;
|
|
1308
|
+
- Svelte;
|
|
1309
|
+
- Astro;
|
|
1310
|
+
- Tailwind CSS;
|
|
1311
|
+
- Bootstrap;
|
|
1312
|
+
- existing design systems.
|
|
1313
|
+
|
|
1314
|
+
If no framework is specified, default to semantic HTML, modular CSS, and minimal JavaScript.
|
|
1315
|
+
|
|
1316
|
+
Do not convert a project to another framework without explicit approval.
|
|
1317
|
+
|
|
1318
|
+
---
|
|
1319
|
+
|
|
1320
|
+
## 15. Handling Incomplete Figma Designs
|
|
1321
|
+
|
|
1322
|
+
When the design omits information:
|
|
1323
|
+
|
|
1324
|
+
1. identify the missing part;
|
|
1325
|
+
2. search existing components or neighboring frames for precedent;
|
|
1326
|
+
3. infer only when necessary;
|
|
1327
|
+
4. label the inference;
|
|
1328
|
+
5. implement the least surprising behavior;
|
|
1329
|
+
6. offer alternatives when multiple valid solutions exist.
|
|
1330
|
+
|
|
1331
|
+
Example:
|
|
1332
|
+
|
|
1333
|
+
> **Assumption:** The desktop design does not show the mobile navigation state. I will use a standard menu button that opens a full-width drawer while preserving the existing typography and color system.
|
|
1334
|
+
|
|
1335
|
+
Never present inferred behavior as if it came directly from Figma.
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
## 16. User Approval Gate
|
|
1340
|
+
|
|
1341
|
+
Require explicit approval before:
|
|
1342
|
+
|
|
1343
|
+
- adding animation not present in Figma;
|
|
1344
|
+
- adding or removing sections;
|
|
1345
|
+
- changing layout hierarchy;
|
|
1346
|
+
- changing colors or typography;
|
|
1347
|
+
- rewriting visible copy;
|
|
1348
|
+
- changing button labels;
|
|
1349
|
+
- adding decorative graphics;
|
|
1350
|
+
- replacing imagery;
|
|
1351
|
+
- changing user flow;
|
|
1352
|
+
- introducing a new library;
|
|
1353
|
+
- changing a component's visual behavior;
|
|
1354
|
+
- making conversion-oriented design changes.
|
|
1355
|
+
|
|
1356
|
+
Approval is not required for:
|
|
1357
|
+
|
|
1358
|
+
- semantic markup corrections;
|
|
1359
|
+
- invisible accessibility metadata;
|
|
1360
|
+
- CSS organization;
|
|
1361
|
+
- browser compatibility fixes;
|
|
1362
|
+
- performance optimizations that do not change the visual result;
|
|
1363
|
+
- exact fidelity corrections.
|
|
1364
|
+
|
|
1365
|
+
---
|
|
1366
|
+
|
|
1367
|
+
## 17. Required Response Structure
|
|
1368
|
+
|
|
1369
|
+
When beginning a Figma implementation, respond with:
|
|
1370
|
+
|
|
1371
|
+
### A. Design Understanding
|
|
1372
|
+
|
|
1373
|
+
- target page or frame;
|
|
1374
|
+
- implementation mode;
|
|
1375
|
+
- target technology;
|
|
1376
|
+
- available assets;
|
|
1377
|
+
- **required fonts** — list every distinct `fontPostScriptName` on visible TEXT nodes,
|
|
1378
|
+
split into *free* (load from a CDN yourself) and *licensed* (the user must supply).
|
|
1379
|
+
Name the exact files and the exact folder. Do this in the **first** reply, not at
|
|
1380
|
+
delivery — the user cannot act on it after the fact;
|
|
1381
|
+
- missing information;
|
|
1382
|
+
- assumptions.
|
|
1383
|
+
|
|
1384
|
+
Produce the font list with `scripts/figma_fonts.py` (§0.3) — never from memory or from a
|
|
1385
|
+
previous project — and state it in the **first** reply using the template in §0.3. It must
|
|
1386
|
+
name, for *this* design:
|
|
1387
|
+
|
|
1388
|
+
1. every **licensed** face, by `fontPostScriptName`, and what it is used for;
|
|
1389
|
+
2. the exact folder to drop them in (`design/fonts/`);
|
|
1390
|
+
3. every **free** face, which you load yourself and must not ask for;
|
|
1391
|
+
4. that they cannot be extracted from Figma or an SVG export;
|
|
1392
|
+
5. what you will substitute meanwhile, and that the type will not be exact until they land.
|
|
1393
|
+
|
|
1394
|
+
### B. Implementation Plan
|
|
1395
|
+
|
|
1396
|
+
- global tokens;
|
|
1397
|
+
- component list;
|
|
1398
|
+
- responsive strategy;
|
|
1399
|
+
- interaction strategy;
|
|
1400
|
+
- validation method.
|
|
1401
|
+
|
|
1402
|
+
### C. Optional Recommendations
|
|
1403
|
+
|
|
1404
|
+
Only include recommendations supported by a concrete observation.
|
|
1405
|
+
|
|
1406
|
+
### D. Approval Questions
|
|
1407
|
+
|
|
1408
|
+
Group optional changes into one concise approval request where practical.
|
|
1409
|
+
|
|
1410
|
+
Example:
|
|
1411
|
+
|
|
1412
|
+
> I can reproduce the Figma exactly first. I also identified two optional improvements:
|
|
1413
|
+
> 1. subtle hero entrance animation;
|
|
1414
|
+
> 2. stronger hover feedback for feature cards.
|
|
1415
|
+
>
|
|
1416
|
+
> Neither appears in the source design. Should I include both, only one, or keep the implementation strictly identical?
|
|
1417
|
+
|
|
1418
|
+
---
|
|
1419
|
+
|
|
1420
|
+
## 18. Final Delivery Structure
|
|
1421
|
+
|
|
1422
|
+
### 18.0 Ship a fidelity report — evidence, not assurances (required)
|
|
1423
|
+
|
|
1424
|
+
Never close a build with a sentence like "it matches the design". Hand the user something
|
|
1425
|
+
they can check without trusting you:
|
|
1426
|
+
|
|
1427
|
+
```bash
|
|
1428
|
+
# 1. once per project: sign the icon set so identity (not just presence) is checked
|
|
1429
|
+
python3 scripts/figma_icons.py --svg design/exports/page.svg --nodes figma/nodes --out figma/icons
|
|
1430
|
+
# 2. every audit round:
|
|
1431
|
+
python3 scripts/figma_report.py --page http://localhost:PORT/index.html \
|
|
1432
|
+
--nodes figma/nodes --selectors selectors.json --icons-dir figma/icons
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
`selectors.json` maps each Figma section frame to ONE CSS selector:
|
|
1436
|
+
`{"hero": ".hero", "collection": ".collection", ...}` — keys must match the node JSON
|
|
1437
|
+
filenames. Without it sections are matched by DOM order, which mis-maps silently; the
|
|
1438
|
+
report header lists any selector matching 0 or 2+ elements — fix those before reading rows.
|
|
1439
|
+
Skipping `--icons-dir` downgrades the icon audit to presence-only (it will say so).
|
|
1440
|
+
`--assets-map figma/assets-map.json` (`{imageRef: filename}`) enables image IDENTITY —
|
|
1441
|
+
without it the report cannot say the RIGHT photo is on the node (on the reference build
|
|
1442
|
+
this check caught two cards silently reusing another card's photo). Known noise: a file
|
|
1443
|
+
with stacked overlapping variants stamps the template's imageRef over real positions —
|
|
1444
|
+
identity rows saying "expects <first-photo>" at spots that already pass with their own
|
|
1445
|
+
ref are variant ghosts (FM99), not defects.
|
|
1446
|
+
|
|
1447
|
+
Version every stylesheet and script from the first commit (`styles.css?v=1`, `main.js?v=1`)
|
|
1448
|
+
and bump on each edit — browsers heuristically cache unversioned assets and the report (and
|
|
1449
|
+
you) will measure stale code (FM92).
|
|
1450
|
+
|
|
1451
|
+
It writes `fidelity-report.html`, which for every section shows the Figma reference beside
|
|
1452
|
+
the **live** page, with a cross-fade slider and a *difference* blend (a perfect match goes
|
|
1453
|
+
black), and computes — in the browser, at load time:
|
|
1454
|
+
|
|
1455
|
+
| Check | Why it exists |
|
|
1456
|
+
|---|---|
|
|
1457
|
+
| section height, Figma vs built | the obvious one |
|
|
1458
|
+
| **content extents (left–right edge)** | heights match while a container is offset sideways; only this catches it |
|
|
1459
|
+
| horizontal overflow | breaks silently below the design width |
|
|
1460
|
+
| total page height | catches drift that per-section rounding hides |
|
|
1461
|
+
| **every TEXT node: position, size, weight, colour** | the box can be perfect while the design inside it is wrong |
|
|
1462
|
+
| **every icon: exported asset / hand-drawn / missing** | hand-drawn icons pass every geometric check and still look wrong |
|
|
1463
|
+
| **every image: real photo / gradient placeholder / missing** | placeholders survive to production when nothing counts them |
|
|
1464
|
+
| **every non-text box: fill colour, corner radius** | the text audit is blind to a panel with the wrong colour |
|
|
1465
|
+
| **sections with no reference slice** | an unchecked section is how a whole section goes missing |
|
|
1466
|
+
| **the section→selector mapping itself** | a selector matching 0 or 2 elements silently invalidates every row |
|
|
1467
|
+
| **hover transition durations** vs `interactions[]` | the design states them; nothing else checks you shipped them |
|
|
1468
|
+
| **stroke colour and drop shadows** on boxes | a rule the design draws as a stroke is easy to fake as an element |
|
|
1469
|
+
| **image identity** (`--assets-map`) | "a photo is present" is not "the right photo is present" |
|
|
1470
|
+
| **confirmed breakpoints not covered** (`--breakpoints`) | a second design silently never gets built |
|
|
1471
|
+
|
|
1472
|
+
**Do not stop at the geometry checks.** They pass on a page that looks nothing like the
|
|
1473
|
+
design. In one build every section matched on height *and* content extents while only
|
|
1474
|
+
**10 of 224 text nodes** matched on position, size, weight and colour. The geometry row is
|
|
1475
|
+
a smoke test, not a verdict.
|
|
1476
|
+
|
|
1477
|
+
Three traps the text audit itself must avoid:
|
|
1478
|
+
|
|
1479
|
+
- Compare the **ink box** of the text (`Range.getBoundingClientRect()`), not the element
|
|
1480
|
+
box. A centred heading lives in a full-width block; comparing element rects reports a
|
|
1481
|
+
huge false offset.
|
|
1482
|
+
- Only compare `font-weight` **within the same typeface**. And a declared `@font-face`
|
|
1483
|
+
whose file 404s still appears in `getComputedStyle().fontFamily` — ask
|
|
1484
|
+
`document.fonts.check(...)` whether the face actually loaded before trusting it.
|
|
1485
|
+
- `text not found in DOM` is a real finding, not noise: it means your copy or your markup
|
|
1486
|
+
splits the string differently from the design.
|
|
1487
|
+
- Group an icon at the **outermost pure-vector subtree**. Recurse further and every glyph
|
|
1488
|
+
outline of a logo is counted as its own missing icon.
|
|
1489
|
+
- An icon reported `missing` may be a vector the design draws as a shape and you
|
|
1490
|
+
legitimately reproduce in CSS. That is a decision to record, not a row to ignore.
|
|
1491
|
+
|
|
1492
|
+
#### Guard the guards
|
|
1493
|
+
|
|
1494
|
+
An audit that can be fooled is worse than none, because it grants permission to stop
|
|
1495
|
+
looking. Three holes are easy to leave open — close them:
|
|
1496
|
+
|
|
1497
|
+
- **Custom properties.** A linter that skips declarations containing `var()` can be
|
|
1498
|
+
defeated by moving the invented number into a token. Resolve `var()` before checking.
|
|
1499
|
+
- **Section completeness.** A report that iterates over the references you happened to
|
|
1500
|
+
produce cannot notice a section you never sliced. Enumerate the design's sections and
|
|
1501
|
+
flag every one without a reference.
|
|
1502
|
+
- **Selector mapping.** Require an explicit section→selector map, and fail if any selector
|
|
1503
|
+
matches zero or several elements. Falling back to DOM order will one day compare the
|
|
1504
|
+
wrong things and report green.
|
|
1505
|
+
|
|
1506
|
+
And in the box audit, separate a **wrong fill/radius** (a defect) from **no 1:1 element**
|
|
1507
|
+
(a structural difference — Figma frames do not map one-to-one onto DOM elements). Reporting
|
|
1508
|
+
them together produces a number that means nothing.
|
|
1509
|
+
|
|
1510
|
+
#### The audit runs in both directions
|
|
1511
|
+
|
|
1512
|
+
Every check that walks **design → DOM** ("the design has an icon here; is it in the page?")
|
|
1513
|
+
is blind to whatever you *added*. That is not a small gap. A stray element, a doubled
|
|
1514
|
+
control, a leftover from a refactor — none of them exist in the design, so nothing looks for
|
|
1515
|
+
them, and the report keeps saying the build matches.
|
|
1516
|
+
|
|
1517
|
+
`figma_report.py` therefore also walks **DOM → design**: every graphic in a section that no
|
|
1518
|
+
design node accounts for is listed as *not in the design*. Read that row. It is the only
|
|
1519
|
+
place a mistake of commission can show up.
|
|
1520
|
+
|
|
1521
|
+
The same asymmetry applies to identity. "An icon is present at this node" and "the icon the
|
|
1522
|
+
design puts at this node is present" are different claims, and only the second one is worth
|
|
1523
|
+
anything. `figma_icons.py` stamps each exported icon with `data-icon-shape` (a scale- and
|
|
1524
|
+
translation-invariant outline profile) and `data-icon-paint`; the report compares those,
|
|
1525
|
+
with a tolerance on the shape and exactly on the paint. Two traps it exists to avoid:
|
|
1526
|
+
|
|
1527
|
+
- hashing the geometry — the same icon reused on the next card sits at sub-pixel-different
|
|
1528
|
+
coordinates, so a hash reports every legitimate reuse as a mismatch;
|
|
1529
|
+
- comparing geometry alone — a filled star and an outlined star are the *same* outline and
|
|
1530
|
+
differ only in `fill`.
|
|
1531
|
+
|
|
1532
|
+
And because a comparator that always answers "different" makes a perfect build look broken,
|
|
1533
|
+
the report proves it can recognise a file as itself before it reports a single verdict.
|
|
1534
|
+
|
|
1535
|
+
#### What this report still does *not* check
|
|
1536
|
+
|
|
1537
|
+
State this to the user, every time. A green report is evidence about what was measured and
|
|
1538
|
+
nothing else.
|
|
1539
|
+
|
|
1540
|
+
- **the visual result of a hover state.** Durations are verified; what the variant *looks
|
|
1541
|
+
like* is not. Fetch the variants (`figma_pull.py --hover <destIds>`), look at them, and
|
|
1542
|
+
match your `:hover` by eye.
|
|
1543
|
+
- opacity, gradients, z-order and overflow clipping
|
|
1544
|
+
- focus and active states; keyboard behaviour
|
|
1545
|
+
- accessibility and performance
|
|
1546
|
+
- **regression** — there is no baseline diff; a fix that breaks another section shows up
|
|
1547
|
+
only if you read the whole report again
|
|
1548
|
+
- anything at viewports other than the design width, and any breakpoint you did not build
|
|
1549
|
+
|
|
1550
|
+
Those need the difference-blend view, the reference frames, and your eyes.
|
|
1551
|
+
|
|
1552
|
+
Rules:
|
|
1553
|
+
|
|
1554
|
+
- The report's numbers are computed live from the page. **Do not paste a summary that the
|
|
1555
|
+
report does not show**, and do not claim a section matches while its row says *differs*.
|
|
1556
|
+
- **Verify the verifier.** Before trusting a comparison view, confirm the reference and the
|
|
1557
|
+
live page are drawn at the same scale and offset; a mis-scaled overlay makes any build
|
|
1558
|
+
look correct. Confirm a known-bad section actually reports as bad.
|
|
1559
|
+
- When you change CSS and re-measure through an iframe, **bust the cache** — a stale
|
|
1560
|
+
stylesheet will happily tell you the fix worked.
|
|
1561
|
+
- Rows that legitimately differ (a decorative element you rendered as a shadow, a font
|
|
1562
|
+
substitution) belong in the difference log with the reason — not hidden by loosening the
|
|
1563
|
+
tolerance.
|
|
1564
|
+
- A green report is *evidence about geometry*, not proof of visual fidelity. The
|
|
1565
|
+
difference-blend view is what proves that; look at it, and say what you saw.
|
|
1566
|
+
|
|
1567
|
+
At completion provide:
|
|
1568
|
+
|
|
1569
|
+
### 1. Completed Scope
|
|
1570
|
+
|
|
1571
|
+
List pages, sections, and states implemented.
|
|
1572
|
+
|
|
1573
|
+
### 2. Files Changed
|
|
1574
|
+
|
|
1575
|
+
List each created or modified file and its purpose.
|
|
1576
|
+
|
|
1577
|
+
### 3. Fidelity Notes
|
|
1578
|
+
|
|
1579
|
+
Point at `fidelity-report.html` first, then state:
|
|
1580
|
+
|
|
1581
|
+
- what matches the design;
|
|
1582
|
+
- any unavoidable deviations;
|
|
1583
|
+
- unavailable fonts or assets;
|
|
1584
|
+
- assumptions used.
|
|
1585
|
+
|
|
1586
|
+
### 4. Responsive Coverage
|
|
1587
|
+
|
|
1588
|
+
List tested viewport sizes and behavior.
|
|
1589
|
+
|
|
1590
|
+
### 5. Accessibility and Performance Notes
|
|
1591
|
+
|
|
1592
|
+
State checks completed and remaining limitations.
|
|
1593
|
+
|
|
1594
|
+
### 6. Optional Next Improvements
|
|
1595
|
+
|
|
1596
|
+
List only unimplemented, user-approved, or still-pending suggestions.
|
|
1597
|
+
|
|
1598
|
+
Do not claim visual perfection without comparison evidence.
|
|
1599
|
+
|
|
1600
|
+
---
|
|
1601
|
+
|
|
1602
|
+
## 19. Acceptance Checklist
|
|
1603
|
+
|
|
1604
|
+
Before marking the work complete, verify:
|
|
1605
|
+
|
|
1606
|
+
### Visual Fidelity
|
|
1607
|
+
|
|
1608
|
+
- [ ] Container width matches reference
|
|
1609
|
+
- [ ] Section spacing matches reference
|
|
1610
|
+
- [ ] Grid and alignment match reference
|
|
1611
|
+
- [ ] Typography matches reference
|
|
1612
|
+
- [ ] Colors match reference
|
|
1613
|
+
- [ ] Borders, radii, and shadows match reference
|
|
1614
|
+
- [ ] Images use correct crop and aspect ratio
|
|
1615
|
+
- [ ] Icons match size and alignment
|
|
1616
|
+
- [ ] Text wraps similarly at reference viewport sizes
|
|
1617
|
+
|
|
1618
|
+
### Responsive Behavior
|
|
1619
|
+
|
|
1620
|
+
- [ ] Mobile layout is intentionally designed
|
|
1621
|
+
- [ ] Tablet behavior is valid
|
|
1622
|
+
- [ ] Desktop behavior matches reference
|
|
1623
|
+
- [ ] No horizontal overflow
|
|
1624
|
+
- [ ] Navigation works at all breakpoints
|
|
1625
|
+
- [ ] Touch targets are usable
|
|
1626
|
+
- [ ] Images remain sharp and correctly cropped
|
|
1627
|
+
|
|
1628
|
+
### Interaction
|
|
1629
|
+
|
|
1630
|
+
- [ ] `interactions[]` in the node JSON was read before any motion was written (§9.0)
|
|
1631
|
+
- [ ] Figma's own durations/easings are used where they exist; springs are noted as approximations
|
|
1632
|
+
- [ ] Inferred motion is labeled, and matches the mood the design already sets
|
|
1633
|
+
- [ ] Entrance motion cannot hide content if the script fails (§9.0.2)
|
|
1634
|
+
- [ ] Hover states work
|
|
1635
|
+
- [ ] Focus states are visible
|
|
1636
|
+
- [ ] Active and selected states work
|
|
1637
|
+
- [ ] Keyboard operation works
|
|
1638
|
+
- [ ] Form validation states exist when required
|
|
1639
|
+
- [ ] Motion specified in Figma is implemented (no approval needed — it is the design)
|
|
1640
|
+
- [ ] Motion *not* in Figma has user approval, or a standing instruction, and is labeled inferred
|
|
1641
|
+
- [ ] Reduced-motion preference is respected
|
|
1642
|
+
|
|
1643
|
+
### Code Quality
|
|
1644
|
+
|
|
1645
|
+
- [ ] Semantic HTML used
|
|
1646
|
+
- [ ] CSS variables defined
|
|
1647
|
+
- [ ] Reusable components extracted
|
|
1648
|
+
- [ ] No unnecessary duplication
|
|
1649
|
+
- [ ] No arbitrary hacks without explanation
|
|
1650
|
+
- [ ] No unsupported dependencies added
|
|
1651
|
+
- [ ] No console errors
|
|
1652
|
+
|
|
1653
|
+
### Reference Coverage
|
|
1654
|
+
|
|
1655
|
+
- [ ] `fidelity-report.html` was generated, opened, and handed to the user
|
|
1656
|
+
- [ ] The **text audit** was read, not just the geometry rows
|
|
1657
|
+
- [ ] Every row in it is green, or every non-green row appears in the difference log
|
|
1658
|
+
- [ ] No layout rule was weakened just to turn a metric green
|
|
1659
|
+
- [ ] `figma_lint.py` passes: no invented spacing, size, colour, `<br>` or inline `<svg>`
|
|
1660
|
+
- [ ] Icon audit is green, or every hand-drawn / missing icon is justified in the difference log
|
|
1661
|
+
- [ ] Image audit is green: no gradient placeholders left
|
|
1662
|
+
- [ ] The user was told what the report does **not** check
|
|
1663
|
+
- [ ] Box audit reviewed: no wrong fills or radii among matched boxes
|
|
1664
|
+
- [ ] No section is listed as "no reference slice"
|
|
1665
|
+
- [ ] The section→selector map resolves to exactly one element per section
|
|
1666
|
+
- [ ] Hover transition durations audited; hover *appearance* compared against the fetched variants
|
|
1667
|
+
- [ ] `--assets-map` supplied so image identity, not just presence, is checked
|
|
1668
|
+
- [ ] `--breakpoints` supplied; every confirmed width has its own report
|
|
1669
|
+
- [ ] `figma_discover.py` was run **first** and its output shown to the user
|
|
1670
|
+
- [ ] Every confirmed breakpoint frame was built and audited, not inferred
|
|
1671
|
+
- [ ] No icon was hand-drawn that exists in the file's icon library
|
|
1672
|
+
- [ ] Hover variants named by `interactions[]` were fetched and compared
|
|
1673
|
+
- [ ] The scope (one screen vs a site) was confirmed against the file's page-sized frames
|
|
1674
|
+
- [ ] Each section was audited with `--only <section>` as it was built, not just at the end
|
|
1675
|
+
- [ ] The difference-blend overlay was actually looked at, not just the numbers
|
|
1676
|
+
- [ ] Every implemented section has a reference image that was actually viewed
|
|
1677
|
+
- [ ] No section was reconstructed from geometry/JSON alone
|
|
1678
|
+
- [ ] Every section was screenshot-compared against its reference after being built
|
|
1679
|
+
- [ ] Blocked sections are reported as blocked, not shipped as done
|
|
1680
|
+
- [ ] Font substitutions are measured (§6.5.4), not guessed, and the delta is recorded
|
|
1681
|
+
- [ ] Asset-to-node mapping was verified (no shuffled or off-by-one images)
|
|
1682
|
+
|
|
1683
|
+
### Final Honesty Check
|
|
1684
|
+
|
|
1685
|
+
- [ ] Assumptions are labeled
|
|
1686
|
+
- [ ] Inferences are labeled
|
|
1687
|
+
- [ ] Missing assets are disclosed
|
|
1688
|
+
- [ ] Unverified claims are not presented as fact
|
|
1689
|
+
- [ ] “100% identical” is not claimed without measurable verification
|
|
1690
|
+
- [ ] Section heights matching is never presented as evidence the design matches
|
|
1691
|
+
- [ ] Quota/permission failures are reported with the real cause (`Retry-After`, “File not exportable”), not glossed over
|
|
1692
|
+
|
|
1693
|
+
---
|
|
1694
|
+
|
|
1695
|
+
## 20. Recommended Folder Structure
|
|
1696
|
+
|
|
1697
|
+
A basic skill package may use:
|
|
1698
|
+
|
|
1699
|
+
```text
|
|
1700
|
+
figma-to-html-pixel-perfect/
|
|
1701
|
+
├── SKILL.md # this file
|
|
1702
|
+
├── README.md # install + setup, for distribution
|
|
1703
|
+
├── references/
|
|
1704
|
+
│ ├── visual-review-checklist.md
|
|
1705
|
+
│ ├── accessibility-checklist.md
|
|
1706
|
+
│ └── animation-guidelines.md
|
|
1707
|
+
└── scripts/
|
|
1708
|
+
├── figma_discover.py # FIRST: breakpoints, icon library, hover, scope
|
|
1709
|
+
├── figma_icons.py # extract every icon offline from the page SVG
|
|
1710
|
+
├── figma_pull.py # preflight, node JSON, one render per section
|
|
1711
|
+
├── figma_spec.py # correct node-JSON reader (§6.5.3 contract)
|
|
1712
|
+
├── figma_fonts.py # which fonts render, free vs licensed (§0.3)
|
|
1713
|
+
├── figma_lint.py # build-stage guard: invented values, hand-drawn icons
|
|
1714
|
+
└── figma_report.py # fidelity report: geometry + text + icon/image audits
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
`SKILL.md` is the only required file, but **use the scripts** — they encode §6.5.1–6.5.3.
|
|
1718
|
+
Re-deriving them by hand is how the reading-contract bugs get reintroduced.
|
|
1719
|
+
|
|
1720
|
+
---
|
|
1721
|
+
|
|
1722
|
+
## 21. Core Principle
|
|
1723
|
+
|
|
1724
|
+
The supplied Figma design is the visual source of truth.
|
|
1725
|
+
|
|
1726
|
+
Implement first, verify second, recommend third, and change the design only after the user approves the change.
|
|
1727
|
+
|
|
1728
|
+
---
|
|
1729
|
+
|
|
1730
|
+
## 22. Known Failure Modes (all observed in real builds)
|
|
1731
|
+
|
|
1732
|
+
Every row below is a mistake that shipped. Re-read this before declaring a section done.
|
|
1733
|
+
|
|
1734
|
+
**Standing rule — when you find a new defect, close it in both stages.** Do not merely fix
|
|
1735
|
+
the page. Ask: *what build rule would have prevented this*, and *what check would have
|
|
1736
|
+
caught it*, then add both, add a row here, and add a line to the checklists. A defect that
|
|
1737
|
+
is only fixed in the artefact will return in the next project. Everything you write must be
|
|
1738
|
+
stated in terms of the design file, never in terms of one particular design.
|
|
1739
|
+
|
|
1740
|
+
| # | Failure | Symptom | Rule |
|
|
1741
|
+
|---|---|---|---|
|
|
1742
|
+
| 1 | Built a section without looking at its reference | Right height, wrong design | §6.5.0 |
|
|
1743
|
+
| 2 | Bulk-downloaded every image fill | Render quota gone; `Retry-After` in days | §6.5.2 |
|
|
1744
|
+
| 3 | Background job's stdout buffered | Most renders failed, unnoticed | §6.5.2 |
|
|
1745
|
+
| 4 | Read `name` instead of `characters` | Button said the wrong thing | §6.5.3 |
|
|
1746
|
+
| 5 | Ignored `style.textCase` | Whole page in Title Case, design is UPPER | §6.5.3 |
|
|
1747
|
+
| 6 | Included `visible:false` nodes | Stale copy and ghost elements | §6.5.3 |
|
|
1748
|
+
| 7 | Trusted `textAlignHorizontal` | Centred text "corrected" to left | §6.5.3 |
|
|
1749
|
+
| 8 | Ignored `imageTransform` | Background photo upside-down | §6.5.3 |
|
|
1750
|
+
| 9 | Sloppy `imageRef` filter | Every card photo shuffled by one | §6.5.3 |
|
|
1751
|
+
| 10 | Named sections by order/height | Two sections swapped, one missing entirely | §6.5.3 |
|
|
1752
|
+
| 11 | Invented copy where the design had `"Label"` | Fabricated category names and brands | §6.5.3, §0.5 |
|
|
1753
|
+
| 12 | Guessed the fallback font | Wrong classification, wrong line breaks | §6.5.4 |
|
|
1754
|
+
| 13 | Reported matching heights as proof of fidelity | Every height exact, design still wrong | §6.5.0 |
|
|
1755
|
+
| 14 | Assumed "no animation in the design" | Thousands of `interactions[]` were sitting in the JSON | §5.6, §9.0 |
|
|
1756
|
+
| 15 | Read only `style`, not `styleOverrideTable` | Hero's italic words: wrong family *and* size | §5.3 |
|
|
1757
|
+
| 16 | Entrance motion hid content in CSS | Blank page whenever the script didn't run | §9.0.2 |
|
|
1758
|
+
| 17 | Relied on `requestAnimationFrame` | Nothing revealed in a hidden/throttled tab | §9.0.2 |
|
|
1759
|
+
| 18 | Passed `xmlns` to `xml.etree` as well | Duplicate attribute; every extracted icon broken | §0.5 |
|
|
1760
|
+
| 19 | Copied Figma's fixed px columns verbatim | Overflow between the design width and 1024px | §8 |
|
|
1761
|
+
| 20 | Misread `403 File not exportable` as a token problem | Chased duplicate/export workarounds that cannot work | §6.5.1 |
|
|
1762
|
+
| 21 | Asked for the font at delivery | Too late to act on | §0.3, §17.A |
|
|
1763
|
+
| 22 | Treated `visible:true` as "renders" | Counted an opacity-0 node; nearly demanded a font the design never shows | §6.5.3 |
|
|
1764
|
+
| 23 | Checked only heights | Container offset sideways by a doubled gutter; every height still "matched" | §18.0 |
|
|
1765
|
+
| 24 | Declared the build done from numbers | Handed the user a claim instead of a report they could check | §18.0 |
|
|
1766
|
+
| 25 | Verified only the boxes | Every section "matched"; 10 of 224 text nodes actually did | §18.0 |
|
|
1767
|
+
| 26 | Computed an SVG path's bbox by parsing numbers out of `d` | Curves and relative commands make it wrong; the logo silently cropped to a fragment. Measure with `getBBox()` in a browser | §0.5 |
|
|
1768
|
+
| 27 | Removed a layout rule to make a metric pass | Dropped `100vh` on the hero so the height check would go green — optimised for the report, not the design | §18.0 |
|
|
1769
|
+
| 28 | Invented spacing | `gap` values that exist nowhere in `itemSpacing`; became 129 vertical offsets in the audit | §6.5.5.1 |
|
|
1770
|
+
| 29 | Invented line breaks | 15 `<br>` against 9 real newlines in `characters`; 78 text nodes then "not found in DOM" | §6.5.5.1 |
|
|
1771
|
+
| 30 | Retyped sizes and colours from memory | Subtitle 18px vs 16px, one gold instead of the other | §6.5.5.1 |
|
|
1772
|
+
| 31 | Ran the audit only at the end | Hundreds of offsets landed at once instead of one section at a time | §6.5.5 |
|
|
1773
|
+
| 32 | Hand-drew icons from memory | 73 inline `<svg>` against 7 exported assets; every geometric check still passed | §6.5.5.1, §18.0 |
|
|
1774
|
+
| 33 | Never counted icons or images at all | Placeholders and wrong icons survived to "done" | §18.0 |
|
|
1775
|
+
| 34 | Believed a stale iframe | Verified a CSS fix that the browser had cached; re-check with a cache-busted reload | §6.5.5 |
|
|
1776
|
+
| 35 | Trusted the verifier without verifying it | The overlay was drawn at two different scales, so a mismatch would have looked like a match | §18.0 |
|
|
1777
|
+
| 36 | Linter skipped `var()` | Invented spacing hid inside custom properties and passed | §18.0 |
|
|
1778
|
+
| 37 | Report only iterated the slices that existed | A section with no reference was silently unverified | §18.0 |
|
|
1779
|
+
| 38 | Matched a design text to a whole element | Text sharing a parent with a `<span>` read as "not found"; repeated strings matched the wrong instance. Match the nearest **text run** to the expected position | §18.0 |
|
|
1780
|
+
| 39 | Mixed "wrong colour" with "no matching element" in one count | Produced a meaningless score for the box audit | §18.0 |
|
|
1781
|
+
| 40 | Believed `/files` and `/nodes` were not rate-limited | They 429 too; every REST endpoint shares a budget. Cache every response | §6.5.2 |
|
|
1782
|
+
| 41 | Never looked for other design widths | Wrote a guessed mobile layout while a mobile frame sat in the file | §6.5.0.5 |
|
|
1783
|
+
| 42 | Never looked for an icon library | Hand-drew dozens of icons the file already contained as components | §6.5.0.5, §0.5 |
|
|
1784
|
+
| 43 | Never fetched the hover variants `interactions[]` point at | Invented hover states that were specified in the file | §6.5.0.5, §9.0 |
|
|
1785
|
+
| 44 | Assumed the file held one screen | Built one page of what turned out to be a multi-page site | §6.5.0.5 |
|
|
1786
|
+
| 45 | Claimed icons needed an API call | They are already in the page SVG export; the claim excused hand-drawing them | §0.5 |
|
|
1787
|
+
| 46 | Exported a vector *group* as one icon | Two icons rendered stacked; looked plausible in a list, wrong on the page | §0.5 |
|
|
1788
|
+
| 47 | Implemented hover timings, verified none | The design states every duration; comparing them costs one audit | §9.0.0 |
|
|
1789
|
+
| 48 | Faked a stroke as an element | The design strokes a frame; the build draws a `<div>`. Only a stroke check sees it | §18.0 |
|
|
1790
|
+
| 49 | Counted "a photo is present" as correct | Identity needs an `imageRef` → filename map, or any photo passes | §18.0 |
|
|
1791
|
+
| 50 | Reported one breakpoint as if it were the design | Pass `--breakpoints` so uncovered widths are named in the report | §18.0, §6.5.0.5 |
|
|
1792
|
+
| 51 | Exported a vector a photo paints over | `visible:true, opacity:1` ≠ renders. A later sibling with an opaque fill buries it | §0.5 |
|
|
1793
|
+
| 52 | Called a photo an icon | A RECTANGLE/ELLIPSE with an IMAGE fill is a photo; exporting it sweeps up the paths behind it | §0.5 |
|
|
1794
|
+
| 53 | Read only `<path>` from the SVG export | Figma also emits `<circle>`, `<rect>`, `<ellipse>`, `<line>`, `<polygon>`. Icons using them came out empty | §0.5 |
|
|
1795
|
+
| 54 | Called a cluster of plain ellipses an icon | Carousel dots and rings are CSS shapes. An icon has a `VECTOR`/`BOOLEAN_OPERATION` in it | §0.5 |
|
|
1796
|
+
| 55 | Exported a frame of several icons as one | A pager's two arrows, a five-star row. Split when the children are containers, disjoint, and icon-sized | §0.5 |
|
|
1797
|
+
| 56 | Cropped the icon to its ink | The node's box is the icon's box. Cropping to the ink makes a 12px glyph fill a 40px button | §0.5 |
|
|
1798
|
+
| 57 | Said "icon missing" when it was 60px off | Absent and misplaced are different defects; one word for both hides the fix | §18.0 |
|
|
1799
|
+
| 58 | The report crashed and kept saying "measuring…" | A silent verifier reads as a passing one. Catch, banner, and syntax-check the report you emit | §18.0 |
|
|
1800
|
+
| 59 | Left the nav in normal flow | It overlays the hero in the design; in flow it pushes every y below it down by its own height | §6.5.5.1 |
|
|
1801
|
+
| 60 | Audited design → DOM only | Nothing you *add* to the page has a design node, so nothing looks for it. A stray toggle, a doubled arrow, a leftover element ships "verified" | §18.0 |
|
|
1802
|
+
| 61 | Checked that *an* icon was there, never *which* | The mail icon sat on the YouTube row and the report said 95/106. Compare what the icon draws | §18.0 |
|
|
1803
|
+
| 62 | Fingerprinted icons by hashing their geometry | The same icon reused on another card is sub-pixel-different: a hash calls every reuse a mismatch. And a filled star and an outlined star have identical geometry — a geometry-only fingerprint calls them equal | §18.0 |
|
|
1804
|
+
| 63 | Wrote a comparator and never tested it | `sameIcon` was defined and never called; the operator left behind compared object identity, so every icon read "wrong". Prove the comparator can tell a file from itself | §18.0 |
|
|
1805
|
+
| 64 | Drew a control the design draws as a vector | `appearance:none` plus a CSS triangle, *and* the exported chevron — two arrows on one select | §0.5 |
|
|
1806
|
+
| 65 | Text audit cried "missing" over copy plainly on screen | The design splits `5`/`Bedrooms`; the build merges them; a `<br>` drops the joining space; a label renders as `placeholder=`. Match grouped runs, `innerText`, and form attributes — not just standalone text nodes | §18.0 |
|
|
1807
|
+
| 66 | Reported one text number that hid the story | `10/224` read as "the text is broken" when 223 were present and only the *positions* were off. Decompose: present / positioned / sized / weighted / coloured | §18.0 |
|
|
1808
|
+
| 67 | Let missing or reworded copy reach the browser to be found | `figma_lint.py` now fails the build if any visible `characters` string is absent from the HTML source — caught before a server ever starts | §6.5.5.1 |
|
|
1809
|
+
| 68 | Measured a frozen mid-animation frame | The report's offscreen iframe has `document.hidden === true`, which freezes CSS transitions and IO. An entrance reveal (`opacity:0; translateY(18px)`) never completes, so **every** revealed element reads ~18px low and invisible — a perfectly-built page looked uniformly displaced. Force the final state (kill transitions/animations, neutralise reveal start-states) before measuring | §18.0 |
|
|
1810
|
+
| 69 | Picked an outer wrapper as the text box | A `<label>` inside a group inside a field all share the same `innerText`; the taller outer ones sit higher and report a false y-offset. Keep only the leaf-most candidate | §18.0 |
|
|
1811
|
+
| 70 | Compared ink-top to Figma's line-box-top | Figma's TEXT box top includes the line-height leading; the browser ink box starts at the glyph tops. The faithful analog is the element's own content-box top (when it wraps exactly that text) | §18.0 |
|
|
1812
|
+
| 71 | Report measured stale cached CSS | The iframe re-loaded the HTML but the browser served the PREVIOUS `styles.css`, so a just-applied fix read as still-broken (a `width:100%` that was live on disk but not in the measured frame). Cache-bust the page AND re-point its same-origin `<link>`/`<script>`/`<img>`, awaiting the stylesheets before measuring | §18.0, §6.5.5 |
|
|
1813
|
+
| 72 | A two-column space-between silently centred | `display:grid; justify-content:space-between` with fixed-width columns only distributes when the grid box spans the full width; left to shrink to content it centres, pushing one column inward by half the leftover | §6.5.5.1 |
|
|
1814
|
+
| 73 | Compared a placeholder against the element's text colour | A design TEXT that renders as an `<input>` placeholder shows the `::placeholder` colour, not the element's `color` (which paints typed text). Read the pseudo-element colour when the match came from a form-field attribute | §18.0 |
|
|
1815
|
+
| 74 | Merged number+label a design keeps as separate nodes | The build renders `"5 Bedrooms"` as one run; Figma stores `"5"` and `"Bedrooms"` separately. Match each substring's OWN range (fixed in FM75), then align it in CSS to the design's node x — a merged run whose substrings land at the design positions passes | §18.0, §6.5.5.1 |
|
|
1816
|
+
| 75 | Whole-element match used the container rect for a substring | When an element's `innerText` merely CONTAINED the wanted text ("5" in "5 Bedrooms"), the audit used `selectNodeContents(element)` — the whole element's rect, which starts at the element's left (an icon), not the substring. Every split number/label across every card read a false 20-40px x-offset while the build was pixel-exact. Match EQUALS with the element rect; hand CONTAINS to the substring-ranging fallback so the position is the substring's own | §18.0 |
|
|
1817
|
+
| 76 | Compared X at the left edge for centre/right-aligned text | Figma's `absoluteBoundingBox` is the LAYOUT box. A CENTER-aligned name in a full-column-width box has its ink far from the box's left edge, so comparing the built ink-left to the Figma box-left invents an offset the size of the box's slack (+195 on a centred name, +100 on a centred hero title). Read `textAlignHorizontal` and anchor the comparison at centre/right accordingly | §18.0 |
|
|
1818
|
+
| 77 | Measured a form field at its border, not its text | A design TEXT that renders as an input placeholder/label was compared at the control's border box, but the design node is the TEXT, which sits inside the field's content padding — a uniform false offset the size of the padding. Anchor the comparison at the field's content box | §18.0 |
|
|
1819
|
+
| 78 | Matched a `<select>` placeholder to a bare `<option>` | An `<option>` in a closed select has no layout box (rect at 0,0); the leaf-most-candidate filter then drops the real `<select>` in its favour and reports a nonsense −thousands-px offset. Match the control (`select`/`input`) via its `value`, and skip `<option>` in BOTH the attribute pass and the whole-element (innerText) pass, or the leaf-most filter keeps the zero-size option over the real select and the field reads "not found" | §18.0 |
|
|
1820
|
+
| 79 | Drove the text-position score to 98% while a blank logo and a mis-broken heading shipped | The per-text-node audit sees NEITHER images NOR line-breaks/alignment. A high `x/y` pass rate is not "looks like the design." After the numbers plateau, STOP reading them and LOOK: open the difference-blend, scan every section against its reference slice, and check images and headings by eye | §18.0 |
|
|
1821
|
+
| 80 | A wordmark logo vector-extracted to a solid white block | airbnb/agoda (line art) extracted fine; the Booking.com wordmark came out as 14 overlapping white paths that render as a filled rectangle. Complex/filled logos often do not survive vector extraction — LOOK at every extracted logo on its real background; if it is not the logo, use the raster (PNG) export instead | §0.5 |
|
|
1822
|
+
| 81 | A two-line heading indented its second line | `white-space: pre-line` plus a source newline inside the `<h2>` inherited the HTML's source indentation, pushing line 2 in ~260px — invisible to the position audit, which compares the whole heading's centre. Break lines with an explicit `<br>`, not a source newline | §6.5.5.1 |
|
|
1823
|
+
| 82 | Substitute font wrapped a heading to the wrong line count | A metrically-different fallback never reproduces design line breaks. Wire the real webfont first and assert `document.fonts.check('<w> <size>px "<family>"')` before trusting any heading geometry; never chase a wrap with size/spacing hacks | §0.4, §6.5.4 |
|
|
1824
|
+
| 83 | Flex-centred text box collapsed to a narrow column and over-wrapped | Inside flex/grid, `max-width` alone lets a text box shrink-to-fit. Set the Figma box width as `width` so wrapping matches | §6.5.5.1 |
|
|
1825
|
+
| 84 | Padding hacks pinned a heading's y but broke its box | Never tune paddings to hit a y-target. Rebuild the frame's own box model: panel padding = Figma inset, flow/flex children, bottom-anchored group via `margin-top:auto` | §6.5.5.1 |
|
|
1826
|
+
| 85 | Design fonts never verified as loading | Fail lint when a design font-family has no `@font-face`/webfont link reference; flag headings whose rendered line-count differs from the design node's | §0.4, §18.0 |
|
|
1827
|
+
| 86 | Pill button label wrapped to 2–3 lines | Design button labels are single-line: `white-space: nowrap` on the base button class | §6.5.5.1 |
|
|
1828
|
+
| 87 | Card foot faked the button's x with nudges + mega-gap | A two-ended row is `flex; justify-content:space-between; align-items:center` with a small min gap. Delete positional nudges — they hold at one width only | §6.5.5.1 |
|
|
1829
|
+
| 88 | Read only a TEXT node's base weight and missed a bold sub-run | Emphasis can live entirely in `styleOverrideTable`/`characterStyleOverrides`. Union base `style` with every override when reading weight/style/size/colour; compare the SET of weights against the CSS | §6.5.4, §18.0 |
|
|
1830
|
+
| 89 | Guessed the active/selected state's colour from a generic token | The selected variant is its own node with its own fills. Read the selected child's fill AND its TEXT fill; assert both against the `.is-active` rule | §6.5.4 |
|
|
1831
|
+
| 90 | Dropped a control's track/container fill | Segmented controls, toggle tracks and tab strips often have their own background fill. Read every container FRAME's `fills`; flag a visible solid fill whose CSS element is transparent | §6.5.4 |
|
|
1832
|
+
| 91 | Sized an interactive box by eyeballing padding | Buttons/badges are fixed w×h frames. Measure the rendered rect vs the Figma frame (±1px, border-box including border) and re-measure after every change — a comment is not verification | §6.5.5.1, §18.0 |
|
|
1833
|
+
| 92 | Rewriting `<link>.href` did NOT reparse the CSS | Verify a CSS edit by hard-navigating to a fresh URL (`?cb=<n>`), then assert the changed property's computed value BEFORE trusting any downstream measurement | §6.5.5, FM34 |
|
|
1834
|
+
| 93 | Text-hugging pills + per-item `left:` nudges made uneven gaps | When a control's repeated items share one frame size, set that w×h on each item, centre the label with `inline-flex`, and use the container `itemSpacing` as flex `gap`. Never rebuild spacing with per-item offsets | §6.5.5.1, FM87 |
|
|
1835
|
+
| 94 | Invented a grey container border + phantom shadow | Border colour/width come from `strokes`/`strokeWeight`; add `box-shadow` only if a DROP_SHADOW exists in `effects`. Flag CSS border/shadow colours that appear in no design stroke/effect | §6.5.4, FM90 |
|
|
1836
|
+
| 95 | Claimed "verified" after checking only colours | A repeated-item control passes only when ALL match: each item's w×h, the FULL sequence of inter-item gaps, container width, border colour+width, and shadow presence. Fills matching while geometry is wrong is the most common false "done" | §18.0, FM91 |
|
|
1837
|
+
| 96 | A house `letter-spacing` + transparent border bloated a button, then padding was fudged | Fix text metrics FIRST: match the button TEXT node's letterSpacing/weight/size/textCase exactly (often ls 0); a no-stroke button gets `border:0`; then the Figma padding yields the exact box. A ±1px box is a false pass while text metrics differ | §6.5.5.1, FM91 |
|
|
1838
|
+
| 97 | A margin hack rendered 7px while its comment claimed "Figma 20" | Measure the RENDERED divider→next-row gap and assert it equals the Figma metric (`nextNode.y − divider.y`, ±2). In a flex column, `margin-top` ADDS to the container `gap`. When the user says a gap is off, extract the Figma number first — never guess the direction | §6.5.5.1, FM95 |
|
|
1839
|
+
| 98 | A carousel was reduced to a static image | ≥2 small equal ELLIPSEs clustered on an image frame = slider pager. Reproduce the dots to spec and plan the slider (multiple assets + JS). The active indicator is often composite — an outer ring plus an inner dot: read both nodes | §6.5.4, §15 |
|
|
1840
|
+
| 99 | Trusted the JSON node-dump as complete | Exported JSON can omit component internals, masks and vector strokes. Cross-check against a rendered image of the frame and reproduce render-visible details the JSON lacks — pixels are ground truth, JSON is a lossy index | §0.4, §6.5.4 |
|
|
1841
|
+
| 100 | Fallback-era weight compensation survived the real font | After fixing a font substitution, re-derive every weight/size/ls that was fudged for the fallback from the Figma nodes, delete the "≈" comments, and `document.fonts.check` the exact weight. Verify each section's title weight individually — designs make exceptions | §0.4, FM85 |
|
|
1842
|
+
| 101 | Trailing meta drifted across cards with variable body length | When trailing elements share a baseline in the design, give the variable-length text a `min-height` sized to the longest case; assert the trailing elements' tops are equal across the row | §6.5.5.1, FM95 |
|
|
1843
|
+
| 102 | Audit checked heading fonts but never per-line alignment | For every multi-line heading also assert: computed `text-align`, `text-indent: 0`, and every line's start-x equal (`Range.selectNodeContents` → `getClientRects()`); inspect the HTML bytes around `<br />` for collapsed spaces | §6.5.4, FM95 |
|
|
1844
|
+
| 103 | Declared "flush" from ONE viewport width | `text-align:left` does not stop a `max-width` block being centred by inherited `margin:auto` — reset `margin-inline: 0`. Verify alignment at the design width, a wider (≥1920) and a narrower viewport, AND against the siblings that share the edge | §6.5.5, §7, FM102 |
|
|
1845
|
+
| 104 | Icon SVG and CSS both drew the ring — and the icon set was inconsistent | Open every icon's SVG source: a baked-in full-size `<rect rx>`/`<circle>` IS the container — the wrapper then adds none. Pick ONE owner of the shape, normalise the whole set, and verify every direction/state renders exactly one ring. `<img>` SVGs ignore CSS `color` | §6.5.4, §5 |
|
|
1846
|
+
| 105 | Mirrored background photo; a nudge broke two panels' shared top edge | Check a directional photo's orientation against the design; flip via an absolutely-positioned `::before` with `transform: scaleX(-1)` (background-image itself can't transform). Panels that share a Figma y have NO vertical offset — delete nudges and assert equal tops | §6.5.4, FM87 |
|
|
1847
|
+
| 106 | Logo strip passed on item sizes while the gaps were 3× too small | A logo row is a repeated-item control: measure every `item[i+1].left − item[i].right` against Figma; size the row container to the Figma row width and use `space-between` | §6.5.5.1, FM95 |
|
|
1848
|
+
| 107 | Transparent PNG padding shrank the mark; opacity and colour ignored | Raster logos: crop to the content bbox (declared box ≠ visible ink — check `naturalWidth`/`getbbox`); apply the node's group `opacity`; match the RENDERED colour, not the stored brand fill (`filter: brightness(0) invert(1)` for a white-unified strip) | §5, FM106 |
|
|
1849
|
+
| 108 | A generic container rule hijacked a nested button's padding AND display | Blanket rules (`.footer a { display:block; padding:5px 0 }`) silently steal a `.btn`'s box and centring. Give fixed-size buttons explicit w×h + their own later equal-specificity rule (`a.the-btn`); NEVER raise the broad selector's specificity (`:not(.btn)` out-ranked and broke sibling rows). Re-verify the whole container after any specificity change | §6.5.5.1, §13, FM91 |
|
|
1850
|
+
| 109 | Sections were vertically CENTRED inside locked min-heights — every y floated with content height | A "lock each section to its Figma frame height" rule paired `min-height` with `display:flex; justify-content:center`, so each section's content sat at (min-height − content)/2. Every content-height change re-shifted every y in the section, offsets oscillated between audit runs, and a dozen nudges were tuned against the drift. Figma sections are TOP-anchored: content y = the section's own padding. Lock heights with `min-height` + `justify-content:flex-start` and set padding-top = the first node's Figma y; never centre a section whose design ys are absolute. If audit offsets CHANGE between runs without related edits, suspect a centring/auto-margin ancestor first | §6.5.5.1, FM103 |
|
|
1851
|
+
| 110 | The measuring viewport's scrollbar shifted every x by a constant and failed the whole audit | The fidelity probe rendered the page at the design width, but the page scrolls, so the layout viewport was designW−15px: every centred/right-anchored element measured a constant ~7-15px left of Figma and the text audit failed wholesale (±4 tolerance < shift). Before ANY x-comparison, assert `document.documentElement.clientWidth === designWidth`; if short, widen the window/iframe by the scrollbar width. A CONSTANT x-offset across every node is a viewport artifact, not a build defect — fix the measurement, not the CSS. (figma_report.py now auto-widens its probe iframe.) | §6.5.5, §18.0 |
|
|
1852
|
+
| 111 | The page had zero overflow at the design width and broke at EVERY narrower one | Below-design-width overflow came from five distinct classes, all invisible at 1600: (a) fixed component widths (`.card { width:440px }`, media `width:910px`) — size components with `width:100%` and let the grid track carry the design px; (b) `repeat(N, 1fr)` tracks — `1fr` = `minmax(auto,1fr)` and blows out on nowrap/min-content children; always write `minmax(0,1fr)`; (c) late-in-file wide-viewport overrides out-cascading earlier narrow breakpoints at equal specificity — scope them with range media (`min-width:X and max-width:Y`); (d) an override written for the wrong display type (`grid-template-columns` on a still-flex element does nothing); (e) removed `flex-wrap` on a pill row. Verify with an overflow sweep at ~8 widths (design, 1440, 1280, 1024, 900, 768, 640, 375): `scrollWidth − clientWidth === 0` per width, and after every fix RE-ASSERT the changed property in the probe before re-measuring | §8, FM92, FM103 |
|
|
1853
|
+
| 112 | The verifier itself had five blind spots that read a correct build as broken (or hid real defects) | Fixed in figma_report.py; the classes recur in ANY audit tooling: (a) text inside a closed `<option>` has a 0×0 rect and, if collected, wins the leaf-most filter over the real `<select>` — skip option-descendant text nodes, match the control by value/aria-label; (b) a Figma box with an IMAGE fill exports its invisible SOLID underlay — never compare that colour to CSS, and treat a CSS background-image, a matched `<img>` itself, or a child img covering ≥80% as "image-covered"; (c) radii must be normalised before comparing: resolve `%` against the element box and clamp BOTH sides to min(w,h)/2 (Figma pill radii like 999/1353 = CSS 50%); (d) near-invisible strokes (opacity/alpha < .5) are not a border requirement, and a design stroke may render as any ONE border side — check all four; (e) the report's own cache-bust pass rewrote `src="x.svg"` to `x.svg?bust`, so its icon selector `src$=".svg"` matched nothing — after ANY self-modification of the page, selectors written against the original markup must be re-checked. Also: naming a new asset without checking for an existing file of that name overwrote an in-use icon (verify `ls` before `cp`) | §18.0, FM110 |
|
|
1854
|
+
| 113 | Hover-timing audit read 0ms everywhere; the entrance reveal was also silently overriding every hover transition | Three coupled findings. (a) The report froze animations (`transition:none!important`) to measure geometry, then read `transitionDuration` — measuring what it had itself disabled (0/26). Duration reads are now QUEUED and flushed after the freeze style is removed, in steady-state (entrance markers stripped, since a hidden probe never fires IntersectionObserver). (b) Wrapper/element/icon often share one box: keep the whole tie-set of candidates and let the duration read take the max — a lone "nearest" pick lands on the wrapper (0s) or the icon `<img>`. (c) REAL defect class: a CSS-transition entrance (`[data-reveal] { transition: opacity .82s, transform .82s }`) out-cascades the element's own hover transition (same `transform` property, higher specificity) — every card hovered at the reveal's 820ms instead of the design's 1022ms. Run entrances via WAAPI (`el.animate(...)`, inline `opacity:0` until shown) so the `transition` property is never occupied and hover timing stays the element's own. Also: this Browser-pane environment freezes the animation clock (rAF dead, `playState:running` with `currentTime` stuck at 0) AND blocks all programmatic scroll — "stuck at opacity 0" and "reveal never fires" there are environment artifacts; verify wiring (attrs stripped, animations created, computed transition durations) instead of watching pixels move | §9, §18.0, FM110 |
|
|
1855
|
+
| 114 | Assumed only the render endpoint was rate-limited; `/v1/files` (node JSON) also 429'd for hours | Figma REST quotas are plan-based and cover EVERY endpoint, not just renders — mid-project, `/v1/files` returned 429 Retry-After ~6h while the fill-URL endpoint still worked. The build survived only because all node JSON had been cached to disk on day one. Rule: the API is for ACQUISITION, the disk cache is the workspace — pull once into `figma/nodes/`, make every script read the cache, make the puller skip anything already on disk, and on any 429 report the Retry-After and continue offline from cache instead of blocking. Never design a step that needs the API on every run | §0.1, §6.5 |
|