Skip to content

Commit b96d988

Browse files
committed
style(tokens): 💄 All Semantic Names Removed from Primitives
1 parent 037f6e4 commit b96d988

File tree

18 files changed

+3539
-799
lines changed

18 files changed

+3539
-799
lines changed

packages/design-tokens/dictionary/primitives.dtcg.json

Lines changed: 256 additions & 760 deletions
Large diffs are not rendered by default.

packages/design-tokens/dictionary/semantic.dtcg.json

Lines changed: 264 additions & 39 deletions
Large diffs are not rendered by default.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Figma Design Tokens Plugin
2+
3+
Figma plugin for importing and exporting DTCG (Design Tokens Community Group) format design tokens as Figma Variables.
4+
5+
## Features
6+
7+
- **Import**: Upload DTCG JSON files to create/update Figma Variable collections
8+
- **Export**: Export existing Figma Variables back to DTCG JSON format
9+
- **Scope inference**: Automatically assigns Figma scopes based on token naming patterns
10+
- **Theme support**: Light/Dark modes via Figma Variable modes
11+
- **Cross-collection aliases**: Semantic tokens can reference primitives across collections
12+
13+
## Build
14+
15+
```bash
16+
yarn install # install dependencies
17+
yarn build # build to dist/
18+
yarn dev # build in watch mode
19+
```
20+
21+
The build produces three files in `dist/`:
22+
- `code.js` — Plugin main thread (IIFE)
23+
- `import.html` — Import UI (single-file)
24+
- `export.html` — Export UI (single-file)
25+
26+
## Usage
27+
28+
1. Build the plugin: `yarn build`
29+
2. In Figma, go to **Plugins → Development → Import plugin from manifest...**
30+
3. Select `manifest.json` from this package
31+
32+
## Token Files
33+
34+
The plugin expects DTCG JSON files. Token source files live in the sibling package [`@clickhouse/design-tokens`](../design-tokens/).
35+
36+
## Import Order
37+
38+
1. Import **primitives** first (e.g., `primitives.dtcg.json`)
39+
2. Then import **semantic** tokens (e.g., `semantic.dtcg.json`) — these reference primitives
40+
3. Then import **spacing**, **radius**, **sizing** tokens
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "Design Tokens",
3+
"id": "1225498390710809905",
4+
"api": "1.0.0",
5+
"editorType": ["figma"],
6+
"permissions": [],
7+
"main": "dist/code.js",
8+
"menu": [
9+
{ "command": "import", "name": "Import Variables" },
10+
{ "command": "export", "name": "Export Variables" }
11+
],
12+
"ui": { "import": "dist/import.html", "export": "dist/export.html" },
13+
"documentAccess": "dynamic-page"
14+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@clickhouse/figma-design-tokens-plugin",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"license": "Apache-2.0",
7+
"scripts": {
8+
"dev": "vite build --watch",
9+
"build": "rm -rf ./dist && vite build",
10+
"lint": "echo 'Skip lint!'",
11+
"lint:fix": "echo 'Skip lint!'",
12+
"format": "echo 'Skip format!'",
13+
"format:fix": "echo 'Skip format!'",
14+
"typecheck": "tsc --noEmit",
15+
"test": "vitest run",
16+
"test:watch": "vitest"
17+
},
18+
"devDependencies": {
19+
"@figma/plugin-typings": "^1.106.0",
20+
"@types/node": "^25.5.0",
21+
"typescript": "^5.7.0",
22+
"vite": "^6.0.0",
23+
"vite-plugin-singlefile": "^2.0.3",
24+
"vitest": "^2.1.9"
25+
}
26+
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { rgbToHex } from "./utils/colors";
2+
import {
3+
createCollection,
4+
getExistingVariables,
5+
processAliases,
6+
traverseToken,
7+
} from "./utils/tokens";
8+
import type {
9+
AliasEntry,
10+
DTCGToken,
11+
DTCGTokenType,
12+
ExportedFile,
13+
PluginMessage,
14+
} from "./utils/types";
15+
16+
async function importJSONFile({
17+
fileName,
18+
body,
19+
}: {
20+
fileName: string;
21+
body: string;
22+
}): Promise<{ wasUpdate: boolean; collectionName: string; tokenCount: number }> {
23+
console.log("Importing file:", fileName);
24+
25+
26+
let wasUpdate = false;
27+
28+
29+
const existingCollections = await figma.variables.getLocalVariableCollectionsAsync();
30+
const existingCollection = existingCollections.find((c) => c.name === fileName);
31+
wasUpdate = !!existingCollection;
32+
33+
34+
const isPrimitivesFile = fileName.toLowerCase().includes("primitives");
35+
36+
const isSemanticFile = fileName.toLowerCase().includes("semantic");
37+
38+
console.log("DEBUG - File name:", fileName);
39+
console.log("DEBUG - isPrimitivesFile detected:", isPrimitivesFile);
40+
console.log("DEBUG - isSemanticFile detected:", isSemanticFile);
41+
42+
if (isPrimitivesFile) {
43+
console.log(
44+
"Detected primitives file - tokens will have NO scope (hidden from UI)",
45+
);
46+
}
47+
if (isSemanticFile) {
48+
console.log(
49+
"Detected semantic file - will create Light/Dark modes",
50+
);
51+
}
52+
53+
const json = JSON.parse(body) as DTCGToken;
54+
console.log("JSON structure keys:", Object.keys(json));
55+
56+
57+
const { collection, modeId, modeIds } = await createCollection(
58+
fileName,
59+
isSemanticFile,
60+
);
61+
const aliases: Record<string, AliasEntry> = {};
62+
const tokens: Record<string, Variable> = {};
63+
64+
const existingVariables = await getExistingVariables();
65+
console.log(
66+
"Existing variables from other collections:",
67+
Object.keys(existingVariables).length,
68+
);
69+
console.log(
70+
"DEBUG - Sample existing variables:",
71+
Object.keys(existingVariables).slice(0, 10),
72+
);
73+
console.log(
74+
"DEBUG - Looking for 'color/white' in existing:",
75+
existingVariables["color/white"] ? "FOUND" : "NOT FOUND",
76+
);
77+
console.log(
78+
"DEBUG - Looking for 'white' in existing:",
79+
existingVariables["white"] ? "FOUND" : "NOT FOUND",
80+
);
81+
82+
83+
const allKeys = Object.keys(existingVariables);
84+
const conflicts: string[] = [];
85+
86+
87+
const colorConflicts = allKeys.filter((k) => k.startsWith("color/"));
88+
if (colorConflicts.length > 0) {
89+
console.log(
90+
"DEBUG - Found existing color/* tokens:",
91+
colorConflicts.slice(0, 15),
92+
"... and",
93+
colorConflicts.length - 15,
94+
"more",
95+
);
96+
conflicts.push(...colorConflicts);
97+
}
98+
99+
100+
const chartConflicts = allKeys.filter((k) => k.startsWith("chart/"));
101+
if (chartConflicts.length > 0) {
102+
console.log("DEBUG - Found existing chart/* tokens:", chartConflicts);
103+
conflicts.push(...chartConflicts);
104+
}
105+
106+
107+
const checkboxConflicts = allKeys.filter((k) => k.startsWith("checkbox/"));
108+
if (checkboxConflicts.length > 0) {
109+
console.log("DEBUG - Found existing checkbox/* tokens:", checkboxConflicts);
110+
conflicts.push(...checkboxConflicts);
111+
}
112+
113+
if (conflicts.length > 0) {
114+
console.log(
115+
"DEBUG - TOTAL CONFLICTS FOUND:",
116+
conflicts.length,
117+
"tokens will fail to create",
118+
);
119+
}
120+
121+
traverseToken({
122+
collection,
123+
modeId,
124+
modeIds,
125+
type: json.$type as DTCGTokenType | undefined,
126+
key: "",
127+
object: json,
128+
tokens,
129+
aliases,
130+
existingVariables,
131+
isPrimitivesFile,
132+
});
133+
134+
console.log("Created tokens:", Object.keys(tokens).length);
135+
console.log("Pending aliases:", Object.keys(aliases).length);
136+
137+
await processAliases({
138+
collection,
139+
modeId,
140+
modeIds,
141+
aliases,
142+
tokens,
143+
existingVariables,
144+
isPrimitivesFile,
145+
});
146+
147+
console.log("Import complete!");
148+
149+
150+
return {
151+
wasUpdate,
152+
collectionName: fileName,
153+
tokenCount: Object.keys(tokens).length,
154+
};
155+
}
156+
157+
async function exportToJSON(): Promise<void> {
158+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
159+
const files: ExportedFile[] = [];
160+
161+
for (const collection of collections) {
162+
const collectionFiles = await processCollection(collection);
163+
files.push(...collectionFiles);
164+
}
165+
166+
figma.ui.postMessage({ type: "EXPORT_RESULT", files });
167+
}
168+
169+
async function processCollection({
170+
name,
171+
modes,
172+
variableIds,
173+
}: VariableCollection): Promise<ExportedFile[]> {
174+
const files: ExportedFile[] = [];
175+
176+
for (const mode of modes) {
177+
const file: ExportedFile = {
178+
fileName: `${name}.${mode.name}.tokens.json`,
179+
body: {},
180+
};
181+
182+
for (const variableId of variableIds) {
183+
const variable = await figma.variables.getVariableByIdAsync(variableId);
184+
185+
if (!variable) continue;
186+
187+
const { name: varName, resolvedType, valuesByMode } = variable;
188+
const value = valuesByMode[mode.modeId];
189+
190+
if (value !== undefined && ["COLOR", "FLOAT"].includes(resolvedType)) {
191+
let obj: Record<string, unknown> = file.body;
192+
193+
varName.split("/").forEach((groupName) => {
194+
obj[groupName] = obj[groupName] || {};
195+
obj = obj[groupName] as Record<string, unknown>;
196+
});
197+
198+
obj.$type = resolvedType === "COLOR" ? "color" : "number";
199+
200+
if (
201+
typeof value === "object" &&
202+
"type" in value &&
203+
value.type === "VARIABLE_ALIAS"
204+
) {
205+
const aliasedVar = await figma.variables.getVariableByIdAsync(
206+
value.id,
207+
);
208+
if (aliasedVar) {
209+
obj.$value = `{${aliasedVar.name.replace(/\//g, ".")}}`;
210+
}
211+
} else if (resolvedType === "COLOR" && typeof value === "object") {
212+
obj.$value = rgbToHex(value as RGBA);
213+
} else {
214+
obj.$value = value;
215+
}
216+
}
217+
}
218+
219+
files.push(file);
220+
}
221+
222+
return files;
223+
}
224+
225+
figma.ui.onmessage = async (e: PluginMessage) => {
226+
console.log("code received message", e);
227+
228+
if (e.type === "IMPORT") {
229+
const result = await importJSONFile({ fileName: e.fileName, body: e.body });
230+
231+
figma.ui.postMessage({
232+
type: "IMPORT_COMPLETE",
233+
wasUpdate: result.wasUpdate,
234+
collectionName: result.collectionName,
235+
tokenCount: result.tokenCount,
236+
});
237+
} else if (e.type === "EXPORT") {
238+
await exportToJSON();
239+
} else if (e.type === "GET_COLLECTIONS") {
240+
241+
const collections =
242+
await figma.variables.getLocalVariableCollectionsAsync();
243+
const collectionsInfo = collections.map((c) => ({
244+
name: c.name,
245+
variableCount: c.variableIds.length,
246+
}));
247+
figma.ui.postMessage({
248+
type: "COLLECTIONS_LIST",
249+
collections: collectionsInfo,
250+
});
251+
}
252+
};
253+
254+
if (figma.command === "import") {
255+
figma.showUI(__uiFiles__["import"] as string, {
256+
width: 500,
257+
height: 500,
258+
themeColors: true,
259+
});
260+
} else if (figma.command === "export") {
261+
figma.showUI(__uiFiles__["export"] as string, {
262+
width: 500,
263+
height: 500,
264+
themeColors: true,
265+
});
266+
}

0 commit comments

Comments
 (0)