canicode 0.9.1 → 0.10.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/README.md +40 -45
- package/dist/cli/index.js +1220 -603
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +308 -2
- package/dist/index.js +275 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +494 -12
- package/dist/mcp/server.js.map +1 -1
- package/package.json +7 -5
- package/skills/canicode/SKILL.md +76 -0
- package/skills/canicode-gotchas/SKILL.md +138 -0
- package/skills/canicode-roundtrip/SKILL.md +367 -0
- package/skills/canicode-roundtrip/helpers.js +263 -0
- package/.claude/skills/design-to-code/PROMPT.md +0 -143
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
var CanICodeRoundtrip = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// src/core/roundtrip/annotations.ts
|
|
5
|
+
function stripAnnotations(annotations) {
|
|
6
|
+
const input = annotations ?? [];
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const a of input) {
|
|
9
|
+
const hasLM = typeof a.labelMarkdown === "string" && a.labelMarkdown.length > 0;
|
|
10
|
+
const hasLabel = typeof a.label === "string" && a.label.length > 0;
|
|
11
|
+
if (!hasLM && !hasLabel) continue;
|
|
12
|
+
const base = hasLM ? { labelMarkdown: a.labelMarkdown } : { label: a.label };
|
|
13
|
+
if (a.categoryId) base.categoryId = a.categoryId;
|
|
14
|
+
if (Array.isArray(a.properties) && a.properties.length > 0) {
|
|
15
|
+
base.properties = a.properties;
|
|
16
|
+
}
|
|
17
|
+
out.push(base);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
async function ensureCanicodeCategories() {
|
|
22
|
+
const api = figma.annotations;
|
|
23
|
+
const existing = await api.getAnnotationCategoriesAsync();
|
|
24
|
+
const byLabel = new Map(existing.map((c) => [c.label, c.id]));
|
|
25
|
+
async function ensure(label, color) {
|
|
26
|
+
const cached = byLabel.get(label);
|
|
27
|
+
if (cached) return cached;
|
|
28
|
+
const created = await api.addAnnotationCategoryAsync({ label, color });
|
|
29
|
+
byLabel.set(label, created.id);
|
|
30
|
+
return created.id;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
gotcha: await ensure("canicode:gotcha", "blue"),
|
|
34
|
+
autoFix: await ensure("canicode:auto-fix", "green"),
|
|
35
|
+
fallback: await ensure("canicode:fallback", "yellow")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function upsertCanicodeAnnotation(node, input) {
|
|
39
|
+
if (!node || !("annotations" in node)) return false;
|
|
40
|
+
const { ruleId, markdown, categoryId, properties } = input;
|
|
41
|
+
const prefix = `**[canicode] ${ruleId}**`;
|
|
42
|
+
const body = markdown.startsWith(prefix) ? markdown : `${prefix}
|
|
43
|
+
|
|
44
|
+
${markdown}`;
|
|
45
|
+
const existing = stripAnnotations(node.annotations);
|
|
46
|
+
const entry = { labelMarkdown: body };
|
|
47
|
+
if (categoryId) entry.categoryId = categoryId;
|
|
48
|
+
if (properties && properties.length > 0) entry.properties = properties;
|
|
49
|
+
const idx = existing.findIndex((a) => {
|
|
50
|
+
const lm = a.labelMarkdown;
|
|
51
|
+
const lb = a.label;
|
|
52
|
+
return typeof lm === "string" && lm.startsWith(prefix) || typeof lb === "string" && lb.startsWith(prefix);
|
|
53
|
+
});
|
|
54
|
+
if (idx >= 0) existing[idx] = entry;
|
|
55
|
+
else existing.push(entry);
|
|
56
|
+
try {
|
|
57
|
+
node.annotations = existing;
|
|
58
|
+
return true;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
const msg = String(e?.message ?? e);
|
|
61
|
+
const isNodeTypeReject = /invalid property .+ for a .+ node/i.test(msg);
|
|
62
|
+
if (!entry.properties || !isNodeTypeReject) throw e;
|
|
63
|
+
delete entry.properties;
|
|
64
|
+
if (idx >= 0) existing[idx] = entry;
|
|
65
|
+
node.annotations = existing;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/core/roundtrip/apply-with-instance-fallback.ts
|
|
71
|
+
var DEFINITION_WRITE_SKIPPED_EVENT = "cic_roundtrip_definition_write_skipped";
|
|
72
|
+
function resolveSourceComponentName(definition, question) {
|
|
73
|
+
if (definition && typeof definition.name === "string" && definition.name) {
|
|
74
|
+
return definition.name;
|
|
75
|
+
}
|
|
76
|
+
const ic = question.instanceContext;
|
|
77
|
+
if (ic && typeof ic.sourceComponentName === "string" && ic.sourceComponentName) {
|
|
78
|
+
return ic.sourceComponentName;
|
|
79
|
+
}
|
|
80
|
+
return "the source component";
|
|
81
|
+
}
|
|
82
|
+
async function routeToDefinitionOrAnnotate(definition, writeFn, ctx) {
|
|
83
|
+
if (definition && !ctx.allowDefinitionWrite && ctx.reason !== "non-override-error") {
|
|
84
|
+
const componentName = resolveSourceComponentName(definition, ctx.question);
|
|
85
|
+
if (ctx.categories) {
|
|
86
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
87
|
+
ruleId: ctx.question.ruleId,
|
|
88
|
+
markdown: `Apply this fix on the source component **${componentName}** to share across all instances. This instance kept its current value to avoid unintended fan-out. Re-run with \`allowDefinitionWrite: true\` to propagate.`,
|
|
89
|
+
categoryId: ctx.categories.fallback
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
ctx.telemetry?.(DEFINITION_WRITE_SKIPPED_EVENT, {
|
|
93
|
+
ruleId: ctx.question.ruleId,
|
|
94
|
+
reason: ctx.reason
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
icon: "\u{1F4DD}",
|
|
98
|
+
label: "definition write skipped (opt-in disabled)"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!definition) {
|
|
102
|
+
if (ctx.categories) {
|
|
103
|
+
const markdown = ctx.reason === "silent-ignore" ? "write accepted but value unchanged; no definition available" : ctx.reason === "override-error" ? `could not apply automatically: ${ctx.errorMessage ?? ""}` : `could not apply automatically: ${ctx.errorMessage ?? ""}`;
|
|
104
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
105
|
+
ruleId: ctx.question.ruleId,
|
|
106
|
+
markdown,
|
|
107
|
+
categoryId: ctx.categories.fallback
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return ctx.reason === "silent-ignore" ? { icon: "\u{1F4DD}", label: "silent-ignore, annotated" } : { icon: "\u{1F4DD}", label: `error: ${ctx.errorMessage ?? ""}` };
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await writeFn(definition);
|
|
114
|
+
return {
|
|
115
|
+
icon: "\u{1F310}",
|
|
116
|
+
label: ctx.reason === "silent-ignore" ? "source definition (silent-ignore fallback)" : "source definition"
|
|
117
|
+
};
|
|
118
|
+
} catch (defErr) {
|
|
119
|
+
const defMsg = String(defErr?.message ?? defErr);
|
|
120
|
+
const isRemoteReadOnly = definition.remote === true || /read-only/i.test(defMsg);
|
|
121
|
+
if (ctx.categories) {
|
|
122
|
+
upsertCanicodeAnnotation(ctx.scene, {
|
|
123
|
+
ruleId: ctx.question.ruleId,
|
|
124
|
+
markdown: isRemoteReadOnly ? "source component lives in an external library and is read-only from this file \u2014 apply the fix in the library file itself." : `could not apply at source definition: ${defMsg}`,
|
|
125
|
+
categoryId: ctx.categories.fallback
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
icon: "\u{1F4DD}",
|
|
130
|
+
label: isRemoteReadOnly ? "external library (read-only)" : `definition error: ${defMsg}`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function applyWithInstanceFallback(question, writeFn, context = {}) {
|
|
135
|
+
const { categories, allowDefinitionWrite = false, telemetry } = context;
|
|
136
|
+
const scene = await figma.getNodeByIdAsync(question.nodeId);
|
|
137
|
+
if (!scene) return { icon: "\u{1F4DD}", label: "missing node" };
|
|
138
|
+
const definition = question.sourceChildId ? await figma.getNodeByIdAsync(question.sourceChildId) : null;
|
|
139
|
+
try {
|
|
140
|
+
const changed = await writeFn(scene);
|
|
141
|
+
if (changed === false) {
|
|
142
|
+
return routeToDefinitionOrAnnotate(definition, writeFn, {
|
|
143
|
+
question,
|
|
144
|
+
scene,
|
|
145
|
+
categories,
|
|
146
|
+
reason: "silent-ignore",
|
|
147
|
+
allowDefinitionWrite,
|
|
148
|
+
telemetry
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { icon: "\u2705", label: "instance/scene" };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = String(e?.message ?? e);
|
|
154
|
+
const looksLikeInstanceOverride = /cannot be overridden/i.test(msg) || /override/i.test(msg);
|
|
155
|
+
if (!looksLikeInstanceOverride) {
|
|
156
|
+
return routeToDefinitionOrAnnotate(null, writeFn, {
|
|
157
|
+
question,
|
|
158
|
+
scene,
|
|
159
|
+
categories,
|
|
160
|
+
reason: "non-override-error",
|
|
161
|
+
errorMessage: msg,
|
|
162
|
+
allowDefinitionWrite,
|
|
163
|
+
telemetry
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return routeToDefinitionOrAnnotate(definition, writeFn, {
|
|
167
|
+
question,
|
|
168
|
+
scene,
|
|
169
|
+
categories,
|
|
170
|
+
reason: "override-error",
|
|
171
|
+
errorMessage: msg,
|
|
172
|
+
allowDefinitionWrite,
|
|
173
|
+
telemetry
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/core/roundtrip/apply-property-mod.ts
|
|
179
|
+
async function resolveVariableByName(name) {
|
|
180
|
+
const locals = await figma.variables.getLocalVariablesAsync();
|
|
181
|
+
return locals.find((v) => v.name === name) ?? null;
|
|
182
|
+
}
|
|
183
|
+
function parseValue(raw) {
|
|
184
|
+
if (raw && typeof raw === "object" && "variable" in raw) {
|
|
185
|
+
const v = raw;
|
|
186
|
+
const parsed = { kind: "binding", name: v.variable };
|
|
187
|
+
if ("fallback" in v) parsed.fallback = v.fallback;
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
if (raw && typeof raw === "object" && "fallback" in raw) {
|
|
191
|
+
return { kind: "scalar", scalar: raw.fallback };
|
|
192
|
+
}
|
|
193
|
+
return { kind: "scalar", scalar: raw };
|
|
194
|
+
}
|
|
195
|
+
function isPaintProp(prop) {
|
|
196
|
+
return prop === "fills" || prop === "strokes";
|
|
197
|
+
}
|
|
198
|
+
function applyPropertyBinding(target, prop, variable) {
|
|
199
|
+
if (isPaintProp(prop)) {
|
|
200
|
+
const current = target[prop];
|
|
201
|
+
if (current === figma.mixed || !Array.isArray(current)) return false;
|
|
202
|
+
const paints = current;
|
|
203
|
+
const bound = paints.map(
|
|
204
|
+
(paint) => figma.variables.setBoundVariableForPaint(paint, "color", variable)
|
|
205
|
+
);
|
|
206
|
+
target[prop] = bound;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
target.setBoundVariable(prop, variable);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
function applyPropertyScalar(target, prop, scalar) {
|
|
213
|
+
const rec = target;
|
|
214
|
+
const before = rec[prop];
|
|
215
|
+
rec[prop] = scalar;
|
|
216
|
+
if (rec[prop] === before && before !== scalar) return false;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
async function applyPropertyMod(question, answerValue, context = {}) {
|
|
220
|
+
const props = Array.isArray(question.targetProperty) ? question.targetProperty : question.targetProperty !== void 0 ? [question.targetProperty] : [];
|
|
221
|
+
return applyWithInstanceFallback(
|
|
222
|
+
question,
|
|
223
|
+
async (target) => {
|
|
224
|
+
if (!target) return void 0;
|
|
225
|
+
let changed = void 0;
|
|
226
|
+
for (const prop of props) {
|
|
227
|
+
if (!(prop in target)) continue;
|
|
228
|
+
const perProp = answerValue && typeof answerValue === "object" && !("variable" in answerValue) && !Array.isArray(answerValue) ? answerValue[prop] : answerValue;
|
|
229
|
+
const parsed = parseValue(perProp);
|
|
230
|
+
if (parsed.kind === "binding") {
|
|
231
|
+
const variable = await resolveVariableByName(parsed.name);
|
|
232
|
+
if (variable) {
|
|
233
|
+
applyPropertyBinding(target, prop, variable);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (parsed.fallback !== void 0) {
|
|
237
|
+
if (!applyPropertyScalar(target, prop, parsed.fallback)) {
|
|
238
|
+
changed = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (parsed.scalar === void 0) continue;
|
|
244
|
+
if (!applyPropertyScalar(target, prop, parsed.scalar)) {
|
|
245
|
+
changed = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return changed;
|
|
249
|
+
},
|
|
250
|
+
context
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
exports.applyPropertyMod = applyPropertyMod;
|
|
255
|
+
exports.applyWithInstanceFallback = applyWithInstanceFallback;
|
|
256
|
+
exports.ensureCanicodeCategories = ensureCanicodeCategories;
|
|
257
|
+
exports.resolveVariableByName = resolveVariableByName;
|
|
258
|
+
exports.stripAnnotations = stripAnnotations;
|
|
259
|
+
exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;
|
|
260
|
+
|
|
261
|
+
return exports;
|
|
262
|
+
|
|
263
|
+
})({});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# Design to Code — Standard Prompt
|
|
2
|
-
|
|
3
|
-
This prompt is used by all code generation pipelines:
|
|
4
|
-
- Calibration Converter
|
|
5
|
-
- Ablation experiments (API-based)
|
|
6
|
-
- User-facing `canicode implement` command (default prompt)
|
|
7
|
-
|
|
8
|
-
## Stack
|
|
9
|
-
- HTML + CSS (single file)
|
|
10
|
-
- No frameworks, no build step
|
|
11
|
-
|
|
12
|
-
## Conventions
|
|
13
|
-
- Semantic HTML elements
|
|
14
|
-
- Flexbox / Grid for layout
|
|
15
|
-
- Always include `* { box-sizing: border-box; margin: 0; padding: 0; }` reset
|
|
16
|
-
|
|
17
|
-
## CRITICAL: Do NOT Interpret. Reproduce Exactly.
|
|
18
|
-
|
|
19
|
-
Every pixel in the Figma file is intentional. A designer made each decision deliberately.
|
|
20
|
-
Your job is to translate the Figma data to HTML+CSS — nothing more.
|
|
21
|
-
|
|
22
|
-
### Priority Order
|
|
23
|
-
1. **Pixel-exact reproduction** — match every dimension, color, spacing, font exactly
|
|
24
|
-
2. **Component reuse** — same component annotation → shared CSS class
|
|
25
|
-
3. **Design tokens** — repeated values → CSS custom properties
|
|
26
|
-
|
|
27
|
-
Never sacrifice #1 for #2 or #3. Reuse and tokens are structural improvements only — they must not change the visual output.
|
|
28
|
-
|
|
29
|
-
### Rules
|
|
30
|
-
- Do NOT add any value that isn't in the Figma data (no extra padding, margin, gap, transition)
|
|
31
|
-
- Do NOT add hover effects unless `[hover]:` data is provided in the design tree
|
|
32
|
-
- Do NOT change any value from the Figma data (if it says 160px padding, use 160px)
|
|
33
|
-
- Do NOT "improve" the design — if something looks wrong, reproduce it anyway
|
|
34
|
-
- Do NOT add responsive behavior unless the Figma data explicitly shows it
|
|
35
|
-
- Do NOT use min-height or min-width unless the design tree explicitly includes them — use exact height and width from the data
|
|
36
|
-
- Do NOT add overflow: auto or scroll unless specified
|
|
37
|
-
- Fonts: load via Google Fonts CDN (`<link>` tag). Do NOT use system font fallbacks as primary — the exact font from the data must render.
|
|
38
|
-
|
|
39
|
-
### Component Reuse
|
|
40
|
-
|
|
41
|
-
Nodes annotated with `[component: ComponentName]` are instances of the same design component.
|
|
42
|
-
|
|
43
|
-
- Define a CSS class for each unique component name (e.g., `[component: Review Card]` → `.review-card { ... }`)
|
|
44
|
-
- If the same component appears multiple times, define the shared styles once in the class, then apply it to each instance
|
|
45
|
-
- `component-properties:` lines show variant overrides — use them to differentiate instances (e.g., different text content, sizes) while keeping shared styles in the class
|
|
46
|
-
- Component name → class name: lowercase, spaces to hyphens (e.g., `Review Card` → `.review-card`)
|
|
47
|
-
- Use CSS classes only — no Web Components, no JavaScript templates
|
|
48
|
-
|
|
49
|
-
### Design Tokens
|
|
50
|
-
|
|
51
|
-
Extract repeated values into CSS custom properties in `:root { }`.
|
|
52
|
-
|
|
53
|
-
**Colors**: When the same hex color appears 3+ times, define it as a CSS variable:
|
|
54
|
-
```css
|
|
55
|
-
:root {
|
|
56
|
-
--color-2C2C2C: #2C2C2C;
|
|
57
|
-
--color-0066CC: #0066CC;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
Then use `var(--color-2C2C2C)` instead of inline `#2C2C2C`.
|
|
61
|
-
|
|
62
|
-
Naming: if a `/* var:... */` comment is present next to a color value, it means the designer bound this color to a design token — always extract these as CSS variables.
|
|
63
|
-
|
|
64
|
-
**Typography**: When `/* text-style: StyleName */` appears in a text node's styles, nodes sharing the same text style name should use a shared CSS class:
|
|
65
|
-
```css
|
|
66
|
-
.text-heading-large { font-family: "Inter"; font-weight: 700; font-size: 32px; line-height: 40px; }
|
|
67
|
-
```
|
|
68
|
-
Style name → class name: lowercase, spaces/slashes to hyphens, prefix with `text-` (e.g., `Heading / Large` → `.text-heading-large`).
|
|
69
|
-
|
|
70
|
-
### SVG Vectors
|
|
71
|
-
|
|
72
|
-
When a node's style includes `svg: <svg>...</svg>`, render it as an inline `<svg>` element:
|
|
73
|
-
- Use the SVG markup exactly as provided — do not modify paths or attributes
|
|
74
|
-
- Preserve the node's dimensions (`width` and `height` from the node header)
|
|
75
|
-
- The `<svg>` replaces the node's HTML element (do not wrap it in an extra `<div>` unless the node has other styles like background or border)
|
|
76
|
-
|
|
77
|
-
### Hover States
|
|
78
|
-
|
|
79
|
-
When a node includes a `[hover]:` line, generate a `:hover` CSS rule with the provided style changes:
|
|
80
|
-
|
|
81
|
-
```text
|
|
82
|
-
Button (INSTANCE, 120x40) [component: Button]
|
|
83
|
-
style: background: #2C2C2C
|
|
84
|
-
[hover]: background: #1E1E1E; Icon: fill: #FFFFFF
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
→ generates:
|
|
88
|
-
|
|
89
|
-
```css
|
|
90
|
-
.button { background: #2C2C2C; }
|
|
91
|
-
.button:hover { background: #1E1E1E; }
|
|
92
|
-
.button:hover .icon { fill: #FFFFFF; }
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
- Only use hover data that is explicitly provided in `[hover]:` — do not invent hover effects
|
|
96
|
-
- Child styles (e.g., `Icon: fill: #FFFFFF`) use `.parent:hover .child` selector pattern
|
|
97
|
-
|
|
98
|
-
### Image Assets
|
|
99
|
-
|
|
100
|
-
The design tree uses two distinct image types:
|
|
101
|
-
|
|
102
|
-
**`content-image:`** — leaf node, no children → render as `<img>` tag
|
|
103
|
-
- `content-image: url(images/...)` → `<img src="images/..." />`
|
|
104
|
-
- `object-fit: cover` or `object-fit: contain` is provided alongside — use it directly
|
|
105
|
-
|
|
106
|
-
**`background-image:`** — node has children on top → keep as CSS `background-image`
|
|
107
|
-
- `background-image: url(images/...)` → `background-image: url(images/...)` in CSS
|
|
108
|
-
- `background-size`, `background-position`, `background-repeat` are provided alongside — use as-is
|
|
109
|
-
|
|
110
|
-
**`[IMAGE]` placeholder** — image asset unavailable → use a placeholder color matching the surrounding design
|
|
111
|
-
|
|
112
|
-
### If data is missing
|
|
113
|
-
When the Figma data does not specify a value, you MUST list it as an interpretation.
|
|
114
|
-
Do not silently guess — always declare what you assumed.
|
|
115
|
-
|
|
116
|
-
## Output
|
|
117
|
-
|
|
118
|
-
### 1. Code
|
|
119
|
-
- Place the entire design inside a single root `<div>` as the first child of `<body>` — do NOT apply design styles directly to `<body>` or `<html>`
|
|
120
|
-
- Output as a code block with filename:
|
|
121
|
-
|
|
122
|
-
```html
|
|
123
|
-
// filename: index.html
|
|
124
|
-
<!DOCTYPE html>
|
|
125
|
-
...
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 2. Interpretations
|
|
129
|
-
After the code block, list every value you had to guess or assume.
|
|
130
|
-
Keep this list to **only genuine ambiguities** — do not list standard defaults (e.g., `body { margin: 0 }` is always expected, not an interpretation).
|
|
131
|
-
**Maximum 10 items.** If you have more than 10, keep only the highest-impact ones.
|
|
132
|
-
|
|
133
|
-
```text
|
|
134
|
-
// interpretations:
|
|
135
|
-
- Used placeholder gray (#CCCCCC) for unavailable image asset
|
|
136
|
-
- Chose "Inter" font weight 500 for ambiguous "Medium" style reference
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
If you did not interpret anything, write:
|
|
140
|
-
|
|
141
|
-
```text
|
|
142
|
-
// interpretations: none
|
|
143
|
-
```
|