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,1003 @@
|
|
|
1
|
+
# Text Extraction & CSS Typography
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Authoritative reference for extracting Figma text properties and generating accurate CSS typography. Encodes production-proven mapping rules covering the complete pipeline: extracting font families, weights, sizes, line heights, letter spacing, alignment, decoration, and text case from Figma TextNodes, handling mixed-style (rich text) segments with per-character styling and hyperlinks, and generating pixel-accurate CSS. This module covers everything needed to convert a Figma TEXT node into correct HTML and CSS.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reference this module when you need to:
|
|
10
|
+
|
|
11
|
+
- Convert a Figma `fontFamily` to a CSS `font-family` with appropriate fallback stack
|
|
12
|
+
- Map Figma font style names ("Semi Bold", "Bold Italic") to numeric CSS `font-weight`
|
|
13
|
+
- Convert Figma line height (percentage or pixel) to unitless CSS `line-height` ratio
|
|
14
|
+
- Handle Figma `letterSpacing` in pixels and percentages
|
|
15
|
+
- Map Figma text alignment (horizontal and vertical) to CSS properties
|
|
16
|
+
- Convert `textDecoration` and `textCase` to CSS `text-decoration` and `text-transform`
|
|
17
|
+
- Map `textAutoResize` behavior to CSS wrapping and overflow properties
|
|
18
|
+
- Extract and render multi-style text (styled segments with different colors, weights, sizes)
|
|
19
|
+
- Handle hyperlinks within text segments (`<a href="...">`)
|
|
20
|
+
- Generate ordered and unordered list HTML from Figma list metadata
|
|
21
|
+
- Apply typography token lookup for CSS custom property substitution
|
|
22
|
+
- Generate Google Fonts URLs from collected font data
|
|
23
|
+
- Avoid common typography conversion pitfalls (line height %, "Regular" weight, mixed-style nodes)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Content
|
|
28
|
+
|
|
29
|
+
### 1. Font Family Mapping
|
|
30
|
+
|
|
31
|
+
Figma stores the font family as a `fontFamily` string property on TextNode (e.g., `"Inter"`, `"Roboto Mono"`, `"SF Pro Display"`). This must be converted to a CSS `font-family` declaration with an appropriate fallback stack.
|
|
32
|
+
|
|
33
|
+
#### Extraction
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// From Figma TextNode
|
|
37
|
+
const fontFamily: string = textNode.fontName.family
|
|
38
|
+
// e.g., "Inter", "Roboto", "Fira Code", "Georgia"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When `fontName` is `figma.mixed` (the node has multiple fonts), extract the first character's font as the dominant family:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
let fontName: FontName
|
|
45
|
+
if (typeof textNode.fontName === 'symbol') {
|
|
46
|
+
// Mixed fonts - use first character as dominant
|
|
47
|
+
fontName = textNode.getRangeFontName(0, 1) as FontName
|
|
48
|
+
} else {
|
|
49
|
+
fontName = textNode.fontName
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### CSS Generation
|
|
54
|
+
|
|
55
|
+
**Default (no token lookup):** Wrap in quotes and add a generic fallback:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
font-family: 'Inter', sans-serif;
|
|
59
|
+
font-family: 'Roboto Mono', monospace;
|
|
60
|
+
font-family: 'Georgia', serif;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**With token lookup:** If a font family token exists, use the CSS custom property:
|
|
64
|
+
|
|
65
|
+
```css
|
|
66
|
+
font-family: var(--font-primary);
|
|
67
|
+
font-family: var(--font-mono);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Fallback Stack Selection
|
|
71
|
+
|
|
72
|
+
| Font Category | Detection | Fallback |
|
|
73
|
+
|---------------|-----------|----------|
|
|
74
|
+
| Sans-serif | Default assumption | `sans-serif` |
|
|
75
|
+
| Monospace | Name contains "mono", "code", "consolas", "courier", "menlo", etc. | `monospace` |
|
|
76
|
+
| Serif | Name contains "serif", "georgia", "times", "garamond", "baskerville" | `serif` |
|
|
77
|
+
|
|
78
|
+
#### Monospace Font Detection
|
|
79
|
+
|
|
80
|
+
The following font name patterns indicate a monospace font:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
mono, code, consolas, courier, fira code, jetbrains, menlo,
|
|
84
|
+
monaco, source code, ubuntu mono, sf mono, roboto mono, ibm plex mono
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Detection is case-insensitive substring matching against the font family name.
|
|
88
|
+
|
|
89
|
+
#### System Fonts
|
|
90
|
+
|
|
91
|
+
System fonts are already available on the user's OS and should NOT be loaded from Google Fonts. Common system fonts:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Arial, Helvetica, Helvetica Neue, Times, Times New Roman, Georgia,
|
|
95
|
+
Verdana, Tahoma, Courier, Courier New, Lucida Console, Monaco,
|
|
96
|
+
Segoe UI, SF Pro, SF Pro Display, SF Pro Text, system-ui, -apple-system,
|
|
97
|
+
Optima, Futura, Avenir, Avenir Next, Gill Sans, Baskerville, Didot,
|
|
98
|
+
Consolas, Menlo, Palatino, Garamond, Century Gothic, Arial Black
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Google Fonts URL Generation
|
|
102
|
+
|
|
103
|
+
For non-system fonts, generate a Google Fonts `<link>` URL:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto+Mono:wght@400&display=swap
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Rules:**
|
|
110
|
+
- Group variants by family
|
|
111
|
+
- Include all used weights per family
|
|
112
|
+
- Append `italic` suffix for italic variants (e.g., `700italic`)
|
|
113
|
+
- Replace spaces with `+` in family names
|
|
114
|
+
- Always add `display=swap` for performance
|
|
115
|
+
- Exclude system fonts from the URL
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### 2. Font Weight from Style String
|
|
120
|
+
|
|
121
|
+
Figma does not store font weight as a number. Instead, it uses a style name string (e.g., `"Regular"`, `"Bold"`, `"Semi Bold"`, `"Bold Italic"`). This must be parsed into a numeric CSS `font-weight`.
|
|
122
|
+
|
|
123
|
+
#### Weight Mapping Table
|
|
124
|
+
|
|
125
|
+
| Figma Style Name | CSS `font-weight` | Notes |
|
|
126
|
+
|------------------|-------------------|-------|
|
|
127
|
+
| `Thin` | `100` | Hairline weight |
|
|
128
|
+
| `ExtraLight` | `200` | Also "Ultra Light" |
|
|
129
|
+
| `Light` | `300` | |
|
|
130
|
+
| `Regular` | `400` | Default. **NOT** output as `font-weight: regular` |
|
|
131
|
+
| `Medium` | `500` | |
|
|
132
|
+
| `SemiBold` | `600` | Also "Semi Bold", "Demi Bold" |
|
|
133
|
+
| `Bold` | `700` | |
|
|
134
|
+
| `ExtraBold` | `800` | Also "Ultra Bold" |
|
|
135
|
+
| `Black` | `900` | Also "Heavy" |
|
|
136
|
+
|
|
137
|
+
#### Extraction Logic
|
|
138
|
+
|
|
139
|
+
The style-to-weight mapping uses substring matching to handle compound styles:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
function styleToWeight(style: string): number {
|
|
143
|
+
const weightMap: Record<string, number> = {
|
|
144
|
+
'Thin': 100,
|
|
145
|
+
'ExtraLight': 200,
|
|
146
|
+
'Light': 300,
|
|
147
|
+
'Regular': 400,
|
|
148
|
+
'Medium': 500,
|
|
149
|
+
'SemiBold': 600,
|
|
150
|
+
'Bold': 700,
|
|
151
|
+
'ExtraBold': 800,
|
|
152
|
+
'Black': 900,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle compound styles like "Bold Italic"
|
|
156
|
+
for (const [name, weight] of Object.entries(weightMap)) {
|
|
157
|
+
if (style.includes(name)) return weight
|
|
158
|
+
}
|
|
159
|
+
return 400 // Default to regular
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Key behavior:** The loop checks in insertion order. "ExtraBold" is checked before "Bold", and "ExtraLight" before "Light", preventing false matches on compound names.
|
|
164
|
+
|
|
165
|
+
#### Italic Detection
|
|
166
|
+
|
|
167
|
+
Italic is detected separately from weight by checking if the style string contains "Italic":
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
if (text.font.style.toLowerCase().includes('italic')) {
|
|
171
|
+
styles.fontStyle = 'italic'
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Combined Style Examples
|
|
176
|
+
|
|
177
|
+
| Figma Style | CSS Output |
|
|
178
|
+
|-------------|------------|
|
|
179
|
+
| `"Regular"` | `font-weight: 400;` |
|
|
180
|
+
| `"Bold"` | `font-weight: 700;` |
|
|
181
|
+
| `"Bold Italic"` | `font-weight: 700; font-style: italic;` |
|
|
182
|
+
| `"SemiBold Italic"` | `font-weight: 600; font-style: italic;` |
|
|
183
|
+
| `"Light"` | `font-weight: 300;` |
|
|
184
|
+
| `"Thin Italic"` | `font-weight: 100; font-style: italic;` |
|
|
185
|
+
|
|
186
|
+
#### Token Integration
|
|
187
|
+
|
|
188
|
+
With token lookup, weight maps to a CSS variable:
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
/* Without tokens */
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
|
|
194
|
+
/* With tokens */
|
|
195
|
+
font-weight: var(--font-bold);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Token names follow the pattern `--font-{weightName}`:
|
|
199
|
+
|
|
200
|
+
| Weight | Token Name |
|
|
201
|
+
|--------|------------|
|
|
202
|
+
| 100 | `--font-thin` |
|
|
203
|
+
| 200 | `--font-extralight` |
|
|
204
|
+
| 300 | `--font-light` |
|
|
205
|
+
| 400 | `--font-regular` |
|
|
206
|
+
| 500 | `--font-medium` |
|
|
207
|
+
| 600 | `--font-semibold` |
|
|
208
|
+
| 700 | `--font-bold` |
|
|
209
|
+
| 800 | `--font-extrabold` |
|
|
210
|
+
| 900 | `--font-black` |
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### 3. Font Size
|
|
215
|
+
|
|
216
|
+
Figma stores `fontSize` as a number in pixels (e.g., `16`, `24`, `48`).
|
|
217
|
+
|
|
218
|
+
#### Extraction
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
let fontSize: number
|
|
222
|
+
if (typeof textNode.fontSize === 'symbol') {
|
|
223
|
+
// Mixed sizes - use first character's size
|
|
224
|
+
fontSize = textNode.getRangeFontSize(0, 1) as number
|
|
225
|
+
} else {
|
|
226
|
+
fontSize = textNode.fontSize
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### CSS Generation
|
|
231
|
+
|
|
232
|
+
**Default:** Direct pixel output:
|
|
233
|
+
|
|
234
|
+
```css
|
|
235
|
+
font-size: 16px;
|
|
236
|
+
font-size: 24px;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**With token lookup:** Map to a CSS variable using a type scale:
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
font-size: var(--text-base); /* 16px */
|
|
243
|
+
font-size: var(--text-lg); /* 18px */
|
|
244
|
+
font-size: var(--text-2xl); /* 24px */
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### Type Scale Naming
|
|
248
|
+
|
|
249
|
+
Font size tokens use a T-shirt size scale centered on a "base" size (the collected size closest to 16px):
|
|
250
|
+
|
|
251
|
+
| Scale Name | Position Relative to Base |
|
|
252
|
+
|------------|--------------------------|
|
|
253
|
+
| `--text-xs` | base - 2 |
|
|
254
|
+
| `--text-sm` | base - 1 |
|
|
255
|
+
| `--text-base` | base (closest to 16px) |
|
|
256
|
+
| `--text-lg` | base + 1 |
|
|
257
|
+
| `--text-xl` | base + 2 |
|
|
258
|
+
| `--text-2xl` | base + 3 |
|
|
259
|
+
| `--text-3xl` | base + 4 |
|
|
260
|
+
| `--text-4xl` | base + 5 |
|
|
261
|
+
| `--text-5xl` | base + 6 |
|
|
262
|
+
| `--text-6xl` | base + 7 |
|
|
263
|
+
|
|
264
|
+
If a size falls outside the scale range, it uses `--text-{px}` (e.g., `--text-72`).
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### 4. Line Height Conversion (Critical)
|
|
269
|
+
|
|
270
|
+
Line height is one of the most error-prone Figma-to-CSS conversions. Figma stores line height in multiple units, and all must be converted to **unitless CSS ratios** for correct scaling.
|
|
271
|
+
|
|
272
|
+
#### Figma Line Height Units
|
|
273
|
+
|
|
274
|
+
| Figma `lineHeight.unit` | Figma Value | Meaning |
|
|
275
|
+
|--------------------------|-------------|---------|
|
|
276
|
+
| `AUTO` | N/A | Browser default line height |
|
|
277
|
+
| `PERCENT` | e.g., `150` | 150% of font size |
|
|
278
|
+
| `PIXELS` | e.g., `24` | Fixed 24px line height |
|
|
279
|
+
|
|
280
|
+
#### Conversion Rules
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
function getLineHeight(lineHeight: LineHeight | symbol, fontSize: number): number | 'auto' {
|
|
284
|
+
if (typeof lineHeight === 'symbol') return 'auto' // figma.mixed
|
|
285
|
+
if (lineHeight.unit === 'AUTO') return 'auto'
|
|
286
|
+
if (lineHeight.unit === 'PERCENT') {
|
|
287
|
+
// Percentage → unitless ratio: 150% → 1.5
|
|
288
|
+
return lineHeight.value / 100
|
|
289
|
+
}
|
|
290
|
+
// PIXELS → unitless ratio: 24px on 16px font → 1.5
|
|
291
|
+
return lineHeight.value / fontSize
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### Critical: Percentage Means % of Font Size
|
|
296
|
+
|
|
297
|
+
In Figma, a line height of "150%" means **150% of the font size**, not 150% of some other dimension. A 16px font with 150% line height has a computed line height of 24px.
|
|
298
|
+
|
|
299
|
+
**Conversion formula:** `lineHeightPercentage / 100 = unitless ratio`
|
|
300
|
+
|
|
301
|
+
- 120% -> `line-height: 1.2`
|
|
302
|
+
- 150% -> `line-height: 1.5`
|
|
303
|
+
- 200% -> `line-height: 2`
|
|
304
|
+
|
|
305
|
+
#### Critical: Always Use Unitless Ratios in CSS
|
|
306
|
+
|
|
307
|
+
**Why unitless?** Unitless `line-height` scales correctly when `font-size` changes (e.g., responsive scaling, user zoom). Pixel or percentage `line-height` in CSS does NOT scale.
|
|
308
|
+
|
|
309
|
+
```css
|
|
310
|
+
/* CORRECT — unitless ratio, scales with font-size */
|
|
311
|
+
line-height: 1.50;
|
|
312
|
+
|
|
313
|
+
/* AVOID — fixed pixel value, does not scale */
|
|
314
|
+
line-height: 24px;
|
|
315
|
+
|
|
316
|
+
/* AVOID — percentage in CSS behaves differently than in Figma */
|
|
317
|
+
line-height: 150%;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### CSS Generation
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
if (text.lineHeight === 'auto') {
|
|
324
|
+
styles.lineHeight = 'normal'
|
|
325
|
+
} else {
|
|
326
|
+
styles.lineHeight = text.lineHeight.toFixed(2)
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
| Figma Line Height | Font Size | CSS Output |
|
|
331
|
+
|-------------------|-----------|------------|
|
|
332
|
+
| AUTO | any | `line-height: normal;` |
|
|
333
|
+
| 150% | 16px | `line-height: 1.50;` |
|
|
334
|
+
| 120% | 16px | `line-height: 1.20;` |
|
|
335
|
+
| 24px | 16px | `line-height: 1.50;` |
|
|
336
|
+
| 144px | 128px | `line-height: 1.13;` |
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### 5. Letter Spacing
|
|
341
|
+
|
|
342
|
+
Figma stores `letterSpacing` with a unit (PIXELS or PERCENT) and a numeric value.
|
|
343
|
+
|
|
344
|
+
#### Extraction
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
function getLetterSpacing(letterSpacing: LetterSpacing | symbol): number {
|
|
348
|
+
if (typeof letterSpacing === 'symbol') return 0 // figma.mixed
|
|
349
|
+
if (letterSpacing.unit === 'PERCENT') {
|
|
350
|
+
// Percentage of font size - returned as raw value for CSS gen
|
|
351
|
+
return letterSpacing.value
|
|
352
|
+
}
|
|
353
|
+
return letterSpacing.value // PIXELS
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### CSS Generation
|
|
358
|
+
|
|
359
|
+
**Pixel values:** Output directly:
|
|
360
|
+
|
|
361
|
+
```css
|
|
362
|
+
letter-spacing: 0.5px;
|
|
363
|
+
letter-spacing: -0.2px;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Percentage values:** Figma percentage letter spacing is relative to font size. In CSS, this maps to `em` units:
|
|
367
|
+
|
|
368
|
+
```css
|
|
369
|
+
/* Figma: 5% letter spacing on any font size */
|
|
370
|
+
letter-spacing: 0.05em;
|
|
371
|
+
|
|
372
|
+
/* Figma: -2% letter spacing */
|
|
373
|
+
letter-spacing: -0.02em;
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Zero or near-zero:** Omit the property entirely:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
if (text.letterSpacing !== 0) {
|
|
380
|
+
styles.letterSpacing = `${text.letterSpacing}px`
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
### 6. Text Alignment
|
|
387
|
+
|
|
388
|
+
Figma text nodes have both horizontal and vertical alignment.
|
|
389
|
+
|
|
390
|
+
#### Horizontal Alignment
|
|
391
|
+
|
|
392
|
+
| Figma `textAlignHorizontal` | CSS `text-align` |
|
|
393
|
+
|-----------------------------|------------------|
|
|
394
|
+
| `LEFT` | `left` |
|
|
395
|
+
| `CENTER` | `center` |
|
|
396
|
+
| `RIGHT` | `right` |
|
|
397
|
+
| `JUSTIFIED` | `justify` |
|
|
398
|
+
|
|
399
|
+
Extraction maps directly to CSS-ready values:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
function mapHorizontalAlign(align: string): 'left' | 'center' | 'right' | 'justify' {
|
|
403
|
+
const map: Record<string, string> = {
|
|
404
|
+
'LEFT': 'left',
|
|
405
|
+
'CENTER': 'center',
|
|
406
|
+
'RIGHT': 'right',
|
|
407
|
+
'JUSTIFIED': 'justify',
|
|
408
|
+
}
|
|
409
|
+
return map[align] || 'left'
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### Vertical Alignment
|
|
414
|
+
|
|
415
|
+
Figma text nodes support vertical alignment within their bounding box. This requires flexbox in CSS because `vertical-align` only works on inline/table elements.
|
|
416
|
+
|
|
417
|
+
| Figma `textAlignVertical` | CSS Output |
|
|
418
|
+
|---------------------------|------------|
|
|
419
|
+
| `TOP` | _(omit - default)_ |
|
|
420
|
+
| `CENTER` | `display: flex; align-items: center;` |
|
|
421
|
+
| `BOTTOM` | `display: flex; align-items: flex-end;` |
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
if (text.align.vertical !== 'top') {
|
|
425
|
+
styles.display = 'flex'
|
|
426
|
+
styles.alignItems = text.align.vertical === 'center' ? 'center' : 'flex-end'
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**Warning:** Adding `display: flex` to a text element for vertical alignment can conflict with the text's natural inline layout. This is an acceptable trade-off for fixed-dimension text boxes with vertical centering, but be aware of potential interaction with `text-align` (which still works on flex items via `justify-content` or `text-align` on the flex item's text content).
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### 7. Text Decoration & Transform
|
|
435
|
+
|
|
436
|
+
#### Text Decoration
|
|
437
|
+
|
|
438
|
+
Figma uses `textDecoration` with values `NONE`, `UNDERLINE`, and `STRIKETHROUGH`.
|
|
439
|
+
|
|
440
|
+
| Figma `textDecoration` | CSS `text-decoration` |
|
|
441
|
+
|------------------------|-----------------------|
|
|
442
|
+
| `NONE` | _(omit)_ |
|
|
443
|
+
| `UNDERLINE` | `underline` |
|
|
444
|
+
| `STRIKETHROUGH` | `line-through` |
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
if (text.decoration !== 'none') {
|
|
448
|
+
styles.textDecoration = text.decoration === 'strikethrough'
|
|
449
|
+
? 'line-through'
|
|
450
|
+
: text.decoration
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Note:** Figma's `STRIKETHROUGH` maps to CSS `line-through`, not `strikethrough`. This is a common mapping error.
|
|
455
|
+
|
|
456
|
+
When `textDecoration` is `figma.mixed` (the symbol type), default to `'none'`:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
function mapDecoration(decoration: string | symbol): 'none' | 'underline' | 'strikethrough' {
|
|
460
|
+
if (typeof decoration === 'symbol') return 'none' // figma.mixed
|
|
461
|
+
const map = { 'NONE': 'none', 'UNDERLINE': 'underline', 'STRIKETHROUGH': 'strikethrough' }
|
|
462
|
+
return map[decoration] || 'none'
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### Text Transform (Case)
|
|
467
|
+
|
|
468
|
+
Figma uses `textCase` with values `ORIGINAL`, `UPPER`, `LOWER`, and `TITLE`.
|
|
469
|
+
|
|
470
|
+
| Figma `textCase` | CSS `text-transform` |
|
|
471
|
+
|-------------------|---------------------|
|
|
472
|
+
| `ORIGINAL` | _(omit)_ |
|
|
473
|
+
| `UPPER` | `uppercase` |
|
|
474
|
+
| `LOWER` | `lowercase` |
|
|
475
|
+
| `TITLE` | `capitalize` |
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
if (text.textCase !== 'none') {
|
|
479
|
+
styles.textTransform = text.textCase
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The extraction maps `ORIGINAL` to `'none'`, which is then omitted from CSS output.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
### 8. Text Auto-Resize Behavior
|
|
488
|
+
|
|
489
|
+
Figma's `textAutoResize` property controls how a text node's bounding box responds to its content. This maps to CSS wrapping, overflow, and sizing behavior.
|
|
490
|
+
|
|
491
|
+
#### Mapping Table
|
|
492
|
+
|
|
493
|
+
| Figma `textAutoResize` | Behavior | CSS Output |
|
|
494
|
+
|-------------------------|----------|------------|
|
|
495
|
+
| `WIDTH_AND_HEIGHT` | Text box resizes both dimensions to fit content. Single line, no wrapping. | `white-space: nowrap;` |
|
|
496
|
+
| `HEIGHT` | Width is fixed, height grows to fit. Text wraps at fixed width. | _(default CSS behavior - no extra properties)_ |
|
|
497
|
+
| `NONE` | Both dimensions are fixed. Text may overflow or clip. | `overflow: hidden;` (may add `width` and `height`) |
|
|
498
|
+
| `TRUNCATE` | Fixed dimensions with ellipsis on overflow. | `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;` |
|
|
499
|
+
|
|
500
|
+
#### CSS Generation
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// WIDTH_AND_HEIGHT: single-line text, no wrap
|
|
504
|
+
if (text.textAutoResize === 'WIDTH_AND_HEIGHT') {
|
|
505
|
+
styles.whiteSpace = 'nowrap'
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// TRUNCATE: ellipsis overflow
|
|
509
|
+
if (text.textAutoResize === 'TRUNCATE') {
|
|
510
|
+
styles.overflow = 'hidden'
|
|
511
|
+
styles.textOverflow = 'ellipsis'
|
|
512
|
+
styles.whiteSpace = 'nowrap'
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// NONE: fixed dimensions, clip overflow
|
|
516
|
+
if (text.textAutoResize === 'NONE') {
|
|
517
|
+
styles.overflow = 'hidden'
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// HEIGHT: default wrapping behavior, no extra CSS needed
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### Interaction with Layout Sizing
|
|
524
|
+
|
|
525
|
+
`textAutoResize` interacts with the parent Auto Layout's sizing modes:
|
|
526
|
+
|
|
527
|
+
- **WIDTH_AND_HEIGHT** in a FILL container: The `white-space: nowrap` prevents wrapping, but `flex-grow: 1` still allows the text to claim available space. The text will not wrap even if the container is narrower.
|
|
528
|
+
- **HEIGHT** in a FILL container: The text width fills the container and wraps normally. This is the most common scenario for body text.
|
|
529
|
+
- **TRUNCATE** in a fixed-width container: Ellipsis appears when text overflows the fixed width.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### 9. Styled Text Segments (Rich Text)
|
|
534
|
+
|
|
535
|
+
Figma supports per-character styling within a single TEXT node. A heading might have one word in bold and another in a different color. This is handled through **styled segments** -- ranges of characters with their own styling properties.
|
|
536
|
+
|
|
537
|
+
#### When Styled Segments Are Needed
|
|
538
|
+
|
|
539
|
+
Segment extraction is triggered when any of these conditions are true:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
const hasMixedFills = typeof textNode.fills === 'symbol' // Different colors
|
|
543
|
+
const hasMixedFont = typeof textNode.fontName === 'symbol' // Different fonts
|
|
544
|
+
const hasMixedSize = typeof textNode.fontSize === 'symbol' // Different sizes
|
|
545
|
+
const hasMixedWeight = typeof textNode.fontWeight === 'symbol' // Different weights
|
|
546
|
+
const hasLinks = hasHyperlinks(textNode) // Hyperlinks present
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
If none of these conditions are true, the text node has uniform styling and styled segments are not extracted (the node-level properties are sufficient).
|
|
550
|
+
|
|
551
|
+
#### Extraction via getStyledTextSegments
|
|
552
|
+
|
|
553
|
+
Figma's `getStyledTextSegments` API returns segments with consistent styling:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
const segments = textNode.getStyledTextSegments([
|
|
557
|
+
'fills',
|
|
558
|
+
'fontName',
|
|
559
|
+
'fontSize',
|
|
560
|
+
'fontWeight',
|
|
561
|
+
'letterSpacing',
|
|
562
|
+
'lineHeight',
|
|
563
|
+
'textDecoration',
|
|
564
|
+
'textCase',
|
|
565
|
+
'hyperlink',
|
|
566
|
+
])
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Each segment contains:
|
|
570
|
+
- `start` / `end` -- character indices
|
|
571
|
+
- `characters` -- the text content
|
|
572
|
+
- `fills` -- array of paint fills for this segment
|
|
573
|
+
- `fontName` -- `{ family, style }` for this segment
|
|
574
|
+
- `fontSize` -- size for this segment
|
|
575
|
+
- `hyperlink` -- `{ type: 'URL' | 'NODE', value: string }` if linked
|
|
576
|
+
|
|
577
|
+
#### Intermediate Type
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
interface StyledSegment {
|
|
581
|
+
start: number
|
|
582
|
+
end: number
|
|
583
|
+
text: string
|
|
584
|
+
fills?: Array<{ color: string; opacity?: number; variable?: VariableReference }>
|
|
585
|
+
font?: {
|
|
586
|
+
family: string
|
|
587
|
+
style: string
|
|
588
|
+
size: number
|
|
589
|
+
weight: number
|
|
590
|
+
}
|
|
591
|
+
letterSpacing?: number
|
|
592
|
+
lineHeight?: number | 'auto'
|
|
593
|
+
decoration?: 'none' | 'underline' | 'strikethrough'
|
|
594
|
+
textCase?: 'none' | 'uppercase' | 'lowercase' | 'capitalize'
|
|
595
|
+
hyperlink?: HyperlinkData
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
interface HyperlinkData {
|
|
599
|
+
type: 'URL' | 'NODE'
|
|
600
|
+
value: string
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
#### HTML Generation for Styled Segments
|
|
605
|
+
|
|
606
|
+
Each segment becomes a `<span>` (or `<a>` for hyperlinks) with only the properties that differ from the base text style:
|
|
607
|
+
|
|
608
|
+
```html
|
|
609
|
+
<!-- Base style on parent element -->
|
|
610
|
+
<p class="hero__heading" style="font-family: 'Inter', sans-serif; font-size: 48px; font-weight: 700;">
|
|
611
|
+
<!-- Segment with different color -->
|
|
612
|
+
<span class="hero__heading__segment-1" style="color: #1A1A1A;">Build </span>
|
|
613
|
+
<!-- Segment with different color and weight -->
|
|
614
|
+
<span class="hero__heading__segment-2" style="color: #6366F1; font-weight: 800;">beautiful </span>
|
|
615
|
+
<!-- Segment matching base - still wrapped for consistency -->
|
|
616
|
+
<span class="hero__heading__segment-3" style="color: #1A1A1A;">interfaces</span>
|
|
617
|
+
</p>
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### Hyperlink Segments
|
|
621
|
+
|
|
622
|
+
When a segment has a hyperlink, it renders as an `<a>` tag instead of `<span>`:
|
|
623
|
+
|
|
624
|
+
```html
|
|
625
|
+
<p class="footer__text">
|
|
626
|
+
<span class="footer__text__segment-1">Read our </span>
|
|
627
|
+
<a class="footer__text__segment-2" href="https://example.com/privacy"
|
|
628
|
+
target="_blank" rel="noopener noreferrer">privacy policy</a>
|
|
629
|
+
<span class="footer__text__segment-3"> for details.</span>
|
|
630
|
+
</p>
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Nested anchor prevention:** If the text node itself is already rendered as an `<a>` tag (e.g., from semantic name detection like "link-with-icon"), hyperlink segments render as `<span>` instead of `<a>` to avoid invalid nested anchor elements.
|
|
634
|
+
|
|
635
|
+
#### Hyperlink Detection
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
function hasHyperlinks(textNode: TextNode): boolean {
|
|
639
|
+
// Uniform hyperlink (all text is linked)
|
|
640
|
+
if (textNode.hyperlink && typeof textNode.hyperlink !== 'symbol') {
|
|
641
|
+
return true
|
|
642
|
+
}
|
|
643
|
+
// Mixed hyperlinks (some ranges are linked)
|
|
644
|
+
if (typeof textNode.hyperlink === 'symbol') {
|
|
645
|
+
return true
|
|
646
|
+
}
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Hyperlinks can be of two types:
|
|
652
|
+
- `URL` -- external web link (`{ type: 'URL', value: 'https://...' }`)
|
|
653
|
+
- `NODE` -- internal Figma node link (`{ type: 'NODE', value: 'nodeId' }`) -- typically not converted to HTML href
|
|
654
|
+
|
|
655
|
+
#### Variable Binding on Segment Colors
|
|
656
|
+
|
|
657
|
+
Styled segments can have Figma Variables bound to their fill colors. The extraction resolves these asynchronously:
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
const variable = await extractPaintVariableAsync(fill)
|
|
661
|
+
// Returns: { id: string, name: string, isLocal: boolean } | undefined
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
When a variable is present, the CSS uses `var()` instead of a raw hex value:
|
|
665
|
+
|
|
666
|
+
```css
|
|
667
|
+
/* Local variable */
|
|
668
|
+
color: var(--color-link-default);
|
|
669
|
+
|
|
670
|
+
/* External library variable (with fallback) */
|
|
671
|
+
color: var(--color-brand-primary, #6366F1);
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
### 10. List Styles
|
|
677
|
+
|
|
678
|
+
Figma text nodes can contain list formatting. The `listOptions` property on the text node indicates the list type.
|
|
679
|
+
|
|
680
|
+
#### Detection
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
const listOptions = (textNode as any).listOptions
|
|
684
|
+
if (listOptions && typeof listOptions !== 'symbol') {
|
|
685
|
+
if (listOptions.type === 'ORDERED') {
|
|
686
|
+
textData.listStyle = 'ordered'
|
|
687
|
+
} else if (listOptions.type === 'UNORDERED') {
|
|
688
|
+
textData.listStyle = 'unordered'
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
**Note:** `listOptions` may not be available in all Figma API contexts. The extraction wraps access in a try/catch.
|
|
694
|
+
|
|
695
|
+
#### HTML Generation
|
|
696
|
+
|
|
697
|
+
List text is split by newlines, and each line becomes a `<li>`:
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
if (node.text.listStyle) {
|
|
701
|
+
const lines = node.text.content.split(/\r?\n/).filter(line => line.trim())
|
|
702
|
+
const listTag = node.text.listStyle === 'ordered' ? 'ol' : 'ul'
|
|
703
|
+
|
|
704
|
+
const listItems = lines.map((line, index) => ({
|
|
705
|
+
tag: 'li',
|
|
706
|
+
className: `${className}__item-${index + 1}`,
|
|
707
|
+
children: [line.trim()],
|
|
708
|
+
}))
|
|
709
|
+
|
|
710
|
+
// Wrap in list container
|
|
711
|
+
children.push({
|
|
712
|
+
tag: listTag,
|
|
713
|
+
className: `${className}__list`,
|
|
714
|
+
children: listItems,
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
#### Generated HTML Examples
|
|
720
|
+
|
|
721
|
+
**Unordered list:**
|
|
722
|
+
|
|
723
|
+
```html
|
|
724
|
+
<div class="features__text">
|
|
725
|
+
<ul class="features__text__list">
|
|
726
|
+
<li class="features__text__item-1">Fast performance</li>
|
|
727
|
+
<li class="features__text__item-2">Easy to use</li>
|
|
728
|
+
<li class="features__text__item-3">Beautiful design</li>
|
|
729
|
+
</ul>
|
|
730
|
+
</div>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Ordered list:**
|
|
734
|
+
|
|
735
|
+
```html
|
|
736
|
+
<div class="steps__text">
|
|
737
|
+
<ol class="steps__text__list">
|
|
738
|
+
<li class="steps__text__item-1">Create your account</li>
|
|
739
|
+
<li class="steps__text__item-2">Choose a template</li>
|
|
740
|
+
<li class="steps__text__item-3">Publish your site</li>
|
|
741
|
+
</ol>
|
|
742
|
+
</div>
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
### 11. Typography Token Integration
|
|
748
|
+
|
|
749
|
+
The typography token system collects font families, sizes, and weights from the entire extracted node tree and generates semantic CSS custom property names.
|
|
750
|
+
|
|
751
|
+
#### Token Collection
|
|
752
|
+
|
|
753
|
+
The `collectTypography` function traverses the ExtractedNode tree and collects:
|
|
754
|
+
|
|
755
|
+
- **Font families** -- unique family names with usage counts
|
|
756
|
+
- **Font sizes** -- unique pixel sizes with usage counts
|
|
757
|
+
- **Font weights** -- unique numeric weights with usage counts
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
interface TypographyTokens {
|
|
761
|
+
families: FontFamilyToken[] // e.g., [{ name: '--font-primary', value: 'Inter', usageCount: 42 }]
|
|
762
|
+
sizes: FontSizeToken[] // e.g., [{ name: '--text-base', value: 16, usageCount: 28 }]
|
|
763
|
+
weights: FontWeightToken[] // e.g., [{ name: '--font-bold', value: 700, usageCount: 15 }]
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
#### Font Family Token Naming
|
|
768
|
+
|
|
769
|
+
Families are named based on their role:
|
|
770
|
+
|
|
771
|
+
| Role | Detection | Token Name |
|
|
772
|
+
|------|-----------|------------|
|
|
773
|
+
| Primary | Most-used font family | `--font-primary` |
|
|
774
|
+
| Monospace | Name matches monospace patterns | `--font-mono` |
|
|
775
|
+
| Secondary | Second font (first non-primary) | `--font-secondary` |
|
|
776
|
+
| Others | Remaining fonts by index | `--font-family-{n}` |
|
|
777
|
+
|
|
778
|
+
#### Font Size Token Naming
|
|
779
|
+
|
|
780
|
+
Sizes use the type scale (Section 3) with the "base" position anchored to the collected size closest to 16px.
|
|
781
|
+
|
|
782
|
+
**Example collected sizes:** `[12, 14, 16, 18, 24, 32, 48]`
|
|
783
|
+
|
|
784
|
+
| Size | Scale Position | Token Name |
|
|
785
|
+
|------|---------------|------------|
|
|
786
|
+
| 12px | base - 2 | `--text-xs` |
|
|
787
|
+
| 14px | base - 1 | `--text-sm` |
|
|
788
|
+
| 16px | base (closest to 16) | `--text-base` |
|
|
789
|
+
| 18px | base + 1 | `--text-lg` |
|
|
790
|
+
| 24px | base + 2 | `--text-xl` |
|
|
791
|
+
| 32px | base + 3 | `--text-2xl` |
|
|
792
|
+
| 48px | base + 4 | `--text-3xl` |
|
|
793
|
+
|
|
794
|
+
#### Font Weight Token Naming
|
|
795
|
+
|
|
796
|
+
Weights use standard CSS weight names:
|
|
797
|
+
|
|
798
|
+
| Weight | Token Name |
|
|
799
|
+
|--------|------------|
|
|
800
|
+
| 100 | `--font-thin` |
|
|
801
|
+
| 200 | `--font-extralight` |
|
|
802
|
+
| 300 | `--font-light` |
|
|
803
|
+
| 400 | `--font-regular` |
|
|
804
|
+
| 500 | `--font-medium` |
|
|
805
|
+
| 600 | `--font-semibold` |
|
|
806
|
+
| 700 | `--font-bold` |
|
|
807
|
+
| 800 | `--font-extrabold` |
|
|
808
|
+
| 900 | `--font-black` |
|
|
809
|
+
|
|
810
|
+
Non-standard weights (e.g., 450) use `--font-weight-{n}`.
|
|
811
|
+
|
|
812
|
+
#### Token Lookup During Generation
|
|
813
|
+
|
|
814
|
+
During CSS generation, raw values are checked against the token lookup map:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
// Font family lookup
|
|
818
|
+
styles.fontFamily = lookupFontFamily(lookup, text.font.family)
|
|
819
|
+
// Returns: var(--font-primary) if found, else 'Inter', sans-serif
|
|
820
|
+
|
|
821
|
+
// Font size lookup
|
|
822
|
+
styles.fontSize = lookupFontSize(lookup, text.font.size)
|
|
823
|
+
// Returns: var(--text-lg) if found, else '18px'
|
|
824
|
+
|
|
825
|
+
// Font weight lookup
|
|
826
|
+
styles.fontWeight = lookupFontWeight(lookup, text.font.weight)
|
|
827
|
+
// Returns: var(--font-bold) if found, else '700'
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
#### Generated CSS with Tokens
|
|
831
|
+
|
|
832
|
+
```css
|
|
833
|
+
/* Without tokens */
|
|
834
|
+
.heading {
|
|
835
|
+
font-family: 'Inter', sans-serif;
|
|
836
|
+
font-size: 32px;
|
|
837
|
+
font-weight: 700;
|
|
838
|
+
line-height: 1.25;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/* With tokens */
|
|
842
|
+
.heading {
|
|
843
|
+
font-family: var(--font-primary);
|
|
844
|
+
font-size: var(--text-2xl);
|
|
845
|
+
font-weight: var(--font-bold);
|
|
846
|
+
line-height: 1.25;
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
### 12. Common Pitfalls
|
|
853
|
+
|
|
854
|
+
#### Pitfall: Line Height Percentage Misinterpretation
|
|
855
|
+
|
|
856
|
+
**Problem:** Figma's 150% line height is treated as CSS `line-height: 150%` instead of `line-height: 1.5`.
|
|
857
|
+
|
|
858
|
+
**Rule:** Always convert Figma line height percentages to unitless ratios by dividing by 100. Figma's percentage means "% of font size", which is exactly what a unitless CSS `line-height` ratio represents. Never output `line-height: 150%` from Figma data.
|
|
859
|
+
|
|
860
|
+
```css
|
|
861
|
+
/* CORRECT */
|
|
862
|
+
line-height: 1.50;
|
|
863
|
+
|
|
864
|
+
/* WRONG */
|
|
865
|
+
line-height: 150%;
|
|
866
|
+
line-height: 24px;
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
#### Pitfall: "Regular" Is Not a CSS Font Weight
|
|
870
|
+
|
|
871
|
+
**Problem:** Outputting `font-weight: regular` instead of `font-weight: 400`.
|
|
872
|
+
|
|
873
|
+
**Rule:** The Figma style name "Regular" must be mapped to the numeric value `400`. CSS `font-weight` only accepts numbers (100-900) or keywords (`normal`, `bold`, `lighter`, `bolder`). The word "regular" is not valid CSS.
|
|
874
|
+
|
|
875
|
+
#### Pitfall: Mixed-Style Nodes Need Segment Extraction
|
|
876
|
+
|
|
877
|
+
**Problem:** Reading `textNode.fontName` on a node with mixed fonts returns `figma.mixed` (a Symbol), not a FontName object. Treating it as a string or object causes runtime errors.
|
|
878
|
+
|
|
879
|
+
**Rule:** Always check `typeof textNode.fontName === 'symbol'` before accessing `.family` or `.style`. When mixed, either:
|
|
880
|
+
1. Use `getRangeFontName(0, 1)` for the dominant style
|
|
881
|
+
2. Extract styled segments for per-character accuracy
|
|
882
|
+
|
|
883
|
+
The same applies to `fontSize`, `fills`, `fontWeight`, `letterSpacing`, `lineHeight`, `textDecoration`, and `textCase` -- all can be `figma.mixed`.
|
|
884
|
+
|
|
885
|
+
#### Pitfall: Vertical Alignment Requires Flex
|
|
886
|
+
|
|
887
|
+
**Problem:** Setting `vertical-align: middle` on a block-level text element (it only works on inline or table cells).
|
|
888
|
+
|
|
889
|
+
**Rule:** Figma's vertical text alignment (CENTER, BOTTOM) requires `display: flex` with `align-items` on the text element. This is the only reliable cross-browser approach for vertically aligning text within a fixed-height container.
|
|
890
|
+
|
|
891
|
+
```css
|
|
892
|
+
/* CORRECT - vertical center */
|
|
893
|
+
display: flex;
|
|
894
|
+
align-items: center;
|
|
895
|
+
text-align: left;
|
|
896
|
+
|
|
897
|
+
/* WRONG */
|
|
898
|
+
vertical-align: middle;
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
#### Pitfall: Flex on Text Conflicts with Inline Content
|
|
902
|
+
|
|
903
|
+
**Problem:** Adding `display: flex` for vertical alignment changes the text element from block/inline to flex container, which can affect how inline children (spans, links) are laid out.
|
|
904
|
+
|
|
905
|
+
**Rule:** When a text node has both vertical alignment (CENTER/BOTTOM) and styled segments, the flex container will lay out the `<span>` children as flex items. This is generally acceptable because text spans naturally flow inline-like within a flex container with `flex-wrap: wrap`, but test with long mixed-style text.
|
|
906
|
+
|
|
907
|
+
#### Pitfall: Strikethrough Mapping
|
|
908
|
+
|
|
909
|
+
**Problem:** Using CSS `text-decoration: strikethrough` (not valid CSS).
|
|
910
|
+
|
|
911
|
+
**Rule:** Figma's `STRIKETHROUGH` maps to CSS `text-decoration: line-through`. The extraction stores the intermediate value as `'strikethrough'` and the generation step converts it:
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
styles.textDecoration = text.decoration === 'strikethrough' ? 'line-through' : text.decoration
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
#### Pitfall: Text Color Comes from Fills, Not a Color Property
|
|
918
|
+
|
|
919
|
+
**Problem:** Looking for a `color` property on Figma text nodes.
|
|
920
|
+
|
|
921
|
+
**Rule:** Figma text nodes use the `fills` array for their text color, the same way non-text nodes use fills for background color. For text nodes, the first SOLID fill becomes `color` (not `background-color`):
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
if (node.text && node.fills && node.fills.length > 0) {
|
|
925
|
+
const fill = node.fills[0]
|
|
926
|
+
if (fill.type === 'SOLID') {
|
|
927
|
+
styles.color = fill.color // Text color, NOT background
|
|
928
|
+
} else if (fill.type === 'GRADIENT') {
|
|
929
|
+
// Gradient text uses background-clip technique
|
|
930
|
+
styles.backgroundImage = gradientCSS
|
|
931
|
+
styles.backgroundClip = 'text'
|
|
932
|
+
styles.WebkitBackgroundClip = 'text'
|
|
933
|
+
styles.color = 'transparent'
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
#### Pitfall: Gradient Text Requires Special CSS
|
|
939
|
+
|
|
940
|
+
**Problem:** Applying a gradient fill to text as `background-color`.
|
|
941
|
+
|
|
942
|
+
**Rule:** Gradient text in CSS requires the `background-clip: text` technique:
|
|
943
|
+
|
|
944
|
+
```css
|
|
945
|
+
.gradient-text {
|
|
946
|
+
background-image: linear-gradient(90deg, #6366F1 0%, #EC4899 100%);
|
|
947
|
+
background-clip: text;
|
|
948
|
+
-webkit-background-clip: text;
|
|
949
|
+
color: transparent;
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
#### Edge Case: Paragraph Spacing
|
|
954
|
+
|
|
955
|
+
Figma's `paragraphSpacing` (space between paragraphs separated by newlines) does not have a direct CSS equivalent. When present and non-zero, it can be approximated with `margin-bottom` on paragraph elements or `padding-bottom` on line groups, but this requires splitting text at paragraph boundaries.
|
|
956
|
+
|
|
957
|
+
#### Edge Case: Single Segment Optimization
|
|
958
|
+
|
|
959
|
+
If `getStyledTextSegments` returns only one segment and there are no hyperlinks, the styled segments are discarded and the node-level properties are used instead. This avoids unnecessary `<span>` wrapping for uniformly-styled text.
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
### Intermediate Type Reference
|
|
964
|
+
|
|
965
|
+
The complete intermediate type used between extraction and generation:
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
interface TextData {
|
|
969
|
+
content: string
|
|
970
|
+
font: {
|
|
971
|
+
family: string // e.g., "Inter"
|
|
972
|
+
style: string // e.g., "Bold Italic"
|
|
973
|
+
size: number // e.g., 16
|
|
974
|
+
weight: number // e.g., 700
|
|
975
|
+
}
|
|
976
|
+
align: {
|
|
977
|
+
horizontal: 'left' | 'center' | 'right' | 'justify'
|
|
978
|
+
vertical: 'top' | 'center' | 'bottom'
|
|
979
|
+
}
|
|
980
|
+
letterSpacing: number // In pixels (0 = omit)
|
|
981
|
+
lineHeight: number | 'auto' // Unitless ratio or 'auto'
|
|
982
|
+
paragraphSpacing: number // Pixels between paragraphs
|
|
983
|
+
decoration: 'none' | 'underline' | 'strikethrough'
|
|
984
|
+
textCase: 'none' | 'uppercase' | 'lowercase' | 'capitalize'
|
|
985
|
+
textAutoResize?: 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT' | 'TRUNCATE'
|
|
986
|
+
listStyle?: 'ordered' | 'unordered'
|
|
987
|
+
styledSegments?: StyledSegment[]
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## Cross-References
|
|
994
|
+
|
|
995
|
+
- **`figma-api-rest.md`** -- Node text properties in REST API response (`characters`, `style` object with font family/weight/size), text node type identification
|
|
996
|
+
- **`figma-api-plugin.md`** -- TextNode API, `fontName`, `fontSize`, `textAlignHorizontal/Vertical`, `textAutoResize`, `getStyledTextSegments()`, `getRangeFontName()`, `getRangeHyperlink()`, `hyperlink`, `listOptions`, `figma.mixed` handling
|
|
997
|
+
- **`figma-api-variables.md`** -- Variable resolution for text fill colors, `figma.variables.getVariableByIdAsync()`, `boundVariables` on SolidPaint fills, local vs external variable handling
|
|
998
|
+
- **`design-to-code-layout.md`** -- Parent Auto Layout context that determines text sizing behavior (FILL width, HUG height), flex container interaction with vertical text alignment
|
|
999
|
+
- **`design-to-code-visual.md`** -- Fill extraction patterns shared with text color extraction (SOLID, GRADIENT), RGB-to-hex conversion, opacity handling, variable binding resolution
|
|
1000
|
+
- **`design-to-code-assets.md`** -- Asset detection on text nodes with image fills (image fill on text = background image, not image replacement)
|
|
1001
|
+
- **`design-to-code-semantic.md`** -- Semantic HTML tag selection for text elements (h1-h6, p, span, a), heading hierarchy with single h1 rule, BEM class naming for text segments, interactive context preventing headings inside buttons/links
|
|
1002
|
+
- **`css-strategy.md`** -- Typography tokens as CSS custom properties in the tokens layer (Layer 2), font-family in Tailwind config, property placement for typography properties (Layer 3).
|
|
1003
|
+
- **`design-tokens.md`** -- Typography token extraction (font families, sizes, weights), type scale naming, token lookup during CSS generation, threshold-based promotion.
|