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,1210 @@
|
|
|
1
|
+
# Figma to PayloadCMS Mapping
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Authoritative reference for mapping Figma design components to PayloadCMS block structures. Documents the complete pipeline from Figma component identification through property-to-field mapping, container nesting rules, design token bridging, variant mapping, rich text extraction, and media asset handling. This module bridges the design-to-code knowledge (layout, visual, typography, assets, semantic) with the CMS block system (payload-blocks.md) to produce actionable, deterministic mapping rules.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reference this module when you need to:
|
|
10
|
+
|
|
11
|
+
- Convert a Figma page design into a PayloadCMS page block tree
|
|
12
|
+
- Determine which Figma component maps to which PayloadCMS block type
|
|
13
|
+
- Map Figma component properties (variants, text, booleans, instance swaps) to block field values
|
|
14
|
+
- Translate Figma Auto Layout nesting into Container block nesting
|
|
15
|
+
- Extract Figma design tokens and bridge them to the `--token-*` CSS custom properties consumed by blocks
|
|
16
|
+
- Map Figma component variants to block type variants (e.g., Hero impact levels)
|
|
17
|
+
- Convert Figma text content into Lexical rich text editor format
|
|
18
|
+
- Build a Figma-to-PayloadCMS importer plugin or automation
|
|
19
|
+
- Understand the mapping rules that power the `/figma:map-payload-block` skill
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Content
|
|
24
|
+
|
|
25
|
+
### 1. Mapping Architecture Overview
|
|
26
|
+
|
|
27
|
+
The Figma-to-PayloadCMS mapping pipeline transforms a Figma page design into a structured block tree that PayloadCMS can store and render.
|
|
28
|
+
|
|
29
|
+
#### Pipeline Stages
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Figma Page
|
|
33
|
+
|
|
|
34
|
+
v
|
|
35
|
+
[1. Extract] ─────────── Figma REST API / Plugin API
|
|
36
|
+
| Read node tree, properties, styles
|
|
37
|
+
v
|
|
38
|
+
[2. Identify] ────────── Component-to-Block Type Mapping (Section 2)
|
|
39
|
+
| Determine which block type each component becomes
|
|
40
|
+
v
|
|
41
|
+
[3. Map Properties] ──── Property-to-Field Mapping (Sections 3-7)
|
|
42
|
+
| Convert Figma properties to PayloadCMS field values
|
|
43
|
+
v
|
|
44
|
+
[4. Structure] ────────── Container Nesting Rules (Section 4)
|
|
45
|
+
| Build the block hierarchy from Auto Layout nesting
|
|
46
|
+
v
|
|
47
|
+
[5. Extract Content] ─── Rich Text + Media (Sections 8-9)
|
|
48
|
+
| Convert text to Lexical, images to Media collection
|
|
49
|
+
v
|
|
50
|
+
[6. Bridge Tokens] ────── Design Token Bridge (Section 5)
|
|
51
|
+
| Map Figma variables/styles to --token-* properties
|
|
52
|
+
v
|
|
53
|
+
[7. Output] ──────────── PayloadCMS Block Tree
|
|
54
|
+
JSON structure ready for Pages.layout.blocks
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Data Flow Example
|
|
58
|
+
|
|
59
|
+
A Figma frame named "Hero Section" with:
|
|
60
|
+
- Background image fill
|
|
61
|
+
- Overlay rectangle
|
|
62
|
+
- Heading text node ("Welcome to Our Platform")
|
|
63
|
+
- Body text node ("We help teams build faster")
|
|
64
|
+
- Two button instances ("Get Started", "Learn More")
|
|
65
|
+
|
|
66
|
+
Maps to this PayloadCMS block:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"blockType": "hero",
|
|
71
|
+
"content": {
|
|
72
|
+
"richText": { "root": { "children": [...] } },
|
|
73
|
+
"buttonGroup": [
|
|
74
|
+
{ "link": { "type": "custom", "url": "#", "label": "Get Started" } },
|
|
75
|
+
{ "link": { "type": "custom", "url": "#", "label": "Learn More" } }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"image": { "image": "<media-id>" },
|
|
79
|
+
"settings": {
|
|
80
|
+
"type": "highImpact",
|
|
81
|
+
"className": "",
|
|
82
|
+
"layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0" }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 2. Component-to-Block Type Mapping
|
|
90
|
+
|
|
91
|
+
Given a Figma node, the following decision tree determines which PayloadCMS block type it maps to. Rules are evaluated in priority order -- the first matching rule wins.
|
|
92
|
+
|
|
93
|
+
#### Decision Tree
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Input: Figma SceneNode
|
|
97
|
+
|
|
|
98
|
+
├─ Is it a COMPONENT_SET or COMPONENT with variant properties?
|
|
99
|
+
| ├─ YES: Check component name and structure against block patterns
|
|
100
|
+
| └─ NO: Analyze frame structure heuristically
|
|
101
|
+
|
|
|
102
|
+
v
|
|
103
|
+
[Rule 1] Hero Detection (confidence: 0.9)
|
|
104
|
+
Match IF:
|
|
105
|
+
- Frame/component name contains "hero" (case-insensitive) OR
|
|
106
|
+
- Frame is full-width (width >= parent width) AND has:
|
|
107
|
+
- Background image fill OR background color fill
|
|
108
|
+
- At least one large text node (fontSize >= 28px)
|
|
109
|
+
- At least one button-like child
|
|
110
|
+
Block: hero
|
|
111
|
+
|
|
|
112
|
+
v
|
|
113
|
+
[Rule 2] Card Detection (confidence: 0.85)
|
|
114
|
+
Match IF:
|
|
115
|
+
- Frame/component name contains "card" (case-insensitive) OR
|
|
116
|
+
- Frame has ALL of:
|
|
117
|
+
- Bounded width (not full-width, typically 200-500px)
|
|
118
|
+
- Image child (image fill or image frame)
|
|
119
|
+
- Text content (heading + body text)
|
|
120
|
+
- Optional CTA/button child
|
|
121
|
+
- Border radius > 0 OR box-shadow
|
|
122
|
+
Block: card
|
|
123
|
+
|
|
|
124
|
+
v
|
|
125
|
+
[Rule 3] Button Detection (confidence: 0.95)
|
|
126
|
+
Match IF:
|
|
127
|
+
- Frame/component name contains "button" or "btn" (case-insensitive) OR
|
|
128
|
+
- Frame has ALL of:
|
|
129
|
+
- Small dimensions (height < 60px, width < 300px)
|
|
130
|
+
- Background fill (solid color)
|
|
131
|
+
- Single text child (short label text, < 30 characters)
|
|
132
|
+
- Border radius > 0
|
|
133
|
+
- No image children
|
|
134
|
+
Block: button
|
|
135
|
+
|
|
|
136
|
+
v
|
|
137
|
+
[Rule 4] Navigation Detection (confidence: 0.85)
|
|
138
|
+
Match IF:
|
|
139
|
+
- Frame/component name contains "nav", "navigation", "menu" (case-insensitive) OR
|
|
140
|
+
- Frame has:
|
|
141
|
+
- Horizontal Auto Layout
|
|
142
|
+
- Multiple small text/link children (3-8 items)
|
|
143
|
+
- No image children
|
|
144
|
+
- Compact height (< 80px)
|
|
145
|
+
Block: subnavigation
|
|
146
|
+
|
|
|
147
|
+
v
|
|
148
|
+
[Rule 5] Accordion Detection (confidence: 0.8)
|
|
149
|
+
Match IF:
|
|
150
|
+
- Frame/component name contains "accordion", "collapse", "expand" OR
|
|
151
|
+
- Frame has:
|
|
152
|
+
- Vertical Auto Layout
|
|
153
|
+
- Repeating child pattern: header row + content area
|
|
154
|
+
- Toggle icon (chevron, plus/minus) in header
|
|
155
|
+
Block: accordion
|
|
156
|
+
|
|
|
157
|
+
v
|
|
158
|
+
[Rule 6] Tabs Detection (confidence: 0.8)
|
|
159
|
+
Match IF:
|
|
160
|
+
- Frame/component name contains "tab" (case-insensitive) OR
|
|
161
|
+
- Frame has:
|
|
162
|
+
- Tab bar (horizontal row of text labels with active/inactive states)
|
|
163
|
+
- Content panel below tab bar
|
|
164
|
+
- Component variants for active tab state
|
|
165
|
+
Block: tabs
|
|
166
|
+
|
|
|
167
|
+
v
|
|
168
|
+
[Rule 7] Stats/Metric Detection (confidence: 0.85)
|
|
169
|
+
Match IF:
|
|
170
|
+
- Frame/component name contains "stat", "metric", "number" OR
|
|
171
|
+
- Frame has:
|
|
172
|
+
- Large prominent text (fontSize >= 32px, often numeric)
|
|
173
|
+
- Smaller label text below
|
|
174
|
+
- Optional description and source text
|
|
175
|
+
- Compact, card-like dimensions
|
|
176
|
+
Block: stats
|
|
177
|
+
|
|
|
178
|
+
v
|
|
179
|
+
[Rule 8] Testimonial Detection (confidence: 0.85)
|
|
180
|
+
Match IF:
|
|
181
|
+
- Frame/component name contains "testimonial", "quote", "review" OR
|
|
182
|
+
- Frame has:
|
|
183
|
+
- Quote icon or quotation marks
|
|
184
|
+
- Body text (italic or quoted)
|
|
185
|
+
- Author name and title text
|
|
186
|
+
- Optional avatar image (small, circular)
|
|
187
|
+
Block: testimonial
|
|
188
|
+
|
|
|
189
|
+
v
|
|
190
|
+
[Rule 9] Carousel/Slider Detection (confidence: 0.8)
|
|
191
|
+
Match IF:
|
|
192
|
+
- Frame/component name contains "carousel", "slider", "swiper" OR
|
|
193
|
+
- Frame has:
|
|
194
|
+
- Horizontal overflow (clipContent: true, children extend beyond bounds)
|
|
195
|
+
- Navigation dots or arrows
|
|
196
|
+
- Multiple card-like children at same size
|
|
197
|
+
Block: carousel
|
|
198
|
+
|
|
|
199
|
+
v
|
|
200
|
+
[Rule 10] CallToAction Detection (confidence: 0.8)
|
|
201
|
+
Match IF:
|
|
202
|
+
- Frame/component name contains "cta", "callToAction", "banner" OR
|
|
203
|
+
- Frame has:
|
|
204
|
+
- Full-width or near-full-width
|
|
205
|
+
- Strong background color (not white/neutral)
|
|
206
|
+
- Title text + description
|
|
207
|
+
- Optional image on one side
|
|
208
|
+
- No nested complex blocks
|
|
209
|
+
Block: callToAction
|
|
210
|
+
|
|
|
211
|
+
v
|
|
212
|
+
[Rule 11] Media/Image Detection (confidence: 0.9)
|
|
213
|
+
Match IF:
|
|
214
|
+
- Node is an image fill frame with no text children OR
|
|
215
|
+
- Frame/component name contains "image", "photo", "illustration" OR
|
|
216
|
+
- Frame has:
|
|
217
|
+
- Single image fill or single image child
|
|
218
|
+
- No text content
|
|
219
|
+
- No interactive children
|
|
220
|
+
Block: media
|
|
221
|
+
|
|
|
222
|
+
v
|
|
223
|
+
[Rule 12] Video Detection (confidence: 0.85)
|
|
224
|
+
Match IF:
|
|
225
|
+
- Frame/component name contains "video", "player", "embed" OR
|
|
226
|
+
- Frame has:
|
|
227
|
+
- Play button icon overlay
|
|
228
|
+
- 16:9 or similar video aspect ratio
|
|
229
|
+
- Thumbnail image with play indicator
|
|
230
|
+
Block: video
|
|
231
|
+
|
|
|
232
|
+
v
|
|
233
|
+
[Rule 13] Form Detection (confidence: 0.85)
|
|
234
|
+
Match IF:
|
|
235
|
+
- Frame/component name contains "form", "input", "signup" OR
|
|
236
|
+
- Frame has:
|
|
237
|
+
- Multiple input field components
|
|
238
|
+
- Submit button
|
|
239
|
+
- Label + input pairs
|
|
240
|
+
Block: form
|
|
241
|
+
|
|
|
242
|
+
v
|
|
243
|
+
[Rule 14] RichText Fallback (confidence: 0.7)
|
|
244
|
+
Match IF:
|
|
245
|
+
- Frame contains primarily text content OR
|
|
246
|
+
- Node is a TEXT type with multiple styled segments OR
|
|
247
|
+
- Frame has only text children (no images, no interactive elements)
|
|
248
|
+
Block: richText
|
|
249
|
+
|
|
|
250
|
+
v
|
|
251
|
+
[Rule 15] Container Fallback (confidence: 0.6)
|
|
252
|
+
Match IF:
|
|
253
|
+
- Frame has Auto Layout AND contains multiple children that
|
|
254
|
+
individually match other block types
|
|
255
|
+
- This is the "structural wrapper" -- it groups children
|
|
256
|
+
Block: container
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Confidence Scores
|
|
260
|
+
|
|
261
|
+
Each rule has a confidence score (0.0-1.0) that indicates how reliably the heuristic identifies the correct block type. When implementing automated mapping:
|
|
262
|
+
|
|
263
|
+
- **0.9+** -- High confidence, auto-map without confirmation
|
|
264
|
+
- **0.8-0.89** -- Good confidence, auto-map with optional review flag
|
|
265
|
+
- **0.7-0.79** -- Moderate confidence, flag for human review
|
|
266
|
+
- **Below 0.7** -- Low confidence, require human selection
|
|
267
|
+
|
|
268
|
+
#### Name-Based vs Structure-Based Detection
|
|
269
|
+
|
|
270
|
+
The decision tree uses two complementary strategies:
|
|
271
|
+
|
|
272
|
+
1. **Name-based** (higher priority) -- If the Figma component or frame name contains a recognizable keyword (hero, card, button, etc.), that provides strong signal. Designers who name their components well make mapping trivial.
|
|
273
|
+
|
|
274
|
+
2. **Structure-based** (fallback) -- When names are generic (e.g., "Frame 427"), analyze the node's children, properties, and layout structure to infer the block type. This is less reliable but handles unnamed designs.
|
|
275
|
+
|
|
276
|
+
Always check name-based rules first, then fall back to structural analysis.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### 3. Property-to-Field Mapping Tables
|
|
281
|
+
|
|
282
|
+
For each block type, these tables define how Figma properties map to PayloadCMS field values.
|
|
283
|
+
|
|
284
|
+
#### Container Mapping
|
|
285
|
+
|
|
286
|
+
The Container block is the most direct mapping from Figma Auto Layout.
|
|
287
|
+
|
|
288
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
289
|
+
|---------------|-----------------|--------------|
|
|
290
|
+
| `layoutMode: HORIZONTAL` | `settings.layout` = `'row'` | Direct |
|
|
291
|
+
| `layoutMode: VERTICAL` | `settings.layout` = `'col'` | Direct |
|
|
292
|
+
| `layoutMode: NONE` + grid-like children | `settings.layout` = `'grid'` | Inferred |
|
|
293
|
+
| `primaryAxisAlignItems: MIN` | `settings.justifyContent` = `'justify-start'` | Direct |
|
|
294
|
+
| `primaryAxisAlignItems: CENTER` | `settings.justifyContent` = `'justify-center'` | Direct |
|
|
295
|
+
| `primaryAxisAlignItems: MAX` | `settings.justifyContent` = `'justify-end'` | Direct |
|
|
296
|
+
| `primaryAxisAlignItems: SPACE_BETWEEN` | `settings.justifyContent` = `'justify-between'` | Direct |
|
|
297
|
+
| `counterAxisAlignItems: MIN` | `settings.alignItems` = `'items-start'` | Direct |
|
|
298
|
+
| `counterAxisAlignItems: CENTER` | `settings.alignItems` = `'items-center'` | Direct |
|
|
299
|
+
| `counterAxisAlignItems: MAX` | `settings.alignItems` = `'items-end'` | Direct |
|
|
300
|
+
| `counterAxisAlignItems: BASELINE` | `settings.alignItems` = `'items-baseline'` | Direct |
|
|
301
|
+
| `itemSpacing` | `settings.gap` | Snap to nearest option (see below) |
|
|
302
|
+
| Frame name containing "section" | `settings.htmlTag` = `'section'` | Name heuristic |
|
|
303
|
+
| Frame name containing "nav" | `settings.htmlTag` = `'nav'` or `'header'` | Name heuristic |
|
|
304
|
+
| Frame name containing "footer" | `settings.htmlTag` = `'footer'` | Name heuristic |
|
|
305
|
+
| Frame name containing "aside" | `settings.htmlTag` = `'aside'` | Name heuristic |
|
|
306
|
+
| Frame width = parent width | `settings.width` = `'full'` | Size analysis |
|
|
307
|
+
| Frame maxWidth ~1400px | `settings.width` = `'wide'` | Size analysis |
|
|
308
|
+
| Frame maxWidth ~800px | `settings.width` = `'narrow'` | Size analysis |
|
|
309
|
+
| `paddingTop` | `settings.layoutMeta.paddingTop` | Snap to nearest |
|
|
310
|
+
| `paddingBottom` | `settings.layoutMeta.paddingBottom` | Snap to nearest |
|
|
311
|
+
|
|
312
|
+
**Gap snapping rule:** Map Figma `itemSpacing` to the nearest gap option:
|
|
313
|
+
|
|
314
|
+
| Figma itemSpacing (px) | PayloadCMS gap |
|
|
315
|
+
|------------------------|---------------|
|
|
316
|
+
| 0 | `gap-0` |
|
|
317
|
+
| 1-3 | `gap-1` |
|
|
318
|
+
| 4-6 | `gap-2` |
|
|
319
|
+
| 7-12 | `gap-4` |
|
|
320
|
+
| 13-20 | `gap-6` |
|
|
321
|
+
| 21-28 | `gap-8` |
|
|
322
|
+
| 29-40 | `gap-12` |
|
|
323
|
+
| 41+ | `gap-16` |
|
|
324
|
+
|
|
325
|
+
**Spacing snapping rule:** Map Figma padding/margin values to LayoutMeta options:
|
|
326
|
+
|
|
327
|
+
| Figma value (px) | PayloadCMS value |
|
|
328
|
+
|------------------|-----------------|
|
|
329
|
+
| 0 | `{prefix}-0` |
|
|
330
|
+
| 1-6 | `{prefix}-2` (XS) |
|
|
331
|
+
| 7-12 | `{prefix}-4` (SM) |
|
|
332
|
+
| 13-20 | `{prefix}-6` (MD) |
|
|
333
|
+
| 21-28 | `{prefix}-8` (LG) |
|
|
334
|
+
| 29-40 | `{prefix}-12` (XL) |
|
|
335
|
+
| 41+ | `{prefix}-16` (2XL) |
|
|
336
|
+
|
|
337
|
+
Where `{prefix}` is `mt`, `mb`, `pt`, or `pb`.
|
|
338
|
+
|
|
339
|
+
#### Hero Mapping
|
|
340
|
+
|
|
341
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
342
|
+
|---------------|-----------------|--------------|
|
|
343
|
+
| Large heading text node | `content.richText` | Extract as Lexical heading |
|
|
344
|
+
| Body text node | `content.richText` | Extract as Lexical paragraph |
|
|
345
|
+
| Button instances | `content.buttonGroup[]` | Map each to linkGroup item |
|
|
346
|
+
| Background image fill | `image.image` | Export and upload to Media |
|
|
347
|
+
| Component variant "impact" = "high" | `settings.type` = `'highImpact'` | Variant mapping |
|
|
348
|
+
| Component variant "impact" = "medium" | `settings.type` = `'mediumImpact'` | Variant mapping |
|
|
349
|
+
| Component variant "impact" = "low" | `settings.type` = `'lowImpact'` | Variant mapping |
|
|
350
|
+
| Frame height >= 400px | `settings.type` = `'highImpact'` | Size heuristic |
|
|
351
|
+
| Frame height 250-399px | `settings.type` = `'mediumImpact'` | Size heuristic |
|
|
352
|
+
| Frame height < 250px | `settings.type` = `'lowImpact'` | Size heuristic |
|
|
353
|
+
|
|
354
|
+
#### Card Mapping
|
|
355
|
+
|
|
356
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
357
|
+
|---------------|-----------------|--------------|
|
|
358
|
+
| Text content (heading + body) | `content.richText` | Extract as Lexical |
|
|
359
|
+
| Image child/fill | `image.image` | Export and upload |
|
|
360
|
+
| Button/CTA child text | `cta.label` | Extract text |
|
|
361
|
+
| Button links to page | `cta.ctaAction` = `'internal'`, `cta.internalLink` | If detectable |
|
|
362
|
+
| Button links to URL | `cta.ctaAction` = `'external'`, `cta.externalLink` | If detectable |
|
|
363
|
+
| CTA wraps entire card | `cta.layout` = `'wrap-all'` | If entire card is clickable |
|
|
364
|
+
| CTA wraps text area only | `cta.layout` = `'wrap-text-description'` | Default |
|
|
365
|
+
|
|
366
|
+
#### Button Mapping
|
|
367
|
+
|
|
368
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
369
|
+
|---------------|-----------------|--------------|
|
|
370
|
+
| Button text | `cta.label` | Extract text content |
|
|
371
|
+
| Links to internal page | `cta.type` = `'link'`, `cta.link.linkType` = `'internal'` | If detectable |
|
|
372
|
+
| Links to external URL | `cta.type` = `'link'`, `cta.link.linkType` = `'external'` | If URL present |
|
|
373
|
+
| Opens modal (no link) | `cta.type` = `'button'`, `cta.action.actionType` = `'modal'` | If no navigation |
|
|
374
|
+
| Scrolls to section | `cta.type` = `'button'`, `cta.action.actionType` = `'scroll'` | If anchor link |
|
|
375
|
+
|
|
376
|
+
#### Stats Mapping
|
|
377
|
+
|
|
378
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
379
|
+
|---------------|-----------------|--------------|
|
|
380
|
+
| Large number/metric text | `content.emphasis` | Extract text (largest font size) |
|
|
381
|
+
| Title text | `content.title` | Extract text (second largest) |
|
|
382
|
+
| Description text | `content.richText` | Extract as Lexical paragraph |
|
|
383
|
+
| Source/citation text | `content.source` | Extract text (smallest font size, often italic) |
|
|
384
|
+
|
|
385
|
+
#### Testimonial Mapping
|
|
386
|
+
|
|
387
|
+
| Figma Property | PayloadCMS Field | Mapping Rule |
|
|
388
|
+
|---------------|-----------------|--------------|
|
|
389
|
+
| Quote text (italic/body) | `content.richText` | Extract as Lexical |
|
|
390
|
+
| Author name text | `content.author` | Extract text (bold, below quote) |
|
|
391
|
+
| Author title/role text | `content.authorDescription` | Extract text (below author name) |
|
|
392
|
+
| Avatar image (small, circular) | `image.image` | Export and upload |
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
### 4. Container Nesting Rules
|
|
397
|
+
|
|
398
|
+
Figma designs naturally have deeply nested Auto Layout frames. PayloadCMS has a maximum nesting depth of 2 for Container blocks. These rules determine when to nest and when to flatten.
|
|
399
|
+
|
|
400
|
+
#### Nesting Decision Tree
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
For each Auto Layout frame in the Figma hierarchy:
|
|
404
|
+
|
|
405
|
+
1. IS IT THE TOP-LEVEL PAGE FRAME?
|
|
406
|
+
YES → Its children become root layout.blocks entries
|
|
407
|
+
(Do not create a Container for the page frame itself)
|
|
408
|
+
|
|
409
|
+
2. DOES IT CONTAIN ONLY ONE CHILD THAT MAPS TO A BLOCK?
|
|
410
|
+
YES → Flatten: emit the child block directly, skip the wrapper
|
|
411
|
+
Rationale: A Container with one child adds no structural value
|
|
412
|
+
|
|
413
|
+
3. DOES IT CONTAIN MULTIPLE CHILDREN OF MIXED TYPES?
|
|
414
|
+
YES → Create a Container block
|
|
415
|
+
- Set layout/alignment/gap from the frame's Auto Layout props
|
|
416
|
+
- Recursively map children as the Container's blocks
|
|
417
|
+
Rationale: This is the primary use case for Container
|
|
418
|
+
|
|
419
|
+
4. DOES IT CONTAIN CHILDREN THAT ARE ALL THE SAME TYPE?
|
|
420
|
+
YES → Consider alternatives:
|
|
421
|
+
- All Cards → Container(row) with Card children
|
|
422
|
+
- All Stats → Container(row) with Stats children
|
|
423
|
+
- All items with images → Carousel (if horizontal scroll pattern)
|
|
424
|
+
Rationale: Homogeneous children often indicate a gallery or grid
|
|
425
|
+
|
|
426
|
+
5. IS THE NESTING DEPTH > 2?
|
|
427
|
+
YES → Flatten intermediate wrappers
|
|
428
|
+
- Keep the outermost Container and the innermost content blocks
|
|
429
|
+
- Collapse intermediate Auto Layout frames by merging their
|
|
430
|
+
layout properties into the nearest preserved Container
|
|
431
|
+
Rationale: PayloadCMS supports Container > NestedContainer (depth 2)
|
|
432
|
+
but not deeper nesting
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### Flattening Strategy
|
|
436
|
+
|
|
437
|
+
When Figma has 3+ levels of nesting:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
Figma:
|
|
441
|
+
Frame A (vertical, padding 32px)
|
|
442
|
+
Frame B (horizontal, gap 24px)
|
|
443
|
+
Frame C (vertical, gap 16px)
|
|
444
|
+
Text "Hello"
|
|
445
|
+
Text "World"
|
|
446
|
+
Frame D (vertical, gap 16px)
|
|
447
|
+
Image
|
|
448
|
+
Text "Caption"
|
|
449
|
+
|
|
450
|
+
PayloadCMS (flattened):
|
|
451
|
+
Container (layout: row, gap: gap-6, paddingTop: pt-8, paddingBottom: pb-8)
|
|
452
|
+
Container (layout: col, gap: gap-4)
|
|
453
|
+
RichText ("Hello\nWorld")
|
|
454
|
+
Container (layout: col, gap: gap-4)
|
|
455
|
+
Media (image)
|
|
456
|
+
RichText ("Caption")
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Frame A and Frame B are merged: A's padding becomes LayoutMeta on the outer Container, B's row layout and gap become the outer Container's settings. Frame C and Frame D each become NestedContainers.
|
|
460
|
+
|
|
461
|
+
#### When to Flatten vs Preserve
|
|
462
|
+
|
|
463
|
+
| Scenario | Action | Reason |
|
|
464
|
+
|----------|--------|--------|
|
|
465
|
+
| Single child in Auto Layout wrapper | Flatten | No structural purpose |
|
|
466
|
+
| Two levels of nesting | Preserve both | Within PayloadCMS limit |
|
|
467
|
+
| Three+ levels of nesting | Merge intermediate levels | Exceeds Container depth limit |
|
|
468
|
+
| Auto Layout frame with only padding (no layout purpose) | Apply padding as LayoutMeta to child | Wrapper adds only spacing |
|
|
469
|
+
| Frame groups items for alignment only | Preserve if it changes layout direction | Container settings carry alignment |
|
|
470
|
+
|
|
471
|
+
#### Group Detection
|
|
472
|
+
|
|
473
|
+
When multiple sibling elements share a common structural container in Figma but would be top-level blocks in PayloadCMS, detect the grouping pattern:
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
Figma: Section frame (vertical)
|
|
477
|
+
├── Heading text
|
|
478
|
+
├── Description text
|
|
479
|
+
└── Cards row (horizontal)
|
|
480
|
+
├── Card 1
|
|
481
|
+
├── Card 2
|
|
482
|
+
└── Card 3
|
|
483
|
+
|
|
484
|
+
PayloadCMS:
|
|
485
|
+
Container (layout: col)
|
|
486
|
+
├── RichText (heading + description)
|
|
487
|
+
└── Container (layout: row)
|
|
488
|
+
├── Card 1
|
|
489
|
+
├── Card 2
|
|
490
|
+
└── Card 3
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
The "Cards row" frame becomes a nested Container because it changes the layout direction from the parent's column to row.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### 5. Design Token Bridge
|
|
498
|
+
|
|
499
|
+
Figma design tokens flow into PayloadCMS blocks through the `tokens.css` file. This section defines the extraction and mapping rules.
|
|
500
|
+
|
|
501
|
+
#### Token Flow Pipeline
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
Figma Variables / Styles
|
|
505
|
+
|
|
|
506
|
+
v
|
|
507
|
+
[Extract] ──── Figma REST API (GET /v1/files/:key/variables)
|
|
508
|
+
| or Plugin API (figma.variables.getLocalVariables())
|
|
509
|
+
v
|
|
510
|
+
[Name] ─────── Apply --token-{category}-{name} naming
|
|
511
|
+
| See design-tokens.md for naming algorithm
|
|
512
|
+
v
|
|
513
|
+
[Render] ────── Generate tokens.css with :root declarations
|
|
514
|
+
|
|
|
515
|
+
v
|
|
516
|
+
[Consume] ──── Block CSS Modules reference via var(--token-*)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### Category Mapping
|
|
520
|
+
|
|
521
|
+
| Figma Source | Token Category | CSS Custom Property | Example |
|
|
522
|
+
|-------------|---------------|--------------------|---------|
|
|
523
|
+
| Color styles / color variables | `--token-color-*` | `--token-color-primary` | `#3b82f6` |
|
|
524
|
+
| Spacing variables | `--token-spacing-*` | `--token-spacing-md` | `16px` |
|
|
525
|
+
| Font size variables | `--token-font-size-*` | `--token-font-size-lg` | `20px` |
|
|
526
|
+
| Font family styles | `--token-font-*` | `--token-font-family` | `system-ui, ...` |
|
|
527
|
+
| Line height variables | `--token-line-height-*` | `--token-line-height-base` | `32px` |
|
|
528
|
+
| Border radius variables | `--token-radius-*` | `--token-radius-lg` | `12px` |
|
|
529
|
+
| Effect styles (shadows) | `--token-shadow-*` | `--token-shadow-md` | `0 4px 6px ...` |
|
|
530
|
+
| Transition tokens | `--token-transition-*` | `--token-transition-fast` | `150ms ease` |
|
|
531
|
+
|
|
532
|
+
#### Figma Value to Token Resolution
|
|
533
|
+
|
|
534
|
+
When mapping a Figma design to PayloadCMS blocks, extracted values must be resolved to their token equivalents:
|
|
535
|
+
|
|
536
|
+
1. **Check for variable binding** -- If a Figma node property is bound to a variable (e.g., `fills[0].color` bound to `primitives/blue-500`), resolve the variable to its token name.
|
|
537
|
+
|
|
538
|
+
2. **Check for style reference** -- If the node references a Figma style (color style, text style, effect style), map the style name to the token name.
|
|
539
|
+
|
|
540
|
+
3. **Check for value match** -- If the raw value matches a known token value (e.g., `#3b82f6` matches `--token-color-primary`), use the token reference.
|
|
541
|
+
|
|
542
|
+
4. **No match** -- If the value does not match any token, use the raw value with a fallback. Flag for potential token promotion.
|
|
543
|
+
|
|
544
|
+
#### Gap Token Mapping
|
|
545
|
+
|
|
546
|
+
The Container block stores gap as Tailwind classes, not token references. The mapping from Figma to Tailwind gap classes is:
|
|
547
|
+
|
|
548
|
+
| Figma itemSpacing | Tailwind Gap Class | Equivalent rem |
|
|
549
|
+
|-------------------|-------------------|----------------|
|
|
550
|
+
| 0px | `gap-0` | 0 |
|
|
551
|
+
| 4px | `gap-1` | 0.25rem |
|
|
552
|
+
| 8px | `gap-2` | 0.5rem |
|
|
553
|
+
| 16px | `gap-4` | 1rem |
|
|
554
|
+
| 24px | `gap-6` | 1.5rem |
|
|
555
|
+
| 32px | `gap-8` | 2rem |
|
|
556
|
+
| 48px | `gap-12` | 3rem |
|
|
557
|
+
| 64px | `gap-16` | 4rem |
|
|
558
|
+
|
|
559
|
+
Non-exact values snap to the nearest option (see Section 3 snapping rules).
|
|
560
|
+
|
|
561
|
+
#### LayoutMeta Token Mapping
|
|
562
|
+
|
|
563
|
+
LayoutMeta spacing values are also Tailwind classes. The mapping from Figma padding/margin:
|
|
564
|
+
|
|
565
|
+
| Figma padding (px) | LayoutMeta value | Tailwind rem |
|
|
566
|
+
|--------------------|--------------------|-------------|
|
|
567
|
+
| 0 | `pt-0` / `pb-0` / `mt-0` / `mb-0` | 0 |
|
|
568
|
+
| 8px | `pt-2` / `pb-2` / etc. | 0.5rem |
|
|
569
|
+
| 16px | `pt-4` / `pb-4` / etc. | 1rem |
|
|
570
|
+
| 24px | `pt-6` / `pb-6` / etc. | 1.5rem |
|
|
571
|
+
| 32px | `pt-8` / `pb-8` / etc. | 2rem |
|
|
572
|
+
| 48px | `pt-12` / `pb-12` / etc. | 3rem |
|
|
573
|
+
| 64px | `pt-16` / `pb-16` / etc. | 4rem |
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
### 6. Variant Mapping (CVA Pattern)
|
|
578
|
+
|
|
579
|
+
Figma component variants map to PayloadCMS block field variants, which are then rendered via CSS Module modifier classes (following the CVA -- Class Variance Authority -- pattern).
|
|
580
|
+
|
|
581
|
+
#### Figma Variants to Block Fields
|
|
582
|
+
|
|
583
|
+
| Figma Variant Property | PayloadCMS Block | Field | Field Type | Options |
|
|
584
|
+
|-----------------------|-----------------|-------|-----------|---------|
|
|
585
|
+
| `impact` (high/medium/low) | Hero | `settings.type` | radio | highImpact, mediumImpact, lowImpact |
|
|
586
|
+
| `size` (fixed/responsive) | Video | `settings.videoSize` | radio | fixed, responsive |
|
|
587
|
+
| `state` (open/closed) | Accordion | `settings.defaultState` | radio | open, closed |
|
|
588
|
+
| `ctaType` (button/link) | Button | `cta.type` | radio | button, link |
|
|
589
|
+
| `appearance` (default/outline) | Link (field factory) | `link.appearance` | select | default, outline |
|
|
590
|
+
|
|
591
|
+
#### Variant Detection Rules
|
|
592
|
+
|
|
593
|
+
1. **Named variant properties** -- If a Figma component set has variant properties (e.g., `impact=high`, `size=large`), map each property to the corresponding block select/radio field.
|
|
594
|
+
|
|
595
|
+
2. **Boolean variant properties** -- Figma boolean properties (e.g., `hasImage=true`) map to block checkbox fields or conditional field visibility.
|
|
596
|
+
|
|
597
|
+
3. **Instance swap properties** -- Figma instance swap slots (e.g., `icon=<IconComponent>`) map to upload or relationship fields.
|
|
598
|
+
|
|
599
|
+
#### CVA Rendering Pattern
|
|
600
|
+
|
|
601
|
+
Block renderers use variant values to select CSS Module classes:
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
// Hero renderer applies variant-based classes
|
|
605
|
+
const impactClass = {
|
|
606
|
+
highImpact: styles.heroContainerHigh, // min-height: 400px
|
|
607
|
+
mediumImpact: styles.heroContainerMedium, // min-height: 300px
|
|
608
|
+
lowImpact: styles.heroContainerLow, // min-height: 200px
|
|
609
|
+
}[block.settings.type]
|
|
610
|
+
|
|
611
|
+
return <div className={cn(styles.heroContainer, impactClass)}>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
The CSS Module defines the modifier classes:
|
|
615
|
+
|
|
616
|
+
```css
|
|
617
|
+
.heroContainerHigh { min-height: 400px; }
|
|
618
|
+
.heroContainerMedium { min-height: 300px; }
|
|
619
|
+
.heroContainerLow { min-height: 200px; }
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
This is the CVA pattern: the block field value selects the CSS class variant.
|
|
623
|
+
|
|
624
|
+
#### Mapping Figma Variants When Names Do Not Match
|
|
625
|
+
|
|
626
|
+
When Figma variant property names differ from block field names, use this resolution order:
|
|
627
|
+
|
|
628
|
+
1. **Exact match** -- Figma variant property name matches block field name (e.g., both called "type")
|
|
629
|
+
2. **Semantic match** -- Figma "variant" or "style" maps to block "type"
|
|
630
|
+
3. **Value match** -- Check if Figma variant values match block option values (e.g., Figma "High" matches block "highImpact")
|
|
631
|
+
4. **Size heuristic** -- If no variant property exists, infer from dimensions (Hero height, Container width)
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
### 7. Instance and Slot Mapping
|
|
636
|
+
|
|
637
|
+
Figma component instances and instance swap properties map to block references and nested block content.
|
|
638
|
+
|
|
639
|
+
#### Component Instance to Nested Block
|
|
640
|
+
|
|
641
|
+
When a Figma component instance appears as a child of another component, it maps to a nested block within the parent's blocks field:
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
Figma:
|
|
645
|
+
Hero Component
|
|
646
|
+
└── Button Instance (text: "Get Started")
|
|
647
|
+
└── Button Instance (text: "Learn More")
|
|
648
|
+
|
|
649
|
+
PayloadCMS:
|
|
650
|
+
Hero block
|
|
651
|
+
content.buttonGroup[0].link.label = "Get Started"
|
|
652
|
+
content.buttonGroup[1].link.label = "Learn More"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
For Container blocks, each child instance becomes a block entry in `layout.content.blocks`.
|
|
656
|
+
|
|
657
|
+
#### Instance Overrides to Field Values
|
|
658
|
+
|
|
659
|
+
Figma instance overrides (text overrides, fill overrides, visibility overrides) map to field values:
|
|
660
|
+
|
|
661
|
+
| Figma Override Type | PayloadCMS Mapping |
|
|
662
|
+
|--------------------|--------------------|
|
|
663
|
+
| Text override | Corresponding text or richText field value |
|
|
664
|
+
| Fill override | Token reference if color matches, otherwise CSS Module customization |
|
|
665
|
+
| Visibility override (hidden) | `settings.layoutMeta.hidden = true` |
|
|
666
|
+
| Component swap | Different block type in the blocks array |
|
|
667
|
+
| Nested instance override | Recursive field value mapping |
|
|
668
|
+
|
|
669
|
+
#### Slot Pattern
|
|
670
|
+
|
|
671
|
+
Figma instance swap properties (where a component exposes a slot for swapping child instances) map to block relationship or blocks fields:
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
Figma Component: Card
|
|
675
|
+
Instance Swap Property: "icon" → accepts Icon components
|
|
676
|
+
Instance Swap Property: "ctaButton" → accepts Button components
|
|
677
|
+
|
|
678
|
+
PayloadCMS:
|
|
679
|
+
Card block:
|
|
680
|
+
image.image → Upload field (for icon slot)
|
|
681
|
+
cta → CTA group fields (for ctaButton slot)
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
The key insight is that Figma's instance swap property is a visual composition mechanism, while PayloadCMS uses typed fields. The mapping must translate the swap slot into the appropriate field type.
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
### 8. Rich Text Extraction
|
|
689
|
+
|
|
690
|
+
Figma text nodes are converted to Lexical editor content for PayloadCMS richText fields.
|
|
691
|
+
|
|
692
|
+
#### Text Node to Lexical Mapping
|
|
693
|
+
|
|
694
|
+
```
|
|
695
|
+
Figma TEXT Node
|
|
696
|
+
|
|
|
697
|
+
├── Extract styledTextSegments
|
|
698
|
+
| Each segment: { characters, fontSize, fontWeight, italic, textDecoration, hyperlink }
|
|
699
|
+
|
|
|
700
|
+
v
|
|
701
|
+
[Determine Block Type]
|
|
702
|
+
fontSize >= 28px AND fontWeight >= 700 → Lexical HeadingNode (h1/h2)
|
|
703
|
+
fontSize >= 24px AND fontWeight >= 600 → Lexical HeadingNode (h2/h3)
|
|
704
|
+
fontSize >= 20px AND fontWeight >= 600 → Lexical HeadingNode (h3/h4)
|
|
705
|
+
Otherwise → Lexical ParagraphNode
|
|
706
|
+
|
|
|
707
|
+
v
|
|
708
|
+
[Map Text Formatting]
|
|
709
|
+
fontWeight >= 700 → Bold format
|
|
710
|
+
italic: true → Italic format
|
|
711
|
+
textDecoration: UNDERLINE → Underline format
|
|
712
|
+
textDecoration: STRIKETHROUGH → Strikethrough format
|
|
713
|
+
hyperlink defined → LinkNode wrapping text
|
|
714
|
+
|
|
|
715
|
+
v
|
|
716
|
+
[Assemble Lexical JSON]
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
#### Heading Hierarchy Detection
|
|
720
|
+
|
|
721
|
+
When multiple text nodes exist in a component, determine heading levels from font size relationships:
|
|
722
|
+
|
|
723
|
+
```
|
|
724
|
+
Text Nodes in Hero:
|
|
725
|
+
"Welcome to Our Platform" (fontSize: 48px, fontWeight: 700) → h1
|
|
726
|
+
"We help teams build faster" (fontSize: 20px, fontWeight: 400) → paragraph
|
|
727
|
+
|
|
728
|
+
Text Nodes in Card:
|
|
729
|
+
"Feature Title" (fontSize: 24px, fontWeight: 600) → h3
|
|
730
|
+
"Description text here" (fontSize: 16px, fontWeight: 400) → paragraph
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The rule: within a single block context, the largest font size becomes the highest heading level, and progressively smaller sizes become lower headings or paragraphs.
|
|
734
|
+
|
|
735
|
+
#### Lexical JSON Structure
|
|
736
|
+
|
|
737
|
+
PayloadCMS Lexical content follows this JSON structure:
|
|
738
|
+
|
|
739
|
+
```json
|
|
740
|
+
{
|
|
741
|
+
"root": {
|
|
742
|
+
"type": "root",
|
|
743
|
+
"children": [
|
|
744
|
+
{
|
|
745
|
+
"type": "heading",
|
|
746
|
+
"tag": "h2",
|
|
747
|
+
"children": [
|
|
748
|
+
{
|
|
749
|
+
"type": "text",
|
|
750
|
+
"text": "Welcome to Our Platform",
|
|
751
|
+
"format": 1
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
"type": "paragraph",
|
|
757
|
+
"children": [
|
|
758
|
+
{
|
|
759
|
+
"type": "text",
|
|
760
|
+
"text": "We help teams build ",
|
|
761
|
+
"format": 0
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
"type": "text",
|
|
765
|
+
"text": "faster",
|
|
766
|
+
"format": 3
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
}
|
|
770
|
+
]
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Format values are bitmasks:
|
|
776
|
+
- `0` = no formatting
|
|
777
|
+
- `1` = bold
|
|
778
|
+
- `2` = italic
|
|
779
|
+
- `3` = bold + italic
|
|
780
|
+
- `4` = strikethrough
|
|
781
|
+
- `8` = underline
|
|
782
|
+
- `16` = code
|
|
783
|
+
|
|
784
|
+
#### Multi-Node Aggregation
|
|
785
|
+
|
|
786
|
+
When a Figma component has multiple separate text nodes that should combine into a single richText field, aggregate them in visual order (top to bottom, left to right):
|
|
787
|
+
|
|
788
|
+
```
|
|
789
|
+
Figma Card:
|
|
790
|
+
Text "Feature Title" (y: 120, fontSize: 24px)
|
|
791
|
+
Text "Description paragraph..." (y: 160, fontSize: 16px)
|
|
792
|
+
|
|
793
|
+
Lexical richText:
|
|
794
|
+
HeadingNode(h3, "Feature Title")
|
|
795
|
+
ParagraphNode("Description paragraph...")
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
Sort text nodes by their Y position (top to bottom), then by X position for same-line text.
|
|
799
|
+
|
|
800
|
+
#### List Detection
|
|
801
|
+
|
|
802
|
+
Figma does not have native list support. Detect lists heuristically:
|
|
803
|
+
|
|
804
|
+
- **Bullet points** -- Text starting with "- ", "* ", or bullet character (U+2022)
|
|
805
|
+
- **Numbered lists** -- Text starting with "1.", "2.", etc.
|
|
806
|
+
- **Repeated icon + text pattern** -- Row of icon instance + text node, repeated vertically
|
|
807
|
+
|
|
808
|
+
Convert detected lists to Lexical ListNode with ListItemNode children.
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
### 9. Media Asset Pipeline
|
|
813
|
+
|
|
814
|
+
Figma images are extracted, exported, and uploaded to the PayloadCMS Media collection.
|
|
815
|
+
|
|
816
|
+
#### Extraction Steps
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
Figma Image Fill or Image Node
|
|
820
|
+
|
|
|
821
|
+
v
|
|
822
|
+
[1. Identify] ─── Is it an image fill on a frame? Or a standalone image?
|
|
823
|
+
| Reference: design-to-code-assets.md for detection rules
|
|
824
|
+
v
|
|
825
|
+
[2. Export] ────── Use Figma REST API: GET /v1/images/:file_key
|
|
826
|
+
| Parameters: ids=[nodeId], format=png, scale=2
|
|
827
|
+
| Export at 2x for retina displays
|
|
828
|
+
v
|
|
829
|
+
[3. Upload] ───── POST to PayloadCMS Media collection
|
|
830
|
+
| Include alt text from:
|
|
831
|
+
| - Figma node name (fallback)
|
|
832
|
+
| - Nearest text sibling (better)
|
|
833
|
+
| - Explicit alt text property if set
|
|
834
|
+
v
|
|
835
|
+
[4. Reference] ── Store the Media document ID in the block's
|
|
836
|
+
image.image field (relationship to media collection)
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
#### Image Export Settings
|
|
840
|
+
|
|
841
|
+
| Setting | Value | Reason |
|
|
842
|
+
|---------|-------|--------|
|
|
843
|
+
| Format | PNG | Lossless, PayloadCMS converts to WebP |
|
|
844
|
+
| Scale | 2 | Retina display support |
|
|
845
|
+
| Fallback format | SVG | For vector-only content |
|
|
846
|
+
|
|
847
|
+
PayloadCMS automatically generates the `og` image size (1200x630 WebP) from uploaded images.
|
|
848
|
+
|
|
849
|
+
#### SVG Handling Decision
|
|
850
|
+
|
|
851
|
+
| Condition | Action |
|
|
852
|
+
|-----------|--------|
|
|
853
|
+
| Vector content (icons, logos, illustrations) | Export as SVG, upload to Media |
|
|
854
|
+
| Photographic content | Export as PNG at 2x |
|
|
855
|
+
| Mixed vector + photo | Export as PNG (vectors rasterize cleanly) |
|
|
856
|
+
| Small icon (< 24px) | Consider inline SVG in CSS Module instead of Media upload |
|
|
857
|
+
|
|
858
|
+
For inline SVG vs upload decisions, reference `design-to-code-assets.md` Section 4 (CSS vs SVG decision tree).
|
|
859
|
+
|
|
860
|
+
#### Alt Text Generation
|
|
861
|
+
|
|
862
|
+
The Media collection requires an `alt` field. Generate alt text using this priority:
|
|
863
|
+
|
|
864
|
+
1. **Figma node name** if descriptive (e.g., "Team photo", "Logo icon")
|
|
865
|
+
2. **Parent component context** (e.g., image in Card titled "AI Feature" → "AI Feature illustration")
|
|
866
|
+
3. **Generic fallback** based on position (e.g., "Hero background image", "Card thumbnail")
|
|
867
|
+
4. **Empty string** for purely decorative images (with `role="presentation"` in HTML)
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
### 10. Responsive Considerations
|
|
872
|
+
|
|
873
|
+
Figma responsive variants map to PayloadCMS Container width settings and CSS Module media queries.
|
|
874
|
+
|
|
875
|
+
#### Figma Responsive Patterns to PayloadCMS
|
|
876
|
+
|
|
877
|
+
| Figma Pattern | PayloadCMS Mapping |
|
|
878
|
+
|--------------|-------------------|
|
|
879
|
+
| Full-width frame (fills viewport) | Container width = `full` |
|
|
880
|
+
| Max-width ~1200-1400px with auto margins | Container width = `wide` |
|
|
881
|
+
| Max-width ~600-800px with auto margins | Container width = `narrow` |
|
|
882
|
+
| Horizontal layout at desktop, vertical at mobile | Container layout = `row` (CSS Module handles responsive override) |
|
|
883
|
+
| Different gap at mobile vs desktop | Container gap = desktop value (CSS Module responsive rules adjust mobile) |
|
|
884
|
+
|
|
885
|
+
#### Responsive Frame Detection
|
|
886
|
+
|
|
887
|
+
When Figma contains multiple responsive variants of the same frame (e.g., "Hero/Desktop", "Hero/Mobile"):
|
|
888
|
+
|
|
889
|
+
1. Use the desktop variant as the primary mapping source
|
|
890
|
+
2. Note layout direction changes between variants
|
|
891
|
+
3. The CSS Module media queries handle responsive behavior -- PayloadCMS blocks store the desktop configuration
|
|
892
|
+
|
|
893
|
+
#### Container Width to CSS
|
|
894
|
+
|
|
895
|
+
| PayloadCMS Width | CSS Module Class | CSS |
|
|
896
|
+
|-----------------|-----------------|-----|
|
|
897
|
+
| `full` | `.container` | No max-width constraint |
|
|
898
|
+
| `wide` | `.containerWide` | `max-width: 1400px; margin: 0 auto;` |
|
|
899
|
+
| `narrow` | `.containerNarrow` | `max-width: 800px; margin: 0 auto;` |
|
|
900
|
+
|
|
901
|
+
#### Mobile-First CSS Module Pattern
|
|
902
|
+
|
|
903
|
+
Block CSS Modules follow a mobile-first responsive approach:
|
|
904
|
+
|
|
905
|
+
```css
|
|
906
|
+
/* Base (mobile) */
|
|
907
|
+
.heroContainer {
|
|
908
|
+
padding: var(--token-spacing-lg, 24px);
|
|
909
|
+
min-height: 200px;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* Tablet */
|
|
913
|
+
@media (min-width: 768px) {
|
|
914
|
+
.heroContainer {
|
|
915
|
+
padding: var(--token-spacing-2xl, 48px);
|
|
916
|
+
min-height: 300px;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/* Desktop */
|
|
921
|
+
@media (min-width: 1024px) {
|
|
922
|
+
.heroContainer {
|
|
923
|
+
padding: var(--token-spacing-3xl, 64px);
|
|
924
|
+
min-height: 400px;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
When Figma Variables use breakpoint modes, the tokens themselves change at breakpoints (see `design-tokens-variables.md`), and blocks automatically adapt without per-block media queries.
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
### 11. Complete Mapping Example
|
|
934
|
+
|
|
935
|
+
This section walks through mapping a complete Figma page to a PayloadCMS block tree.
|
|
936
|
+
|
|
937
|
+
#### Figma Design
|
|
938
|
+
|
|
939
|
+
A landing page with three sections:
|
|
940
|
+
|
|
941
|
+
```
|
|
942
|
+
Page Frame (1440 x 2400, vertical Auto Layout)
|
|
943
|
+
├── Hero Frame (1440 x 600)
|
|
944
|
+
│ ├── Background Image (1440 x 600, image fill)
|
|
945
|
+
│ ├── Overlay (1440 x 600, black 40% opacity)
|
|
946
|
+
│ ├── Content Frame (800 x auto, vertical, gap 24px)
|
|
947
|
+
│ │ ├── Text "Build Better Products" (48px, bold, white)
|
|
948
|
+
│ │ ├── Text "Our platform helps teams..." (20px, regular, white)
|
|
949
|
+
│ │ └── Buttons Frame (horizontal, gap 12px)
|
|
950
|
+
│ │ ├── Button Instance "Get Started" (primary fill)
|
|
951
|
+
│ │ └── Button Instance "Watch Demo" (outline)
|
|
952
|
+
│ └── (padding: 64px 32px)
|
|
953
|
+
│
|
|
954
|
+
├── Features Section (1440 x auto, vertical, gap 48px, padding 64px 32px)
|
|
955
|
+
│ ├── Section Header Frame (vertical, gap 16px, center-aligned)
|
|
956
|
+
│ │ ├── Text "Features" (32px, bold)
|
|
957
|
+
│ │ └── Text "Everything you need" (18px, regular)
|
|
958
|
+
│ └── Cards Row (horizontal, gap 24px, max-width 1200px)
|
|
959
|
+
│ ├── Card Instance (360 x auto)
|
|
960
|
+
│ │ ├── Image (360 x 200)
|
|
961
|
+
│ │ ├── Text "Fast Builds" (24px, semibold)
|
|
962
|
+
│ │ ├── Text "Deploy in seconds..." (16px, regular)
|
|
963
|
+
│ │ └── Button "Learn More"
|
|
964
|
+
│ ├── Card Instance (360 x auto)
|
|
965
|
+
│ │ ├── Image (360 x 200)
|
|
966
|
+
│ │ ├── Text "Team Collab" (24px, semibold)
|
|
967
|
+
│ │ ├── Text "Work together..." (16px, regular)
|
|
968
|
+
│ │ └── Button "Learn More"
|
|
969
|
+
│ └── Card Instance (360 x auto)
|
|
970
|
+
│ ├── Image (360 x 200)
|
|
971
|
+
│ ├── Text "Analytics" (24px, semibold)
|
|
972
|
+
│ ├── Text "Track metrics..." (16px, regular)
|
|
973
|
+
│ └── Button "Learn More"
|
|
974
|
+
│
|
|
975
|
+
└── CTA Section (1440 x auto, horizontal, gap 32px, padding 48px)
|
|
976
|
+
├── Content Frame (vertical, gap 12px)
|
|
977
|
+
│ ├── Text "Ready to get started?" (28px, bold, white)
|
|
978
|
+
│ └── Text "Join thousands of teams..." (16px, regular, white)
|
|
979
|
+
└── Image (400 x 300)
|
|
980
|
+
└── (background: #3b82f6)
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### Mapping Result
|
|
984
|
+
|
|
985
|
+
```json
|
|
986
|
+
{
|
|
987
|
+
"layout": {
|
|
988
|
+
"blocks": [
|
|
989
|
+
{
|
|
990
|
+
"blockType": "hero",
|
|
991
|
+
"content": {
|
|
992
|
+
"richText": {
|
|
993
|
+
"root": {
|
|
994
|
+
"type": "root",
|
|
995
|
+
"children": [
|
|
996
|
+
{
|
|
997
|
+
"type": "heading",
|
|
998
|
+
"tag": "h1",
|
|
999
|
+
"children": [{ "type": "text", "text": "Build Better Products", "format": 1 }]
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
"type": "paragraph",
|
|
1003
|
+
"children": [{ "type": "text", "text": "Our platform helps teams ship faster with confidence.", "format": 0 }]
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
"buttonGroup": [
|
|
1009
|
+
{
|
|
1010
|
+
"link": {
|
|
1011
|
+
"type": "custom",
|
|
1012
|
+
"url": "#get-started",
|
|
1013
|
+
"label": "Get Started",
|
|
1014
|
+
"newTab": false
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"link": {
|
|
1019
|
+
"type": "custom",
|
|
1020
|
+
"url": "#demo",
|
|
1021
|
+
"label": "Watch Demo",
|
|
1022
|
+
"newTab": false
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
]
|
|
1026
|
+
},
|
|
1027
|
+
"image": {
|
|
1028
|
+
"image": "media-id-hero-bg"
|
|
1029
|
+
},
|
|
1030
|
+
"settings": {
|
|
1031
|
+
"type": "highImpact",
|
|
1032
|
+
"className": "",
|
|
1033
|
+
"layoutMeta": {
|
|
1034
|
+
"marginTop": "mt-0",
|
|
1035
|
+
"marginBottom": "mb-0",
|
|
1036
|
+
"paddingTop": "pt-0",
|
|
1037
|
+
"paddingBottom": "pb-0",
|
|
1038
|
+
"hidden": false
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
"blockType": "container",
|
|
1044
|
+
"layout": {
|
|
1045
|
+
"content": {
|
|
1046
|
+
"blocks": [
|
|
1047
|
+
{
|
|
1048
|
+
"blockType": "richText",
|
|
1049
|
+
"content": {
|
|
1050
|
+
"richText": {
|
|
1051
|
+
"root": {
|
|
1052
|
+
"type": "root",
|
|
1053
|
+
"children": [
|
|
1054
|
+
{
|
|
1055
|
+
"type": "heading",
|
|
1056
|
+
"tag": "h2",
|
|
1057
|
+
"children": [{ "type": "text", "text": "Features", "format": 1 }]
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"type": "paragraph",
|
|
1061
|
+
"children": [{ "type": "text", "text": "Everything you need to build better products", "format": 0 }]
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
"settings": { "className": "", "layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false } }
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
"blockType": "container",
|
|
1071
|
+
"layout": {
|
|
1072
|
+
"content": {
|
|
1073
|
+
"blocks": [
|
|
1074
|
+
{
|
|
1075
|
+
"blockType": "card",
|
|
1076
|
+
"content": {
|
|
1077
|
+
"richText": {
|
|
1078
|
+
"root": {
|
|
1079
|
+
"type": "root",
|
|
1080
|
+
"children": [
|
|
1081
|
+
{ "type": "heading", "tag": "h3", "children": [{ "type": "text", "text": "Fast Builds", "format": 1 }] },
|
|
1082
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Deploy in seconds with our streamlined pipeline.", "format": 0 }] }
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
"cta": { "label": "Learn More", "ctaAction": "external", "externalLink": "#", "layout": "wrap-link-button" },
|
|
1088
|
+
"image": { "image": "media-id-card-1" },
|
|
1089
|
+
"settings": { "className": "", "layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false } }
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
"blockType": "card",
|
|
1093
|
+
"content": {
|
|
1094
|
+
"richText": {
|
|
1095
|
+
"root": {
|
|
1096
|
+
"type": "root",
|
|
1097
|
+
"children": [
|
|
1098
|
+
{ "type": "heading", "tag": "h3", "children": [{ "type": "text", "text": "Team Collab", "format": 1 }] },
|
|
1099
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Work together in real-time with live editing.", "format": 0 }] }
|
|
1100
|
+
]
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
"cta": { "label": "Learn More", "ctaAction": "external", "externalLink": "#", "layout": "wrap-link-button" },
|
|
1105
|
+
"image": { "image": "media-id-card-2" },
|
|
1106
|
+
"settings": { "className": "", "layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false } }
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
"blockType": "card",
|
|
1110
|
+
"content": {
|
|
1111
|
+
"richText": {
|
|
1112
|
+
"root": {
|
|
1113
|
+
"type": "root",
|
|
1114
|
+
"children": [
|
|
1115
|
+
{ "type": "heading", "tag": "h3", "children": [{ "type": "text", "text": "Analytics", "format": 1 }] },
|
|
1116
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Track every metric with real-time dashboards.", "format": 0 }] }
|
|
1117
|
+
]
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
"cta": { "label": "Learn More", "ctaAction": "external", "externalLink": "#", "layout": "wrap-link-button" },
|
|
1122
|
+
"image": { "image": "media-id-card-3" },
|
|
1123
|
+
"settings": { "className": "", "layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false } }
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
"settings": {
|
|
1129
|
+
"htmlTag": "div",
|
|
1130
|
+
"layout": "row",
|
|
1131
|
+
"width": "wide",
|
|
1132
|
+
"alignItems": "items-stretch",
|
|
1133
|
+
"justifyContent": "justify-center",
|
|
1134
|
+
"gap": "gap-6",
|
|
1135
|
+
"layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false }
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
"settings": {
|
|
1142
|
+
"htmlTag": "section",
|
|
1143
|
+
"layout": "col",
|
|
1144
|
+
"width": "full",
|
|
1145
|
+
"alignItems": "items-center",
|
|
1146
|
+
"justifyContent": "justify-start",
|
|
1147
|
+
"gap": "gap-12",
|
|
1148
|
+
"layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-16", "paddingBottom": "pb-16", "hidden": false }
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
"blockType": "callToAction",
|
|
1153
|
+
"content": {
|
|
1154
|
+
"title": "Ready to get started?",
|
|
1155
|
+
"richText": {
|
|
1156
|
+
"root": {
|
|
1157
|
+
"type": "root",
|
|
1158
|
+
"children": [
|
|
1159
|
+
{ "type": "paragraph", "children": [{ "type": "text", "text": "Join thousands of teams building better products.", "format": 0 }] }
|
|
1160
|
+
]
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
"image": { "image": "media-id-cta" },
|
|
1165
|
+
"settings": {
|
|
1166
|
+
"className": "",
|
|
1167
|
+
"newTab": false,
|
|
1168
|
+
"layoutMeta": { "marginTop": "mt-0", "marginBottom": "mb-0", "paddingTop": "pt-0", "paddingBottom": "pb-0", "hidden": false }
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
]
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
#### Mapping Decisions Explained
|
|
1177
|
+
|
|
1178
|
+
1. **Hero Section** -- Matched by Rule 1 (full-width, background image, large heading, buttons). Impact set to `highImpact` because the frame height is 600px (>= 400px threshold).
|
|
1179
|
+
|
|
1180
|
+
2. **Features Section** -- The outer section frame becomes a Container (Rule 15, structural wrapper with mixed children). `htmlTag` set to `section` based on frame name. The inner "Section Header" is flattened into a RichText block (heading + description, no structural purpose). The "Cards Row" becomes a nested Container (layout: row) because it changes direction from the parent's column layout.
|
|
1181
|
+
|
|
1182
|
+
3. **Card Instances** -- Each matched by Rule 2 (bounded width, image, text, CTA). The three Card blocks are children of the row Container.
|
|
1183
|
+
|
|
1184
|
+
4. **CTA Section** -- Matched by Rule 10 (full-width, strong background color, title + description + image). Maps to CallToAction block rather than a Container with RichText, because the structure matches the CTA block's fields exactly.
|
|
1185
|
+
|
|
1186
|
+
5. **Nesting depth** -- The result has 2 levels: outer Container (section) > inner Container (cards row) > Card blocks. This is within the PayloadCMS depth limit.
|
|
1187
|
+
|
|
1188
|
+
---
|
|
1189
|
+
|
|
1190
|
+
## Cross-References
|
|
1191
|
+
|
|
1192
|
+
- **`payload-blocks.md`** -- Complete block catalog, field definitions, Container settings, and Lexical configuration. The mapping tables in this module reference the exact field names and types defined there.
|
|
1193
|
+
|
|
1194
|
+
- **`payload-visual-builder.md`** -- Visual Builder plugin architecture for editing mapped blocks. Defines the editing UX for blocks produced by this mapping pipeline.
|
|
1195
|
+
|
|
1196
|
+
- **`design-to-code-layout.md`** -- Auto Layout to Flexbox mapping rules. The Container mapping in Section 3 directly applies the layout interpretation rules from this module (sizing modes, axis alignment, gap, wrap, constraints).
|
|
1197
|
+
|
|
1198
|
+
- **`design-to-code-visual.md`** -- Visual property extraction (fills, strokes, effects, gradients, opacity). Used during token bridge (Section 5) to determine which visual values map to `--token-*` properties.
|
|
1199
|
+
|
|
1200
|
+
- **`design-to-code-typography.md`** -- Font mapping and text style extraction. Rich text extraction (Section 8) uses the styled segment and heading hierarchy rules from this module.
|
|
1201
|
+
|
|
1202
|
+
- **`design-to-code-assets.md`** -- Image and vector export rules. The media asset pipeline (Section 9) references the SVG vs CSS decision tree and image deduplication patterns.
|
|
1203
|
+
|
|
1204
|
+
- **`design-to-code-semantic.md`** -- Semantic HTML element selection. The Container block's `htmlTag` mapping and the BEM naming patterns used in CSS Modules follow the conventions defined here.
|
|
1205
|
+
|
|
1206
|
+
- **`css-strategy.md`** -- Three-layer CSS architecture. Blocks use Layer 1 (Tailwind) for Container settings, Layer 2 (tokens) for design values, and Layer 3 (CSS Modules) for visual skin -- as defined in this strategy module.
|
|
1207
|
+
|
|
1208
|
+
- **`design-tokens.md`** -- Token extraction pipeline and naming algorithm. The design token bridge (Section 5) follows the extraction, naming, and rendering pipeline from this module.
|
|
1209
|
+
|
|
1210
|
+
- **`design-tokens-variables.md`** -- Figma Variables to CSS custom property resolution. Variable-bound properties are resolved through the chain documented here before mapping to `--token-*` values.
|