@geonosis/themekit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +226 -0
- package/dist/index.d.ts +456 -0
- package/dist/index.js +541 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// src/emit.ts
|
|
2
|
+
var isEach = (emission) => "each" in emission;
|
|
3
|
+
var isMix = (emission) => "mix" in emission;
|
|
4
|
+
var valueAt = (theme, path) => {
|
|
5
|
+
let current = theme;
|
|
6
|
+
for (const segment of path.split(".")) {
|
|
7
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
8
|
+
current = current[segment];
|
|
9
|
+
}
|
|
10
|
+
return typeof current === "string" && current !== "" ? current : void 0;
|
|
11
|
+
};
|
|
12
|
+
var mapOf = (theme, which) => which === "extra" ? theme.extra ?? {} : theme[which];
|
|
13
|
+
var emitVariables = (theme, emit) => {
|
|
14
|
+
const variables = {};
|
|
15
|
+
for (const emission of emit) {
|
|
16
|
+
if (isEach(emission)) {
|
|
17
|
+
for (const [key, value2] of Object.entries(mapOf(theme, emission.each))) {
|
|
18
|
+
variables[`${emission.prefix}${key}`] = value2;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (isMix(emission)) {
|
|
23
|
+
const { amount, role, space = "srgb", with: into } = emission.mix;
|
|
24
|
+
const from = valueAt(theme, role);
|
|
25
|
+
if (from === void 0) continue;
|
|
26
|
+
variables[emission.variable] = `color-mix(in ${space}, ${from} ${amount}, ${into})`;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const value = valueAt(theme, emission.role);
|
|
30
|
+
if (value !== void 0) variables[emission.variable] = value;
|
|
31
|
+
}
|
|
32
|
+
return variables;
|
|
33
|
+
};
|
|
34
|
+
var kebab = (name) => name.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
35
|
+
var COLOR_ROLES = [
|
|
36
|
+
"primary",
|
|
37
|
+
"onPrimary",
|
|
38
|
+
"secondary",
|
|
39
|
+
"onSecondary",
|
|
40
|
+
"accent",
|
|
41
|
+
"onAccent",
|
|
42
|
+
"background",
|
|
43
|
+
"foreground",
|
|
44
|
+
"surface",
|
|
45
|
+
"onSurface",
|
|
46
|
+
"surfaceInverse",
|
|
47
|
+
"onSurfaceInverse",
|
|
48
|
+
"muted",
|
|
49
|
+
"border",
|
|
50
|
+
"focusRing",
|
|
51
|
+
"link",
|
|
52
|
+
"success",
|
|
53
|
+
"warning",
|
|
54
|
+
"danger"
|
|
55
|
+
];
|
|
56
|
+
var STATUSES = ["info", "success", "warning", "danger"];
|
|
57
|
+
var defaultEmission = (prefix = "--theme-") => [
|
|
58
|
+
...COLOR_ROLES.map((role) => ({ role: `colors.${role}`, variable: `${prefix}${kebab(role)}` })),
|
|
59
|
+
...STATUSES.flatMap((status) => [
|
|
60
|
+
{ role: `status.${status}.surface`, variable: `${prefix}${status}-surface` },
|
|
61
|
+
{ role: `status.${status}.onSurface`, variable: `${prefix}on-${status}-surface` }
|
|
62
|
+
]),
|
|
63
|
+
{ each: "fonts", prefix: `${prefix}font-` },
|
|
64
|
+
{ each: "radii", prefix: `${prefix}radius-` },
|
|
65
|
+
{ each: "shadows", prefix: `${prefix}shadow-` },
|
|
66
|
+
{ each: "extra", prefix }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// src/compile.ts
|
|
70
|
+
var flatten = (value, path, into) => {
|
|
71
|
+
if (typeof value === "string") {
|
|
72
|
+
if (value !== "") into[path] = value;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value !== "object" || value === null) return;
|
|
76
|
+
for (const [key, child] of Object.entries(value)) {
|
|
77
|
+
flatten(child, path === "" ? key : `${path}.${key}`, into);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var TOKEN_GROUPS = ["colors", "status", "fonts", "radii", "shadows", "extra"];
|
|
81
|
+
var plainOf = (theme) => {
|
|
82
|
+
const plain = {};
|
|
83
|
+
for (const group of TOKEN_GROUPS) flatten(theme[group], group, plain);
|
|
84
|
+
return plain;
|
|
85
|
+
};
|
|
86
|
+
var block = (selector, entries) => entries.length === 0 ? "" : `${selector} {
|
|
87
|
+
${entries.map(([name, value]) => ` ${name}: ${value};`).join("\n")}
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
var compile = (theme, options = {}) => {
|
|
91
|
+
const { emit = defaultEmission(), selector = ":root", themeBlock } = options;
|
|
92
|
+
const cssVariables = emitVariables(theme, emit);
|
|
93
|
+
const mapped = themeBlock === void 0 ? [] : Object.keys(cssVariables).flatMap((variable) => {
|
|
94
|
+
const name = themeBlock(variable);
|
|
95
|
+
return name === void 0 ? [] : [[name, `var(${variable})`]];
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
css: block(selector, Object.entries(cssVariables)),
|
|
99
|
+
cssVariables,
|
|
100
|
+
object: theme,
|
|
101
|
+
plain: plainOf(theme),
|
|
102
|
+
themeBlock: block("@theme inline", mapped)
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/contract.ts
|
|
107
|
+
var defineTheme = (theme) => theme;
|
|
108
|
+
|
|
109
|
+
// src/dtcg.ts
|
|
110
|
+
var GEONOSIS_EXTENSION = "com.microcompanies.geonosis";
|
|
111
|
+
var PROFILE = "css-values";
|
|
112
|
+
var GROUP_TYPE = {
|
|
113
|
+
colors: "color",
|
|
114
|
+
fonts: "fontFamily",
|
|
115
|
+
radii: "dimension",
|
|
116
|
+
shadows: "shadow",
|
|
117
|
+
status: "color"
|
|
118
|
+
};
|
|
119
|
+
var GRADIENT = /^(?:repeating-)?(?:linear|radial|conic)-gradient\(/;
|
|
120
|
+
var extraType = (value) => GRADIENT.test(value) ? "gradient" : "color";
|
|
121
|
+
var typeFor = (path, value) => {
|
|
122
|
+
const [group = ""] = path.split(".");
|
|
123
|
+
return group === "extra" ? extraType(value) : GROUP_TYPE[group];
|
|
124
|
+
};
|
|
125
|
+
var tokensOf = (values, path) => {
|
|
126
|
+
const group = {};
|
|
127
|
+
for (const [key, value] of Object.entries(values)) {
|
|
128
|
+
if (value === void 0) continue;
|
|
129
|
+
group[key] = { $type: typeFor(path, value), $value: value };
|
|
130
|
+
}
|
|
131
|
+
return group;
|
|
132
|
+
};
|
|
133
|
+
var statusOf = (theme) => {
|
|
134
|
+
if (theme.status === void 0) return void 0;
|
|
135
|
+
const group = {};
|
|
136
|
+
for (const [status, pair] of Object.entries(theme.status)) {
|
|
137
|
+
if (pair === void 0) continue;
|
|
138
|
+
group[status] = tokensOf(pair, "status");
|
|
139
|
+
}
|
|
140
|
+
return Object.keys(group).length === 0 ? void 0 : group;
|
|
141
|
+
};
|
|
142
|
+
var toDtcg = (theme) => {
|
|
143
|
+
const identity = { id: theme.id, name: theme.name, profile: PROFILE };
|
|
144
|
+
const document = { $extensions: { [GEONOSIS_EXTENSION]: identity } };
|
|
145
|
+
const groups = [
|
|
146
|
+
["colors", theme.colors],
|
|
147
|
+
["fonts", theme.fonts],
|
|
148
|
+
["radii", theme.radii],
|
|
149
|
+
["shadows", theme.shadows],
|
|
150
|
+
["extra", theme.extra]
|
|
151
|
+
];
|
|
152
|
+
for (const [name, values] of groups) {
|
|
153
|
+
if (values === void 0) continue;
|
|
154
|
+
const tokens = tokensOf(values, name);
|
|
155
|
+
if (Object.keys(tokens).length > 0) document[name] = tokens;
|
|
156
|
+
}
|
|
157
|
+
const status = statusOf(theme);
|
|
158
|
+
if (status !== void 0) document["status"] = status;
|
|
159
|
+
return document;
|
|
160
|
+
};
|
|
161
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
+
var valuesOf = (node) => {
|
|
163
|
+
const values = {};
|
|
164
|
+
if (!isRecord(node)) return values;
|
|
165
|
+
for (const [key, token] of Object.entries(node)) {
|
|
166
|
+
if (key.startsWith("$")) continue;
|
|
167
|
+
if (isRecord(token) && typeof token["$value"] === "string") values[key] = token["$value"];
|
|
168
|
+
}
|
|
169
|
+
return values;
|
|
170
|
+
};
|
|
171
|
+
var fromDtcg = (document) => {
|
|
172
|
+
const extensions = isRecord(document["$extensions"]) ? document["$extensions"] : {};
|
|
173
|
+
const identity = isRecord(extensions[GEONOSIS_EXTENSION]) ? extensions[GEONOSIS_EXTENSION] : {};
|
|
174
|
+
const status = {};
|
|
175
|
+
if (isRecord(document["status"])) {
|
|
176
|
+
for (const [name, pair] of Object.entries(document["status"])) {
|
|
177
|
+
const values = valuesOf(pair);
|
|
178
|
+
if (values["surface"] !== void 0 && values["onSurface"] !== void 0) {
|
|
179
|
+
status[name] = { onSurface: values["onSurface"], surface: values["surface"] };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const extra = valuesOf(document["extra"]);
|
|
184
|
+
const theme = {
|
|
185
|
+
colors: valuesOf(document["colors"]),
|
|
186
|
+
fonts: valuesOf(document["fonts"]),
|
|
187
|
+
id: typeof identity["id"] === "string" ? identity["id"] : "",
|
|
188
|
+
name: typeof identity["name"] === "string" ? identity["name"] : "",
|
|
189
|
+
radii: valuesOf(document["radii"]),
|
|
190
|
+
shadows: valuesOf(document["shadows"])
|
|
191
|
+
};
|
|
192
|
+
if (document["extra"] !== void 0 && Object.keys(extra).length > 0) theme.extra = extra;
|
|
193
|
+
if (Object.keys(status).length > 0) theme.status = status;
|
|
194
|
+
return theme;
|
|
195
|
+
};
|
|
196
|
+
var STRICT_UNHOLDABLE = {
|
|
197
|
+
// A colour the draft can place needs a colour space and components. `var()` has neither and
|
|
198
|
+
// `color-mix()` is a computation the file cannot resolve.
|
|
199
|
+
color: /^(?:var|color-mix|env)\(/,
|
|
200
|
+
// `{ value, unit }` takes a number and px or rem, so a calc or a var is out.
|
|
201
|
+
dimension: /^(?:var|calc|clamp|min|max|env)\(|[a-z%]{1,4}$(?<!px|rem)/,
|
|
202
|
+
fontFamily: null,
|
|
203
|
+
// Both are composite types with named sub-fields in the draft, and both are stored here as one
|
|
204
|
+
// whole CSS function or declaration.
|
|
205
|
+
gradient: /.*/,
|
|
206
|
+
shadow: /.*/
|
|
207
|
+
};
|
|
208
|
+
var isBrokenToken = (node) => {
|
|
209
|
+
const keys = Object.keys(node);
|
|
210
|
+
return keys.length > 0 && keys.every((key) => key.startsWith("$"));
|
|
211
|
+
};
|
|
212
|
+
var walkTokens = (node, path, visit) => {
|
|
213
|
+
if (!isRecord(node)) return;
|
|
214
|
+
for (const [key, child] of Object.entries(node)) {
|
|
215
|
+
if (key.startsWith("$")) continue;
|
|
216
|
+
const here = path === "" ? key : `${path}.${key}`;
|
|
217
|
+
if (!isRecord(child)) continue;
|
|
218
|
+
if ("$value" in child || isBrokenToken(child)) visit(here, child);
|
|
219
|
+
else walkTokens(child, here, visit);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var expectedType = (path, token) => {
|
|
223
|
+
const [group = ""] = path.split(".");
|
|
224
|
+
if (group === "extra") {
|
|
225
|
+
return typeof token["$value"] === "string" ? extraType(token["$value"]) : void 0;
|
|
226
|
+
}
|
|
227
|
+
return GROUP_TYPE[group];
|
|
228
|
+
};
|
|
229
|
+
var validateDtcg = (document, options = {}) => {
|
|
230
|
+
const errors = [];
|
|
231
|
+
const lossy = [];
|
|
232
|
+
const extensions = isRecord(document["$extensions"]) ? document["$extensions"] : void 0;
|
|
233
|
+
const identity = extensions !== void 0 && isRecord(extensions[GEONOSIS_EXTENSION]) ? extensions[GEONOSIS_EXTENSION] : void 0;
|
|
234
|
+
if (identity === void 0 || typeof identity["id"] !== "string" || identity["id"] === "") {
|
|
235
|
+
errors.push(
|
|
236
|
+
`the document carries no brand identity: $extensions["${GEONOSIS_EXTENSION}"].id must be a non-empty string`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
walkTokens(document, "", (path, token) => {
|
|
240
|
+
if (typeof token["$value"] !== "string" || token["$value"] === "") {
|
|
241
|
+
errors.push(`${path}: $value must be a non-empty CSS string in the "${PROFILE}" profile`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const expected = expectedType(path, token);
|
|
245
|
+
if (expected === void 0) {
|
|
246
|
+
errors.push(`${path}: is in no group this profile knows`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (token["$type"] !== expected) {
|
|
250
|
+
errors.push(`${path}: $type is ${String(token["$type"])}, but this group holds ${expected}`);
|
|
251
|
+
}
|
|
252
|
+
if (token["$description"] !== void 0) {
|
|
253
|
+
lossy.push(`${path}: $description is not carried by the tokens contract and will be dropped`);
|
|
254
|
+
}
|
|
255
|
+
if (options.strict === true) {
|
|
256
|
+
const unholdable = STRICT_UNHOLDABLE[expected];
|
|
257
|
+
if (unholdable !== null && unholdable.test(token["$value"])) {
|
|
258
|
+
errors.push(
|
|
259
|
+
`${path}: the DTCG draft's ${expected} $value cannot hold "${token["$value"]}" \u2014 it needs the "${PROFILE}" extension (a CSS string $value)`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return { errors, lossy, valid: errors.length === 0 };
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/figma.ts
|
|
268
|
+
var KNOWN_GROUPS = /* @__PURE__ */ new Set(["colors", "extra", "fonts", "radii", "shadows", "status"]);
|
|
269
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
270
|
+
var isAlias = (value) => isRecord2(value) && value["type"] === "VARIABLE_ALIAS" && typeof value["id"] === "string";
|
|
271
|
+
var isColor = (value) => isRecord2(value) && typeof value["r"] === "number" && typeof value["g"] === "number" && typeof value["b"] === "number";
|
|
272
|
+
var channel = (value) => Math.max(0, Math.min(255, Math.round(value * 255)));
|
|
273
|
+
var hex = (value) => channel(value).toString(16).padStart(2, "0");
|
|
274
|
+
var cssColor = (color) => {
|
|
275
|
+
const alpha = color.a ?? 1;
|
|
276
|
+
if (alpha >= 1) return `#${hex(color.r)}${hex(color.g)}${hex(color.b)}`;
|
|
277
|
+
const rounded = Number(alpha.toFixed(4));
|
|
278
|
+
return `rgb(${channel(color.r)} ${channel(color.g)} ${channel(color.b)} / ${rounded})`;
|
|
279
|
+
};
|
|
280
|
+
var slug = (name) => name.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
281
|
+
var extraKey = (name) => slug(name);
|
|
282
|
+
var resolveValue = (variable, modeId, variables, collections) => {
|
|
283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
284
|
+
let current = variable;
|
|
285
|
+
let currentMode = modeId;
|
|
286
|
+
for (; ; ) {
|
|
287
|
+
const collection = collections[current.variableCollectionId];
|
|
288
|
+
const inMode = Object.hasOwn(current.valuesByMode, currentMode) ? currentMode : collection?.defaultModeId ?? "";
|
|
289
|
+
const value = current.valuesByMode[inMode];
|
|
290
|
+
if (value === void 0) return { reason: "has no value in this mode or its default" };
|
|
291
|
+
if (!isAlias(value)) return { value };
|
|
292
|
+
if (seen.has(value.id)) return { reason: "alias cycle \u2014 it points back at itself" };
|
|
293
|
+
seen.add(value.id);
|
|
294
|
+
const next = variables[value.id];
|
|
295
|
+
if (next === void 0) return { reason: `aliases ${value.id}, which is not in this payload` };
|
|
296
|
+
current = next;
|
|
297
|
+
currentMode = inMode;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var tokenOf = (variable, value, floatUnit) => {
|
|
301
|
+
if (variable.resolvedType === "COLOR") {
|
|
302
|
+
if (!isColor(value)) return { reason: "is a COLOR with a value that is not one" };
|
|
303
|
+
return { token: { $type: "color", $value: cssColor(value) }, type: "color" };
|
|
304
|
+
}
|
|
305
|
+
if (variable.resolvedType === "FLOAT") {
|
|
306
|
+
if (typeof value !== "number") return { reason: "is a FLOAT with a value that is not a number" };
|
|
307
|
+
if (floatUnit === void 0) {
|
|
308
|
+
return {
|
|
309
|
+
reason: "FLOAT carries no unit in the Figma payload \u2014 set `floatUnit` to say px or rem, because this importer will not guess one"
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return { token: { $type: "dimension", $value: `${value}${floatUnit}` }, type: "dimension" };
|
|
313
|
+
}
|
|
314
|
+
if (variable.resolvedType === "STRING") {
|
|
315
|
+
if (typeof value !== "string") return { reason: "is a STRING with a value that is not one" };
|
|
316
|
+
return { token: { $type: "fontFamily", $value: value }, type: "fontFamily" };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
reason: `${variable.resolvedType} has no token type \u2014 it is not a design token this contract models`
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
var place = (document, path, token) => {
|
|
323
|
+
const segments = path.split(".");
|
|
324
|
+
let node = document;
|
|
325
|
+
for (const segment of segments.slice(0, -1)) {
|
|
326
|
+
if (!isRecord2(node[segment])) node[segment] = {};
|
|
327
|
+
node = node[segment];
|
|
328
|
+
}
|
|
329
|
+
node[segments.at(-1) ?? path] = token;
|
|
330
|
+
};
|
|
331
|
+
var GROUP_TYPE2 = {
|
|
332
|
+
colors: "color",
|
|
333
|
+
fonts: "fontFamily",
|
|
334
|
+
radii: "dimension",
|
|
335
|
+
shadows: "shadow",
|
|
336
|
+
status: "color"
|
|
337
|
+
};
|
|
338
|
+
var fromFigmaVariables = (payload, options = {}) => {
|
|
339
|
+
const { collection: wanted, floatUnit, map = {} } = options;
|
|
340
|
+
for (const [name, path] of Object.entries(map)) {
|
|
341
|
+
const [group = ""] = path.split(".");
|
|
342
|
+
if (!KNOWN_GROUPS.has(group)) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`the map sends "${name}" to "${path}", and "${group}" is not a group this profile knows (${[...KNOWN_GROUPS].toSorted().join(", ")})`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const meta = isRecord2(payload["meta"]) ? payload["meta"] : void 0;
|
|
349
|
+
const variables = isRecord2(meta?.["variables"]) ? meta["variables"] : void 0;
|
|
350
|
+
const collections = isRecord2(meta?.["variableCollections"]) ? meta["variableCollections"] : void 0;
|
|
351
|
+
if (variables === void 0 || collections === void 0) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
"this is not a Figma variables payload: expected meta.variables and meta.variableCollections, as GET /v1/files/:key/variables/local returns them"
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const candidates = Object.values(collections).filter((one) => (one.modes?.length ?? 0) > 0);
|
|
357
|
+
const themed = wanted === void 0 ? candidates.toSorted((a, b) => b.modes.length - a.modes.length)[0] : candidates.find((one) => one.name === wanted);
|
|
358
|
+
if (themed === void 0) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
wanted === void 0 ? "no variable collection in this payload has any modes, so there is nothing to make a theme from" : `no variable collection is named "${wanted}"`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const unsupported = [];
|
|
364
|
+
const unmapped = /* @__PURE__ */ new Set();
|
|
365
|
+
const themes = [];
|
|
366
|
+
const reported = /* @__PURE__ */ new Set();
|
|
367
|
+
const note = (name, reason) => {
|
|
368
|
+
if (reported.has(name)) return;
|
|
369
|
+
reported.add(name);
|
|
370
|
+
unsupported.push(`${name}: ${reason}`);
|
|
371
|
+
};
|
|
372
|
+
for (const mode of themed.modes) {
|
|
373
|
+
const tokens = {
|
|
374
|
+
$extensions: {
|
|
375
|
+
[GEONOSIS_EXTENSION]: { id: slug(mode.name), name: mode.name, profile: PROFILE }
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
for (const variable of Object.values(variables)) {
|
|
379
|
+
const resolved = resolveValue(variable, mode.modeId, variables, collections);
|
|
380
|
+
if (resolved.value === void 0) {
|
|
381
|
+
note(variable.name, resolved.reason ?? "could not be resolved");
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const built = tokenOf(variable, resolved.value, floatUnit);
|
|
385
|
+
if ("reason" in built) {
|
|
386
|
+
note(variable.name, built.reason);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const path = map[variable.name];
|
|
390
|
+
if (path === void 0) {
|
|
391
|
+
unmapped.add(variable.name);
|
|
392
|
+
place(tokens, `extra.${extraKey(variable.name)}`, built.token);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const [group = ""] = path.split(".");
|
|
396
|
+
const expected = GROUP_TYPE2[group];
|
|
397
|
+
if (expected !== void 0 && expected !== built.type) {
|
|
398
|
+
note(
|
|
399
|
+
variable.name,
|
|
400
|
+
`is a ${variable.resolvedType} mapped to "${path}", where a ${expected} goes`
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
place(tokens, path, built.token);
|
|
405
|
+
}
|
|
406
|
+
themes.push({ id: slug(mode.name), name: mode.name, tokens });
|
|
407
|
+
}
|
|
408
|
+
return { themes, unmapped: [...unmapped].toSorted(), unsupported };
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/load.ts
|
|
412
|
+
import { existsSync, readFileSync } from "fs";
|
|
413
|
+
import { basename, dirname, join, resolve } from "path";
|
|
414
|
+
var REQUIRED_COLOR_ROLES = [
|
|
415
|
+
"background",
|
|
416
|
+
"border",
|
|
417
|
+
"danger",
|
|
418
|
+
"focusRing",
|
|
419
|
+
"foreground",
|
|
420
|
+
"muted",
|
|
421
|
+
"onPrimary",
|
|
422
|
+
"onSecondary",
|
|
423
|
+
"onSurface",
|
|
424
|
+
"primary",
|
|
425
|
+
"secondary",
|
|
426
|
+
"success",
|
|
427
|
+
"surface",
|
|
428
|
+
"warning"
|
|
429
|
+
];
|
|
430
|
+
var MISSING_ROLES = (theme) => {
|
|
431
|
+
const colors = theme.colors;
|
|
432
|
+
return REQUIRED_COLOR_ROLES.filter((role) => {
|
|
433
|
+
const value = colors[role];
|
|
434
|
+
return typeof value !== "string" || value === "";
|
|
435
|
+
}).map((role) => `colors.${role}`);
|
|
436
|
+
};
|
|
437
|
+
var readJson = (dir, file) => {
|
|
438
|
+
const path = join(dir, file);
|
|
439
|
+
let raw;
|
|
440
|
+
try {
|
|
441
|
+
raw = readFileSync(path, "utf8");
|
|
442
|
+
} catch (error) {
|
|
443
|
+
throw new Error(`${path} could not be read`, { cause: error });
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
return JSON.parse(raw);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new Error(`${path} is not valid JSON: ${error.message}`, { cause: error });
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
452
|
+
var manifestOf = (dir) => {
|
|
453
|
+
if (!existsSync(join(dir, "themekit.json"))) {
|
|
454
|
+
throw new Error(`${dir} is not a themekit: it has no themekit.json`);
|
|
455
|
+
}
|
|
456
|
+
const raw = readJson(dir, "themekit.json");
|
|
457
|
+
if (!isRecord3(raw)) throw new Error(`${join(dir, "themekit.json")} is not an object`);
|
|
458
|
+
if (typeof raw["name"] !== "string" || raw["name"] === "") {
|
|
459
|
+
throw new Error(`${join(dir, "themekit.json")} has no name`);
|
|
460
|
+
}
|
|
461
|
+
if (typeof raw["version"] !== "string" || raw["version"] === "") {
|
|
462
|
+
throw new Error(`${join(dir, "themekit.json")} has no version \u2014 a themekit is consumed by one`);
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
complete: raw["complete"] === true,
|
|
466
|
+
...typeof raw["description"] === "string" ? { description: raw["description"] } : {},
|
|
467
|
+
...typeof raw["emit"] === "string" ? { emit: raw["emit"] } : {},
|
|
468
|
+
name: raw["name"],
|
|
469
|
+
version: raw["version"]
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
var emitOf = (dir, pointer) => {
|
|
473
|
+
const path = pointer === void 0 ? join(dir, "emit.json") : resolve(dir, pointer);
|
|
474
|
+
if (!existsSync(path)) {
|
|
475
|
+
if (pointer === void 0) return defaultEmission();
|
|
476
|
+
throw new Error(
|
|
477
|
+
`${join(dir, "themekit.json")} points its emission map at ${path}, which is not there`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const raw = readJson(dirname(path), basename(path));
|
|
481
|
+
if (!Array.isArray(raw)) throw new Error(`${path} must be an array of emissions`);
|
|
482
|
+
return raw;
|
|
483
|
+
};
|
|
484
|
+
var brandOf = (dir, fallback) => {
|
|
485
|
+
if (!existsSync(join(dir, "brand.json"))) return { id: fallback, name: fallback };
|
|
486
|
+
const raw = readJson(dir, "brand.json");
|
|
487
|
+
if (!isRecord3(raw)) throw new Error(`${join(dir, "brand.json")} is not an object`);
|
|
488
|
+
return {
|
|
489
|
+
...raw,
|
|
490
|
+
id: typeof raw["id"] === "string" ? raw["id"] : fallback,
|
|
491
|
+
name: typeof raw["name"] === "string" ? raw["name"] : fallback
|
|
492
|
+
};
|
|
493
|
+
};
|
|
494
|
+
var loadThemekit = (dir, options = {}) => {
|
|
495
|
+
const manifest = manifestOf(dir);
|
|
496
|
+
if (options.pinned !== void 0 && options.pinned !== manifest.version) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${dir} is version ${manifest.version}, but ${options.pinned} was pinned. A themekit is consumed by version: move the pin deliberately, or check out the version you asked for.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const tokens = readJson(dir, "theme.tokens.json");
|
|
502
|
+
if (!isRecord3(tokens)) throw new Error(`${join(dir, "theme.tokens.json")} is not an object`);
|
|
503
|
+
const report = validateDtcg(tokens);
|
|
504
|
+
if (!report.valid) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`${join(dir, "theme.tokens.json")} is not this profile:
|
|
507
|
+
${report.errors.join("\n ")}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const theme = fromDtcg(tokens);
|
|
511
|
+
const missingRoles = MISSING_ROLES(theme);
|
|
512
|
+
if (manifest.complete && missingRoles.length > 0) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`${join(dir, "themekit.json")} says complete, but the theme fills no ${missingRoles.join(", ")}. Set complete to false, or fill the roles.`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
brand: brandOf(dir, manifest.name),
|
|
519
|
+
emit: emitOf(dir, manifest.emit),
|
|
520
|
+
manifest,
|
|
521
|
+
missingRoles,
|
|
522
|
+
theme,
|
|
523
|
+
tokens
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
export {
|
|
527
|
+
GEONOSIS_EXTENSION,
|
|
528
|
+
MISSING_ROLES,
|
|
529
|
+
PROFILE,
|
|
530
|
+
REQUIRED_COLOR_ROLES,
|
|
531
|
+
compile,
|
|
532
|
+
defaultEmission,
|
|
533
|
+
defineTheme,
|
|
534
|
+
emitVariables,
|
|
535
|
+
fromDtcg,
|
|
536
|
+
fromFigmaVariables,
|
|
537
|
+
loadThemekit,
|
|
538
|
+
toDtcg,
|
|
539
|
+
validateDtcg,
|
|
540
|
+
valueAt
|
|
541
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geonosis/themekit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The tokens contract, a variable map that is data, and DTCG in and out — themes as files, never components.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"design-tokens",
|
|
7
|
+
"dtcg",
|
|
8
|
+
"theme",
|
|
9
|
+
"css-variables",
|
|
10
|
+
"tailwind",
|
|
11
|
+
"figma",
|
|
12
|
+
"monorepo"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/microcompanies/geonosis/tree/main/packages/themekit",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/microcompanies/geonosis.git",
|
|
18
|
+
"directory": "packages/themekit"
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|