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,1085 @@
1
+ # Semantic HTML Generation & BEM Class Naming
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for generating semantic, accessible HTML from Figma design nodes. Encodes production-proven heuristics covering the complete pipeline: selecting the correct HTML element for each Figma node based on name, type, and context; generating BEM class names with flat hierarchy; adding ARIA attributes for accessibility; structuring the generated element tree; and integrating with the layered CSS strategy (Tailwind + CSS Custom Properties + CSS Modules). This module ties together layout, visual, typography, and asset extraction into production-quality HTML output.
6
+
7
+ ## When to Use
8
+
9
+ Reference this module when you need to:
10
+
11
+ - Select the correct HTML tag for a Figma node (div, section, header, nav, button, h1-h6, p, img, etc.)
12
+ - Implement heading hierarchy rules (single h1, sequential levels, no headings inside interactive elements)
13
+ - Detect interactive elements (buttons, links, inputs, forms) from Figma layer names
14
+ - Map structural landmarks (header, nav, footer, main, aside, section, article) from layer names
15
+ - Generate BEM class names with flat hierarchy (never `block__element__sub`)
16
+ - Deduplicate class names within a component tree
17
+ - Add ARIA attributes for accessibility (aria-label, role, alt, aria-hidden)
18
+ - Build the GeneratedElement tree structure for HTML rendering
19
+ - Match elements across responsive breakpoint frames using BEM suffix matching
20
+ - Decide where CSS properties belong in the layered strategy (Tailwind vs Custom Properties vs Modules)
21
+ - Avoid common semantic HTML pitfalls (nested buttons, headings for size, deep BEM nesting)
22
+
23
+ ---
24
+
25
+ ## Content
26
+
27
+ ### 1. Semantic Tag Selection Heuristics
28
+
29
+ The semantic tag selection system determines which HTML element each Figma node should render as. It uses a multi-factor approach combining node type, layer name pattern matching, font size, and tree context to produce semantically correct HTML.
30
+
31
+ #### SemanticContext Class
32
+
33
+ The `SemanticContext` class tracks state across the tree traversal to enforce structural rules:
34
+
35
+ ```typescript
36
+ class SemanticContext {
37
+ private h1Used: boolean = false
38
+ private insideButton: boolean = false
39
+ private insideLink: boolean = false
40
+ private headingLevelStack: number[] = [0]
41
+
42
+ canUseH1(): boolean { return !this.h1Used }
43
+ useH1(): void { this.h1Used = true }
44
+
45
+ enterInteractive(type: 'button' | 'link'): void
46
+ exitInteractive(type: 'button' | 'link'): void
47
+ isInsideInteractive(): boolean { return this.insideButton || this.insideLink }
48
+
49
+ getNextHeadingLevel(): number {
50
+ const current = this.headingLevelStack[this.headingLevelStack.length - 1]
51
+ return Math.min(current + 1, 6)
52
+ }
53
+
54
+ reset(): void // Reset for new tree generation
55
+ }
56
+ ```
57
+
58
+ **Key responsibilities:**
59
+ - **Single h1 enforcement** -- tracks whether `<h1>` has been used in the current tree
60
+ - **Interactive context** -- tracks when traversal is inside a `<button>` or `<a>`, preventing headings inside interactive elements
61
+ - **Heading hierarchy** -- provides the next appropriate heading level based on context
62
+
63
+ #### Tag Selection Decision Tree
64
+
65
+ The overall tag selection follows this priority order:
66
+
67
+ ```
68
+ getSemanticTag(node, context, isRoot, parentTag):
69
+
70
+ 1. VECTOR CONTAINER → 'img' (exported as SVG/PNG, render as image)
71
+ 2. ELLIPSE → 'div' (CSS-renderable circle/ellipse, not an image)
72
+ 3. VECTOR / BOOLEAN_OPERATION / STAR / POLYGON / LINE:
73
+ ├─ Has asset export or imageHash → 'img'
74
+ └─ Otherwise → 'div' (decorative shape)
75
+ 4. TEXT → getTextSemanticTag() (see Section 1.1)
76
+ 5. IMAGE FILL without children → 'img'
77
+ 6. BUTTON pattern (see Section 2) → 'button'
78
+ 7. LINK pattern → 'a'
79
+ 8. LANDMARK patterns (see Section 3) → 'header' | 'footer' | 'nav' | 'main' | 'aside'
80
+ 9. ARTICLE pattern → 'article'
81
+ 10. LIST pattern → 'ul'
82
+ 11. ROOT FRAME with section name → 'section'
83
+ 12. SECTION pattern → 'section'
84
+ 13. DEFAULT → 'div'
85
+ ```
86
+
87
+ #### Supported Semantic Tags
88
+
89
+ ```typescript
90
+ type SemanticTag =
91
+ | 'div' | 'section' | 'article' | 'header' | 'footer' | 'nav'
92
+ | 'main' | 'aside'
93
+ | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span'
94
+ | 'button' | 'a' | 'img'
95
+ | 'ul' | 'ol' | 'li'
96
+ ```
97
+
98
+ #### Default Tag: `<div>`
99
+
100
+ The default tag for layout containers (FRAME, GROUP, COMPONENT, INSTANCE) is `<div>`. This keeps the generated HTML clean -- semantic tags are only used when there is positive evidence from the layer name, content, or context.
101
+
102
+ #### Name Pattern Matching
103
+
104
+ All name-based detection uses word-boundary matching, not substring matching. This prevents false positives like "Navigation" matching "nav" within a larger word:
105
+
106
+ ```typescript
107
+ function matchesPattern(name: string, patterns: string[]): boolean {
108
+ return patterns.some(pattern => {
109
+ if (name === pattern) return true
110
+ // Word boundary: start/end of string, spaces, hyphens, underscores, punctuation
111
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
112
+ const regex = new RegExp(
113
+ `(?:^|[\\s\\-_()\\[\\]{}.,;:!?])${escaped}(?:[\\s\\-_()\\[\\]{}.,;:!?]|$)`,
114
+ 'i'
115
+ )
116
+ return regex.test(name)
117
+ })
118
+ }
119
+ ```
120
+
121
+ **Examples:**
122
+ - `"Primary Button"` matches `['button']` (word boundary at space)
123
+ - `"button-group"` matches `['button']` (word boundary at hyphen)
124
+ - `"Unbutton"` does NOT match `['button']` (no word boundary)
125
+ - `"nav-item"` matches `['nav']` AND `['item']` (both have word boundaries)
126
+
127
+ ---
128
+
129
+ ### 1.1 Heading Detection for Text Nodes
130
+
131
+ Text nodes use a multi-rule heading detection system that considers name, font size, and context:
132
+
133
+ ```
134
+ getTextSemanticTag(node, context, parentTag):
135
+
136
+ RULE 1: Inside interactive element (button/link)? → 'span' (NEVER heading)
137
+
138
+ RULE 2: Explicit heading names from designer:
139
+ "h1", "hero-title", "main-title", "page-title" → 'h1' (if available, else 'h2')
140
+ "h2", "section-title", "section-heading" → 'h2'
141
+ "h3", "subsection-title", "card-title" → 'h3'
142
+ "h4", "item-title" → 'h4'
143
+ "h5" → 'h5'
144
+ "h6" → 'h6'
145
+
146
+ RULE 3: Explicit paragraph/text names:
147
+ "paragraph", "body", "description", "content",
148
+ "text", "copy", "supporting" → 'p'
149
+
150
+ RULE 4: Headline pattern with size-based level:
151
+ "headline", "title" →
152
+ fontSize >= 48 AND h1 available → 'h1'
153
+ fontSize >= 32 → 'h2'
154
+ fontSize >= 24 → 'h3'
155
+ otherwise → 'h4'
156
+
157
+ RULE 5: Subtitle pattern (never h1):
158
+ "subtitle", "subheading", "tagline" →
159
+ fontSize >= 24 → 'h2'
160
+ otherwise → 'h3'
161
+
162
+ RULE 6: Size-based fallback with strict h1 control:
163
+ fontSize >= 48 AND h1 available AND name matches heading pattern → 'h1'
164
+ fontSize >= 32 AND name matches heading pattern → 'h2'
165
+ fontSize >= 32 (no heading name) → 'p'
166
+ fontSize >= 16 → 'p'
167
+
168
+ DEFAULT: Small text (< 16px) → 'span'
169
+ ```
170
+
171
+ #### Single h1 Rule
172
+
173
+ Only one `<h1>` is allowed per page or component tree. When h1 has already been used, nodes that would qualify for h1 fall back to `<h2>`:
174
+
175
+ ```typescript
176
+ if (matchesPattern(name, ['h1', 'hero-title', 'main-title', 'page-title'])) {
177
+ if (context.canUseH1()) {
178
+ context.useH1()
179
+ return 'h1'
180
+ }
181
+ return 'h2' // Fallback if h1 already used
182
+ }
183
+ ```
184
+
185
+ #### Sequential Heading Hierarchy
186
+
187
+ Headings should be sequential: h1, then h2, then h3. Skipping levels (h1 then h3) creates accessibility issues for screen reader users who navigate by heading level. The `getNextHeadingLevel()` method on SemanticContext provides the appropriate next level based on the current context.
188
+
189
+ #### No Headings Inside Interactive Elements
190
+
191
+ Text inside `<button>` or `<a>` elements always renders as `<span>`, regardless of name or font size. Browser rendering of headings inside interactive elements is inconsistent and creates accessibility problems:
192
+
193
+ ```typescript
194
+ if (context.isInsideInteractive() || parentTag === 'button' || parentTag === 'a') {
195
+ return 'span'
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ### 2. Interactive Element Detection
202
+
203
+ Interactive elements are detected from layer names with exclusion rules to prevent false positives on layout containers.
204
+
205
+ #### Button Detection
206
+
207
+ | Pattern | Match Examples | Tag |
208
+ |---------|---------------|-----|
209
+ | `button`, `btn`, `cta` | "Primary Button", "btn-submit", "CTA" | `<button>` |
210
+
211
+ **Exclusion rules** -- a node matching button patterns is NOT a button if:
212
+
213
+ 1. **It is a layout container**: name also matches `row`, `rows`, `container`, `wrapper`, `group`, `stack`, `grid`, `list`, `column`, `col`
214
+ 2. **It has multiple children**: more than 1 direct child suggests a layout container holding multiple items
215
+ 3. **It contains a button descendant**: a parent named "Button" that has a child also named "Button" -- the parent is the wrapper, the child is the actual button
216
+
217
+ ```typescript
218
+ const isLayoutContainer = matchesPattern(name, [
219
+ 'row', 'rows', 'container', 'wrapper', 'group',
220
+ 'stack', 'grid', 'list', 'column', 'col'
221
+ ])
222
+ const hasMultipleChildren = node.children && node.children.length > 1
223
+ const hasButtonDescendant = containsButtonDescendant(node)
224
+
225
+ if (matchesPattern(name, ['button', 'btn', 'cta'])
226
+ && !isLayoutContainer
227
+ && !hasMultipleChildren
228
+ && !hasButtonDescendant) {
229
+ context.enterInteractive('button')
230
+ return 'button'
231
+ }
232
+ ```
233
+
234
+ **Common pattern:** "Button Group" or "Button Row" is a layout container holding multiple buttons. The word "button" matches, but the layout container exclusion prevents it from becoming `<button>`.
235
+
236
+ #### Link Detection
237
+
238
+ | Pattern | Match Examples | Tag |
239
+ |---------|---------------|-----|
240
+ | `link`, `anchor` | "Footer Link", "anchor-text" | `<a>` |
241
+
242
+ Links always include `href`. When no URL is found in child text hyperlinks, use `#` as a placeholder:
243
+
244
+ ```typescript
245
+ if (tag === 'a') {
246
+ const hyperlink = findHyperlinkInTree(node)
247
+ if (hyperlink && hyperlink.type === 'URL') {
248
+ attributes.href = hyperlink.value
249
+ attributes.target = '_blank'
250
+ attributes.rel = 'noopener noreferrer'
251
+ }
252
+ }
253
+ ```
254
+
255
+ #### Input Detection (Future)
256
+
257
+ | Pattern | Match Examples | Tag |
258
+ |---------|---------------|-----|
259
+ | `input`, `field`, `text-field` | "Email Input", "Search Field" | `<input>` |
260
+ | `textarea` | "Message Textarea" | `<textarea>` |
261
+ | `form` | "Contact Form" | `<form>` |
262
+
263
+ > **Note:** Input and form detection are planned extensions. The current implementation focuses on button and link detection. Inputs in Figma designs are visual representations and require manual configuration of type, name, and validation attributes.
264
+
265
+ ---
266
+
267
+ ### 3. Structural Landmark Elements
268
+
269
+ Landmark elements provide page structure for screen readers and assistive technology. Detection is name-based with context awareness.
270
+
271
+ #### Landmark Detection Rules
272
+
273
+ | Patterns | Tag | Context Rule |
274
+ |----------|-----|-------------|
275
+ | `header`, `head`, `top-bar`, `topbar`, `navbar` | `<header>` | Only at root level (`isRoot === true`) |
276
+ | `footer`, `foot`, `bottom-bar`, `bottombar` | `<footer>` | Only at root level |
277
+ | `nav`, `navigation`, `menu` (NOT `item`) | `<nav>` | Any level; excludes "nav-item" |
278
+ | `main`, `content`, `body` | `<main>` | Only at root level |
279
+ | `aside`, `sidebar`, `side-panel` | `<aside>` | Any level |
280
+ | `section`, `hero`, `about`, `features`, `contact`, `pricing` | `<section>` | Root FRAME only |
281
+ | `article`, `post`, `blog-post`, `news-item` | `<article>` | Any level |
282
+
283
+ **Root-level restriction:** `<header>`, `<footer>`, and `<main>` are only applied at the root level of the exported tree. A nested frame named "header" inside a card component would not become `<header>` -- it would remain `<div>`.
284
+
285
+ **Nav exclusion:** Nodes with "item" in the name (like "nav-item") do not match `<nav>`. This prevents individual navigation links from becoming `<nav>` elements.
286
+
287
+ #### Section Usage
288
+
289
+ `<section>` is reserved for major page sections, not generic UI components:
290
+
291
+ ```typescript
292
+ // Root frame with section-like name → section
293
+ if (isRoot && type === 'FRAME') {
294
+ if (matchesPattern(name, ['section', 'hero', 'about', 'features', 'contact', 'pricing'])) {
295
+ return 'section'
296
+ }
297
+ return 'div'
298
+ }
299
+
300
+ // Non-root section: only when explicitly named "section" and not a UI component
301
+ if (matchesPattern(name, ['section']) && !matchesPattern(name, ['button', 'btn', 'card', 'item'])) {
302
+ return 'section'
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ### 4. Image & Media Elements
309
+
310
+ Image and media element selection coordinates with the asset detection module to render nodes as `<img>` or CSS `background-image`.
311
+
312
+ #### Vector Containers to `<img>`
313
+
314
+ Nodes identified as vector containers by the asset module render as `<img>` elements. Their children are baked into the exported SVG/PNG file:
315
+
316
+ ```typescript
317
+ if (node.asset?.isVectorContainer) {
318
+ return 'img'
319
+ }
320
+ ```
321
+
322
+ The children array is set to empty for vector containers during traversal, so the `<img>` tag has no child elements.
323
+
324
+ #### Image Fills: Foreground vs Background
325
+
326
+ The rendering strategy depends on whether the node has children:
327
+
328
+ | Condition | Rendering | HTML |
329
+ |-----------|-----------|------|
330
+ | IMAGE fill, no children | `<img>` element | `<img src="..." alt="...">` |
331
+ | IMAGE fill, has children | CSS `background-image` on container | `<div style="background-image: url(...)">...children...</div>` |
332
+
333
+ ```typescript
334
+ // Image fills without children are foreground images
335
+ if (node.asset?.imageHash && (!node.children || node.children.length === 0)) {
336
+ return 'img'
337
+ }
338
+ ```
339
+
340
+ #### Asset Attributes on `<img>` Tags
341
+
342
+ When a node renders as `<img>`, the asset map provides the `src` path, and the node name provides the `alt` text:
343
+
344
+ ```typescript
345
+ if (tag === 'img' && node.asset && node.asset.exportAs && options.assetMap) {
346
+ const filename = options.assetMap.get(node.id)
347
+ if (filename) {
348
+ attributes.src = `./assets/${filename}`
349
+ attributes.alt = node.name || ''
350
+ }
351
+ }
352
+ ```
353
+
354
+ #### Decorative Shapes
355
+
356
+ ELLIPSE nodes and simple shapes (VECTOR, LINE without export settings) are decorative -- they render as `<div>` with CSS styling, not `<img>`:
357
+
358
+ ```typescript
359
+ if (type === 'ELLIPSE') {
360
+ return 'div' // CSS border-radius: 50%, not an image
361
+ }
362
+ ```
363
+
364
+ ---
365
+
366
+ ### 5. BEM Class Naming Convention
367
+
368
+ Generated class names follow a BEM (Block Element Modifier) convention with a strict flat hierarchy rule. The naming system sanitizes Figma layer names into valid CSS identifiers and deduplicates within a component tree.
369
+
370
+ #### BEM Structure
371
+
372
+ ```
373
+ .block → Root component
374
+ .block__element → Child within component
375
+ .block__element--mod → Variation of element (future)
376
+ ```
377
+
378
+ **Flat hierarchy rule:** Never nest deeper than `block__element`. This is enforced by always extracting the block from the first segment before `__`:
379
+
380
+ ```typescript
381
+ function generateBEMClassName(
382
+ nodeName: string,
383
+ parentClassName: string | null,
384
+ depth: number,
385
+ prefix?: string
386
+ ): string {
387
+ const elementName = generateClassName(nodeName)
388
+
389
+ // Root element - no parent
390
+ if (!parentClassName || depth === 0) {
391
+ if (prefix) return `${prefix}${elementName}`
392
+ return elementName
393
+ }
394
+
395
+ // BEM element: block__element
396
+ // Extract block from parent (FIRST part before __)
397
+ const block = parentClassName.split('__')[0]
398
+ return `${block}__${elementName}`
399
+ }
400
+ ```
401
+
402
+ **Example hierarchy:**
403
+
404
+ ```
405
+ Figma Layer Tree BEM Class Output
406
+ ───────────────── ────────────────
407
+ Card .card
408
+ ├── Header .card__header
409
+ │ └── Title .card__title (NOT .card__header__title)
410
+ │ └── Subtitle .card__subtitle (NOT .card__header__title__subtitle)
411
+ ├── Body .card__body
412
+ └── Footer .card__footer
413
+ └── Button .card__button
414
+ ```
415
+
416
+ #### Name Sanitization
417
+
418
+ ```typescript
419
+ function generateClassName(name: string): string {
420
+ let cleaned = name
421
+ // Remove common Figma prefixes/suffixes
422
+ .replace(/^(frame|group|component|instance|vector|text)\s*/i, '')
423
+ .replace(/\s*(copy|duplicate|\d+)$/i, '')
424
+
425
+ let sanitized = cleaned
426
+ .toLowerCase()
427
+ .trim()
428
+ .replace(/[^a-z0-9]+/g, '-') // Non-alphanumeric to hyphen
429
+ .replace(/^-+|-+$/g, '') // Trim hyphens
430
+ .replace(/-+/g, '-') // Collapse multiple hyphens
431
+
432
+ // Truncate at 32 characters (word boundary if possible)
433
+ if (sanitized.length > MAX_CLASS_NAME_LENGTH) {
434
+ let truncated = sanitized.substring(0, MAX_CLASS_NAME_LENGTH)
435
+ const lastHyphen = truncated.lastIndexOf('-')
436
+ if (lastHyphen > MAX_CLASS_NAME_LENGTH / 2) {
437
+ truncated = truncated.substring(0, lastHyphen)
438
+ }
439
+ sanitized = truncated.replace(/-+$/, '')
440
+ }
441
+
442
+ // Ensure doesn't start with number
443
+ if (/^[0-9]/.test(sanitized)) {
444
+ sanitized = 'el-' + sanitized
445
+ }
446
+
447
+ // Empty name fallback
448
+ if (!sanitized) sanitized = 'element'
449
+
450
+ return sanitized
451
+ }
452
+ ```
453
+
454
+ **Sanitization examples:**
455
+
456
+ | Figma Layer Name | Generated Class |
457
+ |-----------------|----------------|
458
+ | `"Frame 123"` | `element` (prefix "Frame" removed, "123" suffix removed) |
459
+ | `"Primary Button"` | `primary-button` |
460
+ | `"hero__title"` | `hero-title` (double underscore collapsed) |
461
+ | `"Card / Header"` | `card-header` |
462
+ | `"A very long name that exceeds the limit"` | `a-very-long-name-that-exceeds` (truncated at word boundary) |
463
+ | `"123-items"` | `el-123-items` (prefixed to avoid leading digit) |
464
+
465
+ #### 32-Character Maximum
466
+
467
+ Class names are truncated to 32 characters. Truncation prefers word boundaries (hyphens) when the midpoint offers one:
468
+
469
+ ```typescript
470
+ const MAX_CLASS_NAME_LENGTH = 32
471
+
472
+ if (sanitized.length > MAX_CLASS_NAME_LENGTH) {
473
+ let truncated = sanitized.substring(0, MAX_CLASS_NAME_LENGTH)
474
+ const lastHyphen = truncated.lastIndexOf('-')
475
+ if (lastHyphen > MAX_CLASS_NAME_LENGTH / 2) {
476
+ truncated = truncated.substring(0, lastHyphen)
477
+ }
478
+ sanitized = truncated.replace(/-+$/, '')
479
+ }
480
+ ```
481
+
482
+ #### ClassNameTracker (Deduplication)
483
+
484
+ When multiple siblings share the same name (common with "Frame 1", "Frame 2" patterns), the tracker appends a numeric suffix:
485
+
486
+ ```typescript
487
+ class ClassNameTracker {
488
+ private used: Map<string, number> = new Map()
489
+
490
+ getUnique(baseName: string): string {
491
+ const count = this.used.get(baseName) || 0
492
+ this.used.set(baseName, count + 1)
493
+ if (count === 0) return baseName
494
+ return `${baseName}-${count + 1}`
495
+ }
496
+
497
+ reset(): void { this.used.clear() }
498
+ }
499
+ ```
500
+
501
+ | First occurrence | Second occurrence | Third occurrence |
502
+ |-----------------|-------------------|------------------|
503
+ | `.card__item` | `.card__item-2` | `.card__item-3` |
504
+
505
+ #### Optional Class Prefix
506
+
507
+ A prefix can be applied to root-level block names for namespace isolation:
508
+
509
+ ```typescript
510
+ // With prefix 'fr-'
511
+ generateBEMClassName('card', null, 0, 'fr-') // → 'fr-card'
512
+ generateBEMClassName('title', 'fr-card', 1) // → 'fr-card__title'
513
+ ```
514
+
515
+ ---
516
+
517
+ ### 6. ARIA Attributes
518
+
519
+ ARIA attributes ensure generated HTML is accessible to screen readers and assistive technologies. The following rules apply during element generation.
520
+
521
+ #### Buttons
522
+
523
+ | Scenario | Attributes |
524
+ |----------|-----------|
525
+ | Native `<button>` element | No extra `role` needed (implicit) |
526
+ | Non-button element acting as button | `role="button"`, `tabindex="0"` |
527
+ | Icon-only button | `aria-label="descriptive action"` (derived from layer name) |
528
+ | Button with visible text | No `aria-label` (text content is the label) |
529
+
530
+ **Icon-only button detection:** When a `<button>` has no text children -- only image or vector children -- it needs an `aria-label` derived from the layer name:
531
+
532
+ ```html
533
+ <!-- Button with text: no aria-label needed -->
534
+ <button class="card__submit">Submit</button>
535
+
536
+ <!-- Icon-only button: needs aria-label -->
537
+ <button class="nav__menu-btn" aria-label="Open menu">
538
+ <img class="nav__menu-icon" src="./assets/menu.svg" alt="">
539
+ </button>
540
+ ```
541
+
542
+ #### Links
543
+
544
+ | Scenario | Attributes |
545
+ |----------|-----------|
546
+ | Link with URL from hyperlink data | `href="url"`, `target="_blank"`, `rel="noopener noreferrer"` |
547
+ | Link detected by name, no URL found | `href="#"` (placeholder) |
548
+
549
+ Links always need an `href` attribute. The generation searches the subtree for hyperlink data in text segments:
550
+
551
+ ```typescript
552
+ if (tag === 'a') {
553
+ const hyperlink = findHyperlinkInTree(node)
554
+ if (hyperlink && hyperlink.type === 'URL') {
555
+ attributes.href = hyperlink.value
556
+ attributes.target = '_blank'
557
+ attributes.rel = 'noopener noreferrer'
558
+ }
559
+ }
560
+ ```
561
+
562
+ #### Images
563
+
564
+ | Scenario | `alt` Attribute |
565
+ |----------|----------------|
566
+ | Content image (photo, illustration) | Descriptive text from layer name |
567
+ | Decorative image | `alt=""` (empty string, not omitted) |
568
+ | Icon inside interactive element | `alt=""` (the parent button/link provides the label) |
569
+
570
+ ```typescript
571
+ if (tag === 'img' && node.asset && node.asset.exportAs) {
572
+ attributes.alt = node.name || ''
573
+ }
574
+ ```
575
+
576
+ #### Hidden Decorative Elements
577
+
578
+ Purely visual elements that provide no semantic meaning should be hidden from assistive technology:
579
+
580
+ ```html
581
+ <!-- Decorative divider line -->
582
+ <div class="section__divider" aria-hidden="true"></div>
583
+
584
+ <!-- Background decoration -->
585
+ <div class="hero__bg-shape" aria-hidden="true"></div>
586
+ ```
587
+
588
+ #### Landmark Roles
589
+
590
+ Native landmark elements do not need explicit ARIA roles:
591
+
592
+ | Element | Implicit ARIA Role | Explicit `role=` Needed? |
593
+ |---------|-------------------|------------------------|
594
+ | `<header>` | `banner` | No |
595
+ | `<nav>` | `navigation` | No |
596
+ | `<main>` | `main` | No |
597
+ | `<footer>` | `contentinfo` | No |
598
+ | `<aside>` | `complementary` | No |
599
+ | `<section>` | `region` (with `aria-label`) | No |
600
+
601
+ When a `<div>` is used for a landmark due to nesting constraints, add the explicit role:
602
+
603
+ ```html
604
+ <!-- When <nav> can't be used (e.g., inside another nav) -->
605
+ <div role="navigation" aria-label="Footer links">...</div>
606
+ ```
607
+
608
+ #### Live Regions
609
+
610
+ If content updates dynamically (notifications, status messages), add live region attributes:
611
+
612
+ ```html
613
+ <div class="toast" role="alert" aria-live="polite">
614
+ Message sent successfully
615
+ </div>
616
+ ```
617
+
618
+ ---
619
+
620
+ ### 7. Generated Element Tree Structure
621
+
622
+ The output of semantic HTML generation is a tree of `GeneratedElement` objects ready for rendering as HTML strings or React JSX.
623
+
624
+ #### GeneratedElement Type
625
+
626
+ ```typescript
627
+ interface GeneratedElement {
628
+ tag: string // 'div', 'section', 'h1', 'button', etc.
629
+ className: string // BEM class name
630
+ styles: CSSStyles // CSS properties for this element
631
+ children: (GeneratedElement | string)[] // Child elements or text content
632
+ attributes?: Record<string, string> // href, src, alt, role, aria-*, data-*
633
+ figmaId?: string // Original Figma node ID
634
+ }
635
+ ```
636
+
637
+ #### Children Types
638
+
639
+ Children can be:
640
+ - **`GeneratedElement`** -- nested HTML elements
641
+ - **`string`** -- text content (leaf nodes)
642
+
643
+ ```typescript
644
+ // Text node with styled segments
645
+ {
646
+ tag: 'h1',
647
+ className: 'hero__title',
648
+ styles: { fontSize: '48px', fontWeight: '700' },
649
+ children: [
650
+ { tag: 'span', className: 'hero__title__segment-1', styles: { color: '#1A1A1A' }, children: ['Build '] },
651
+ { tag: 'span', className: 'hero__title__segment-2', styles: { color: '#6366F1' }, children: ['beautiful '] },
652
+ { tag: 'span', className: 'hero__title__segment-3', styles: { color: '#1A1A1A' }, children: ['interfaces'] },
653
+ ]
654
+ }
655
+
656
+ // Simple text node
657
+ {
658
+ tag: 'p',
659
+ className: 'hero__description',
660
+ styles: { fontSize: '18px' },
661
+ children: ['Create stunning designs with ease.']
662
+ }
663
+ ```
664
+
665
+ #### Attributes
666
+
667
+ The `attributes` record holds HTML attributes beyond `class` and `style`:
668
+
669
+ ```typescript
670
+ // Image element
671
+ { tag: 'img', attributes: { src: './assets/hero.png', alt: 'Hero image' } }
672
+
673
+ // Link element
674
+ { tag: 'a', attributes: { href: 'https://example.com', target: '_blank', rel: 'noopener noreferrer' } }
675
+
676
+ // Button with ARIA
677
+ { tag: 'button', attributes: { 'aria-label': 'Close dialog' } }
678
+ ```
679
+
680
+ #### CSSStyles Interface
681
+
682
+ ```typescript
683
+ interface CSSStyles {
684
+ // Layout
685
+ display?: string
686
+ flexDirection?: string
687
+ flexWrap?: string
688
+ justifyContent?: string
689
+ alignItems?: string
690
+ alignContent?: string
691
+ gap?: string
692
+ rowGap?: string
693
+ columnGap?: string
694
+ padding?: string
695
+
696
+ // Sizing
697
+ width?: string
698
+ height?: string
699
+ minWidth?: string
700
+ maxWidth?: string
701
+ flexGrow?: string
702
+ flexShrink?: string
703
+ flexBasis?: string
704
+ alignSelf?: string
705
+
706
+ // Position
707
+ position?: string
708
+ top?: string
709
+ left?: string
710
+ zIndex?: string
711
+
712
+ // Visual
713
+ backgroundColor?: string
714
+ backgroundImage?: string
715
+ border?: string
716
+ borderRadius?: string
717
+ boxShadow?: string
718
+ opacity?: string
719
+
720
+ // Typography
721
+ fontFamily?: string
722
+ fontSize?: string
723
+ fontWeight?: string
724
+ lineHeight?: string
725
+ color?: string
726
+
727
+ [key: string]: string | undefined
728
+ }
729
+ ```
730
+
731
+ #### Style Merging Order
732
+
733
+ Styles are merged from multiple generators in this order:
734
+
735
+ ```typescript
736
+ const styles: CSSStyles = {}
737
+ Object.assign(styles, generateContainerStyles(node.layout)) // 1. Container position
738
+ Object.assign(styles, generateLayoutStyles(node.layout)) // 2. Flexbox layout
739
+ Object.assign(styles, generateLayoutChildStyles(node.layoutChild)) // 3. Child flex props
740
+ Object.assign(styles, generatePositionStyles(node.bounds)) // 4. Absolute positioning
741
+ Object.assign(styles, generateTypographyStyles(node.text)) // 5. Typography
742
+ Object.assign(styles, generateVisualStyles(node.fills, ...)) // 6. Visual properties
743
+ ```
744
+
745
+ Later assignments override earlier ones if they share the same property key.
746
+
747
+ #### Full Generated Output
748
+
749
+ ```typescript
750
+ interface GeneratedOutput {
751
+ html: GeneratedElement // Root element tree
752
+ css: CSSRule[] // Flattened CSS rules
753
+ fonts: string[] // Google Fonts URLs
754
+ tokens?: DesignTokens // Design token definitions
755
+ }
756
+ ```
757
+
758
+ ---
759
+
760
+ ### 8. Responsive Class Considerations
761
+
762
+ When generating responsive CSS from multiple Figma frames representing breakpoints, class naming plays a critical role in matching elements across frames.
763
+
764
+ #### BEM Suffix Matching
765
+
766
+ Elements are matched across breakpoint frames using the BEM element suffix (everything after the block name):
767
+
768
+ ```
769
+ Mobile frame: .card---mobile → suffix: "" (root)
770
+ .card---mobile__title → suffix: "__title"
771
+ .card---mobile__body → suffix: "__body"
772
+
773
+ Desktop frame: .card---desktop → suffix: "" (root)
774
+ .card---desktop__title → suffix: "__title"
775
+ .card---desktop__body → suffix: "__body"
776
+ ```
777
+
778
+ After matching, selectors are remapped to a unified component class:
779
+ - `.card---mobile__title` and `.card---desktop__title` both become `.card__title`
780
+
781
+ #### Variant Component Class Mapping
782
+
783
+ When responsive variants exist as COMPONENT_SET children, the `componentClassMap` option overrides the BEM block for that subtree:
784
+
785
+ ```typescript
786
+ interface GenerateElementOptions {
787
+ componentClassMap?: Map<string, string>
788
+ // e.g., Map { 'card' → 'card', 'hero' → 'hero' }
789
+ }
790
+ ```
791
+
792
+ When a child node's base name (stripped of `#breakpoint` suffix) matches a key in the map, the child's subtree uses the component class as the BEM block instead of inheriting from the parent layout frame:
793
+
794
+ ```typescript
795
+ if (depth > 0 && options.componentClassMap) {
796
+ const parsed = parseBreakpointSuffix(node.name)
797
+ const componentClass = options.componentClassMap.get(parsed.baseName.toLowerCase())
798
+ if (componentClass) {
799
+ className = componentClass
800
+ effectiveParentClassName = componentClass
801
+ // Fresh tracker for component subtree
802
+ componentClassTracker = new ClassNameTracker()
803
+ }
804
+ }
805
+ ```
806
+
807
+ This ensures that identical components at different breakpoints produce the same class names (`.card__title`), enabling the responsive CSS diffing system to generate correct media query overrides.
808
+
809
+ #### Same Element, Different Breakpoints
810
+
811
+ The unified class name receives base styles from the smallest breakpoint and override styles in media query blocks:
812
+
813
+ ```css
814
+ /* Base (mobile) */
815
+ .card__title {
816
+ font-size: 18px;
817
+ font-weight: 700;
818
+ }
819
+
820
+ /* Tablet override */
821
+ @media (min-width: 768px) {
822
+ .card__title {
823
+ font-size: 22px;
824
+ }
825
+ }
826
+
827
+ /* Desktop override */
828
+ @media (min-width: 1024px) {
829
+ .card__title {
830
+ font-size: 28px;
831
+ }
832
+ }
833
+ ```
834
+
835
+ ---
836
+
837
+ ### 9. CSS Generation Integration (Layered Strategy)
838
+
839
+ The semantic module produces elements with styles. Those styles are distributed across three layers in the generated CSS.
840
+
841
+ #### Three-Layer Architecture
842
+
843
+ | Layer | Technology | What Goes Here | Specificity |
844
+ |-------|-----------|---------------|-------------|
845
+ | 1. Layout bones | Tailwind CSS classes | Flexbox, gap, padding, margin, sizing, positioning | Zero (utility classes) |
846
+ | 2. Design tokens | CSS Custom Properties | Colors, font sizes, spacing values, radii, shadows | Defined in `:root` |
847
+ | 3. Visual skin | CSS Modules | Component-specific backgrounds, borders, effects, overrides | Scoped to component |
848
+
849
+ #### Property Placement Decision Tree
850
+
851
+ ```
852
+ For each CSS property on a GeneratedElement:
853
+
854
+ IS IT A LAYOUT PROPERTY?
855
+ (display, flex-direction, justify-content, align-items, gap,
856
+ padding, margin, width, height, flex-grow, flex-basis, position)
857
+ → Layer 1: Tailwind class
858
+
859
+ IS THE VALUE A DESIGN TOKEN?
860
+ (color resolves to var(--color-*), font-size to var(--text-*),
861
+ spacing to var(--spacing-*), radius to var(--radius-*))
862
+ → Layer 2: CSS Custom Property in the value
863
+
864
+ IS IT A COMPONENT-SPECIFIC VISUAL STYLE?
865
+ (background-color, background-image, border, border-radius,
866
+ box-shadow, opacity, filter, specific color assignments)
867
+ → Layer 3: CSS Module rule
868
+ ```
869
+
870
+ #### Tailwind for Layout (Layer 1)
871
+
872
+ Layout properties map to Tailwind utilities:
873
+
874
+ | CSS Property | Tailwind Class | Example |
875
+ |-------------|---------------|---------|
876
+ | `display: flex` | `flex` | |
877
+ | `flex-direction: column` | `flex-col` | |
878
+ | `justify-content: center` | `justify-center` | |
879
+ | `align-items: center` | `items-center` | |
880
+ | `gap: 16px` | `gap-4` | |
881
+ | `padding: 24px` | `p-6` | |
882
+ | `flex-grow: 1; flex-basis: 0` | `flex-1` | |
883
+ | `width: 100%` | `w-full` | |
884
+ | `position: relative` | `relative` | |
885
+
886
+ #### CSS Custom Properties for Tokens (Layer 2)
887
+
888
+ Token values use CSS custom properties defined in a `tokens.css` file:
889
+
890
+ ```css
891
+ /* tokens.css (generated from Figma Variables or value-based extraction) */
892
+ :root {
893
+ --color-primary: #1a73e8;
894
+ --color-neutral-800: #333333;
895
+ --text-base: 16px;
896
+ --text-lg: 18px;
897
+ --spacing-md: 16px;
898
+ --radius-md: 8px;
899
+ --shadow-md: 0px 4px 12px rgba(0, 0, 0, 0.15);
900
+ }
901
+ ```
902
+
903
+ Used in component styles:
904
+
905
+ ```css
906
+ .card__title {
907
+ color: var(--color-neutral-800);
908
+ font-size: var(--text-lg);
909
+ }
910
+ ```
911
+
912
+ #### CSS Modules for Visual Skin (Layer 3)
913
+
914
+ Component-specific visual styles live in CSS Module files, scoped to prevent leaking:
915
+
916
+ ```css
917
+ /* Card.module.css */
918
+ .card {
919
+ background-color: var(--color-neutral-100);
920
+ border: 1px solid var(--color-neutral-300);
921
+ border-radius: var(--radius-md);
922
+ box-shadow: var(--shadow-md);
923
+ }
924
+ ```
925
+
926
+ #### When to Use Inline Styles
927
+
928
+ Inline styles on the `GeneratedElement.styles` object are used during the initial generation phase. They are later extracted into CSS rules (either Tailwind classes, token references, or CSS Module rules) during the CSS optimization phase. The `styles` object serves as the intermediate representation before final layer assignment.
929
+
930
+ ---
931
+
932
+ ### 10. Common Pitfalls
933
+
934
+ #### Pitfall: Using Heading Tags for Visual Size
935
+
936
+ **Problem:** A large font size (e.g., 48px) causes a node to be rendered as `<h1>`, even when the content is decorative text like a large number or price.
937
+
938
+ **Rule:** Headings represent document hierarchy, not visual size. The tag selection system requires BOTH large font size AND a heading-like name pattern to assign heading tags. Font size alone produces `<p>`:
939
+
940
+ ```typescript
941
+ // 48px text named "Hero Price" → <p> (no heading pattern)
942
+ // 48px text named "Hero Title" → <h1> (heading pattern + large size)
943
+ ```
944
+
945
+ #### Pitfall: "Button Group" Is a Layout Container
946
+
947
+ **Problem:** A frame named "Button Group" is detected as a `<button>` because the name contains "button".
948
+
949
+ **Rule:** The exclusion rules check for layout container patterns. "Button Group" matches both `['button']` and `['group']`. The layout container exclusion takes precedence, and the node renders as `<div>`.
950
+
951
+ #### Pitfall: Links Without href
952
+
953
+ **Problem:** An `<a>` element is generated without an `href` attribute, making it inaccessible to keyboard users and screen readers.
954
+
955
+ **Rule:** Every `<a>` element must have an `href`. The generation searches the subtree for hyperlink data. If no URL is found, use `href="#"` as a placeholder.
956
+
957
+ #### Pitfall: Redundant ARIA Roles
958
+
959
+ **Problem:** Adding `role="button"` to a native `<button>` element, or `role="navigation"` to `<nav>`.
960
+
961
+ **Rule:** Native HTML elements have implicit ARIA roles. Adding the same role explicitly is redundant and clutters the markup:
962
+
963
+ ```html
964
+ <!-- WRONG: redundant role -->
965
+ <button role="button">Submit</button>
966
+ <nav role="navigation">...</nav>
967
+
968
+ <!-- CORRECT: implicit role is sufficient -->
969
+ <button>Submit</button>
970
+ <nav>...</nav>
971
+ ```
972
+
973
+ #### Pitfall: Icon-Only Interactive Elements Without Labels
974
+
975
+ **Problem:** A button or link containing only an icon (SVG/image) with no visible text has no accessible name.
976
+
977
+ **Rule:** Icon-only interactive elements MUST have `aria-label`:
978
+
979
+ ```html
980
+ <!-- WRONG: no accessible name -->
981
+ <button class="close-btn">
982
+ <img src="./assets/x-icon.svg" alt="">
983
+ </button>
984
+
985
+ <!-- CORRECT: aria-label provides accessible name -->
986
+ <button class="close-btn" aria-label="Close">
987
+ <img src="./assets/x-icon.svg" alt="">
988
+ </button>
989
+ ```
990
+
991
+ #### Pitfall: Deep BEM Nesting
992
+
993
+ **Problem:** Class names like `.card__header__title__icon` that nest multiple BEM levels.
994
+
995
+ **Rule:** BEM elements are always flat. The block is extracted from the first part of the parent class, preventing accumulation:
996
+
997
+ ```
998
+ .card__header → block = "card"
999
+ .card__title → block = "card" (extracted from "card__header", NOT "card__header__title")
1000
+ .card__icon → block = "card"
1001
+ ```
1002
+
1003
+ #### Pitfall: Nested Buttons (Invalid HTML)
1004
+
1005
+ **Problem:** A frame named "Button" contains a child also named "Button", producing `<button><button>...</button></button>` which is invalid HTML.
1006
+
1007
+ **Rule:** The `containsButtonDescendant()` check prevents a parent from becoming a button when its subtree already contains one:
1008
+
1009
+ ```typescript
1010
+ function containsButtonDescendant(node: ExtractedNode): boolean {
1011
+ if (!node.children || node.children.length === 0) return false
1012
+ for (const child of node.children) {
1013
+ const childName = child.name.toLowerCase()
1014
+ const isButton = matchesPattern(childName, ['button', 'btn', 'cta'])
1015
+ const isContainer = matchesPattern(childName, ['row', 'rows', 'container', ...])
1016
+ if (isButton && !isContainer) return true
1017
+ if (containsButtonDescendant(child)) return true
1018
+ }
1019
+ return false
1020
+ }
1021
+ ```
1022
+
1023
+ #### Pitfall: Nested Anchors (Invalid HTML)
1024
+
1025
+ **Problem:** A text node inside an `<a>` tag has a hyperlink segment, producing nested `<a>` tags.
1026
+
1027
+ **Rule:** When generating styled text segments inside an anchor, hyperlink segments render as `<span>` instead of `<a>`:
1028
+
1029
+ ```typescript
1030
+ const isInsideAnchor = tag === 'a' || parentTag === 'a'
1031
+ if (segment.hyperlink && segment.hyperlink.type === 'URL' && !isInsideAnchor) {
1032
+ return { tag: 'a', ... } // Only when not nested
1033
+ }
1034
+ return { tag: 'span', ... } // Fallback inside anchors
1035
+ ```
1036
+
1037
+ #### Pitfall: Section Overuse
1038
+
1039
+ **Problem:** Every frame in the design tree becomes `<section>`, drowning out the semantic meaning.
1040
+
1041
+ **Rule:** `<section>` is reserved for explicitly named page sections. Generic frames default to `<div>`. Only root frames matching section-like patterns (`hero`, `about`, `features`, `contact`, `pricing`) or explicitly named "section" qualify.
1042
+
1043
+ ---
1044
+
1045
+ ### Intermediate Type Reference
1046
+
1047
+ The complete types used between extraction and semantic HTML generation:
1048
+
1049
+ ```typescript
1050
+ interface ExtractedNode {
1051
+ id: string
1052
+ name: string
1053
+ type: string // 'FRAME' | 'TEXT' | 'VECTOR' | etc.
1054
+ bounds: { x: number; y: number; width: number; height: number }
1055
+ opacity: number
1056
+ visible: boolean
1057
+ layout?: LayoutProperties // Auto Layout data (from layout module)
1058
+ layoutChild?: LayoutChildProperties
1059
+ fills?: FillData[] // Visual fills (from visual module)
1060
+ strokes?: StrokeData[]
1061
+ effects?: EffectData[]
1062
+ cornerRadius?: CornerRadius
1063
+ text?: TextData // Text properties (from typography module)
1064
+ asset?: AssetData // Asset metadata (from assets module)
1065
+ children?: ExtractedNode[]
1066
+ }
1067
+
1068
+ // Output: GeneratedElement tree (Section 7)
1069
+ // Input to CSS flattening, HTML rendering, or React JSX generation
1070
+ ```
1071
+
1072
+ ---
1073
+
1074
+ ## Cross-References
1075
+
1076
+ - **`figma-api-plugin.md`** -- SceneNode types used in tag selection (FrameNode, TextNode, GroupNode, VectorNode, BooleanOperationNode, EllipseNode), `visible` property for filtering, `children` access for traversal, `fontName` and `fontSize` for heading detection
1077
+ - **`figma-api-rest.md`** -- Node tree structure for understanding parent-child relationships in the design hierarchy, `absoluteBoundingBox` for bounds used in icon size detection
1078
+ - **`figma-api-variables.md`** -- Variable resolution for text fill colors in styled segments, variable bindings that flow through to CSS custom property references in the layered strategy
1079
+ - **`design-to-code-layout.md`** -- Auto Layout properties that determine container behavior, sizing modes (FIXED, HUG, FILL) that interact with element dimensions, responsive multi-frame pattern and BEM suffix matching for breakpoint CSS
1080
+ - **`design-to-code-visual.md`** -- Fill extraction for text colors, gradient text CSS technique (`background-clip: text`), visual styles that are skipped for exported asset nodes, opacity handling (node-level vs fill-level)
1081
+ - **`design-to-code-typography.md`** -- TextData properties (font size, family, weight, styled segments, hyperlinks, list styles) used in heading detection, segment rendering, and list HTML generation
1082
+ - **`design-to-code-assets.md`** -- Vector container detection that triggers `<img>` tag selection, image fill handling (foreground vs background), asset map for `src` attribute resolution, icon detection heuristic
1083
+ - **`css-strategy.md`** -- Three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules) that consumes the generated styles, property placement decision tree, BEM class names as selectors in Layer 3 CSS Modules.
1084
+ - **`design-tokens.md`** -- Token lookup system for CSS variable substitution in generated styles, semantic color naming, type scale naming, threshold-based promotion.
1085
+ - **`payload-figma-mapping.md`** -- Figma component to PayloadCMS block mapping. The semantic HTML elements chosen by this module (section, article, nav, aside) map to the Container block's `htmlTag` field. Block type identification rules use the same semantic heuristics (heading hierarchy, landmark roles) documented here.