figma-code-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +133 -0
  2. package/bin/install.js +328 -0
  3. package/knowledge/README.md +62 -0
  4. package/knowledge/css-strategy.md +973 -0
  5. package/knowledge/design-to-code-assets.md +855 -0
  6. package/knowledge/design-to-code-layout.md +929 -0
  7. package/knowledge/design-to-code-semantic.md +1085 -0
  8. package/knowledge/design-to-code-typography.md +1003 -0
  9. package/knowledge/design-to-code-visual.md +1145 -0
  10. package/knowledge/design-tokens-variables.md +1261 -0
  11. package/knowledge/design-tokens.md +960 -0
  12. package/knowledge/figma-api-devmode.md +894 -0
  13. package/knowledge/figma-api-plugin.md +920 -0
  14. package/knowledge/figma-api-rest.md +742 -0
  15. package/knowledge/figma-api-variables.md +848 -0
  16. package/knowledge/figma-api-webhooks.md +876 -0
  17. package/knowledge/payload-blocks.md +1184 -0
  18. package/knowledge/payload-figma-mapping.md +1210 -0
  19. package/knowledge/payload-visual-builder.md +1004 -0
  20. package/knowledge/plugin-architecture.md +1176 -0
  21. package/knowledge/plugin-best-practices.md +1206 -0
  22. package/knowledge/plugin-codegen.md +1313 -0
  23. package/package.json +31 -0
  24. package/skills/README.md +103 -0
  25. package/skills/audit-plugin/SKILL.md +244 -0
  26. package/skills/build-codegen-plugin/SKILL.md +279 -0
  27. package/skills/build-importer/SKILL.md +320 -0
  28. package/skills/build-plugin/SKILL.md +199 -0
  29. package/skills/build-token-pipeline/SKILL.md +363 -0
  30. package/skills/ref-html/SKILL.md +290 -0
  31. package/skills/ref-layout/SKILL.md +150 -0
  32. package/skills/ref-payload-block/SKILL.md +415 -0
  33. package/skills/ref-react/SKILL.md +222 -0
  34. package/skills/ref-tokens/SKILL.md +347 -0
@@ -0,0 +1,1261 @@
1
+ # Figma Variables → Design Token → CSS Bridge
2
+
3
+ ## Purpose
4
+
5
+ Authoritative reference for the bridge between Figma Variables and CSS Custom Properties in a design token system. Documents how to extract variable collections, detect and classify their modes (breakpoint vs theme), resolve variable values through alias chains, generate mode-aware CSS output with media queries and theme selectors, and handle fallback values for external library variables. Encodes production patterns for variable collection processing, mode classification, CSS rendering, and token lookup.
6
+
7
+ This module fills the gap between `figma-api-variables.md` (API-level reference for Variables endpoints and data model) and `design-tokens.md` (general token extraction pipeline). It focuses specifically on the Variables-to-Token-to-CSS transformation that neither of those modules covers in depth.
8
+
9
+ ## When to Use
10
+
11
+ Reference this module when you need to:
12
+
13
+ - Understand how Figma Variable collections map to token categories for CSS output
14
+ - Detect whether a collection's modes represent breakpoints (responsive) or themes (light/dark)
15
+ - Determine which mode is the "default" (base) mode for mobile-first or light-default rendering
16
+ - Resolve variable alias chains to terminal values for each mode
17
+ - Extract bound variable references from node properties (fills, strokes, layout, text)
18
+ - Convert Figma variable paths to CSS custom property names
19
+ - Generate mode-aware CSS: `:root` for base, `@media (min-width)` for breakpoints, `prefers-color-scheme` and `[data-theme]` for themes
20
+ - Decide when to include fallback values in `var()` references (local vs external variables)
21
+ - Map variable scopes to applicable CSS properties
22
+ - Design multi-mode token systems (responsive + theme cross-products)
23
+ - Understand how variable tokens integrate with and take priority over auto-detected tokens
24
+
25
+ ---
26
+
27
+ ## Content
28
+
29
+ ### 1. Purpose & When to Use
30
+
31
+ Figma Variables are the highest-priority source for design tokens. They provide structured, designer-curated token definitions with explicit names, types, scopes, and per-mode values -- far superior to heuristic-based token extraction from file traversal.
32
+
33
+ #### When This Module Applies
34
+
35
+ - **The Figma file uses Variables** for colors, spacing, typography, or other design values
36
+ - **The file has multi-mode collections** (responsive breakpoints, light/dark themes, brand variants)
37
+ - **You need CSS Custom Properties** that adapt to viewport size or theme preference
38
+ - **You need to resolve variable bindings** from node properties to CSS `var()` references
39
+ - **You are building a token pipeline** that bridges Figma Variables to a CSS/SCSS/Tailwind output
40
+
41
+ #### When to Use Other Modules Instead
42
+
43
+ | Scenario | Module |
44
+ |----------|--------|
45
+ | Variables API endpoints, authentication, data model | `figma-api-variables.md` |
46
+ | General token extraction (collect, promote, name, render) | `design-tokens.md` |
47
+ | Where tokens go in the CSS output (Layer 1/2/3) | `css-strategy.md` |
48
+ | Visual property extraction (fills, strokes, effects) | `design-to-code-visual.md` |
49
+ | Layout property extraction (gap, padding) | `design-to-code-layout.md` |
50
+ | Typography property extraction (fonts, sizes, weights) | `design-to-code-typography.md` |
51
+
52
+ ---
53
+
54
+ ### 2. Variables as Token Source
55
+
56
+ Variables are the preferred token source because they provide structured, explicit token definitions -- names, types, modes, scopes, and alias relationships -- rather than requiring heuristic detection from raw property values.
57
+
58
+ #### Token Source Priority
59
+
60
+ The token extraction pipeline uses this priority order (documented in `figma-api-variables.md`):
61
+
62
+ ```
63
+ 1. Figma Variables API → Best: structured, multi-mode, scoped
64
+ 2. Published styles → Good: limited to published content
65
+ 3. File tree traversal → Fallback: heuristic-based, all plans
66
+ 4. Plugin API (in-context) → Fallback: for non-Enterprise REST access
67
+ ```
68
+
69
+ Variables provide everything the token system needs without heuristics:
70
+
71
+ | Feature | Variables API | File Traversal |
72
+ |---------|:---:|:---:|
73
+ | Explicit token names | Yes (`color/primary/500`) | No (inferred from HSL) |
74
+ | Multi-mode values | Yes (per breakpoint/theme) | No (single value per node) |
75
+ | Type safety | Yes (`COLOR`, `FLOAT`, `STRING`) | Inferred from context |
76
+ | Scope constraints | Yes (`ALL_FILLS`, `GAP`, etc.) | No |
77
+ | Alias chains | Yes (variable referencing variable) | No |
78
+ | Designer intent | Explicit | Inferred |
79
+
80
+ #### When Variables API Is Not Available
81
+
82
+ The Variables REST API requires Enterprise plan + full member access. When this is not available:
83
+
84
+ 1. **Plugin API** -- Access local variables via `figma.variables.getLocalVariableCollectionsAsync()` and `figma.variables.getVariableByIdAsync()`. This is the recommended approach for Figma plugins (running inside the Figma sandbox, not as a REST client).
85
+
86
+ 2. **Bound variable extraction** -- Even without the Variables API, nodes in the file tree expose `boundVariables` that reference variable IDs. The file tree response includes these bindings.
87
+
88
+ 3. **File traversal + promotion** -- Fall back to the threshold-based promotion system documented in `design-tokens.md`. Traverse the node tree, collect raw values, and promote repeated values to tokens.
89
+
90
+ #### How Variables and Auto-Detected Tokens Coexist
91
+
92
+ Variables and auto-detected tokens are collected in parallel and merged. Variables always win conflicts:
93
+
94
+ ```
95
+ collectAllTokens(node) →
96
+ ├── collectColors(node) → ColorToken[] (auto-detected)
97
+ ├── collectSpacing(node) → SpacingToken[] (auto-detected)
98
+ ├── collectTypography(node) → TypographyTokens (auto-detected)
99
+ ├── collectEffects(node) → EffectTokens (auto-detected)
100
+ └── collectFigmaVariables(node) → FigmaVariableToken[] (explicit bindings)
101
+ ```
102
+
103
+ During the lookup phase, the priority chain is:
104
+
105
+ 1. **Bound variable** on the property -- use `var(--css-name)` directly
106
+ 2. **Auto-detected token** from the lookup map -- use `var(--token-name)`
107
+ 3. **Raw value** -- use the literal CSS value
108
+
109
+ > See `design-tokens.md` Section 9 for the complete token lookup system.
110
+
111
+ ---
112
+
113
+ ### 3. Collection Structure for Tokens
114
+
115
+ Figma variable collections group related variables that share the same set of modes. Collections map naturally to token categories.
116
+
117
+ #### Collection → Token Category Mapping
118
+
119
+ Designers typically organize collections by token category:
120
+
121
+ | Collection Name | Token Category | Variable `resolvedType` | CSS Output |
122
+ |----------------|---------------|:-----------------------:|------------|
123
+ | `Colors` / `Theme` | Color tokens | `COLOR` | `--color-primary: #1a73e8` |
124
+ | `Spacing` | Spacing tokens | `FLOAT` | `--spacing-md: 16px` |
125
+ | `Typography` | Font tokens | `FLOAT` / `STRING` | `--text-lg: 18px`, `--font-primary: 'Inter'` |
126
+ | `Radii` | Border radius tokens | `FLOAT` | `--radius-md: 8px` |
127
+ | `Effects` | Shadow/elevation tokens | `FLOAT` | `--shadow-blur: 8px` |
128
+
129
+ #### Collection Naming Conventions
130
+
131
+ The pipeline does not enforce collection naming conventions -- it processes all local collections regardless of name. The collection name is used for:
132
+
133
+ 1. **CSS comment headers** in the rendered output (e.g., `/* Colors */`)
134
+ 2. **Mode type detection** -- mode names within the collection determine breakpoint vs theme rendering
135
+ 3. **Grouping** -- variables from the same collection are rendered together in the CSS output
136
+
137
+ #### Multi-Collection Files
138
+
139
+ A single Figma file can have multiple collections, each with its own modes:
140
+
141
+ ```
142
+ File: "Design System"
143
+ Collection: "Theme"
144
+ Modes: [Light, Dark]
145
+ Variables: color/primary, color/bg, color/text, ...
146
+
147
+ Collection: "Spacing"
148
+ Modes: [Mobile, Tablet, Desktop]
149
+ Variables: spacing/sm, spacing/md, spacing/lg, ...
150
+
151
+ Collection: "Typography"
152
+ Modes: [Mobile, Desktop]
153
+ Variables: text/body, text/heading, ...
154
+ ```
155
+
156
+ Each collection is rendered independently with its own mode-aware CSS blocks. Collections with a single mode render only in the base `:root` block.
157
+
158
+ #### Collection Processing
159
+
160
+ The recommended collection processing approach:
161
+
162
+ ```typescript
163
+ const collections = await figma.variables.getLocalVariableCollectionsAsync()
164
+ return collections.map(collection => {
165
+ const modes = collection.modes.map(m => ({ id: m.modeId, name: m.name }))
166
+ const modeType = detectModeType(modes.map(m => m.name))
167
+ return {
168
+ id: collection.id,
169
+ name: collection.name,
170
+ modeType, // 'breakpoint' | 'theme' | 'unknown'
171
+ modes,
172
+ defaultModeId: getDefaultModeId(modes, modeType),
173
+ }
174
+ })
175
+ ```
176
+
177
+ Key points:
178
+ - Only **local** collections are processed (external library collections are skipped)
179
+ - Each collection gets a `modeType` classification (see Section 4)
180
+ - Each collection gets a `defaultModeId` identifying which mode provides base values
181
+
182
+ ---
183
+
184
+ ### 4. Mode Detection & Classification
185
+
186
+ Variable collections can have multiple modes. The token system must classify each collection's modes to determine how they map to CSS constructs (media queries vs theme selectors).
187
+
188
+ #### Mode Type Classification
189
+
190
+ The pipeline classifies collections into three types:
191
+
192
+ | Mode Type | Detection | CSS Rendering |
193
+ |-----------|-----------|---------------|
194
+ | `breakpoint` | Mode names contain "mobile", "tablet", "desktop", or pixel values | `@media (min-width)` queries |
195
+ | `theme` | Mode names contain "light", "dark", or "theme" | `prefers-color-scheme` + `[data-theme]` selectors |
196
+ | `unknown` | Neither pattern detected | Single mode in `:root` only |
197
+
198
+ #### Detection Algorithm
199
+
200
+ ```typescript
201
+ function detectModeType(modeNames: string[]): 'breakpoint' | 'theme' | 'unknown' {
202
+ const normalized = modeNames.map(n => n.toLowerCase().trim())
203
+
204
+ // Check theme patterns FIRST
205
+ const hasThemePatterns = normalized.some(name =>
206
+ name.includes('light') ||
207
+ name.includes('dark') ||
208
+ name.includes('theme')
209
+ )
210
+ if (hasThemePatterns) return 'theme'
211
+
212
+ // Check breakpoint patterns
213
+ const hasBreakpointPatterns = normalized.some(name =>
214
+ name.includes('desktop') ||
215
+ name.includes('mobile') ||
216
+ name.includes('tablet') ||
217
+ /^\d+px?$/.test(name) // "800px" or "800"
218
+ )
219
+ if (hasBreakpointPatterns) return 'breakpoint'
220
+
221
+ return 'unknown'
222
+ }
223
+ ```
224
+
225
+ **Important ordering:** Theme detection runs before breakpoint detection. If a collection has modes named "Light Desktop" and "Dark Desktop", the "light"/"dark" patterns match first, classifying it as a theme collection.
226
+
227
+ #### Breakpoint Mode Detection Patterns
228
+
229
+ | Mode Name | Detected As | Breakpoint Value |
230
+ |-----------|:-----------:|:----------------:|
231
+ | `Mobile` | breakpoint | 0 (base) |
232
+ | `Tablet` | breakpoint | 768px |
233
+ | `Desktop` | breakpoint | 1024px |
234
+ | `800` | breakpoint | 800px |
235
+ | `800px` | breakpoint | 800px |
236
+ | `1440px` | breakpoint | 1440px |
237
+
238
+ Pixel-value mode names (e.g., `"800px"` or `"800"`) are parsed directly:
239
+
240
+ ```typescript
241
+ function parseBreakpointValue(modeName: string): number | null {
242
+ const normalized = modeName.toLowerCase().trim()
243
+
244
+ // Standard names
245
+ const BREAKPOINT_MAP = { 'mobile': 0, 'tablet': 768, 'desktop': 1024 }
246
+ if (BREAKPOINT_MAP[normalized] !== undefined) {
247
+ return BREAKPOINT_MAP[normalized]
248
+ }
249
+
250
+ // Pixel values: "800px" or "800"
251
+ const pxMatch = normalized.match(/^(\d+)px?$/)
252
+ if (pxMatch) return parseInt(pxMatch[1], 10)
253
+
254
+ return null
255
+ }
256
+ ```
257
+
258
+ #### Theme Mode Detection Patterns
259
+
260
+ | Mode Name | Detected As | Default? |
261
+ |-----------|:-----------:|:--------:|
262
+ | `Light` | theme | Yes (default) |
263
+ | `Dark` | theme | No |
264
+ | `Light Theme` | theme | Yes |
265
+ | `Dark Mode` | theme | No |
266
+
267
+ #### Default Mode Identification
268
+
269
+ The "default" mode is the one that renders in the base `:root` block (no media query, no theme selector). Non-default modes render in conditional blocks.
270
+
271
+ ```typescript
272
+ function getDefaultModeId(modes, modeType): string {
273
+ if (modeType === 'breakpoint') {
274
+ // Mobile-first: smallest breakpoint value is default
275
+ let minValue = Infinity
276
+ for (const mode of modes) {
277
+ const value = parseBreakpointValue(mode.name)
278
+ if (value !== null && value < minValue) {
279
+ minValue = value
280
+ defaultId = mode.id
281
+ }
282
+ }
283
+ return defaultId
284
+ }
285
+
286
+ if (modeType === 'theme') {
287
+ // "Light" is default
288
+ const lightMode = modes.find(m => m.name.toLowerCase().includes('light'))
289
+ if (lightMode) return lightMode.id
290
+ }
291
+
292
+ // Unknown: first mode is default
293
+ return modes[0].id
294
+ }
295
+ ```
296
+
297
+ | Mode Type | Default Rule | Rationale |
298
+ |-----------|-------------|-----------|
299
+ | Breakpoint | Smallest value (mobile = 0) | Mobile-first CSS: base styles have no media query |
300
+ | Theme | Mode containing "light" | Light theme is the conventional default |
301
+ | Unknown | First mode in array | Figma's default mode is typically first |
302
+
303
+ #### Breakpoint Sort Order (Mobile-First)
304
+
305
+ Breakpoint modes are sorted ascending by pixel value for CSS output. The default mode (mobile, value 0) comes first with no media query. Larger breakpoints follow with `min-width` queries:
306
+
307
+ ```typescript
308
+ function sortBreakpointModes(modes, defaultModeId) {
309
+ return modes
310
+ .map(mode => ({
311
+ ...mode,
312
+ breakpoint: mode.id === defaultModeId ? null : parseBreakpointValue(mode.name)
313
+ }))
314
+ .sort((a, b) => {
315
+ if (a.breakpoint === null) return -1 // Default first
316
+ if (b.breakpoint === null) return 1
317
+ return (a.breakpoint || 0) - (b.breakpoint || 0) // Ascending
318
+ })
319
+ }
320
+ ```
321
+
322
+ Result: `[mobile (null), tablet (768), desktop (1024)]`
323
+
324
+ ---
325
+
326
+ ### 5. Variable Resolution Chain
327
+
328
+ The complete resolution from a Figma variable binding to a CSS custom property value follows this chain:
329
+
330
+ ```
331
+ Node property has bound variable
332
+
333
+ Look up Variable by ID
334
+
335
+ Check valuesByMode for requested mode
336
+
337
+ Is value a VARIABLE_ALIAS?
338
+ ├─ YES → Resolve referenced variable (recursively)
339
+ └─ NO → Terminal value (COLOR, FLOAT, STRING, BOOLEAN)
340
+
341
+ Format terminal value as CSS string
342
+
343
+ Generate CSS custom property: --name: value
344
+ ```
345
+
346
+ #### Alias Resolution
347
+
348
+ Variables can reference other variables (aliases). An alias is detected by the `VARIABLE_ALIAS` type:
349
+
350
+ ```typescript
351
+ if (typeof modeValue === 'object' && modeValue !== null &&
352
+ 'type' in modeValue && modeValue.type === 'VARIABLE_ALIAS') {
353
+ // Resolve the aliased variable
354
+ const aliasedVar = await figma.variables.getVariableByIdAsync(modeValue.id)
355
+ if (aliasedVar) {
356
+ resolvedValue = aliasedVar.valuesByMode[modeId]
357
+ ?? aliasedVar.valuesByMode[collection.defaultModeId]
358
+ }
359
+ }
360
+ ```
361
+
362
+ Key behaviors:
363
+ - Aliases can chain: Variable A references Variable B which references Variable C
364
+ - Cross-collection aliases are valid: a color variable in "Theme" can reference a variable in "Primitives"
365
+ - When a referenced variable has no value for the requested mode, the collection's default mode value is used
366
+ - Figma prevents alias cycles (no self-referencing)
367
+
368
+ > For the full alias resolution algorithm and API details, see `figma-api-variables.md` Section "Variable Resolution".
369
+
370
+ #### Terminal Value Types
371
+
372
+ After resolving all aliases, the terminal value is one of four types:
373
+
374
+ | `resolvedType` | TypeScript Type | CSS Formatting |
375
+ |----------------|----------------|----------------|
376
+ | `COLOR` | `{ r, g, b, a }` (0-1 floats) | `#RRGGBB` or `rgba(R, G, B, A)` |
377
+ | `FLOAT` | `number` | `{value}px` |
378
+ | `STRING` | `string` | `'{value}'` (quoted for fonts) |
379
+ | `BOOLEAN` | `boolean` | `1` or `0` |
380
+
381
+ #### Value Formatting
382
+
383
+ From `variables.ts`:
384
+
385
+ ```typescript
386
+ function formatVariableValue(value, type): string {
387
+ if (type === 'COLOR' && typeof value === 'object' && 'r' in value) {
388
+ const r = Math.round(value.r * 255)
389
+ const g = Math.round(value.g * 255)
390
+ const b = Math.round(value.b * 255)
391
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase()
392
+ }
393
+ if (type === 'FLOAT' && typeof value === 'number') {
394
+ return `${value}px`
395
+ }
396
+ return String(value)
397
+ }
398
+ ```
399
+
400
+ > **Note:** Color values use the 0-1 float range from Figma. Always multiply by 255 and round before hex conversion. See `design-to-code-visual.md` Section 1 for the RGB conversion formula.
401
+
402
+ #### Mode Value Extraction
403
+
404
+ For multi-mode collections, each variable's value is extracted per mode:
405
+
406
+ ```typescript
407
+ async function extractModeValues(variableId, collections) {
408
+ const variable = await figma.variables.getVariableByIdAsync(variableId)
409
+ const collection = collections.find(c => c.id === variable.variableCollectionId)
410
+
411
+ // Single-mode collections: no per-mode extraction needed
412
+ if (!collection || collection.modes.length <= 1) {
413
+ return { collectionId: collection?.id }
414
+ }
415
+
416
+ // Multi-mode: extract value for each mode
417
+ const modeValues = []
418
+ for (const mode of collection.modes) {
419
+ const modeValue = variable.valuesByMode[mode.id]
420
+ // Resolve aliases...
421
+ const stringValue = formatVariableValue(resolvedValue, variable.resolvedType)
422
+ modeValues.push({
423
+ modeId: mode.id,
424
+ modeName: mode.name,
425
+ value: stringValue,
426
+ })
427
+ }
428
+
429
+ return { collectionId: collection.id, modeValues }
430
+ }
431
+ ```
432
+
433
+ The result is an array of `{ modeId, modeName, value }` objects -- one per mode -- that the renderer uses to generate mode-aware CSS.
434
+
435
+ ---
436
+
437
+ ### 6. Bound Variable Extraction
438
+
439
+ Bound variables are the connection between a Figma node's properties and the variable system. When a designer binds a variable to a property (e.g., binding `color/primary` to a frame's fill), the node's `boundVariables` field records this binding.
440
+
441
+ #### Extraction Sources
442
+
443
+ The extraction pipeline captures variable bindings from three property categories:
444
+
445
+ | Category | Properties | Intermediate Type Field |
446
+ |----------|-----------|------------------------|
447
+ | **Fill colors** | `paint.boundVariables.color` | `fill.variable` |
448
+ | **Layout spacing** | `node.boundVariables.itemSpacing`, `paddingTop`, etc. | `layout.gapVariable`, `layout.paddingVariables` |
449
+ | **Text fills** | `segment.fills[].variable` | `styledSegment.fills[].variable` |
450
+
451
+ #### Fill Variable Extraction
452
+
453
+ Fill variables are extracted at the paint level (each fill can have its own variable binding):
454
+
455
+ ```typescript
456
+ function collectVariableReferences(fill: FillData): void {
457
+ if (fill.type !== 'SOLID' || !fill.variable) return
458
+ const { id, name, isLocal } = fill.variable
459
+ // Deduplicate by variable ID
460
+ variableIdsToProcess.push({ id, name, value: fill.color, isLocal })
461
+ }
462
+ ```
463
+
464
+ The `fill.variable` field is populated during the extraction phase when `paint.boundVariables.color` is present on a solid fill.
465
+
466
+ #### Layout Variable Extraction
467
+
468
+ Layout variables (gap, padding) are extracted from the layout intermediate type:
469
+
470
+ ```typescript
471
+ // Gap variable
472
+ if (node.layout.gapVariable) {
473
+ addLayoutVariable(node.layout.gapVariable, node.layout.gap)
474
+ }
475
+
476
+ // Counter axis gap variable
477
+ if (node.layout.counterAxisGapVariable && node.layout.counterAxisGap !== undefined) {
478
+ addLayoutVariable(node.layout.counterAxisGapVariable, node.layout.counterAxisGap)
479
+ }
480
+
481
+ // Padding variables (per-side)
482
+ if (node.layout.paddingVariables) {
483
+ const pv = node.layout.paddingVariables
484
+ if (pv.top) addLayoutVariable(pv.top, node.layout.padding.top)
485
+ if (pv.right) addLayoutVariable(pv.right, node.layout.padding.right)
486
+ if (pv.bottom) addLayoutVariable(pv.bottom, node.layout.padding.bottom)
487
+ if (pv.left) addLayoutVariable(pv.left, node.layout.padding.left)
488
+ }
489
+ ```
490
+
491
+ > For how layout variable bindings are extracted from Figma nodes, see `design-to-code-layout.md` Section 3.
492
+
493
+ #### Text Fill Variable Extraction
494
+
495
+ Styled text segments can have variable bindings on their fill colors:
496
+
497
+ ```typescript
498
+ if (node.text?.styledSegments) {
499
+ for (const segment of node.text.styledSegments) {
500
+ if (segment.fills) {
501
+ for (const fill of segment.fills) {
502
+ if (fill.variable) {
503
+ variableIdsToProcess.push({
504
+ id: fill.variable.id,
505
+ name: fill.variable.name,
506
+ value: fill.color,
507
+ isLocal: fill.variable.isLocal ?? false,
508
+ })
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ ```
515
+
516
+ > For text fill variable binding patterns, see `design-to-code-typography.md` Section 9.
517
+
518
+ #### Variable Binding Priority
519
+
520
+ When generating CSS for a property, the system checks for variable bindings before falling back to auto-detected tokens:
521
+
522
+ ```
523
+ For a fill color:
524
+ 1. fill.variable exists?
525
+ ├─ YES, isLocal → var(--color-name) (no fallback)
526
+ ├─ YES, external → var(--color-name, #rawColor) (with fallback)
527
+ └─ NO → check auto-detected token lookup
528
+ 2. tokenLookup.colors has this value?
529
+ ├─ YES → var(--color-primary) (from token system)
530
+ └─ NO → #rawColor (inline value)
531
+ ```
532
+
533
+ This priority chain ensures explicit designer intent (variable bindings) always takes precedence over heuristic token detection.
534
+
535
+ > For the complete lookup chain implementation, see `design-tokens.md` Section 9.
536
+
537
+ #### Deduplication
538
+
539
+ Variables are deduplicated by ID before processing:
540
+
541
+ ```typescript
542
+ const variableMap = new Map<string, FigmaVariableToken>()
543
+ // ... collect all references ...
544
+ for (const varRef of variableIdsToProcess) {
545
+ const cssName = figmaVariableToCssName(varRef.name)
546
+ const { collectionId, modeValues } = await extractModeValues(varRef.id, collections)
547
+
548
+ variableMap.set(varRef.id, {
549
+ name: cssName,
550
+ figmaName: varRef.name,
551
+ value: varRef.value,
552
+ isLocal: varRef.isLocal,
553
+ usageCount: 1,
554
+ collectionId,
555
+ modeValues,
556
+ })
557
+ }
558
+ ```
559
+
560
+ If the same variable is bound to multiple nodes (e.g., `color/primary` used on 10 different fills), it is collected once and produces one CSS custom property definition.
561
+
562
+ ---
563
+
564
+ ### 7. CSS Custom Property Generation from Variables
565
+
566
+ Variable names from Figma use `/` as a path separator (e.g., `color/link/default`). These must be converted to valid CSS custom property names using `-` separators.
567
+
568
+ #### Name Conversion Algorithm
569
+
570
+ From `lookup.ts`:
571
+
572
+ ```typescript
573
+ function figmaVariableToCssName(variableName: string): string {
574
+ return '--' + variableName
575
+ .toLowerCase() // Lowercase everything
576
+ .replace(/\//g, '-') // Slashes to dashes
577
+ .replace(/\s+/g, '-') // Spaces to dashes
578
+ .replace(/[^a-z0-9-_]/g, '') // Remove invalid characters
579
+ }
580
+ ```
581
+
582
+ #### Conversion Examples
583
+
584
+ | Figma Variable Name | CSS Custom Property |
585
+ |---------------------|---------------------|
586
+ | `color/primary` | `--color-primary` |
587
+ | `color/link/default` | `--color-link-default` |
588
+ | `spacing/md` | `--spacing-md` |
589
+ | `Color/Background/Primary` | `--color-background-primary` |
590
+ | `radius/md` | `--radius-md` |
591
+ | `Typography/Heading/Size` | `--typography-heading-size` |
592
+ | `brand color/accent` | `--brand-color-accent` |
593
+
594
+ #### Collection Prefix Handling
595
+
596
+ The pipeline does **not** prepend the collection name to the variable's CSS name. The variable's own path is used directly:
597
+
598
+ ```
599
+ Collection: "Spacing"
600
+ Variable: "spacing/md"
601
+ CSS name: --spacing-md (NOT --spacing-spacing-md)
602
+ ```
603
+
604
+ This works because Figma designers typically include the category in the variable name path. If a collection named "Colors" contains a variable named "primary", the CSS name would be `--primary` (not `--colors-primary`). Designers should use paths like `color/primary` to get `--color-primary`.
605
+
606
+ #### `var()` Reference Generation
607
+
608
+ The CSS `var()` reference wraps the custom property name:
609
+
610
+ ```typescript
611
+ function figmaVariableToCss(variableName: string): string {
612
+ return `var(${figmaVariableToCssName(variableName)})`
613
+ }
614
+ // "color/link/default" → "var(--color-link-default)"
615
+ ```
616
+
617
+ #### Consistency with Auto-Detected Token Names
618
+
619
+ The variable path conversion uses the same algorithm as the design-tokens module's naming conventions. This means:
620
+
621
+ - `color/primary` (variable) → `--color-primary` (same as auto-detected `--color-primary`)
622
+ - `spacing/4` (variable) → `--spacing-4` (same as auto-detected `--spacing-4`)
623
+
624
+ When both a variable token and an auto-detected token produce the same name, the variable token takes priority. The auto-detected token is effectively shadowed.
625
+
626
+ > For the complete token naming conventions, see `design-tokens.md` Section 8.
627
+
628
+ ---
629
+
630
+ ### 8. Mode-Aware CSS Rendering
631
+
632
+ The CSS renderer generates different output structures based on the collection's mode type. All rendering follows a mobile-first, light-default approach.
633
+
634
+ #### Rendering Flow
635
+
636
+ From `render.ts`:
637
+
638
+ ```
639
+ 1. Open :root { ... } block
640
+ 2. Render variables without collections (simple variables)
641
+ 3. For each collection:
642
+ a. Render default mode values inside :root
643
+ 4. Close :root }
644
+ 5. For each breakpoint collection:
645
+ a. Render non-default modes as @media (min-width) blocks
646
+ 6. For each theme collection:
647
+ a. Render non-default modes as prefers-color-scheme + [data-theme] blocks
648
+ ```
649
+
650
+ #### Base `:root` Block
651
+
652
+ All variables render their default mode values in the base `:root` block:
653
+
654
+ ```css
655
+ :root {
656
+ /* Theme */
657
+ --color-bg: #ffffff;
658
+ --color-text: #1a1a1a;
659
+ --color-primary: #1a73e8;
660
+
661
+ /* Spacing */
662
+ --spacing-sm: 8px;
663
+ --spacing-md: 16px;
664
+ --spacing-lg: 24px;
665
+ }
666
+ ```
667
+
668
+ Variables without mode values (single-mode collections) also render here.
669
+
670
+ #### Breakpoint Mode Rendering
671
+
672
+ For collections classified as `breakpoint`, non-default modes generate `@media (min-width)` blocks. The default mode (mobile = 0) is already in `:root`.
673
+
674
+ ```css
675
+ /* Base: mobile values (from :root above) */
676
+
677
+ @media (min-width: 768px) {
678
+ :root {
679
+ /* Spacing — tablet overrides */
680
+ --spacing-sm: 12px;
681
+ --spacing-md: 24px;
682
+ --spacing-lg: 32px;
683
+ }
684
+ }
685
+
686
+ @media (min-width: 1024px) {
687
+ :root {
688
+ /* Spacing — desktop overrides */
689
+ --spacing-sm: 16px;
690
+ --spacing-md: 32px;
691
+ --spacing-lg: 48px;
692
+ }
693
+ }
694
+ ```
695
+
696
+ **Key behaviors:**
697
+ - The default mode (smallest breakpoint, typically "mobile") is already in `:root`
698
+ - Modes with breakpoint value 0 (mobile) are skipped in the media query loop
699
+ - Breakpoint modes are sorted ascending for correct mobile-first cascade
700
+ - Each media query re-opens `:root` to override the base values
701
+
702
+ #### Theme Mode Rendering
703
+
704
+ For collections classified as `theme`, non-default modes generate **two** selectors: a `prefers-color-scheme` media query and a `[data-theme]` attribute selector.
705
+
706
+ ```css
707
+ /* Base: light values (from :root above) */
708
+
709
+ @media (prefers-color-scheme: dark) {
710
+ :root {
711
+ --color-bg: #1a1a1a;
712
+ --color-text: #ffffff;
713
+ --color-primary: #5c9aff;
714
+ }
715
+ }
716
+
717
+ [data-theme="dark"] {
718
+ --color-bg: #1a1a1a;
719
+ --color-text: #ffffff;
720
+ --color-primary: #5c9aff;
721
+ }
722
+ ```
723
+
724
+ **Why dual selectors:**
725
+
726
+ 1. **`prefers-color-scheme`** -- Automatic theme based on OS/browser preference. No JavaScript required.
727
+ 2. **`[data-theme]`** -- Manual theme toggle via JavaScript (`document.documentElement.setAttribute('data-theme', 'dark')`). Overrides OS preference when set.
728
+
729
+ The `[data-theme]` selector has higher specificity than the media query, so manual selection takes precedence.
730
+
731
+ #### Dark Mode Detection
732
+
733
+ Only modes whose name contains "dark" get the `prefers-color-scheme: dark` media query. Other non-default theme modes (e.g., "Brand B") get only the `[data-theme]` attribute selector:
734
+
735
+ ```typescript
736
+ if (themeName.includes('dark')) {
737
+ // Generate @media (prefers-color-scheme: dark) { :root { ... } }
738
+ }
739
+ // Always generate [data-theme="..."] { ... }
740
+ ```
741
+
742
+ #### Complete Rendered Output Example
743
+
744
+ A file with both a theme collection and a spacing collection:
745
+
746
+ ```css
747
+ :root {
748
+ /* Theme */
749
+ --color-bg: #ffffff;
750
+ --color-text: #1a1a1a;
751
+ --color-primary: #1a73e8;
752
+
753
+ /* Spacing */
754
+ --spacing-sm: 8px;
755
+ --spacing-md: 16px;
756
+ --spacing-lg: 24px;
757
+
758
+ /* Colors (auto-detected) */
759
+ --color-neutral-100: #f5f5f5;
760
+ --color-neutral-800: #333333;
761
+
762
+ /* Font Families */
763
+ --font-primary: 'Inter', sans-serif;
764
+ }
765
+
766
+ @media (min-width: 768px) {
767
+ :root {
768
+ --spacing-sm: 12px;
769
+ --spacing-md: 24px;
770
+ --spacing-lg: 32px;
771
+ }
772
+ }
773
+
774
+ @media (min-width: 1024px) {
775
+ :root {
776
+ --spacing-sm: 16px;
777
+ --spacing-md: 32px;
778
+ --spacing-lg: 48px;
779
+ }
780
+ }
781
+
782
+ @media (prefers-color-scheme: dark) {
783
+ :root {
784
+ --color-bg: #1a1a1a;
785
+ --color-text: #ffffff;
786
+ --color-primary: #5c9aff;
787
+ }
788
+ }
789
+
790
+ [data-theme="dark"] {
791
+ --color-bg: #1a1a1a;
792
+ --color-text: #ffffff;
793
+ --color-primary: #5c9aff;
794
+ }
795
+ ```
796
+
797
+ Note how auto-detected tokens (colors, font families) and variable tokens coexist in the same `:root` block. Variable tokens are grouped by collection, auto-detected tokens are grouped by category.
798
+
799
+ > For the `:root` block structure and category ordering, see `design-tokens.md` Section 10.
800
+
801
+ ---
802
+
803
+ ### 9. Fallback Values
804
+
805
+ Fallback values in `var()` references protect against missing variable definitions. The fallback strategy depends on whether the variable is local or from an external library.
806
+
807
+ #### Local Variables: No Fallback
808
+
809
+ Local variables (defined in the same Figma file) are always included in the generated `tokens.css` output. Since the token definition and the `var()` reference ship together, fallbacks are unnecessary:
810
+
811
+ ```css
812
+ /* Local variable — guaranteed to be defined in tokens.css */
813
+ background-color: var(--color-primary);
814
+ gap: var(--spacing-md);
815
+ border-radius: var(--radius-md);
816
+ ```
817
+
818
+ #### External Library Variables: Always Include Fallback
819
+
820
+ External library variables (from a separate Figma file) may not have corresponding definitions in the current project's `tokens.css`. The library token definitions live in the library's file, not the consuming file. Include the resolved raw value as a fallback:
821
+
822
+ ```css
823
+ /* External library variable — fallback protects against missing definition */
824
+ background-color: var(--color-link-default, #1a73e8);
825
+ color: var(--color-brand-primary, #6366F1);
826
+ gap: var(--spacing-external-md, 16px);
827
+ ```
828
+
829
+ #### Fallback Extraction
830
+
831
+ The fallback value comes from the variable's resolved value at extraction time (the raw CSS value that the variable currently resolves to):
832
+
833
+ ```typescript
834
+ function lookupFillColor(fill, lookup): ColorLookupResult {
835
+ if (fill.variable && fill.variable.name) {
836
+ const cssVarName = figmaVariableToCssName(fill.variable.name)
837
+
838
+ if (fill.variable.isLocal) {
839
+ return { value: `var(${cssVarName})` }
840
+ }
841
+
842
+ // External: include raw value as fallback
843
+ return {
844
+ value: `var(${cssVarName}, ${fill.color})`,
845
+ comment: `/* fallback: "${fill.variable.name}" is from external library */`,
846
+ }
847
+ }
848
+ return { value: lookupColor(lookup, fill.color) }
849
+ }
850
+ ```
851
+
852
+ #### Token File Inclusion
853
+
854
+ During promotion, only **local** Figma Variables are included in the token output file. External variables are excluded because their definitions belong to the library:
855
+
856
+ ```typescript
857
+ // From promote.ts
858
+ const figmaVariables = tokens.figmaVariables?.filter((v) => v.isLocal)
859
+ ```
860
+
861
+ This means:
862
+ - Local variables appear in `tokens.css` as `:root { --name: value; }` definitions
863
+ - Local variables are referenced without fallbacks: `var(--name)`
864
+ - External variables do NOT appear in `tokens.css`
865
+ - External variables are referenced WITH fallbacks: `var(--name, rawValue)`
866
+
867
+ #### When to Use Fallbacks on Local Variables
868
+
869
+ While the default approach omits fallbacks for local variables, some projects may want fallbacks for resilience:
870
+
871
+ | Scenario | Recommendation |
872
+ |----------|---------------|
873
+ | Single project, tokens and components shipped together | No fallback needed |
874
+ | Component library consumed by multiple projects | Include fallback |
875
+ | Visual Builder / iframe isolation | Include fallback (token aliasing may break) |
876
+ | Development/preview mode | Include fallback (tokens may not be loaded) |
877
+
878
+ > For the Visual Builder token aliasing pattern that bridges this gap, see `css-strategy.md` Section 4.
879
+
880
+ ---
881
+
882
+ ### 10. Variable Scopes & Token Applicability
883
+
884
+ Figma variable scopes constrain which UI pickers display a variable in the Figma editor. While scopes do NOT prevent programmatic binding via API or plugin code, they signal the designer's intended usage for each variable.
885
+
886
+ #### Scope → CSS Property Mapping
887
+
888
+ | Variable Scope | Applies To | CSS Properties |
889
+ |---------------|-----------|----------------|
890
+ | `ALL_SCOPES` | Everything | Any property |
891
+ | `ALL_FILLS` | All fill types | `background-color`, `color`, `fill` |
892
+ | `FRAME_FILL` | Frame fills only | `background-color` |
893
+ | `SHAPE_FILL` | Shape fills only | `background-color`, `fill` |
894
+ | `TEXT_FILL` | Text fills only | `color` |
895
+ | `STROKE_COLOR` | Stroke colors | `border-color`, `outline-color` |
896
+ | `EFFECT_COLOR` | Effect colors | Color component of `box-shadow` |
897
+ | `CORNER_RADIUS` | Border radius | `border-radius` |
898
+ | `WIDTH_HEIGHT` | Dimensions | `width`, `height` |
899
+ | `GAP` | Spacing/gap | `gap`, `row-gap`, `column-gap`, `padding`, `margin` |
900
+ | `OPACITY` | Opacity | `opacity` |
901
+ | `STROKE_FLOAT` | Stroke width | `border-width`, `outline-width` |
902
+ | `EFFECT_FLOAT` | Effect values | `blur()` radius, shadow spread |
903
+ | `FONT_FAMILY` | Font family | `font-family` |
904
+ | `FONT_STYLE` | Font style | `font-style` |
905
+ | `FONT_WEIGHT` | Font weight | `font-weight` |
906
+ | `FONT_SIZE` | Font size | `font-size` |
907
+ | `LINE_HEIGHT` | Line height | `line-height` |
908
+ | `LETTER_SPACING` | Letter spacing | `letter-spacing` |
909
+
910
+ #### Scope-Based Token Filtering
911
+
912
+ Variable scopes can be used to filter which tokens apply to which CSS properties. A variable scoped to `TEXT_FILL` should not be used as a `background-color` token, even if the color value matches.
913
+
914
+ However, the current recommended approach does not filter by scope -- it uses all variables as general-purpose tokens. Scope-based filtering is a future enhancement that would improve token specificity.
915
+
916
+ #### Scope Validity by Type
917
+
918
+ Only certain scopes are valid for each `resolvedType`:
919
+
920
+ | resolvedType | Valid Scopes |
921
+ |-------------|-------------|
922
+ | `COLOR` | `ALL_SCOPES`, `ALL_FILLS`, `FRAME_FILL`, `SHAPE_FILL`, `TEXT_FILL`, `STROKE_COLOR`, `EFFECT_COLOR` |
923
+ | `FLOAT` | `ALL_SCOPES`, `CORNER_RADIUS`, `WIDTH_HEIGHT`, `GAP`, `OPACITY`, `STROKE_FLOAT`, `EFFECT_FLOAT`, `FONT_WEIGHT`, `FONT_SIZE`, `LINE_HEIGHT`, `LETTER_SPACING` |
924
+ | `STRING` | `ALL_SCOPES`, `TEXT_CONTENT`, `FONT_FAMILY`, `FONT_STYLE` |
925
+ | `BOOLEAN` | `ALL_SCOPES` |
926
+
927
+ > For the complete scope reference and API details, see `figma-api-variables.md` Section "VariableScope".
928
+
929
+ ---
930
+
931
+ ### 11. Multi-Mode Design Patterns
932
+
933
+ Multi-mode variables enable designs that adapt to different contexts without changing component structure. The token system supports several patterns.
934
+
935
+ #### Pattern 1: Responsive Spacing
936
+
937
+ The same spacing variable has different values per breakpoint. Components automatically adapt when the viewport changes because they reference `var(--spacing-md)`:
938
+
939
+ ```
940
+ Collection: "Spacing"
941
+ Modes: [Mobile, Tablet, Desktop]
942
+
943
+ Variable: spacing/md
944
+ Mobile: 16px
945
+ Tablet: 24px
946
+ Desktop: 32px
947
+
948
+ Variable: spacing/lg
949
+ Mobile: 24px
950
+ Tablet: 32px
951
+ Desktop: 48px
952
+ ```
953
+
954
+ **CSS Output:**
955
+
956
+ ```css
957
+ :root {
958
+ --spacing-md: 16px;
959
+ --spacing-lg: 24px;
960
+ }
961
+
962
+ @media (min-width: 768px) {
963
+ :root {
964
+ --spacing-md: 24px;
965
+ --spacing-lg: 32px;
966
+ }
967
+ }
968
+
969
+ @media (min-width: 1024px) {
970
+ :root {
971
+ --spacing-md: 32px;
972
+ --spacing-lg: 48px;
973
+ }
974
+ }
975
+ ```
976
+
977
+ **Component CSS (unchanged across breakpoints):**
978
+
979
+ ```css
980
+ .card {
981
+ padding: var(--spacing-md);
982
+ gap: var(--spacing-lg);
983
+ }
984
+ ```
985
+
986
+ #### Pattern 2: Light/Dark Theme Colors
987
+
988
+ Color variables switch between light and dark palettes. The component CSS stays identical:
989
+
990
+ ```
991
+ Collection: "Theme"
992
+ Modes: [Light, Dark]
993
+
994
+ Variable: color/bg
995
+ Light: #ffffff
996
+ Dark: #1a1a1a
997
+
998
+ Variable: color/text
999
+ Light: #1a1a1a
1000
+ Dark: #ffffff
1001
+
1002
+ Variable: color/primary
1003
+ Light: #1a73e8
1004
+ Dark: #5c9aff
1005
+ ```
1006
+
1007
+ **CSS Output:**
1008
+
1009
+ ```css
1010
+ :root {
1011
+ --color-bg: #ffffff;
1012
+ --color-text: #1a1a1a;
1013
+ --color-primary: #1a73e8;
1014
+ }
1015
+
1016
+ @media (prefers-color-scheme: dark) {
1017
+ :root {
1018
+ --color-bg: #1a1a1a;
1019
+ --color-text: #ffffff;
1020
+ --color-primary: #5c9aff;
1021
+ }
1022
+ }
1023
+
1024
+ [data-theme="dark"] {
1025
+ --color-bg: #1a1a1a;
1026
+ --color-text: #ffffff;
1027
+ --color-primary: #5c9aff;
1028
+ }
1029
+ ```
1030
+
1031
+ #### Pattern 3: Responsive Typography
1032
+
1033
+ Font sizes and line heights that scale with viewport:
1034
+
1035
+ ```
1036
+ Collection: "Typography"
1037
+ Modes: [Mobile, Desktop]
1038
+
1039
+ Variable: text/body
1040
+ Mobile: 16px
1041
+ Desktop: 18px
1042
+
1043
+ Variable: text/heading
1044
+ Mobile: 28px
1045
+ Desktop: 40px
1046
+ ```
1047
+
1048
+ **CSS Output:**
1049
+
1050
+ ```css
1051
+ :root {
1052
+ --text-body: 16px;
1053
+ --text-heading: 28px;
1054
+ }
1055
+
1056
+ @media (min-width: 1024px) {
1057
+ :root {
1058
+ --text-body: 18px;
1059
+ --text-heading: 40px;
1060
+ }
1061
+ }
1062
+ ```
1063
+
1064
+ #### Pattern 4: Brand Theming
1065
+
1066
+ Multiple brand variants sharing a common token structure:
1067
+
1068
+ ```
1069
+ Collection: "Brand"
1070
+ Modes: [Brand A, Brand B, Brand C]
1071
+
1072
+ Variable: color/primary
1073
+ Brand A: #0066ff
1074
+ Brand B: #ff3366
1075
+ Brand C: #00cc88
1076
+ ```
1077
+
1078
+ Brand modes do not match theme or breakpoint patterns, so they are classified as `unknown`. The pipeline renders only the default mode (Brand A) in `:root`. For multi-brand support, custom rendering logic is needed to generate class-based selectors:
1079
+
1080
+ ```css
1081
+ /* Default (Brand A) */
1082
+ :root {
1083
+ --color-primary: #0066ff;
1084
+ }
1085
+
1086
+ /* Custom rendering (not auto-generated) */
1087
+ .brand-b { --color-primary: #ff3366; }
1088
+ .brand-c { --color-primary: #00cc88; }
1089
+ ```
1090
+
1091
+ > For the class-based brand theming pattern, see `figma-api-variables.md` Section "Pattern: Brand Theming".
1092
+
1093
+ #### Pattern 5: Combined Responsive + Theme (Cross-Product)
1094
+
1095
+ When a file has both breakpoint and theme collections, they produce independent CSS blocks that compose naturally:
1096
+
1097
+ ```
1098
+ Collection: "Theme" (theme mode type)
1099
+ Modes: [Light, Dark]
1100
+ Variables: color tokens...
1101
+
1102
+ Collection: "Spacing" (breakpoint mode type)
1103
+ Modes: [Mobile, Tablet, Desktop]
1104
+ Variables: spacing tokens...
1105
+ ```
1106
+
1107
+ **CSS Output:**
1108
+
1109
+ ```css
1110
+ :root {
1111
+ /* Theme — Light (default) */
1112
+ --color-bg: #ffffff;
1113
+ --color-text: #1a1a1a;
1114
+
1115
+ /* Spacing — Mobile (default) */
1116
+ --spacing-md: 16px;
1117
+ --spacing-lg: 24px;
1118
+ }
1119
+
1120
+ /* Spacing overrides (responsive) */
1121
+ @media (min-width: 768px) {
1122
+ :root {
1123
+ --spacing-md: 24px;
1124
+ --spacing-lg: 32px;
1125
+ }
1126
+ }
1127
+
1128
+ @media (min-width: 1024px) {
1129
+ :root {
1130
+ --spacing-md: 32px;
1131
+ --spacing-lg: 48px;
1132
+ }
1133
+ }
1134
+
1135
+ /* Theme overrides (dark) */
1136
+ @media (prefers-color-scheme: dark) {
1137
+ :root {
1138
+ --color-bg: #1a1a1a;
1139
+ --color-text: #ffffff;
1140
+ }
1141
+ }
1142
+
1143
+ [data-theme="dark"] {
1144
+ --color-bg: #1a1a1a;
1145
+ --color-text: #ffffff;
1146
+ }
1147
+ ```
1148
+
1149
+ These compose correctly because:
1150
+ - Spacing media queries override spacing tokens (independent of theme)
1151
+ - Theme selectors override color tokens (independent of viewport)
1152
+ - A dark-theme user on a tablet gets both dark colors AND tablet spacing
1153
+
1154
+ #### Limitations of Cross-Product
1155
+
1156
+ Figma does not support cross-product modes within a single collection (e.g., you cannot have "Light Mobile" and "Dark Desktop" as modes in one collection). The cross-product emerges naturally from having separate collections with independent mode axes.
1157
+
1158
+ If a design needs values that vary by **both** theme and breakpoint simultaneously (e.g., different spacing in dark mode), this requires either:
1159
+ - Two separate collections that compose
1160
+ - Custom post-processing to merge the outputs
1161
+
1162
+ ---
1163
+
1164
+ ### 12. Integration with Token Pipeline
1165
+
1166
+ Variables integrate with the existing token pipeline at two points: collection (Stage 1) and promotion (Stage 2).
1167
+
1168
+ #### Collection Integration
1169
+
1170
+ Variables are collected alongside auto-detected tokens in `collectAllTokens`:
1171
+
1172
+ ```typescript
1173
+ async function collectAllTokens(node: ExtractedNode): Promise<DesignTokens> {
1174
+ const variableCollections = await getVariableCollections()
1175
+
1176
+ return {
1177
+ colors: collectColors(node), // Auto-detected
1178
+ spacing: collectSpacing(node), // Auto-detected
1179
+ typography: collectTypography(node), // Auto-detected
1180
+ effects: collectEffects(node), // Auto-detected
1181
+ figmaVariables: await collectFigmaVariables(node, variableCollections), // Explicit
1182
+ variableCollections, // Mode metadata
1183
+ }
1184
+ }
1185
+ ```
1186
+
1187
+ Variable collections are fetched first because the `collectFigmaVariables` function needs mode information to extract per-mode values.
1188
+
1189
+ #### Promotion Integration
1190
+
1191
+ During promotion, variables and auto-detected tokens are filtered differently:
1192
+
1193
+ ```typescript
1194
+ function promoteTokens(tokens: DesignTokens, threshold: number = 2): DesignTokens {
1195
+ // Auto-detected tokens: filter by usage count
1196
+ const colors = tokens.colors.filter(c => c.usageCount >= threshold)
1197
+ const spacing = tokens.spacing.filter(s => s.usageCount >= threshold)
1198
+ // ... etc.
1199
+
1200
+ // Figma variables: ALWAYS included (explicit bindings), but only LOCAL
1201
+ const figmaVariables = tokens.figmaVariables?.filter(v => v.isLocal)
1202
+
1203
+ return { colors, spacing, typography, effects, figmaVariables, variableCollections }
1204
+ }
1205
+ ```
1206
+
1207
+ Key differences:
1208
+
1209
+ | Aspect | Auto-Detected Tokens | Figma Variable Tokens |
1210
+ |--------|:---:|:---:|
1211
+ | Promotion filter | Usage count >= threshold | Always promoted |
1212
+ | Local/external | N/A (always from current file) | Only local included |
1213
+ | Naming | Heuristic (HSL, scale, role) | From variable path |
1214
+ | Mode values | None (single value) | Per-mode values |
1215
+ | Conflict priority | Lower | Higher (wins conflicts) |
1216
+
1217
+ #### Rendering Integration
1218
+
1219
+ The `renderTokensCSS` function renders both token types in a single output:
1220
+
1221
+ 1. **`:root` block** contains:
1222
+ - Figma Variables (default mode values), grouped by collection
1223
+ - Auto-detected tokens (colors, spacing, typography, effects), grouped by category
1224
+
1225
+ 2. **Media query blocks** contain:
1226
+ - Breakpoint mode overrides (Figma Variables only)
1227
+
1228
+ 3. **Theme selector blocks** contain:
1229
+ - Theme mode overrides (Figma Variables only)
1230
+
1231
+ Auto-detected tokens do not have mode-aware rendering -- they always appear in the base `:root` block with a single value.
1232
+
1233
+ #### Lookup Integration
1234
+
1235
+ The token lookup system checks variable bindings first, then falls back to auto-detected tokens:
1236
+
1237
+ ```
1238
+ lookupFillColor(fill, lookup):
1239
+ 1. fill.variable exists? → var(--css-name) [± fallback]
1240
+ 2. lookup.colors has this value? → var(--color-token)
1241
+ 3. Neither → raw color value
1242
+ ```
1243
+
1244
+ This means a color that is both:
1245
+ - Bound to a variable named `color/link/default`
1246
+ - Auto-detected as `--color-primary` (most-used saturated color)
1247
+
1248
+ ...will render as `var(--color-link-default)`, not `var(--color-primary)`. The explicit variable binding wins.
1249
+
1250
+ > For the complete token pipeline stages, see `design-tokens.md` Section 1.
1251
+
1252
+ ---
1253
+
1254
+ ## Cross-References
1255
+
1256
+ - **`figma-api-variables.md`** -- Variables API endpoints, data model (Variable, VariableCollection, modes, scopes), alias resolution mechanics, multi-mode theming patterns, access requirements (Enterprise plan). The API-level reference for all Variables operations.
1257
+ - **`design-tokens.md`** -- General token extraction pipeline (collect, promote, name, render), threshold-based promotion, HSL-based color naming, spacing scale detection, token lookup system, CSS/SCSS/Tailwind rendering. The pipeline that Variables plug into.
1258
+ - **`css-strategy.md`** -- Three-layer CSS architecture (Tailwind + Custom Properties + CSS Modules), property placement decision tree, specificity management, responsive strategy, theme support, Visual Builder token aliasing. Where generated tokens are consumed.
1259
+ - **`design-to-code-visual.md`** -- Visual property extraction (fills, strokes, effects, corners) with variable binding resolution. Fill variable priority chain. HSL-based color token naming.
1260
+ - **`design-to-code-layout.md`** -- Layout property extraction (gap, padding) with variable bindings. Variable-aware spacing in CSS generation. Mobile-first responsive pattern.
1261
+ - **`design-to-code-typography.md`** -- Typography extraction with variable bindings on text fill colors in styled segments. Font family/size/weight token lookup during generation.