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.
- package/README.md +133 -0
- package/bin/install.js +328 -0
- package/knowledge/README.md +62 -0
- package/knowledge/css-strategy.md +973 -0
- package/knowledge/design-to-code-assets.md +855 -0
- package/knowledge/design-to-code-layout.md +929 -0
- package/knowledge/design-to-code-semantic.md +1085 -0
- package/knowledge/design-to-code-typography.md +1003 -0
- package/knowledge/design-to-code-visual.md +1145 -0
- package/knowledge/design-tokens-variables.md +1261 -0
- package/knowledge/design-tokens.md +960 -0
- package/knowledge/figma-api-devmode.md +894 -0
- package/knowledge/figma-api-plugin.md +920 -0
- package/knowledge/figma-api-rest.md +742 -0
- package/knowledge/figma-api-variables.md +848 -0
- package/knowledge/figma-api-webhooks.md +876 -0
- package/knowledge/payload-blocks.md +1184 -0
- package/knowledge/payload-figma-mapping.md +1210 -0
- package/knowledge/payload-visual-builder.md +1004 -0
- package/knowledge/plugin-architecture.md +1176 -0
- package/knowledge/plugin-best-practices.md +1206 -0
- package/knowledge/plugin-codegen.md +1313 -0
- package/package.json +31 -0
- package/skills/README.md +103 -0
- package/skills/audit-plugin/SKILL.md +244 -0
- package/skills/build-codegen-plugin/SKILL.md +279 -0
- package/skills/build-importer/SKILL.md +320 -0
- package/skills/build-plugin/SKILL.md +199 -0
- package/skills/build-token-pipeline/SKILL.md +363 -0
- package/skills/ref-html/SKILL.md +290 -0
- package/skills/ref-layout/SKILL.md +150 -0
- package/skills/ref-payload-block/SKILL.md +415 -0
- package/skills/ref-react/SKILL.md +222 -0
- 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.
|