Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
+888
@@ -0,0 +1,888 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Token Creation Reference
|
||||
|
||||
This document covers Phase 1: creating variable collections, modes, primitives, semantic aliases, scopes, code syntax, styles, and validation. All code is copy-paste ready for `use_figma`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Collection Architecture
|
||||
|
||||
Choose the pattern that matches your token count and complexity:
|
||||
|
||||
### Simple Pattern (< 50 tokens)
|
||||
|
||||
One collection, 2 modes. Appropriate for small projects or brand kits.
|
||||
|
||||
```
|
||||
Collection: "Tokens" modes: ["Light", "Dark"]
|
||||
color/bg/primary → Light: #FFFFFF, Dark: #1A1A1A
|
||||
spacing/sm = 8
|
||||
```
|
||||
|
||||
### Standard Pattern (50–200 tokens) — Recommended Starting Point
|
||||
|
||||
Separate primitives from semantics. The real-world reference is Figma's Simple Design System (SDS): 7 collections, 368 variables, light/dark modes on semantic colors, single-mode primitives.
|
||||
|
||||
```
|
||||
Collection: "Primitives" modes: ["Value"] ← raw hex values, no modes
|
||||
blue/500 = #3B82F6
|
||||
gray/900 = #111827
|
||||
white/1000 = #FFFFFF
|
||||
|
||||
Collection: "Color" modes: ["Light", "Dark"] ← aliases to Primitives
|
||||
color/bg/primary → Light: alias Primitives/white/1000, Dark: alias Primitives/gray/900
|
||||
color/text/primary → Light: alias Primitives/gray/900, Dark: alias Primitives/white/1000
|
||||
|
||||
Collection: "Spacing" modes: ["Value"]
|
||||
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, spacing/lg = 24, spacing/xl = 32
|
||||
|
||||
Collection: "Typography Primitives" modes: ["Value"]
|
||||
family/sans = "Inter", scale/01 = 12, scale/02 = 14, scale/03 = 16, weight/regular = 400
|
||||
|
||||
Collection: "Typography" modes: ["Value"] ← aliases to Typography Primitives
|
||||
body/font-family → alias family/sans
|
||||
body/size-md → alias scale/03
|
||||
```
|
||||
|
||||
### Advanced Pattern (200+ tokens) — M3 Model
|
||||
|
||||
Multiple semantic collections, 4–8 modes. Use when you need light/dark × contrast × brand or responsive breakpoints.
|
||||
|
||||
```
|
||||
Collection: "M3" modes: ["Light", "Dark", "Light High Contrast", "Dark High Contrast", ...]
|
||||
Collection: "Typeface" modes: ["Baseline", "Wireframe"]
|
||||
Collection: "Typescale" modes: ["Value"] ← aliases into Typeface
|
||||
Collection: "Shape" modes: ["Value"]
|
||||
```
|
||||
|
||||
Key insight from M3: ALL 196 semantic color variables live in a SINGLE collection with 8 modes. Switching a frame's mode once updates every color simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## 2. Creating Collections + Modes
|
||||
|
||||
### Creating a Primitives Collection
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001"; // use the same RUN_ID throughout the build
|
||||
|
||||
// Create the collection
|
||||
const primColl = figma.variables.createVariableCollection("Primitives");
|
||||
|
||||
// Rename the default "Mode 1" to "Value"
|
||||
primColl.renameMode(primColl.modes[0].modeId, "Value");
|
||||
const valueMode = primColl.modes[0].modeId;
|
||||
|
||||
// Tag for idempotency
|
||||
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
primColl.setSharedPluginData('dsb', 'key', 'collection/primitives');
|
||||
|
||||
return {
|
||||
collectionId: primColl.id,
|
||||
modeId: valueMode,
|
||||
name: primColl.name
|
||||
};
|
||||
```
|
||||
|
||||
### Creating a Semantic Color Collection with Light/Dark Modes
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
const colorColl = figma.variables.createVariableCollection("Color");
|
||||
|
||||
// Rename default "Mode 1" to "Light"
|
||||
colorColl.renameMode(colorColl.modes[0].modeId, "Light");
|
||||
const lightModeId = colorColl.modes[0].modeId;
|
||||
|
||||
// Add "Dark" mode — requires Professional plan or higher
|
||||
// Throws "in addMode: Limited to N modes only" on Starter plan
|
||||
const darkModeId = colorColl.addMode("Dark");
|
||||
|
||||
colorColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
colorColl.setSharedPluginData('dsb', 'key', 'collection/color');
|
||||
|
||||
return {
|
||||
collectionId: colorColl.id,
|
||||
lightModeId,
|
||||
darkModeId
|
||||
};
|
||||
```
|
||||
|
||||
**Mode plan limits:** Starter = 1 mode, Professional = 4 modes, Organization/Enterprise = 40 modes. If `addMode` throws, the file is on a Starter plan — tell the user and ask how to proceed.
|
||||
|
||||
### Creating a Spacing Collection (single mode)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
const spacingColl = figma.variables.createVariableCollection("Spacing");
|
||||
spacingColl.renameMode(spacingColl.modes[0].modeId, "Value");
|
||||
const valueMode = spacingColl.modes[0].modeId;
|
||||
|
||||
spacingColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
spacingColl.setSharedPluginData('dsb', 'key', 'collection/spacing');
|
||||
|
||||
return {
|
||||
collectionId: spacingColl.id,
|
||||
modeId: valueMode
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Creating All Variable Types
|
||||
|
||||
### hex → {r, g, b} Conversion Helper
|
||||
|
||||
Colors in the Figma Plugin API are 0–1 range, not 0–255. Embed this helper in any script that creates color variables:
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const clean = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16) / 255,
|
||||
g: parseInt(clean.substring(2, 4), 16) / 255,
|
||||
b: parseInt(clean.substring(4, 6), 16) / 255
|
||||
};
|
||||
}
|
||||
|
||||
// With alpha channel (for semi-transparent primitives like Black/200 at 10%):
|
||||
function hexToRgba(hex) {
|
||||
const clean = hex.replace('#', '');
|
||||
const hasAlpha = clean.length === 8;
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16) / 255,
|
||||
g: parseInt(clean.substring(2, 4), 16) / 255,
|
||||
b: parseInt(clean.substring(4, 6), 16) / 255,
|
||||
a: hasAlpha ? parseInt(clean.substring(6, 8), 16) / 255 : 1
|
||||
};
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// hexToRgb('#3B82F6') → {r: 0.231, g: 0.510, b: 0.965}
|
||||
// hexToRgb('#14AE5C') → {r: 0.078, g: 0.682, b: 0.361}
|
||||
// hexToRgba('#0c0c0d1a') → {r: 0.047, g: 0.047, b: 0.051, a: 0.102}
|
||||
```
|
||||
|
||||
### Creating Primitive Color Variables (Real SDS Data)
|
||||
|
||||
This creates a subset of the Simple Design System's `Color Primitives` collection (Blue family, from the Standard pattern used by real design systems):
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const c = hex.replace('#', '');
|
||||
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
|
||||
}
|
||||
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Get the Primitives collection created in the previous step
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
|
||||
if (!primColl) throw new Error("Primitives collection not found — run collection creation first");
|
||||
const valueMode = primColl.modes[0].modeId;
|
||||
|
||||
// Define primitives — use real values from your codebase
|
||||
const primitiveColors = [
|
||||
// Blue scale
|
||||
{ name: 'blue/100', hex: '#EFF6FF' },
|
||||
{ name: 'blue/200', hex: '#DBEAFE' },
|
||||
{ name: 'blue/300', hex: '#93C5FD' },
|
||||
{ name: 'blue/400', hex: '#60A5FA' },
|
||||
{ name: 'blue/500', hex: '#3B82F6' },
|
||||
{ name: 'blue/600', hex: '#2563EB' },
|
||||
{ name: 'blue/700', hex: '#1D4ED8' },
|
||||
{ name: 'blue/800', hex: '#1E40AF' },
|
||||
{ name: 'blue/900', hex: '#1E3A8A' },
|
||||
// Gray scale
|
||||
{ name: 'gray/100', hex: '#F9FAFB' },
|
||||
{ name: 'gray/200', hex: '#F3F4F6' },
|
||||
{ name: 'gray/300', hex: '#D1D5DB' },
|
||||
{ name: 'gray/400', hex: '#9CA3AF' },
|
||||
{ name: 'gray/500', hex: '#6B7280' },
|
||||
{ name: 'gray/600', hex: '#4B5563' },
|
||||
{ name: 'gray/700', hex: '#374151' },
|
||||
{ name: 'gray/800', hex: '#1F2937' },
|
||||
{ name: 'gray/900', hex: '#111827' },
|
||||
// White / Black
|
||||
{ name: 'white/1000', hex: '#FFFFFF' },
|
||||
{ name: 'black/1000', hex: '#000000' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, hex } of primitiveColors) {
|
||||
const v = figma.variables.createVariable(name, primColl, 'COLOR');
|
||||
v.setValueForMode(valueMode, hexToRgb(hex));
|
||||
// Primitives: EMPTY scopes (hidden from all pickers — designers use semantics)
|
||||
v.scopes = [];
|
||||
// Code syntax from the actual CSS variable name
|
||||
v.setVariableCodeSyntax('WEB', `var(--color-${name.replace('/', '-')})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', `primitive/${name}`);
|
||||
created.push({ name, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
**Critical scope rule for primitives:** Set `v.scopes = []`. This hides primitives from every picker. Designers should only see semantic tokens. The exception is semi-transparent overlay primitives (Black/White with alpha) — those get `["EFFECT_COLOR"]` so they appear in shadow pickers.
|
||||
|
||||
### Creating FLOAT Variables (Spacing, Radius, Font Size)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const spacingColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/spacing');
|
||||
if (!spacingColl) throw new Error("Spacing collection not found");
|
||||
const valueMode = spacingColl.modes[0].modeId;
|
||||
|
||||
const spacingTokens = [
|
||||
{ name: 'spacing/xs', value: 4, scope: 'GAP', cssVar: '--spacing-xs' },
|
||||
{ name: 'spacing/sm', value: 8, scope: 'GAP', cssVar: '--spacing-sm' },
|
||||
{ name: 'spacing/md', value: 16, scope: 'GAP', cssVar: '--spacing-md' },
|
||||
{ name: 'spacing/lg', value: 24, scope: 'GAP', cssVar: '--spacing-lg' },
|
||||
{ name: 'spacing/xl', value: 32, scope: 'GAP', cssVar: '--spacing-xl' },
|
||||
{ name: 'spacing/2xl', value: 48, scope: 'GAP', cssVar: '--spacing-2xl' },
|
||||
];
|
||||
|
||||
const radiusTokens = [
|
||||
{ name: 'radius/none', value: 0, scope: 'CORNER_RADIUS', cssVar: '--radius-none' },
|
||||
{ name: 'radius/sm', value: 4, scope: 'CORNER_RADIUS', cssVar: '--radius-sm' },
|
||||
{ name: 'radius/md', value: 8, scope: 'CORNER_RADIUS', cssVar: '--radius-md' },
|
||||
{ name: 'radius/lg', value: 16, scope: 'CORNER_RADIUS', cssVar: '--radius-lg' },
|
||||
{ name: 'radius/full', value: 9999, scope: 'CORNER_RADIUS', cssVar: '--radius-full' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, value, scope, cssVar } of [...spacingTokens, ...radiusTokens]) {
|
||||
const v = figma.variables.createVariable(name, spacingColl, 'FLOAT');
|
||||
v.setValueForMode(valueMode, value);
|
||||
v.scopes = [scope];
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', name);
|
||||
created.push({ name, value, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating STRING Variables (Font Family, Font Style)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const typoPrimColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/typography-primitives');
|
||||
if (!typoPrimColl) throw new Error("Typography Primitives collection not found");
|
||||
const valueMode = typoPrimColl.modes[0].modeId;
|
||||
|
||||
const fontTokens = [
|
||||
{ name: 'family/sans', value: 'Inter', scope: 'FONT_FAMILY', cssVar: '--font-family-sans' },
|
||||
{ name: 'family/mono', value: 'Roboto Mono', scope: 'FONT_FAMILY', cssVar: '--font-family-mono' },
|
||||
// Font style strings — these are the Figma fontName.style values:
|
||||
{ name: 'weight/regular', value: 'Regular', scope: 'FONT_STYLE', cssVar: '--font-weight-regular' },
|
||||
{ name: 'weight/medium', value: 'Medium', scope: 'FONT_STYLE', cssVar: '--font-weight-medium' },
|
||||
{ name: 'weight/semibold', value: 'Semi Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-semibold' },
|
||||
{ name: 'weight/bold', value: 'Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-bold' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, value, scope, cssVar } of fontTokens) {
|
||||
const v = figma.variables.createVariable(name, typoPrimColl, 'STRING');
|
||||
v.setValueForMode(valueMode, value);
|
||||
v.scopes = [scope];
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', `typo-prim/${name}`);
|
||||
created.push({ name, value, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating BOOLEAN Variables
|
||||
|
||||
BOOLEAN variables have no scopes (scopes are not supported for BOOLEAN type).
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const coll = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/tokens');
|
||||
if (!coll) throw new Error("Collection not found");
|
||||
const valueMode = coll.modes[0].modeId;
|
||||
|
||||
const v = figma.variables.createVariable('feature-flags/show-beta-badge', coll, 'BOOLEAN');
|
||||
v.setValueForMode(valueMode, false);
|
||||
// No scopes — BOOLEAN does not support scopes
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', 'feature-flags/show-beta-badge');
|
||||
|
||||
return { id: v.id, name: v.name };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Variable Aliasing (VARIABLE_ALIAS) — Primitive → Semantic Chain
|
||||
|
||||
Semantic tokens reference primitives via `VARIABLE_ALIAS`. This is the core pattern that makes light/dark theming work.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
Color Primitives collection (1 mode: Value)
|
||||
blue/500 = #3B82F6 ← raw value
|
||||
|
||||
Color collection (2 modes: Light, Dark)
|
||||
color/bg/accent/default:
|
||||
Light → VARIABLE_ALIAS → Primitives/blue/500
|
||||
Dark → VARIABLE_ALIAS → Primitives/blue/300
|
||||
```
|
||||
|
||||
### Complete Semantic Alias Creation Script (SDS-style)
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const c = hex.replace('#', '');
|
||||
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
|
||||
}
|
||||
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
|
||||
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
|
||||
const colorColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/color');
|
||||
if (!primColl || !colorColl) throw new Error("Collections not found — run primitive/color collection creation first");
|
||||
|
||||
const primValueMode = primColl.modes[0].modeId;
|
||||
const lightModeId = colorColl.modes.find(m => m.name === 'Light').modeId;
|
||||
const darkModeId = colorColl.modes.find(m => m.name === 'Dark').modeId;
|
||||
|
||||
// Load all primitive variables for lookup
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const primsByKey = {};
|
||||
for (const v of allVars) {
|
||||
if (v.variableCollectionId === primColl.id) {
|
||||
primsByKey[v.getSharedPluginData('dsb', 'key')] = v;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrim(name) {
|
||||
const v = primsByKey[`primitive/${name}`];
|
||||
if (!v) throw new Error(`Primitive not found: primitive/${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Define semantic → [lightPrimitiveName, darkPrimitiveName]
|
||||
// Following the SDS pattern: Background/{Intent}/{Emphasis}
|
||||
const semanticColors = [
|
||||
// Background
|
||||
{ name: 'color/bg/default/default', lightPrim: 'white/1000', darkPrim: 'gray/900',
|
||||
cssVar: '--color-bg-default-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ name: 'color/bg/default/secondary', lightPrim: 'gray/100', darkPrim: 'gray/800',
|
||||
cssVar: '--color-bg-default-secondary', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ name: 'color/bg/brand/default', lightPrim: 'blue/600', darkPrim: 'blue/300',
|
||||
cssVar: '--color-bg-brand-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
// Text
|
||||
{ name: 'color/text/default/default', lightPrim: 'gray/900', darkPrim: 'white/1000',
|
||||
cssVar: '--color-text-default-default', scopes: ['TEXT_FILL'] },
|
||||
{ name: 'color/text/default/secondary', lightPrim: 'gray/500', darkPrim: 'gray/400',
|
||||
cssVar: '--color-text-default-secondary', scopes: ['TEXT_FILL'] },
|
||||
{ name: 'color/text/brand/default', lightPrim: 'blue/700', darkPrim: 'blue/200',
|
||||
cssVar: '--color-text-brand-default', scopes: ['TEXT_FILL'] },
|
||||
// Border
|
||||
{ name: 'color/border/default/default', lightPrim: 'gray/300', darkPrim: 'gray/600',
|
||||
cssVar: '--color-border-default-default', scopes: ['STROKE_COLOR'] },
|
||||
{ name: 'color/border/brand/default', lightPrim: 'blue/500', darkPrim: 'blue/400',
|
||||
cssVar: '--color-border-brand-default', scopes: ['STROKE_COLOR'] },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, lightPrim, darkPrim, cssVar, scopes } of semanticColors) {
|
||||
const v = figma.variables.createVariable(name, colorColl, 'COLOR');
|
||||
// Alias to primitive in Light mode
|
||||
v.setValueForMode(lightModeId, figma.variables.createVariableAlias(getPrim(lightPrim)));
|
||||
// Alias to primitive in Dark mode
|
||||
v.setValueForMode(darkModeId, figma.variables.createVariableAlias(getPrim(darkPrim)));
|
||||
// Set scopes (semantic layer — these ARE shown in pickers)
|
||||
v.scopes = scopes;
|
||||
// Code syntax
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', name);
|
||||
created.push({ name, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
**Key API points:**
|
||||
- `figma.variables.createVariableAlias(variable)` — takes a Variable object, returns `{type:'VARIABLE_ALIAS', id: variable.id}`
|
||||
- The aliased variable MUST have the same `resolvedType` as the semantic variable
|
||||
- Never duplicate raw values in the semantic layer — always alias
|
||||
|
||||
---
|
||||
|
||||
## 5. Variable Scopes — Complete Reference Table
|
||||
|
||||
| Semantic Role | Recommended Scopes | Variable Type |
|
||||
|---|---|---|
|
||||
| Primitive colors (raw) | `[]` — empty, hidden from all pickers | COLOR |
|
||||
| Semi-transparent overlay primitives | `["EFFECT_COLOR"]` | COLOR |
|
||||
| Background fills (frame, shape) | `["FRAME_FILL", "SHAPE_FILL"]` | COLOR |
|
||||
| Text color | `["TEXT_FILL"]` | COLOR |
|
||||
| Icon / shape fill | `["SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
|
||||
| Border / stroke color | `["STROKE_COLOR"]` | COLOR |
|
||||
| Background + border combined | `["FRAME_FILL", "SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
|
||||
| Shadow color | `["EFFECT_COLOR"]` | COLOR |
|
||||
| Spacing / gap between items | `["GAP"]` | FLOAT |
|
||||
| Padding (if separate from gap) | `["GAP"]` | FLOAT |
|
||||
| Corner radius | `["CORNER_RADIUS"]` | FLOAT |
|
||||
| Width / height dimensions | `["WIDTH_HEIGHT"]` | FLOAT |
|
||||
| Font size | `["FONT_SIZE"]` | FLOAT |
|
||||
| Line height | `["LINE_HEIGHT"]` | FLOAT |
|
||||
| Letter spacing | `["LETTER_SPACING"]` | FLOAT |
|
||||
| Font weight (numeric) | `["FONT_WEIGHT"]` | FLOAT |
|
||||
| Stroke width | `["STROKE_FLOAT"]` | FLOAT |
|
||||
| Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT |
|
||||
| Opacity | `["OPACITY"]` | FLOAT |
|
||||
| Font family | `["FONT_FAMILY"]` | STRING |
|
||||
| Font style (e.g. "Semi Bold") | `["FONT_STYLE"]` | STRING |
|
||||
| Boolean flags | *(scopes not supported)* | BOOLEAN |
|
||||
|
||||
**Never use `ALL_SCOPES`** on any variable. It pollutes every picker with irrelevant tokens. The Simple Design System (SDS), the gold standard, uses targeted scopes on every variable.
|
||||
|
||||
**`ALL_FILLS` note:** `ALL_FILLS` is exclusive among fill scopes — it covers `FRAME_FILL`, `SHAPE_FILL`, and `TEXT_FILL` together. If set, you cannot also add individual fill scopes. Prefer specifying individual scopes for precision.
|
||||
|
||||
### Batch Scope-Setting (After Variables are Created)
|
||||
|
||||
If you created variables without scopes and need to set them in batch:
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
|
||||
// Scope mapping: partial name match → scopes
|
||||
const scopeRules = [
|
||||
{ match: 'color/bg/', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ match: 'color/text/', scopes: ['TEXT_FILL'] },
|
||||
{ match: 'color/icon/', scopes: ['SHAPE_FILL', 'STROKE_COLOR'] },
|
||||
{ match: 'color/border/', scopes: ['STROKE_COLOR'] },
|
||||
{ match: 'spacing/', scopes: ['GAP'] },
|
||||
{ match: 'radius/', scopes: ['CORNER_RADIUS'] },
|
||||
{ match: 'blue/', scopes: [] }, // primitives — hide
|
||||
{ match: 'gray/', scopes: [] },
|
||||
{ match: 'white/', scopes: [] },
|
||||
{ match: 'black/', scopes: [] },
|
||||
];
|
||||
|
||||
const updated = [];
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue; // skip library variables
|
||||
for (const rule of scopeRules) {
|
||||
if (v.name.startsWith(rule.match)) {
|
||||
v.scopes = rule.scopes;
|
||||
updated.push({ name: v.name, scopes: rule.scopes });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, count: updated.length };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Code Syntax — WEB/ANDROID/iOS
|
||||
|
||||
Every variable must have code syntax set. This is what powers the developer handoff experience:
|
||||
|
||||
**What code syntax does:** When a developer inspects any element in Figma Dev Mode that has a variable-bound property (fill, padding, radius, etc.), the code snippet shown uses the variable's code syntax name — not the Figma variable name. For example, a button's background fill bound to `color/bg/primary` will show `background: var(--color-bg-primary)` in the CSS snippet, not `color/bg/primary`. Without code syntax set, Dev Mode shows raw hex values or nothing useful.
|
||||
|
||||
You can set up to **3 syntaxes per variable** — one per platform (Web, iOS, Android). Set all three if the codebase targets multiple platforms; set only WEB if it's a web-only project.
|
||||
|
||||
```javascript
|
||||
// WEB: MUST include the var() wrapper — this is the full CSS function syntax
|
||||
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
|
||||
// ^^^^ ^
|
||||
// var() wrapper is REQUIRED
|
||||
|
||||
// ANDROID: Kotlin property name — camelCase, no wrapper
|
||||
variable.setVariableCodeSyntax('ANDROID', 'colorBgPrimary');
|
||||
|
||||
// iOS: Swift property — dot-notation, no wrapper
|
||||
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
|
||||
```
|
||||
|
||||
> **CRITICAL — WEB code syntax MUST use the `var()` wrapper.** Setting just `--color-bg-primary` (without `var()`) will cause Dev Mode to show raw hex values instead of the CSS variable reference. Always use the full `var(--name)` form. ANDROID and iOS do NOT use a wrapper.
|
||||
|
||||
**Platform derivation rules from the CSS variable name:**
|
||||
|
||||
| Platform | Pattern | Example |
|
||||
|---|---|---|
|
||||
| WEB | **`var(--{css-var-name})`** — `var()` wrapper required | `var(--sds-color-bg-primary)` |
|
||||
| ANDROID | camelCase, no wrapper, strip `--` prefix | `sdsColorBgPrimary` |
|
||||
| iOS | PascalCase after `.`, no wrapper, strip `--` prefix | `Color.SdsColorBgPrimary` or `Color.bgPrimary` |
|
||||
|
||||
**Always use the actual CSS variable name from the codebase** — do not derive it from the Figma variable name. If the code uses `--sds-color-background-brand-default`, that exact string is the WEB code syntax (minus the `var()` wrapper that you add).
|
||||
|
||||
### Batch Code Syntax Setting
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const updated = [];
|
||||
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue;
|
||||
// If code syntax already set, skip
|
||||
if (v.codeSyntax['WEB']) continue;
|
||||
|
||||
// FALLBACK: derive from Figma name: color/bg/primary → var(--color-bg-primary)
|
||||
// PREFERRED: pass in a cssVarMap built from actual codebase CSS variable names
|
||||
// e.g. cssVarMap = { 'color/bg/primary': '--color-bg-primary', ... }
|
||||
const cssName = cssVarMap?.[v.name]
|
||||
?? v.name.replace(/\//g, '-').replace(/\s/g, '-').toLowerCase();
|
||||
v.setVariableCodeSyntax('WEB', `var(--${cssName})`);
|
||||
updated.push({ name: v.name, web: `var(--${cssName})` });
|
||||
}
|
||||
|
||||
return { updated, count: updated.length };
|
||||
```
|
||||
|
||||
Note: derived names are a fallback only. Always prefer overriding with actual CSS variable names from the codebase when they are known.
|
||||
|
||||
---
|
||||
|
||||
## 7. Effect Styles (Shadows) and Text Styles
|
||||
|
||||
Shadows and composite typography cannot be variables — they are Styles.
|
||||
|
||||
### Creating Effect Styles (Shadows)
|
||||
|
||||
Reference from SDS (15 effect styles) and the SDS shadow pattern `Shadow/{Level}`:
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Shadow definitions — CSS equivalent in comments
|
||||
// CSS: 0 1px 2px rgba(0,0,0,0.05)
|
||||
const shadows = [
|
||||
{
|
||||
name: 'Shadow/Subtle',
|
||||
effects: [{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.05 },
|
||||
offset: { x: 0, y: 1 },
|
||||
radius: 2,
|
||||
spread: 0,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}]
|
||||
},
|
||||
{
|
||||
// CSS: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)
|
||||
name: 'Shadow/Medium',
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.10 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 6,
|
||||
spread: -1,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
},
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.06 },
|
||||
offset: { x: 0, y: 2 },
|
||||
radius: 4,
|
||||
spread: -1,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// CSS: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -2px rgba(0,0,0,0.05)
|
||||
name: 'Shadow/Strong',
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.10 },
|
||||
offset: { x: 0, y: 10 },
|
||||
radius: 15,
|
||||
spread: -3,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
},
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.05 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 6,
|
||||
spread: -2,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// M3-style dual shadow (umbra + penumbra pattern):
|
||||
const m3Shadows = [
|
||||
{
|
||||
name: 'Elevation/1',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:1}, radius:3, spread:1, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Elevation/2',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:2}, radius:6, spread:2, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Elevation/3',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:3, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:4}, radius:8, spread:3, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, effects } of shadows) {
|
||||
const style = figma.createEffectStyle();
|
||||
style.name = name;
|
||||
style.effects = effects;
|
||||
style.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
style.setSharedPluginData('dsb', 'key', `effect-style/${name}`);
|
||||
created.push({ name, id: style.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating Text Styles
|
||||
|
||||
Fonts must be loaded before creating text styles.
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Define text styles — based on SDS typography hierarchy
|
||||
const textStyles = [
|
||||
// Display / Hero
|
||||
{ name: 'Display/Hero', family: 'Inter', style: 'Bold', size: 72, lineHeight: 80, letterSpacing: -1.5 },
|
||||
// Headings
|
||||
{ name: 'Heading/H1', family: 'Inter', style: 'Bold', size: 48, lineHeight: 56, letterSpacing: -1.0 },
|
||||
{ name: 'Heading/H2', family: 'Inter', style: 'Bold', size: 40, lineHeight: 48, letterSpacing: -0.5 },
|
||||
{ name: 'Heading/H3', family: 'Inter', style: 'Semi Bold', size: 32, lineHeight: 40, letterSpacing: 0 },
|
||||
{ name: 'Heading/H4', family: 'Inter', style: 'Semi Bold', size: 24, lineHeight: 32, letterSpacing: 0 },
|
||||
// Body
|
||||
{ name: 'Body/Large', family: 'Inter', style: 'Regular', size: 18, lineHeight: 28, letterSpacing: 0 },
|
||||
{ name: 'Body/Medium', family: 'Inter', style: 'Regular', size: 16, lineHeight: 24, letterSpacing: 0 },
|
||||
{ name: 'Body/Small', family: 'Inter', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
|
||||
// Label
|
||||
{ name: 'Label/Large', family: 'Inter', style: 'Medium', size: 14, lineHeight: 20, letterSpacing: 0.1 },
|
||||
{ name: 'Label/Medium', family: 'Inter', style: 'Medium', size: 12, lineHeight: 16, letterSpacing: 0.5 },
|
||||
{ name: 'Label/Small', family: 'Inter', style: 'Medium', size: 11, lineHeight: 16, letterSpacing: 0.5 },
|
||||
// Code
|
||||
{ name: 'Code/Base', family: 'Roboto Mono', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
|
||||
];
|
||||
|
||||
// Load all required fonts first
|
||||
const fontSet = new Set(textStyles.map(s => JSON.stringify({ family: s.family, style: s.style })));
|
||||
await Promise.all([...fontSet].map(f => figma.loadFontAsync(JSON.parse(f))));
|
||||
|
||||
const created = [];
|
||||
for (const { name, family, style, size, lineHeight, letterSpacing } of textStyles) {
|
||||
const ts = figma.createTextStyle();
|
||||
ts.name = name;
|
||||
ts.fontName = { family, style };
|
||||
ts.fontSize = size;
|
||||
ts.lineHeight = { value: lineHeight, unit: 'PIXELS' };
|
||||
ts.letterSpacing = { value: letterSpacing, unit: 'PIXELS' };
|
||||
ts.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
ts.setSharedPluginData('dsb', 'key', `text-style/${name}`);
|
||||
created.push({ name, id: ts.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Idempotency — Check-Before-Create Pattern
|
||||
|
||||
Every creation script should check whether the entity already exists before creating it. This prevents duplicates when a script is re-run after partial failure.
|
||||
|
||||
### Check-Before-Create for Collections
|
||||
|
||||
```javascript
|
||||
const DSB_KEY = 'collection/primitives';
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Check if already exists
|
||||
const existing = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
let primColl = existing.find(c => c.getSharedPluginData('dsb', 'key') === DSB_KEY);
|
||||
|
||||
if (primColl) {
|
||||
return { status: 'already_exists', collectionId: primColl.id, name: primColl.name };
|
||||
}
|
||||
|
||||
// Create only if not found
|
||||
primColl = figma.variables.createVariableCollection("Primitives");
|
||||
primColl.renameMode(primColl.modes[0].modeId, "Value");
|
||||
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
primColl.setSharedPluginData('dsb', 'key', DSB_KEY);
|
||||
|
||||
return { status: 'created', collectionId: primColl.id };
|
||||
```
|
||||
|
||||
### Check-Before-Create for Variables
|
||||
|
||||
```javascript
|
||||
const VARIABLE_KEY = 'primitive/blue/500';
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Check if already exists by sharedPluginData key
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const existing = allVars.find(v => v.getSharedPluginData('dsb', 'key') === VARIABLE_KEY);
|
||||
|
||||
if (existing) {
|
||||
return { status: 'already_exists', id: existing.id, name: existing.name };
|
||||
}
|
||||
|
||||
// ... create the variable ...
|
||||
return { status: 'created' };
|
||||
```
|
||||
|
||||
### sharedPluginData Tagging Strategy
|
||||
|
||||
Tag every created node immediately after creation. The `key` is the stable logical identifier used for idempotency checks. The `run_id` identifies which build run created it (useful for cleanup).
|
||||
|
||||
```javascript
|
||||
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // build run ID
|
||||
node.setSharedPluginData('dsb', 'phase', 'phase1'); // which phase
|
||||
node.setSharedPluginData('dsb', 'key', 'color/bg/primary'); // stable logical key
|
||||
```
|
||||
|
||||
**Cleanup by run ID (safe — targets only tagged nodes, never user-owned nodes):**
|
||||
|
||||
```javascript
|
||||
const TARGET_RUN_ID = "ds-build-2024-001"; // run to remove
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const removed = [];
|
||||
for (const v of allVars) {
|
||||
if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
|
||||
removed.push(v.name);
|
||||
v.remove();
|
||||
}
|
||||
}
|
||||
return { removed, count: removed.length };
|
||||
```
|
||||
|
||||
**Never clean up by name prefix** (e.g., deleting everything starting with `color/`). This will destroy user-created variables that happen to share the prefix.
|
||||
|
||||
---
|
||||
|
||||
## 9. Validation — Verify Counts, Aliases, and Scopes
|
||||
|
||||
Run these scripts after Phase 1 to verify everything was created correctly before proceeding to Phase 2.
|
||||
|
||||
### Verify Collection and Variable Counts
|
||||
|
||||
```javascript
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
|
||||
const summary = collections.map(c => {
|
||||
const vars = allVars.filter(v => v.variableCollectionId === c.id);
|
||||
return {
|
||||
name: c.name,
|
||||
id: c.id,
|
||||
modes: c.modes.map(m => m.name),
|
||||
variableCount: vars.length,
|
||||
missingScopes: vars.filter(v => v.scopes.length === 0 && v.resolvedType !== 'BOOLEAN').length,
|
||||
missingCodeSyntax: vars.filter(v => !v.codeSyntax['WEB'] && !v.remote).length,
|
||||
sampleVariables: vars.slice(0, 3).map(v => v.name)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
collectionCount: collections.length,
|
||||
totalVariables: allVars.length,
|
||||
collections: summary
|
||||
};
|
||||
```
|
||||
|
||||
Interpret: `missingScopes > 0` (for non-primitives and non-BOOLEANs) → scope-setting failed, re-run scope script. `missingCodeSyntax > 0` → code syntax not set, run batch code syntax script.
|
||||
|
||||
Note: primitives correctly have `scopes = []` (empty, hidden). `missingScopes` above counts non-BOOLEAN variables with empty scopes — review the list to confirm they are all primitives.
|
||||
|
||||
### Verify Aliases Resolve
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
|
||||
const brokenAliases = [];
|
||||
const aliasedVars = [];
|
||||
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue;
|
||||
const coll = collections.find(c => c.id === v.variableCollectionId);
|
||||
if (!coll) continue;
|
||||
|
||||
for (const [modeId, val] of Object.entries(v.valuesByMode)) {
|
||||
if (val && typeof val === 'object' && val.type === 'VARIABLE_ALIAS') {
|
||||
aliasedVars.push({ name: v.name, aliasTargetId: val.id });
|
||||
// Verify the target exists
|
||||
const target = allVars.find(t => t.id === val.id);
|
||||
if (!target) {
|
||||
brokenAliases.push({ variable: v.name, modeId, missingTargetId: val.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalAliased: aliasedVars.length,
|
||||
brokenAliases,
|
||||
brokenCount: brokenAliases.length,
|
||||
status: brokenAliases.length === 0 ? 'all_aliases_resolve' : 'BROKEN_ALIASES_FOUND'
|
||||
};
|
||||
```
|
||||
|
||||
Interpret: `brokenCount > 0` means a semantic variable references a primitive that was deleted or not yet created. Create the missing primitives, then re-run alias creation for the affected semantic variables.
|
||||
|
||||
### Verify Style Counts
|
||||
|
||||
```javascript
|
||||
const [textStyles, effectStyles] = await Promise.all([
|
||||
figma.getLocalTextStylesAsync(),
|
||||
figma.getLocalEffectStylesAsync()
|
||||
]);
|
||||
|
||||
return {
|
||||
textStyles: textStyles.map(s => ({ name: s.name, fontSize: s.fontSize, fontFamily: s.fontName.family })),
|
||||
effectStyles: effectStyles.map(s => ({ name: s.name, effectCount: s.effects.length })),
|
||||
counts: { text: textStyles.length, effect: effectStyles.length }
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 1 Exit Criteria Checklist
|
||||
|
||||
Before proceeding to Phase 2, verify all of the following:
|
||||
|
||||
- Every planned collection exists with the correct number of modes
|
||||
- Primitive variables: `scopes = []`, code syntax set
|
||||
- Semantic variables: targeted scopes set, code syntax set, aliases pointing to primitives (not raw values)
|
||||
- All broken alias count = 0
|
||||
- All planned text styles exist with correct font family/size/weight
|
||||
- All planned effect styles exist with correct shadow values
|
||||
- No variable has `ALL_SCOPES` unless explicitly approved by the user
|
||||
Reference in New Issue
Block a user