figma-code-agent 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.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. package/skills/ref-tokens/SKILL.md +347 -0
@@ -0,0 +1,855 @@
1
+ # Vector Container Detection & Asset Management
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for detecting which Figma nodes should be exported as image assets (SVG, PNG, JPG) versus rendered as CSS, and for managing the full asset pipeline: vector container detection, CSS-renderable shape identification, image fill handling, asset deduplication, and export format selection. Encodes production-proven heuristics that prevent attempting to render complex vector geometry as CSS divs while avoiding unnecessary asset exports for simple shapes that CSS handles natively.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Determine if a Figma frame/group should be exported as a single SVG (vector container detection)
12
+ - Decide whether a shape node (ELLIPSE, RECTANGLE, VECTOR, etc.) should be CSS or SVG
13
+ - Handle nodes with IMAGE fills (photos, textures, backgrounds)
14
+ - Map Figma `scaleMode` (FILL, FIT, CROP, TILE) to CSS `object-fit` / `background-size`
15
+ - Build an asset map for deduplication based on `imageHash`
16
+ - Select the correct export format (SVG, PNG@2x, JPG) for different asset types
17
+ - Understand the interaction between Auto Layout containers and vector container detection
18
+ - Handle edge cases like BOOLEAN_OPERATION nodes, mixed content containers, and icon detection
19
+ - Generate `<img>` tags with correct `src` and `alt` attributes from asset data
20
+
21
+ ---
22
+
23
+ ## Content
24
+
25
+ ### 1. Vector Container Detection (Critical Heuristic)
26
+
27
+ The most important decision in asset management is determining which containers should be exported as a single SVG unit versus traversed as HTML/CSS. A **vector container** is a frame or group whose visible children are all vector-compatible types and that contains at least one "true" vector child (not just simple CSS shapes).
28
+
29
+ #### Why This Matters
30
+
31
+ Without vector container detection:
32
+ - An icon made of 5 VECTOR paths would produce 5 individual `<div>` elements (broken)
33
+ - A logo with BOOLEAN_OPERATION children would attempt CSS rendering of complex geometry (impossible)
34
+ - An illustration frame would generate dozens of meaningless positioned divs
35
+
36
+ With vector container detection:
37
+ - The entire icon frame is exported as a single SVG file
38
+ - The logo renders as one `<img src="logo.svg">` element
39
+ - The illustration becomes a clean image reference
40
+
41
+ #### Vector-Compatible Types
42
+
43
+ These node types can exist inside a vector container:
44
+
45
+ ```typescript
46
+ const VECTOR_COMPATIBLE_TYPES = new Set([
47
+ 'VECTOR', // Path-based vector shape
48
+ 'BOOLEAN_OPERATION', // Union, Intersect, Subtract, Exclude of vectors
49
+ 'STAR', // Star polygon
50
+ 'POLYGON', // Regular polygon
51
+ 'LINE', // Line segment
52
+ 'ELLIPSE', // Circle or ellipse
53
+ 'RECTANGLE', // Rectangle (may have rounded corners)
54
+ ])
55
+ ```
56
+
57
+ #### Container Types
58
+
59
+ These node types can hold vector children:
60
+
61
+ ```typescript
62
+ const CONTAINER_TYPES = new Set([
63
+ 'GROUP', // Figma group (no Auto Layout)
64
+ 'FRAME', // Figma frame (may have Auto Layout)
65
+ 'COMPONENT', // Component definition
66
+ 'INSTANCE', // Component instance
67
+ ])
68
+ ```
69
+
70
+ #### Detection Algorithm
71
+
72
+ The detection is a multi-step process with several guard rails:
73
+
74
+ ```
75
+ isVectorContainer(node):
76
+ 1. Must be a container type (GROUP, FRAME, COMPONENT, INSTANCE)
77
+ 2. Must have children
78
+ 3. Must have at least one visible child
79
+ 4. Must have at least one "true" vector child:
80
+ - VECTOR, BOOLEAN_OPERATION, STAR, POLYGON, or LINE
81
+ - (ELLIPSE and RECTANGLE alone do NOT qualify -- they're CSS-renderable)
82
+ 5. ALL visible children must be vector-compatible
83
+ 6. If ANY child is not vector-compatible → NOT a vector container
84
+ ```
85
+
86
+ **Key insight:** A frame containing only RECTANGLE and ELLIPSE children is NOT a vector container. Those shapes are CSS-renderable. The container must have at least one type that requires SVG (VECTOR, BOOLEAN_OPERATION, STAR, POLYGON, LINE).
87
+
88
+ #### Implementation
89
+
90
+ ```typescript
91
+ function isVectorContainer(node: SceneNode): boolean {
92
+ // Must be a container type with children
93
+ if (!CONTAINER_TYPES.has(node.type) || !('children' in node)) return false
94
+
95
+ const visibleChildren = (node as ChildrenMixin & SceneNode)
96
+ .children.filter(child => child.visible)
97
+
98
+ // Must have at least one visible child
99
+ if (visibleChildren.length === 0) return false
100
+
101
+ // Must have at least one TRUE vector child (not just CSS shapes)
102
+ const hasVectorChildren = visibleChildren.some(child =>
103
+ child.type === 'VECTOR' ||
104
+ child.type === 'BOOLEAN_OPERATION' ||
105
+ child.type === 'STAR' ||
106
+ child.type === 'POLYGON' ||
107
+ child.type === 'LINE'
108
+ )
109
+
110
+ if (!hasVectorChildren) return false
111
+
112
+ // ALL children must be vector-compatible
113
+ return visibleChildren.every(child => isVectorCompatible(child))
114
+ }
115
+ ```
116
+
117
+ #### Recursive Vector Compatibility
118
+
119
+ A container child (GROUP, FRAME) is vector-compatible if ALL of its own children are recursively vector-compatible:
120
+
121
+ ```typescript
122
+ function isVectorCompatible(node: SceneNode): boolean {
123
+ // Direct vector types are always compatible
124
+ if (VECTOR_COMPATIBLE_TYPES.has(node.type)) return true
125
+
126
+ // Container types: check all children recursively
127
+ if (CONTAINER_TYPES.has(node.type) && 'children' in node) {
128
+ const container = node as ChildrenMixin & SceneNode
129
+ if (container.children.length === 0) return false // Empty = not vector
130
+ return container.children
131
+ .filter(child => child.visible)
132
+ .every(child => isVectorCompatible(child))
133
+ }
134
+
135
+ return false
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ### 2. CSS-Renderable Shapes vs SVG Export
142
+
143
+ Not every shape node needs to be exported as an image. Simple geometric shapes can be rendered more efficiently as CSS.
144
+
145
+ #### Decision Matrix
146
+
147
+ | Node Type | CSS-Renderable? | SVG Export? | CSS Technique |
148
+ |-----------|----------------|-------------|---------------|
149
+ | `RECTANGLE` | Yes | Only if complex fills | `<div>` with `border-radius`, `background-color` |
150
+ | `ELLIPSE` | Yes (if circle) | If non-circular | `border-radius: 50%` on equal width/height |
151
+ | `VECTOR` | No | Always | Complex path geometry |
152
+ | `BOOLEAN_OPERATION` | No | Always | Union/Intersect/Subtract/Exclude results |
153
+ | `STAR` | No | Always | Multi-point star polygon |
154
+ | `POLYGON` | No | Always | Regular polygon (triangle, hexagon, etc.) |
155
+ | `LINE` | Partial | Usually | `border-top` or `<hr>` for simple lines |
156
+
157
+ #### Decision Tree (Pseudocode)
158
+
159
+ ```
160
+ decideRendering(node):
161
+ IF node is TEXT:
162
+ → Render as HTML text (NEVER export as image)
163
+ → Exception: text with IMAGE fill = text with background image
164
+
165
+ IF node is a vector container (Section 1):
166
+ → Export entire container as single SVG
167
+
168
+ IF node.type is VECTOR, BOOLEAN_OPERATION, STAR, POLYGON:
169
+ → Export as SVG (single node)
170
+
171
+ IF node.type is LINE:
172
+ → IF simple horizontal/vertical line with solid stroke:
173
+ → CSS border or `<hr>`
174
+ → ELSE: Export as SVG
175
+
176
+ IF node.type is ELLIPSE:
177
+ → IF width === height (circle) AND solid fill only:
178
+ → CSS `border-radius: 50%`
179
+ → ELSE: Export as SVG (ellipse or complex fills)
180
+
181
+ IF node.type is RECTANGLE:
182
+ → IF solid/gradient fill only:
183
+ → CSS `<div>` with background and border-radius
184
+ → IF has IMAGE fill:
185
+ → Render as `<img>` or `background-image`
186
+
187
+ IF node is FRAME/GROUP with children:
188
+ → Traverse children recursively (layout container, not asset)
189
+ ```
190
+
191
+ #### Circle Detection
192
+
193
+ ```typescript
194
+ if (node.type === 'ELLIPSE') {
195
+ if (Math.abs(node.bounds.width - node.bounds.height) < 1) {
196
+ styles.borderRadius = '50%' // Circle
197
+ }
198
+ // Non-circular ellipses need SVG
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ### 3. SVG Export via Figma API
205
+
206
+ When a node is identified for SVG export, the actual export happens via the Figma REST API image export endpoint or the Plugin API's `exportAsync`.
207
+
208
+ #### REST API Export
209
+
210
+ ```
211
+ GET /v1/files/:file_key/images?ids=NODE_ID&format=svg
212
+ ```
213
+
214
+ **Parameters:**
215
+ - `ids` -- Comma-separated node IDs (URL-encoded, colons as `%3A`)
216
+ - `format` -- `svg`, `png`, `jpg`, or `pdf`
217
+ - `svg_include_id` -- Include Figma node IDs in SVG (default: false)
218
+ - `svg_include_node_id` -- Include `data-node-id` attributes (default: false)
219
+ - `svg_simplify_stroke` -- Simplify stroke geometry (default: true)
220
+
221
+ **Response:**
222
+
223
+ ```json
224
+ {
225
+ "err": null,
226
+ "images": {
227
+ "1:23": "https://figma-alpha-api.s3.us-west-2.amazonaws.com/images/..."
228
+ }
229
+ }
230
+ ```
231
+
232
+ The response contains temporary URLs to the generated SVG files. Download these immediately as URLs expire.
233
+
234
+ #### Plugin API Export
235
+
236
+ Within a Figma plugin, use `exportAsync`:
237
+
238
+ ```typescript
239
+ const svgBuffer = await node.exportAsync({
240
+ format: 'SVG',
241
+ svgIdAttribute: false,
242
+ })
243
+ const svgString = String.fromCharCode(...new Uint8Array(svgBuffer))
244
+ ```
245
+
246
+ #### SVG Cleanup
247
+
248
+ Exported SVGs from Figma often contain unnecessary metadata. Consider cleaning:
249
+
250
+ - Remove `xmlns:xlink` if no xlink references
251
+ - Remove empty `<defs>` blocks
252
+ - Remove Figma-specific metadata comments
253
+ - Optimize with SVGO (external tool) for production
254
+ - Preserve `viewBox` attribute (critical for correct scaling)
255
+ - Keep `fill` and `stroke` attributes that define the visual appearance
256
+
257
+ #### Inline SVG vs External File
258
+
259
+ | Approach | Use When | Advantages |
260
+ |----------|----------|------------|
261
+ | Inline `<svg>` | Icons, small graphics, need CSS styling | Styleable via CSS, no extra request, can change colors |
262
+ | External `<img src="icon.svg">` | Illustrations, logos, larger graphics | Cacheable, cleaner HTML, simpler code |
263
+ | CSS `background-image: url(icon.svg)` | Decorative, repeated patterns | No HTML impact, easy positioning |
264
+
265
+ **General rule:** Icons < 2KB inline, everything else external.
266
+
267
+ ---
268
+
269
+ ### 4. Image Fill Handling
270
+
271
+ Nodes with IMAGE fills contain raster image data referenced by an `imageHash`. These need to be exported and referenced correctly.
272
+
273
+ #### Detection
274
+
275
+ ```typescript
276
+ if ('fills' in node) {
277
+ const fills = node.fills
278
+ if (fills !== figma.mixed && Array.isArray(fills)) {
279
+ for (const fill of fills) {
280
+ if (fill.type === 'IMAGE' && fill.imageHash) {
281
+ asset.imageHash = fill.imageHash
282
+ asset.exportAs = 'PNG' // Default for image fills
283
+ break // Take first image fill
284
+ }
285
+ }
286
+ }
287
+ }
288
+ ```
289
+
290
+ #### scaleMode to CSS Mapping
291
+
292
+ Figma's `scaleMode` on IMAGE fills controls how the image is sized within its container. This maps directly to CSS properties.
293
+
294
+ | Figma `scaleMode` | CSS `object-fit` | CSS `background-size` | Behavior |
295
+ |-------------------|------------------|----------------------|----------|
296
+ | `FILL` | `cover` | `cover` | Image covers entire container, may crop |
297
+ | `FIT` | `contain` | `contain` | Image fits inside container, may letterbox |
298
+ | `CROP` | `cover` + `object-position` | `cover` + `background-position` | Cropped to specific region |
299
+ | `TILE` | N/A | `repeat` | Image tiles to fill the container |
300
+
301
+ #### CSS Generation for Image Fills
302
+
303
+ **As `<img>` element** (node without children):
304
+
305
+ ```css
306
+ .hero-image {
307
+ width: 400px;
308
+ height: 300px;
309
+ }
310
+
311
+ .hero-image img {
312
+ width: 100%;
313
+ height: 100%;
314
+ object-fit: cover; /* scaleMode: FILL */
315
+ }
316
+ ```
317
+
318
+ **As `background-image`** (node with children -- image is behind child content):
319
+
320
+ ```css
321
+ .hero-section {
322
+ background-image: url('./assets/hero-bg.png');
323
+ background-size: cover; /* scaleMode: FILL */
324
+ background-position: center;
325
+ background-repeat: no-repeat;
326
+ }
327
+ ```
328
+
329
+ #### CROP Mode with Position
330
+
331
+ When `scaleMode` is `CROP`, the image has specific crop bounds. Map these to `object-position`:
332
+
333
+ ```css
334
+ .cropped-image img {
335
+ object-fit: cover;
336
+ object-position: 30% 20%; /* Based on crop offset */
337
+ }
338
+ ```
339
+
340
+ #### TILE Mode
341
+
342
+ ```css
343
+ .tiled-background {
344
+ background-image: url('./assets/pattern.png');
345
+ background-size: auto; /* Natural image size */
346
+ background-repeat: repeat; /* scaleMode: TILE */
347
+ }
348
+ ```
349
+
350
+ #### Retina Export
351
+
352
+ Always export image fills at **2x scale** for crisp display on high-DPI screens:
353
+
354
+ ```
355
+ GET /v1/files/:key/images?ids=NODE_ID&format=png&scale=2
356
+ ```
357
+
358
+ Or via Plugin API:
359
+
360
+ ```typescript
361
+ const pngBuffer = await node.exportAsync({
362
+ format: 'PNG',
363
+ constraint: { type: 'SCALE', value: 2 },
364
+ })
365
+ ```
366
+
367
+ #### Nodes WITH Children vs WITHOUT Children
368
+
369
+ The rendering strategy differs based on whether the node has visible children:
370
+
371
+ | Has Children? | Image Fill Rendering | HTML Output |
372
+ |---------------|---------------------|-------------|
373
+ | No | `<img>` element with `src` attribute | `<img src="./assets/photo.png" alt="Photo">` |
374
+ | Yes | CSS `background-image` on the container | `<div style="background-image: url(...)">...children...</div>` |
375
+
376
+ A frame with an image fill AND text children is a common pattern for hero sections: the image is the background, and the text sits on top.
377
+
378
+ ---
379
+
380
+ ### 5. Asset Map & Deduplication
381
+
382
+ When multiple nodes reference the same image (e.g., a profile photo used in several cards), the asset should be downloaded and stored only once.
383
+
384
+ #### Hash-Based Deduplication
385
+
386
+ Figma provides an `imageHash` for each IMAGE fill. This hash uniquely identifies the image content regardless of which node uses it.
387
+
388
+ ```typescript
389
+ // Build asset map during extraction
390
+ const assetMap = new Map<string, { filename: string; url: string }>()
391
+
392
+ function registerAsset(nodeId: string, imageHash: string, nodeName: string): string {
393
+ // Check if we already have this image
394
+ if (assetMap.has(imageHash)) {
395
+ return assetMap.get(imageHash)!.filename
396
+ }
397
+
398
+ // New image - create entry
399
+ const filename = sanitizeFilename(nodeName)
400
+ assetMap.set(imageHash, { filename, url: '' }) // URL filled later
401
+ return filename
402
+ }
403
+ ```
404
+
405
+ #### Filename Sanitization
406
+
407
+ Node names from Figma may contain characters invalid for filenames:
408
+
409
+ ```typescript
410
+ function sanitizeFilename(name: string): string {
411
+ return name
412
+ .toLowerCase()
413
+ .replace(/\s+/g, '-') // Spaces to hyphens
414
+ .replace(/[^a-z0-9-_]/g, '') // Remove special characters
415
+ .replace(/-+/g, '-') // Collapse multiple hyphens
416
+ .replace(/^-|-$/g, '') // Trim leading/trailing hyphens
417
+ || 'asset' // Fallback for empty names
418
+ }
419
+ ```
420
+
421
+ **Examples:**
422
+ - `"Hero Image"` -> `hero-image`
423
+ - `"icon/arrow-right"` -> `iconarrow-right`
424
+ - `"Photo (2)"` -> `photo-2`
425
+ - `""` -> `asset`
426
+
427
+ #### Asset Map in HTML Generation
428
+
429
+ During HTML generation, the asset map provides `src` paths for `<img>` elements:
430
+
431
+ ```typescript
432
+ if (tag === 'img' && node.asset && node.asset.exportAs && options.assetMap) {
433
+ const filename = options.assetMap.get(node.id)
434
+ if (filename) {
435
+ attributes.src = `./assets/${filename}`
436
+ attributes.alt = node.name || ''
437
+ }
438
+ }
439
+ ```
440
+
441
+ Generated HTML:
442
+
443
+ ```html
444
+ <img class="card__photo" src="./assets/hero-image.png" alt="Hero Image">
445
+ <img class="card__icon" src="./assets/arrow-right.svg" alt="arrow-right">
446
+ ```
447
+
448
+ ---
449
+
450
+ ### 6. Multi-Factor Detection Heuristics
451
+
452
+ Beyond the basic vector container check, several nuanced cases require additional heuristics.
453
+
454
+ #### Auto Layout Overrides Vector Detection
455
+
456
+ **Rule:** A frame with Auto Layout (`layoutMode !== 'NONE'`) is **ALWAYS** a layout container, never a vector container, regardless of its children.
457
+
458
+ **Rationale:** Auto Layout implies structural/layout intent. A designer who enables Auto Layout is building a layout, not an illustration.
459
+
460
+ ```typescript
461
+ function hasAutoLayout(node: SceneNode): boolean {
462
+ if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
463
+ return (node as FrameNode).layoutMode !== 'NONE'
464
+ }
465
+ return false
466
+ }
467
+
468
+ // In getContainerExportStrategy:
469
+ if (hasAutoLayout(node)) {
470
+ return 'none' // Always layout, never asset
471
+ }
472
+ ```
473
+
474
+ #### Text Children Limit
475
+
476
+ A container with more than one TEXT child is a layout container, not a vector illustration. A single TEXT child is allowed (e.g., an icon with a label that gets flattened into the SVG).
477
+
478
+ ```typescript
479
+ const textChildren = visibleChildren.filter(child => child.type === 'TEXT')
480
+ if (textChildren.length > 1) {
481
+ return 'none' // Multiple text = layout container
482
+ }
483
+ ```
484
+
485
+ When a single TEXT child is present, the text gets flattened before SVG export. The `hasTextToFlatten` flag is set on the AssetData:
486
+
487
+ ```typescript
488
+ interface AssetData {
489
+ // ...
490
+ isVectorContainer?: boolean
491
+ hasTextToFlatten?: boolean // True if container has text to flatten pre-export
492
+ }
493
+ ```
494
+
495
+ #### Container Export Strategy
496
+
497
+ The full detection algorithm combines all heuristics into a three-way decision:
498
+
499
+ ```
500
+ getContainerExportStrategy(node) → 'svg' | 'png' | 'none'
501
+
502
+ 1. Not a container type? → 'none'
503
+ 2. Has Auto Layout? → 'none' (always layout)
504
+ 3. No visible children? → 'none'
505
+ 4. No true vector children? → 'none' (just CSS shapes)
506
+ 5. Multiple TEXT children? → 'none' (layout, not illustration)
507
+ 6. Any non-vector-compatible child? → 'none' (mixed content)
508
+ 7. Has IMAGE fills in children? → 'png' (rasterize vectors + images)
509
+ 8. All checks pass → 'svg' (pure vector content)
510
+ ```
511
+
512
+ #### Simple Shapes: CSS, Not Assets
513
+
514
+ | Scenario | Rendering |
515
+ |----------|-----------|
516
+ | Single RECTANGLE with solid fill | CSS `<div>` with `background-color` and `border-radius` |
517
+ | Single ELLIPSE with equal width/height | CSS `<div>` with `border-radius: 50%` |
518
+ | Frame with only RECTANGLE + ELLIPSE children | CSS (no true vectors present) |
519
+ | RECTANGLE with IMAGE fill | `<img>` element (the image is the content) |
520
+
521
+ #### Complex Shapes: Always SVG
522
+
523
+ | Scenario | Rendering |
524
+ |----------|-----------|
525
+ | Group of VECTOR paths forming an icon | Single SVG asset |
526
+ | BOOLEAN_OPERATION (any sub-type) | SVG (complex geometry) |
527
+ | STAR node | SVG (multi-point polygon) |
528
+ | Frame with VECTOR + ELLIPSE children | SVG (has true vector content) |
529
+ | INSTANCE of a vector component | SVG (export the instance) |
530
+
531
+ #### Icon Detection Heuristic
532
+
533
+ Small vector containers (typically <= 48x48 pixels) are likely icons:
534
+
535
+ ```typescript
536
+ function isLikelyIcon(node: SceneNode): boolean {
537
+ if (node.width <= 48 && node.height <= 48) {
538
+ if (node.type === 'VECTOR' || node.type === 'BOOLEAN_OPERATION') {
539
+ return true
540
+ }
541
+ if ('children' in node) {
542
+ return (node as ChildrenMixin & SceneNode).children.every(child =>
543
+ child.type === 'VECTOR' ||
544
+ child.type === 'BOOLEAN_OPERATION' ||
545
+ child.type === 'ELLIPSE' ||
546
+ child.type === 'RECTANGLE' ||
547
+ child.type === 'LINE'
548
+ )
549
+ }
550
+ }
551
+ return false
552
+ }
553
+ ```
554
+
555
+ Icons are always exported as SVG for crisp scaling at any size.
556
+
557
+ ---
558
+
559
+ ### 7. Export Format Selection
560
+
561
+ Different asset types require different formats for optimal quality and file size.
562
+
563
+ #### Format Decision Matrix
564
+
565
+ | Content Type | Format | Scale | Rationale |
566
+ |-------------|--------|-------|-----------|
567
+ | Vector icons | SVG | N/A (scalable) | Infinitely scalable, small file size, CSS-styleable |
568
+ | Vector illustrations | SVG | N/A | Scalable, preserves sharp edges |
569
+ | Photos | PNG@2x | 2x | Lossless, crisp on retina displays |
570
+ | Large photos | JPG@2x | 2x | Lossy but much smaller file size |
571
+ | Complex gradients | PNG@2x | 2x | Preserves gradient fidelity |
572
+ | Icons with image fills | PNG@2x | 2x | Can't be SVG if contains raster data |
573
+ | Print assets | PDF | N/A | Rare in web context |
574
+
575
+ #### Format Selection Logic
576
+
577
+ ```typescript
578
+ function selectExportFormat(node: SceneNode, asset: AssetData): 'SVG' | 'PNG' | 'JPG' {
579
+ // Vectors always export as SVG
580
+ if (asset.isVector && !asset.imageHash) {
581
+ return 'SVG'
582
+ }
583
+
584
+ // Vector containers with no image content → SVG
585
+ if (asset.isVectorContainer && !hasImageContent(node)) {
586
+ return 'SVG'
587
+ }
588
+
589
+ // Image fills default to PNG
590
+ if (asset.imageHash) {
591
+ return 'PNG'
592
+ }
593
+
594
+ // Fallback
595
+ return 'PNG'
596
+ }
597
+ ```
598
+
599
+ #### Figma Export Settings Override
600
+
601
+ When a designer has explicitly set export settings on a node in Figma, those settings take priority (for nodes without children):
602
+
603
+ ```typescript
604
+ if ('exportSettings' in node && node.exportSettings.length > 0) {
605
+ if (!hasChildren) {
606
+ const format = node.exportSettings[0].format // 'SVG', 'PNG', 'JPG'
607
+ asset.exportAs = format
608
+ }
609
+ }
610
+ ```
611
+
612
+ **Exception:** Nodes WITH children ignore export settings for format selection. Their image fills become CSS `background-image` instead of `<img>` exports.
613
+
614
+ #### SVG for Vectors
615
+
616
+ **Always use SVG for:**
617
+ - Icons (typically < 48x48)
618
+ - Logos and wordmarks
619
+ - Geometric illustrations
620
+ - UI decorations (arrows, chevrons, checkmarks)
621
+ - Any VECTOR, BOOLEAN_OPERATION, STAR, POLYGON, LINE node
622
+
623
+ **SVG advantages:**
624
+ - Infinitely scalable without quality loss
625
+ - Typically tiny file size (< 5KB for icons)
626
+ - Can be styled with CSS (fill, stroke colors)
627
+ - Can be inlined for zero-request rendering
628
+ - Accessible (can include `<title>` and `<desc>`)
629
+
630
+ #### PNG for Raster Content
631
+
632
+ **Use PNG@2x for:**
633
+ - Photos and photographic imagery
634
+ - Complex imagery with transparency
635
+ - Screenshots or UI captures
636
+ - Anything with IMAGE fills
637
+
638
+ **2x scale rationale:** Modern displays (Retina, 4K) render at 2x or higher device pixel ratios. Exporting at 2x ensures crisp display:
639
+
640
+ ```css
641
+ /* Image is 800x600 actual pixels, displayed at 400x300 CSS pixels */
642
+ .photo {
643
+ width: 400px;
644
+ height: 300px;
645
+ }
646
+ .photo img {
647
+ width: 100%;
648
+ height: 100%;
649
+ }
650
+ ```
651
+
652
+ #### JPG for Photos (Size Optimization)
653
+
654
+ When file size matters more than pixel-perfect quality (e.g., large hero images, photo galleries), JPG offers significant compression:
655
+
656
+ ```
657
+ GET /v1/files/:key/images?ids=NODE_ID&format=jpg&scale=2
658
+ ```
659
+
660
+ JPG should only be used when:
661
+ - The image is photographic (no sharp edges or text)
662
+ - No transparency is needed
663
+ - File size reduction is a priority
664
+
665
+ ---
666
+
667
+ ### 8. Common Pitfalls
668
+
669
+ #### Pitfall: Exporting Every Node as an Image
670
+
671
+ **Problem:** Treating all visual nodes as assets, generating hundreds of unnecessary image files for simple rectangles, circles, and solid-fill elements.
672
+
673
+ **Rule:** Only export nodes that **cannot** be rendered as CSS. The vast majority of UI elements (rectangles, circles, containers with fills, gradients) are CSS-renderable. Only export:
674
+ - VECTOR, BOOLEAN_OPERATION, STAR, POLYGON (complex geometry)
675
+ - Nodes with IMAGE fills (raster content)
676
+ - Vector containers (groups/frames of vectors)
677
+
678
+ #### Pitfall: BOOLEAN_OPERATION Is Always SVG
679
+
680
+ **Problem:** Attempting to render a BOOLEAN_OPERATION as CSS. These nodes represent the union, intersection, subtraction, or exclusion of two or more shapes -- the resulting geometry cannot be expressed in CSS.
681
+
682
+ **Rule:** BOOLEAN_OPERATION nodes have a `booleanOperation` property indicating the operation type (UNION, INTERSECT, SUBTRACT, EXCLUDE). Regardless of the operation, always export as SVG.
683
+
684
+ ```typescript
685
+ if (node.type === 'BOOLEAN_OPERATION') {
686
+ asset.isVector = true
687
+ asset.exportAs = 'SVG'
688
+ }
689
+ ```
690
+
691
+ #### Pitfall: Image Fills on Text Nodes
692
+
693
+ **Problem:** Treating a text node with an IMAGE fill as an image asset, losing the text content.
694
+
695
+ **Rule:** An IMAGE fill on a TEXT node means the text has a **background image** or **clipping mask**, not that the text should be replaced with an image. The text must remain as HTML text for accessibility and SEO. The image fill can be rendered as a CSS `background-image` with `background-clip: text` for a clipped text effect, or ignored if it's decorative.
696
+
697
+ #### Pitfall: Vector Nodes Have Fills and Strokes
698
+
699
+ **Problem:** Trying to extract CSS `background-color` or `border` from VECTOR node fills/strokes.
700
+
701
+ **Rule:** VECTOR node fills and strokes define the SVG path's appearance. These become SVG `fill` and `stroke` attributes in the exported SVG, not CSS properties. Do not apply CSS visual property extraction to nodes marked for SVG export.
702
+
703
+ ```typescript
704
+ // In HTML generation:
705
+ const isExportedAsset = (node.asset && node.asset.exportAs && tag === 'img') ||
706
+ node.asset?.isVectorContainer
707
+
708
+ if (!isExportedAsset) {
709
+ // Only apply visual styles (background, border, etc.) to non-asset nodes
710
+ Object.assign(styles, generateVisualStyles(...))
711
+ }
712
+ ```
713
+
714
+ #### Pitfall: Missing Retina Export
715
+
716
+ **Problem:** Exporting images at 1x scale, producing blurry results on Retina/HiDPI displays.
717
+
718
+ **Rule:** Always export raster images at **2x minimum**. This applies to both the REST API (`scale=2`) and Plugin API (`constraint: { type: 'SCALE', value: 2 }`). The CSS should display the image at half the exported pixel dimensions.
719
+
720
+ #### Pitfall: Large SVG File Size
721
+
722
+ **Problem:** A vector container with many children produces an SVG with thousands of path elements, resulting in a large file that's slower to render than a raster image.
723
+
724
+ **Rule:** Consider PNG fallback for vector containers with excessive complexity. Signs that SVG may be too large:
725
+ - Container has > 50 visible children
726
+ - The SVG output exceeds 50KB
727
+ - The content is an illustration with many overlapping gradients
728
+
729
+ In these cases, PNG@2x may be a better choice for web performance.
730
+
731
+ #### Pitfall: Empty Containers as Vector Containers
732
+
733
+ **Problem:** An empty GROUP or FRAME with no children passes the "all children are vector-compatible" check vacuously (all zero of them are compatible).
734
+
735
+ **Rule:** The detection explicitly checks for at least one visible child:
736
+
737
+ ```typescript
738
+ if (visibleChildren.length === 0) return false
739
+ ```
740
+
741
+ #### Pitfall: Confusing Export Settings with Image Fills
742
+
743
+ **Problem:** A node has export settings (configured in Figma's export panel) AND image fills. The export settings might say "SVG" but the node has a raster image fill.
744
+
745
+ **Rule:** Export settings on nodes WITHOUT children drive the export format. Export settings on nodes WITH children are informational only -- the image fill renders as `background-image` regardless. When a node has both export settings and an image fill:
746
+
747
+ ```typescript
748
+ if (asset.imageHash) {
749
+ // Image fill always exports as PNG, overriding SVG export settings
750
+ if (!asset.exportAs) {
751
+ asset.exportAs = 'PNG'
752
+ }
753
+ }
754
+ ```
755
+
756
+ #### Edge Case: INSTANCE of a Vector Component
757
+
758
+ When an INSTANCE node references a component that is a vector container, the instance itself should be exported as SVG. The detection checks the instance's own children (which mirror the component's structure), not the component definition.
759
+
760
+ #### Edge Case: Visible vs Hidden Children
761
+
762
+ Only **visible** children are considered in vector container detection. Hidden children (nodes with `visible: false`) are filtered out:
763
+
764
+ ```typescript
765
+ const visibleChildren = containerNode.children.filter(child => child.visible)
766
+ ```
767
+
768
+ This prevents a hidden TEXT layer inside an icon frame from disqualifying the frame as a vector container.
769
+
770
+ ---
771
+
772
+ ### Intermediate Type Reference
773
+
774
+ The complete intermediate type used for asset data between extraction and generation:
775
+
776
+ ```typescript
777
+ interface AssetData {
778
+ /** Export format: 'SVG', 'PNG', or 'JPG' */
779
+ exportAs?: 'SVG' | 'PNG' | 'JPG'
780
+ /** Image hash for IMAGE fill deduplication */
781
+ imageHash?: string
782
+ /** True if this is a vector type (VECTOR, STAR, POLYGON, BOOLEAN_OPERATION, ELLIPSE, LINE) */
783
+ isVector?: boolean
784
+ /** True if the node has export settings configured in Figma */
785
+ hasExportSettings?: boolean
786
+ /** True if this is a container to export as a single unit (SVG/PNG) */
787
+ isVectorContainer?: boolean
788
+ /** True if container has text that needs flattening before SVG export */
789
+ hasTextToFlatten?: boolean
790
+ }
791
+ ```
792
+
793
+ **Note on `isVector` for ELLIPSE and LINE:** These types are marked as `isVector: true` but do NOT trigger the `exportAs` flag by themselves. They are "potential assets" -- the layout engine decides whether to CSS-render them or include them in a parent vector container's SVG export.
794
+
795
+ ```typescript
796
+ // ELLIPSE and LINE: marked as vector but not auto-exported
797
+ if (node.type === 'ELLIPSE' || node.type === 'LINE') {
798
+ asset.isVector = true
799
+ // No asset.exportAs set -- decision deferred to layout phase
800
+ }
801
+
802
+ // True vectors: marked AND set for SVG export
803
+ if (node.type === 'VECTOR' || node.type === 'STAR' ||
804
+ node.type === 'POLYGON' || node.type === 'BOOLEAN_OPERATION') {
805
+ asset.isVector = true
806
+ asset.exportAs = 'SVG' // Definite SVG export
807
+ }
808
+ ```
809
+
810
+ ---
811
+
812
+ ### Asset Rendering in HTML
813
+
814
+ Vector containers render as self-closing `<img>` tags with no children, because their visual children are baked into the exported SVG/PNG:
815
+
816
+ ```typescript
817
+ // In the traversal phase:
818
+ // Vector containers have children=[] set so they don't generate child elements
819
+ if (node.asset?.isVectorContainer) {
820
+ node.children = [] // Children are in the SVG, not in HTML
821
+ }
822
+
823
+ // In HTML generation:
824
+ if (node.children && node.children.length > 0 && !node.asset?.isVectorContainer) {
825
+ // Only generate child elements for non-vector-container nodes
826
+ }
827
+ ```
828
+
829
+ **Generated HTML:**
830
+
831
+ ```html
832
+ <!-- Vector container (icon) -->
833
+ <img class="nav__icon" src="./assets/menu-icon.svg" alt="menu-icon">
834
+
835
+ <!-- Image fill (photo) -->
836
+ <img class="card__photo" src="./assets/team-photo.png" alt="Team Photo">
837
+
838
+ <!-- Frame with image fill AND children (hero section) -->
839
+ <div class="hero" style="background-image: url('./assets/hero-bg.png'); background-size: cover;">
840
+ <h1 class="hero__heading">Welcome</h1>
841
+ <p class="hero__text">Get started today</p>
842
+ </div>
843
+ ```
844
+
845
+ ---
846
+
847
+ ## Cross-References
848
+
849
+ - **`figma-api-rest.md`** -- Image export endpoint (`GET /v1/files/:key/images`), format options (svg, png, jpg, pdf), scale parameter, SVG options (`svg_include_id`, `svg_simplify_stroke`), temporary URL handling
850
+ - **`figma-api-plugin.md`** -- SceneNode types (VECTOR, BOOLEAN_OPERATION, STAR, POLYGON, LINE, ELLIPSE, RECTANGLE), `exportAsync` method, `fills` property, `imageHash`, `scaleMode`, `visible` property, `children` access
851
+ - **`design-to-code-layout.md`** -- Auto Layout detection (`layoutMode !== 'NONE'`) that overrides vector container detection, parent layout context for absolute positioning of asset `<img>` elements
852
+ - **`design-to-code-visual.md`** -- Fill extraction patterns shared with asset detection (SOLID, GRADIENT, IMAGE fill types), visual styles that are skipped for exported assets
853
+ - **`design-to-code-typography.md`** -- Text nodes that should never be exported as images, IMAGE fills on text nodes as background/clip effect, text flattening in vector containers
854
+ - **`design-to-code-semantic.md`** -- `<img>` tag selection for asset nodes (vector containers and image fills), `alt` attribute generation from node names, decorative shapes rendered as `<div>` not `<img>`, semantic context around asset elements
855
+ - **`css-strategy.md`** -- Asset file references in generated CSS (`background-image: url(...)`) and HTML (`<img src="...">`) within the layered CSS architecture. Asset references appear in Layer 3 (CSS Modules).