tokka 0.2.4 → 0.2.5
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/compiler/index.d.ts +478 -0
- package/compiler/index.js +732 -0
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/compiler/generators/css.ts +0 -146
- package/compiler/generators/figma.ts +0 -147
- package/compiler/generators/tailwind.ts +0 -106
- package/compiler/generators/typescript.ts +0 -113
- package/compiler/index.ts +0 -45
- package/compiler/loader.ts +0 -92
- package/compiler/resolver.ts +0 -177
- package/compiler/types.ts +0 -118
- package/compiler/validator.ts +0 -194
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var tokenTypeSchema = z.enum([
|
|
4
|
+
"color",
|
|
5
|
+
"number",
|
|
6
|
+
"dimension",
|
|
7
|
+
"radius",
|
|
8
|
+
"shadow",
|
|
9
|
+
"typography",
|
|
10
|
+
"motion",
|
|
11
|
+
"opacity",
|
|
12
|
+
"zIndex"
|
|
13
|
+
]);
|
|
14
|
+
var tokenSourceSchema = z.enum(["primitive", "semantic", "component"]);
|
|
15
|
+
var tokenSchema = z.object({
|
|
16
|
+
id: z.string().regex(
|
|
17
|
+
/^[a-z][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/,
|
|
18
|
+
"Token ID must be lowercase, dot-separated"
|
|
19
|
+
),
|
|
20
|
+
type: tokenTypeSchema,
|
|
21
|
+
description: z.string(),
|
|
22
|
+
source: tokenSourceSchema,
|
|
23
|
+
value: z.any().optional(),
|
|
24
|
+
modes: z.record(z.string(), z.any()).optional(),
|
|
25
|
+
references: z.array(z.string()).optional(),
|
|
26
|
+
tags: z.array(z.string()).optional(),
|
|
27
|
+
deprecated: z.boolean().optional(),
|
|
28
|
+
replacedBy: z.string().optional(),
|
|
29
|
+
figma: z.object({
|
|
30
|
+
collection: z.string().optional(),
|
|
31
|
+
scopes: z.array(z.string()).optional(),
|
|
32
|
+
variableName: z.string().optional()
|
|
33
|
+
}).optional()
|
|
34
|
+
}).refine((data) => data.value !== void 0 || data.modes !== void 0, {
|
|
35
|
+
message: "Token must have either value or modes"
|
|
36
|
+
});
|
|
37
|
+
var tokenFileSchema = z.object({
|
|
38
|
+
tokens: z.array(tokenSchema)
|
|
39
|
+
});
|
|
40
|
+
var systemSchema = z.object({
|
|
41
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/, "System ID must be lowercase kebab-case"),
|
|
42
|
+
name: z.string(),
|
|
43
|
+
description: z.string(),
|
|
44
|
+
tags: z.array(z.string()).optional(),
|
|
45
|
+
modes: z.array(z.string()).min(1),
|
|
46
|
+
policies: z.object({
|
|
47
|
+
radius: z.enum(["sharp", "rounded", "pill"]).optional(),
|
|
48
|
+
density: z.enum(["compact", "comfortable", "spacious"]).optional(),
|
|
49
|
+
contrast: z.enum(["low", "medium", "high"]).optional(),
|
|
50
|
+
motion: z.enum(["none", "subtle", "expressive"]).optional()
|
|
51
|
+
}).optional(),
|
|
52
|
+
defaults: z.object({
|
|
53
|
+
font: z.string().optional(),
|
|
54
|
+
iconStyle: z.string().optional()
|
|
55
|
+
}).optional(),
|
|
56
|
+
figma: z.object({
|
|
57
|
+
collections: z.array(z.string()).optional()
|
|
58
|
+
}).optional()
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// src/loader.ts
|
|
62
|
+
import fs from "fs/promises";
|
|
63
|
+
import path from "path";
|
|
64
|
+
async function loadTokens(options) {
|
|
65
|
+
const tokensDir = options.tokensDir || path.join(options.cwd, "tokens");
|
|
66
|
+
const primitivesPath = path.join(tokensDir, "primitives.json");
|
|
67
|
+
const primitivesContent = await fs.readFile(primitivesPath, "utf-8");
|
|
68
|
+
const primitivesData = tokenFileSchema.parse(JSON.parse(primitivesContent));
|
|
69
|
+
const primitives = primitivesData.tokens;
|
|
70
|
+
const semanticsPath = path.join(tokensDir, "semantics.json");
|
|
71
|
+
const semanticsContent = await fs.readFile(semanticsPath, "utf-8");
|
|
72
|
+
const semanticsData = tokenFileSchema.parse(JSON.parse(semanticsContent));
|
|
73
|
+
const semantics = semanticsData.tokens;
|
|
74
|
+
const components = [];
|
|
75
|
+
const componentsDir = path.join(tokensDir, "components");
|
|
76
|
+
try {
|
|
77
|
+
const componentFiles = await fs.readdir(componentsDir);
|
|
78
|
+
for (const file of componentFiles) {
|
|
79
|
+
if (file.endsWith(".json")) {
|
|
80
|
+
const componentPath = path.join(componentsDir, file);
|
|
81
|
+
const componentContent = await fs.readFile(componentPath, "utf-8");
|
|
82
|
+
const componentData = tokenFileSchema.parse(JSON.parse(componentContent));
|
|
83
|
+
components.push(...componentData.tokens);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
primitives,
|
|
90
|
+
semantics,
|
|
91
|
+
components,
|
|
92
|
+
all: [...primitives, ...semantics, ...components]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function loadSystem(options) {
|
|
96
|
+
const systemPath = path.join(options.cwd, "system.json");
|
|
97
|
+
const systemContent = await fs.readFile(systemPath, "utf-8");
|
|
98
|
+
const system = systemSchema.parse(JSON.parse(systemContent));
|
|
99
|
+
return system;
|
|
100
|
+
}
|
|
101
|
+
async function hasTokens(cwd) {
|
|
102
|
+
try {
|
|
103
|
+
await fs.access(path.join(cwd, "tokens"));
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function hasSystem(cwd) {
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(path.join(cwd, "system.json"));
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/validator.ts
|
|
119
|
+
var SEMANTIC_PREFIXES = [
|
|
120
|
+
"surface.",
|
|
121
|
+
"text.",
|
|
122
|
+
"border.",
|
|
123
|
+
"icon.",
|
|
124
|
+
"shadow.",
|
|
125
|
+
"focus.",
|
|
126
|
+
"overlay.",
|
|
127
|
+
"motion.",
|
|
128
|
+
"space.",
|
|
129
|
+
"radius.",
|
|
130
|
+
"typography."
|
|
131
|
+
];
|
|
132
|
+
function validateTokenNaming(tokens) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
for (const token of tokens) {
|
|
135
|
+
if (token.source === "semantic") {
|
|
136
|
+
const hasValidPrefix = SEMANTIC_PREFIXES.some(
|
|
137
|
+
(prefix) => token.id.startsWith(prefix)
|
|
138
|
+
);
|
|
139
|
+
if (!hasValidPrefix) {
|
|
140
|
+
errors.push({
|
|
141
|
+
type: "naming",
|
|
142
|
+
message: `Semantic token "${token.id}" must start with one of: ${SEMANTIC_PREFIXES.join(", ")}`,
|
|
143
|
+
tokenId: token.id
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (token.source === "component") {
|
|
148
|
+
const parts = token.id.split(".");
|
|
149
|
+
if (parts.length < 2) {
|
|
150
|
+
errors.push({
|
|
151
|
+
type: "naming",
|
|
152
|
+
message: `Component token "${token.id}" must follow format: <component>.<property>`,
|
|
153
|
+
tokenId: token.id
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
160
|
+
function validateTokenUniqueness(tokens) {
|
|
161
|
+
const errors = [];
|
|
162
|
+
const seen = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const token of tokens) {
|
|
164
|
+
if (seen.has(token.id)) {
|
|
165
|
+
errors.push({
|
|
166
|
+
type: "schema",
|
|
167
|
+
message: `Duplicate token ID: "${token.id}"`,
|
|
168
|
+
tokenId: token.id
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
seen.set(token.id, token);
|
|
172
|
+
}
|
|
173
|
+
return errors;
|
|
174
|
+
}
|
|
175
|
+
function validateTokenReferences(tokens) {
|
|
176
|
+
const errors = [];
|
|
177
|
+
const tokenIds = new Set(tokens.map((t) => t.id));
|
|
178
|
+
for (const token of tokens) {
|
|
179
|
+
if (token.references) {
|
|
180
|
+
for (const ref of token.references) {
|
|
181
|
+
if (!tokenIds.has(ref)) {
|
|
182
|
+
errors.push({
|
|
183
|
+
type: "reference",
|
|
184
|
+
message: `Token "${token.id}" references non-existent token "${ref}"`,
|
|
185
|
+
tokenId: token.id
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return errors;
|
|
192
|
+
}
|
|
193
|
+
function validateTokenLayering(tokens, strict = false) {
|
|
194
|
+
if (!strict) return [];
|
|
195
|
+
const errors = [];
|
|
196
|
+
const tokenMap = new Map(tokens.map((t) => [t.id, t]));
|
|
197
|
+
for (const token of tokens) {
|
|
198
|
+
if (!token.references || token.references.length === 0) continue;
|
|
199
|
+
if (token.source === "primitive") {
|
|
200
|
+
errors.push({
|
|
201
|
+
type: "layering",
|
|
202
|
+
message: `Primitive token "${token.id}" may not reference other tokens`,
|
|
203
|
+
tokenId: token.id
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (token.source === "semantic") {
|
|
207
|
+
for (const ref of token.references) {
|
|
208
|
+
const refToken = tokenMap.get(ref);
|
|
209
|
+
if (refToken && refToken.source === "component") {
|
|
210
|
+
errors.push({
|
|
211
|
+
type: "layering",
|
|
212
|
+
message: `Semantic token "${token.id}" may not reference component token "${ref}"`,
|
|
213
|
+
tokenId: token.id
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (token.source === "component") {
|
|
219
|
+
for (const ref of token.references) {
|
|
220
|
+
const refToken = tokenMap.get(ref);
|
|
221
|
+
if (refToken && refToken.source === "component") {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return errors;
|
|
227
|
+
}
|
|
228
|
+
function validateModeCoverage(tokens, requiredModes) {
|
|
229
|
+
const errors = [];
|
|
230
|
+
for (const token of tokens) {
|
|
231
|
+
if (token.modes) {
|
|
232
|
+
const tokenModes = Object.keys(token.modes);
|
|
233
|
+
const missingModes = requiredModes.filter((m) => !tokenModes.includes(m));
|
|
234
|
+
if (missingModes.length > 0) {
|
|
235
|
+
errors.push({
|
|
236
|
+
type: "schema",
|
|
237
|
+
message: `Token "${token.id}" missing required modes: ${missingModes.join(", ")}`,
|
|
238
|
+
tokenId: token.id
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
245
|
+
function validateTokens(tokens, requiredModes, options = {}) {
|
|
246
|
+
return [
|
|
247
|
+
...validateTokenUniqueness(tokens),
|
|
248
|
+
...validateTokenNaming(tokens),
|
|
249
|
+
...validateTokenReferences(tokens),
|
|
250
|
+
...validateModeCoverage(tokens, requiredModes),
|
|
251
|
+
...validateTokenLayering(tokens, options.strict)
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/resolver.ts
|
|
256
|
+
function buildDependencyGraph(tokens) {
|
|
257
|
+
const graph = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const token of tokens) {
|
|
259
|
+
graph.set(token.id, /* @__PURE__ */ new Set());
|
|
260
|
+
}
|
|
261
|
+
for (const token of tokens) {
|
|
262
|
+
if (token.references) {
|
|
263
|
+
const deps = graph.get(token.id);
|
|
264
|
+
for (const ref of token.references) {
|
|
265
|
+
deps.add(ref);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return graph;
|
|
270
|
+
}
|
|
271
|
+
function detectCycles(graph) {
|
|
272
|
+
const cycles = [];
|
|
273
|
+
const visited = /* @__PURE__ */ new Set();
|
|
274
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
275
|
+
function dfs(node, path2) {
|
|
276
|
+
visited.add(node);
|
|
277
|
+
recursionStack.add(node);
|
|
278
|
+
path2.push(node);
|
|
279
|
+
const neighbors = graph.get(node) || /* @__PURE__ */ new Set();
|
|
280
|
+
for (const neighbor of neighbors) {
|
|
281
|
+
if (!visited.has(neighbor)) {
|
|
282
|
+
if (dfs(neighbor, path2)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
} else if (recursionStack.has(neighbor)) {
|
|
286
|
+
const cycleStart = path2.indexOf(neighbor);
|
|
287
|
+
cycles.push([...path2.slice(cycleStart), neighbor]);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
recursionStack.delete(node);
|
|
292
|
+
path2.pop();
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
for (const node of graph.keys()) {
|
|
296
|
+
if (!visited.has(node)) {
|
|
297
|
+
dfs(node, []);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return cycles;
|
|
301
|
+
}
|
|
302
|
+
function topologicalSort(tokens, graph) {
|
|
303
|
+
const sorted = [];
|
|
304
|
+
const visited = /* @__PURE__ */ new Set();
|
|
305
|
+
const tokenMap = new Map(tokens.map((t) => [t.id, t]));
|
|
306
|
+
function visit(tokenId) {
|
|
307
|
+
if (visited.has(tokenId)) return;
|
|
308
|
+
visited.add(tokenId);
|
|
309
|
+
const deps = graph.get(tokenId) || /* @__PURE__ */ new Set();
|
|
310
|
+
for (const dep of deps) {
|
|
311
|
+
visit(dep);
|
|
312
|
+
}
|
|
313
|
+
const token = tokenMap.get(tokenId);
|
|
314
|
+
if (token) {
|
|
315
|
+
sorted.push(token);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const token of tokens) {
|
|
319
|
+
visit(token.id);
|
|
320
|
+
}
|
|
321
|
+
return sorted;
|
|
322
|
+
}
|
|
323
|
+
function resolveTokens(tokens, graph) {
|
|
324
|
+
const errors = [];
|
|
325
|
+
const cycles = detectCycles(graph);
|
|
326
|
+
if (cycles.length > 0) {
|
|
327
|
+
for (const cycle of cycles) {
|
|
328
|
+
errors.push({
|
|
329
|
+
type: "cycle",
|
|
330
|
+
message: `Circular reference detected: ${cycle.join(" \u2192 ")}`,
|
|
331
|
+
tokenId: cycle[0]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return { resolved: [], errors };
|
|
335
|
+
}
|
|
336
|
+
const sorted = topologicalSort(tokens, graph);
|
|
337
|
+
const resolvedMap = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const token of sorted) {
|
|
339
|
+
const resolved = { ...token };
|
|
340
|
+
if (token.value !== void 0) {
|
|
341
|
+
resolved.resolvedValue = resolveValue(token.value, resolvedMap);
|
|
342
|
+
}
|
|
343
|
+
if (token.modes) {
|
|
344
|
+
resolved.resolvedModes = {};
|
|
345
|
+
for (const [mode, value] of Object.entries(token.modes)) {
|
|
346
|
+
resolved.resolvedModes[mode] = resolveValue(value, resolvedMap);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
resolvedMap.set(token.id, resolved);
|
|
350
|
+
}
|
|
351
|
+
return { resolved: Array.from(resolvedMap.values()), errors };
|
|
352
|
+
}
|
|
353
|
+
function resolveValue(value, resolvedMap) {
|
|
354
|
+
if (typeof value !== "string") {
|
|
355
|
+
return String(value);
|
|
356
|
+
}
|
|
357
|
+
if (value.startsWith("var(--")) {
|
|
358
|
+
const match = value.match(/var\(--([a-z][a-z0-9-]*)\)/);
|
|
359
|
+
if (match) {
|
|
360
|
+
const tokenId = match[1].replace(/-/g, ".");
|
|
361
|
+
const refToken = resolvedMap.get(tokenId);
|
|
362
|
+
if (refToken?.resolvedValue) {
|
|
363
|
+
return refToken.resolvedValue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/generators/css.ts
|
|
371
|
+
function tokenIdToCSSVar(id) {
|
|
372
|
+
return `--${id.replace(/\./g, "-")}`;
|
|
373
|
+
}
|
|
374
|
+
function parseColorToHSLTriplet(value) {
|
|
375
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value.trim())) {
|
|
376
|
+
return value.trim();
|
|
377
|
+
}
|
|
378
|
+
const hslMatch = value.match(/hsl\(([^)]+)\)/);
|
|
379
|
+
if (hslMatch) {
|
|
380
|
+
return hslMatch[1].trim();
|
|
381
|
+
}
|
|
382
|
+
const oklchMatch = value.match(/oklch\(([^)]+)\)/);
|
|
383
|
+
if (oklchMatch) {
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function generateCSSForMode(tokens, mode, options = {}) {
|
|
389
|
+
const lines = [];
|
|
390
|
+
const selector = mode === "default" ? ":root" : options.modeSelector?.strategy === "data-attribute" ? options.modeSelector.selectors?.[mode] || `[data-theme="${mode}"]` : options.modeSelector?.selectors?.[mode] || `.${mode}`;
|
|
391
|
+
lines.push(`${selector} {`);
|
|
392
|
+
for (const token of tokens) {
|
|
393
|
+
const cssVar = tokenIdToCSSVar(token.id);
|
|
394
|
+
let value;
|
|
395
|
+
if (mode === "default") {
|
|
396
|
+
value = token.resolvedValue || token.value;
|
|
397
|
+
} else if (token.resolvedModes?.[mode]) {
|
|
398
|
+
value = token.resolvedModes[mode];
|
|
399
|
+
} else if (token.modes?.[mode]) {
|
|
400
|
+
value = token.modes[mode];
|
|
401
|
+
}
|
|
402
|
+
if (value === void 0) continue;
|
|
403
|
+
if (token.type === "color") {
|
|
404
|
+
const triplet = parseColorToHSLTriplet(String(value));
|
|
405
|
+
lines.push(` ${cssVar}: ${triplet};`);
|
|
406
|
+
} else {
|
|
407
|
+
lines.push(` ${cssVar}: ${value};`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
lines.push("}");
|
|
411
|
+
return lines.join("\n");
|
|
412
|
+
}
|
|
413
|
+
function generateCSS(tokens, system, options = {}) {
|
|
414
|
+
const output = [];
|
|
415
|
+
output.push("/**");
|
|
416
|
+
output.push(` * Design tokens for ${system.name}`);
|
|
417
|
+
output.push(" * Generated by figma-base - do not edit directly");
|
|
418
|
+
output.push(" */");
|
|
419
|
+
output.push("");
|
|
420
|
+
const lightMode = system.modes.includes("light") ? "light" : system.modes[0];
|
|
421
|
+
output.push(generateCSSForMode(tokens, "default", options));
|
|
422
|
+
for (const mode of system.modes) {
|
|
423
|
+
if (mode !== lightMode) {
|
|
424
|
+
output.push("");
|
|
425
|
+
output.push(generateCSSForMode(tokens, mode, options));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return output.join("\n");
|
|
429
|
+
}
|
|
430
|
+
function generateCSSOutput(tokens, system, options = {}) {
|
|
431
|
+
return {
|
|
432
|
+
"tokens.css": generateCSS(tokens, system, options)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/generators/tailwind.ts
|
|
437
|
+
var SEMANTIC_TO_TAILWIND_MAPPING = {
|
|
438
|
+
"surface.brand": "primary",
|
|
439
|
+
"text.on-brand": "primary-foreground",
|
|
440
|
+
"surface.secondary": "secondary",
|
|
441
|
+
"text.on-secondary": "secondary-foreground",
|
|
442
|
+
"surface.destructive": "destructive",
|
|
443
|
+
"text.on-destructive": "destructive-foreground",
|
|
444
|
+
"surface.default": "background",
|
|
445
|
+
"text.default": "foreground",
|
|
446
|
+
"surface.card": "card",
|
|
447
|
+
"text.on-card": "card-foreground",
|
|
448
|
+
"surface.popover": "popover",
|
|
449
|
+
"text.on-popover": "popover-foreground",
|
|
450
|
+
"surface.muted": "muted",
|
|
451
|
+
"text.muted": "muted-foreground",
|
|
452
|
+
"surface.accent": "accent",
|
|
453
|
+
"text.on-accent": "accent-foreground",
|
|
454
|
+
"border.default": "border",
|
|
455
|
+
"border.input": "input",
|
|
456
|
+
"focus.ring": "ring"
|
|
457
|
+
};
|
|
458
|
+
function generateTailwindConfig(tokens, _system) {
|
|
459
|
+
const colors = {};
|
|
460
|
+
const spacing = {};
|
|
461
|
+
const borderRadius = {};
|
|
462
|
+
const boxShadow = {};
|
|
463
|
+
for (const token of tokens) {
|
|
464
|
+
if (token.source !== "semantic") continue;
|
|
465
|
+
const cssVar = tokenIdToCSSVar(token.id);
|
|
466
|
+
if (token.type === "color") {
|
|
467
|
+
const tailwindName = SEMANTIC_TO_TAILWIND_MAPPING[token.id];
|
|
468
|
+
if (tailwindName) {
|
|
469
|
+
colors[tailwindName] = `hsl(var(${cssVar}))`;
|
|
470
|
+
} else {
|
|
471
|
+
const name = token.id.replace(/\./g, "-");
|
|
472
|
+
colors[name] = `hsl(var(${cssVar}))`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (token.type === "dimension" && token.id.startsWith("space.")) {
|
|
476
|
+
const name = token.id.replace("space.", "");
|
|
477
|
+
spacing[name] = `var(${cssVar})`;
|
|
478
|
+
}
|
|
479
|
+
if (token.type === "radius" && token.id.startsWith("radius.")) {
|
|
480
|
+
const name = token.id.replace("radius.", "");
|
|
481
|
+
borderRadius[name] = `var(${cssVar})`;
|
|
482
|
+
}
|
|
483
|
+
if (token.type === "shadow" && token.id.startsWith("shadow.")) {
|
|
484
|
+
const name = token.id.replace("shadow.", "");
|
|
485
|
+
boxShadow[name] = `var(${cssVar})`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
theme: {
|
|
490
|
+
extend: {
|
|
491
|
+
colors,
|
|
492
|
+
spacing,
|
|
493
|
+
borderRadius,
|
|
494
|
+
boxShadow
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function generateTailwindOutput(tokens, system) {
|
|
500
|
+
const config = generateTailwindConfig(tokens, system);
|
|
501
|
+
return `/**
|
|
502
|
+
* Tailwind config for ${system.name}
|
|
503
|
+
* Generated by figma-base - do not edit directly
|
|
504
|
+
*/
|
|
505
|
+
export default ${JSON.stringify(config, null, 2)}
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/generators/typescript.ts
|
|
510
|
+
function generateTypeScript(tokens, system) {
|
|
511
|
+
const lines = [];
|
|
512
|
+
lines.push("/**");
|
|
513
|
+
lines.push(` * Token type definitions for ${system.name}`);
|
|
514
|
+
lines.push(" * Generated by figma-base - do not edit directly");
|
|
515
|
+
lines.push(" */");
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push("export type TokenType =");
|
|
518
|
+
lines.push(' | "color"');
|
|
519
|
+
lines.push(' | "number"');
|
|
520
|
+
lines.push(' | "dimension"');
|
|
521
|
+
lines.push(' | "radius"');
|
|
522
|
+
lines.push(' | "shadow"');
|
|
523
|
+
lines.push(' | "typography"');
|
|
524
|
+
lines.push(' | "motion"');
|
|
525
|
+
lines.push(' | "opacity"');
|
|
526
|
+
lines.push(' | "zIndex"');
|
|
527
|
+
lines.push("");
|
|
528
|
+
lines.push('export type TokenSource = "primitive" | "semantic" | "component"');
|
|
529
|
+
lines.push("");
|
|
530
|
+
lines.push("export interface Token {");
|
|
531
|
+
lines.push(" id: string");
|
|
532
|
+
lines.push(" type: TokenType");
|
|
533
|
+
lines.push(" source: TokenSource");
|
|
534
|
+
lines.push(" description: string");
|
|
535
|
+
lines.push(" value?: string");
|
|
536
|
+
lines.push(" modes?: Record<string, string>");
|
|
537
|
+
lines.push("}");
|
|
538
|
+
lines.push("");
|
|
539
|
+
lines.push("export type TokenId =");
|
|
540
|
+
tokens.forEach((token, index) => {
|
|
541
|
+
const isLast = index === tokens.length - 1;
|
|
542
|
+
lines.push(` | "${token.id}"${isLast ? "" : ""}`);
|
|
543
|
+
});
|
|
544
|
+
lines.push("");
|
|
545
|
+
lines.push("export const tokens: Record<TokenId, Token> = {");
|
|
546
|
+
for (const token of tokens) {
|
|
547
|
+
lines.push(` "${token.id}": {`);
|
|
548
|
+
lines.push(` id: "${token.id}",`);
|
|
549
|
+
lines.push(` type: "${token.type}",`);
|
|
550
|
+
lines.push(` source: "${token.source}",`);
|
|
551
|
+
lines.push(` description: ${JSON.stringify(token.description)},`);
|
|
552
|
+
if (token.resolvedValue) {
|
|
553
|
+
lines.push(` value: ${JSON.stringify(token.resolvedValue)},`);
|
|
554
|
+
}
|
|
555
|
+
if (token.resolvedModes && Object.keys(token.resolvedModes).length > 0) {
|
|
556
|
+
lines.push(` modes: {`);
|
|
557
|
+
for (const [mode, value] of Object.entries(token.resolvedModes)) {
|
|
558
|
+
lines.push(` "${mode}": ${JSON.stringify(value)},`);
|
|
559
|
+
}
|
|
560
|
+
lines.push(` },`);
|
|
561
|
+
}
|
|
562
|
+
lines.push(` },`);
|
|
563
|
+
}
|
|
564
|
+
lines.push("} as const");
|
|
565
|
+
lines.push("");
|
|
566
|
+
lines.push("export function getToken(id: TokenId): Token | undefined {");
|
|
567
|
+
lines.push(" return tokens[id]");
|
|
568
|
+
lines.push("}");
|
|
569
|
+
lines.push("");
|
|
570
|
+
lines.push("export function getTokensByType(type: TokenType): Token[] {");
|
|
571
|
+
lines.push(" return Object.values(tokens).filter(t => t.type === type)");
|
|
572
|
+
lines.push("}");
|
|
573
|
+
lines.push("");
|
|
574
|
+
lines.push("export function getTokensBySource(source: TokenSource): Token[] {");
|
|
575
|
+
lines.push(" return Object.values(tokens).filter(t => t.source === source)");
|
|
576
|
+
lines.push("}");
|
|
577
|
+
return lines.join("\n");
|
|
578
|
+
}
|
|
579
|
+
function generateTypeScriptOutput(tokens, system) {
|
|
580
|
+
return {
|
|
581
|
+
"tokens.ts": generateTypeScript(tokens, system)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/generators/figma.ts
|
|
586
|
+
function formatColorValue(value) {
|
|
587
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value.trim())) {
|
|
588
|
+
return `hsl(${value})`;
|
|
589
|
+
}
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
function formatTokenValue(token, value) {
|
|
593
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
594
|
+
return value;
|
|
595
|
+
}
|
|
596
|
+
if (token.type === "color") {
|
|
597
|
+
return formatColorValue(value);
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
function generateFigmaTokens(tokens, _system) {
|
|
602
|
+
const output = {};
|
|
603
|
+
const primitiveTokens = tokens.filter((t) => t.source === "primitive");
|
|
604
|
+
const semanticTokens = tokens.filter((t) => t.source === "semantic");
|
|
605
|
+
const componentTokens = tokens.filter((t) => t.source === "component");
|
|
606
|
+
const primitiveTree = {};
|
|
607
|
+
for (const token of primitiveTokens) {
|
|
608
|
+
const parts = token.id.split(".");
|
|
609
|
+
let current = primitiveTree;
|
|
610
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
611
|
+
if (!current[parts[i]]) {
|
|
612
|
+
current[parts[i]] = {};
|
|
613
|
+
}
|
|
614
|
+
current = current[parts[i]];
|
|
615
|
+
}
|
|
616
|
+
const lastPart = parts[parts.length - 1];
|
|
617
|
+
const rawValue = token.resolvedValue || token.value;
|
|
618
|
+
const formattedValue = formatTokenValue(token, rawValue);
|
|
619
|
+
current[lastPart] = {
|
|
620
|
+
value: formattedValue,
|
|
621
|
+
type: token.type,
|
|
622
|
+
description: token.description
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
output.global = primitiveTree;
|
|
626
|
+
const semanticTree = {};
|
|
627
|
+
for (const token of semanticTokens) {
|
|
628
|
+
const parts = token.id.split(".");
|
|
629
|
+
let current = semanticTree;
|
|
630
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
631
|
+
if (!current[parts[i]]) {
|
|
632
|
+
current[parts[i]] = {};
|
|
633
|
+
}
|
|
634
|
+
current = current[parts[i]];
|
|
635
|
+
}
|
|
636
|
+
const lastPart = parts[parts.length - 1];
|
|
637
|
+
const rawValue = token.references && token.references.length > 0 ? `{${token.references[0]}}` : token.resolvedValue || token.value;
|
|
638
|
+
const formattedValue = formatTokenValue(token, rawValue);
|
|
639
|
+
current[lastPart] = {
|
|
640
|
+
value: formattedValue,
|
|
641
|
+
type: token.type,
|
|
642
|
+
description: token.description
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
output.semantic = semanticTree;
|
|
646
|
+
if (componentTokens.length > 0) {
|
|
647
|
+
const componentTree = {};
|
|
648
|
+
for (const token of componentTokens) {
|
|
649
|
+
const parts = token.id.split(".");
|
|
650
|
+
let current = componentTree;
|
|
651
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
652
|
+
if (!current[parts[i]]) {
|
|
653
|
+
current[parts[i]] = {};
|
|
654
|
+
}
|
|
655
|
+
current = current[parts[i]];
|
|
656
|
+
}
|
|
657
|
+
const lastPart = parts[parts.length - 1];
|
|
658
|
+
const rawValue = token.references && token.references.length > 0 ? `{${token.references[0]}}` : token.resolvedValue || token.value;
|
|
659
|
+
const formattedValue = formatTokenValue(token, rawValue);
|
|
660
|
+
current[lastPart] = {
|
|
661
|
+
value: formattedValue,
|
|
662
|
+
type: token.type,
|
|
663
|
+
description: token.description
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
output.component = componentTree;
|
|
667
|
+
}
|
|
668
|
+
return output;
|
|
669
|
+
}
|
|
670
|
+
function generateFigmaTokenOutput(tokens, system) {
|
|
671
|
+
const tokensObject = generateFigmaTokens(tokens, system);
|
|
672
|
+
return {
|
|
673
|
+
"tokka.tokens.json": JSON.stringify(tokensObject, null, 2)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/index.ts
|
|
678
|
+
async function compile(options) {
|
|
679
|
+
const loadedTokens = await loadTokens(options);
|
|
680
|
+
const system = await loadSystem(options);
|
|
681
|
+
const validationErrors = validateTokens(loadedTokens.all, system.modes, {
|
|
682
|
+
strict: false
|
|
683
|
+
// Tier 0 default
|
|
684
|
+
});
|
|
685
|
+
if (validationErrors.length > 0) {
|
|
686
|
+
return {
|
|
687
|
+
tokens: [],
|
|
688
|
+
errors: validationErrors,
|
|
689
|
+
warnings: []
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const graph = buildDependencyGraph(loadedTokens.all);
|
|
693
|
+
const { resolved, errors } = resolveTokens(loadedTokens.all, graph);
|
|
694
|
+
return {
|
|
695
|
+
tokens: resolved,
|
|
696
|
+
errors,
|
|
697
|
+
warnings: []
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
export {
|
|
701
|
+
buildDependencyGraph,
|
|
702
|
+
compile,
|
|
703
|
+
detectCycles,
|
|
704
|
+
generateCSS,
|
|
705
|
+
generateCSSForMode,
|
|
706
|
+
generateCSSOutput,
|
|
707
|
+
generateFigmaTokenOutput,
|
|
708
|
+
generateFigmaTokens,
|
|
709
|
+
generateTailwindConfig,
|
|
710
|
+
generateTailwindOutput,
|
|
711
|
+
generateTypeScript,
|
|
712
|
+
generateTypeScriptOutput,
|
|
713
|
+
hasSystem,
|
|
714
|
+
hasTokens,
|
|
715
|
+
loadSystem,
|
|
716
|
+
loadTokens,
|
|
717
|
+
parseColorToHSLTriplet,
|
|
718
|
+
resolveTokens,
|
|
719
|
+
systemSchema,
|
|
720
|
+
tokenFileSchema,
|
|
721
|
+
tokenIdToCSSVar,
|
|
722
|
+
tokenSchema,
|
|
723
|
+
tokenSourceSchema,
|
|
724
|
+
tokenTypeSchema,
|
|
725
|
+
topologicalSort,
|
|
726
|
+
validateModeCoverage,
|
|
727
|
+
validateTokenLayering,
|
|
728
|
+
validateTokenNaming,
|
|
729
|
+
validateTokenReferences,
|
|
730
|
+
validateTokenUniqueness,
|
|
731
|
+
validateTokens
|
|
732
|
+
};
|