Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -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 (50200 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, 48 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 01 range, not 0255. 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