@sproutsocial/seeds-theme 0.1.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/README.md +259 -0
- package/dist/css-tokens.json +134 -0
- package/dist/legacy-dark.css +437 -0
- package/dist/legacy-light.css +437 -0
- package/dist/legacy.css +659 -0
- package/dist/schema.json +11798 -0
- package/dist/shadcn-dark.css +2 -0
- package/dist/shadcn-light.css +2 -0
- package/dist/shadcn.css +2 -0
- package/dist/shadcn.registry.json +77 -0
- package/dist/source/shadcn-dark.tokens.json +169 -0
- package/dist/source/shadcn-light.tokens.json +169 -0
- package/dist/source/theme-dark.tokens.json +1945 -0
- package/dist/source/theme-light.tokens.json +1945 -0
- package/dist/styled-components/index.cjs +1648 -0
- package/dist/styled-components/index.d.ts +3654 -0
- package/dist/styled-components/index.js +1641 -0
- package/dist/styled-components/index.ts +5285 -0
- package/dist/tailwind.css +40 -0
- package/dist/theme-dark.css +388 -0
- package/dist/theme-dark.d.ts +420 -0
- package/dist/theme-dark.js +401 -0
- package/dist/theme-dark.json +712 -0
- package/dist/theme-light.css +388 -0
- package/dist/theme-light.d.ts +420 -0
- package/dist/theme-light.js +400 -0
- package/dist/theme-light.json +712 -0
- package/dist/theme.css +611 -0
- package/mode-authoring.md +54 -0
- package/package.json +96 -0
- package/src/cli.js +42 -0
- package/src/compiler.d.ts +9 -0
- package/src/compiler.js +533 -0
- package/src/config.d.ts +39 -0
- package/src/config.js +280 -0
- package/src/contract.js +351 -0
- package/src/extension-schema.js +88 -0
- package/src/extension.d.ts +18 -0
- package/src/extension.js +81 -0
- package/src/index.d.ts +90 -0
- package/src/index.js +9 -0
- package/src/primitives.js +98 -0
package/src/compiler.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import StyleDictionary from "style-dictionary";
|
|
4
|
+
import { primitiveRoots } from "./primitives.js";
|
|
5
|
+
import { formattedVariables } from "style-dictionary/utils";
|
|
6
|
+
import {
|
|
7
|
+
assertThemeSelectors,
|
|
8
|
+
selectorsForTheme,
|
|
9
|
+
themeModes,
|
|
10
|
+
} from "./contract.js";
|
|
11
|
+
|
|
12
|
+
const LEGACY_ABBREVIATIONS = {
|
|
13
|
+
background: "bg",
|
|
14
|
+
foreground: "fg",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SHADCN_COLOR_SLOTS = [
|
|
18
|
+
"background",
|
|
19
|
+
"foreground",
|
|
20
|
+
"card",
|
|
21
|
+
"card-foreground",
|
|
22
|
+
"popover",
|
|
23
|
+
"popover-foreground",
|
|
24
|
+
"primary",
|
|
25
|
+
"primary-foreground",
|
|
26
|
+
"secondary",
|
|
27
|
+
"secondary-foreground",
|
|
28
|
+
"muted",
|
|
29
|
+
"muted-foreground",
|
|
30
|
+
"accent",
|
|
31
|
+
"accent-foreground",
|
|
32
|
+
"destructive",
|
|
33
|
+
"destructive-foreground",
|
|
34
|
+
"border",
|
|
35
|
+
"input",
|
|
36
|
+
"ring",
|
|
37
|
+
"chart-1",
|
|
38
|
+
"chart-2",
|
|
39
|
+
"chart-3",
|
|
40
|
+
"chart-4",
|
|
41
|
+
"chart-5",
|
|
42
|
+
"sidebar",
|
|
43
|
+
"sidebar-foreground",
|
|
44
|
+
"sidebar-primary",
|
|
45
|
+
"sidebar-primary-foreground",
|
|
46
|
+
"sidebar-accent",
|
|
47
|
+
"sidebar-accent-foreground",
|
|
48
|
+
"sidebar-border",
|
|
49
|
+
"sidebar-ring",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const isObject = (value) =>
|
|
53
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
54
|
+
|
|
55
|
+
const isToken = (value) => isObject(value) && "$value" in value;
|
|
56
|
+
const isPrimitiveToken = (token) => primitiveRoots.has(token.path[0]);
|
|
57
|
+
const isShadcnToken = (token) => token.path[0] === "shadcn";
|
|
58
|
+
const isLegacyToken = (token) => token.path[0] === "legacy";
|
|
59
|
+
const isThemeToken = (token) =>
|
|
60
|
+
!isPrimitiveToken(token) && !isShadcnToken(token) && !isLegacyToken(token);
|
|
61
|
+
|
|
62
|
+
function kebabCase(value) {
|
|
63
|
+
return value
|
|
64
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
65
|
+
.replace(/[_\s]+/g, "-")
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cssVariableName(tokenPath, node) {
|
|
70
|
+
const publicName = node?.$extensions?.["com.sproutsocial.theme"]?.cssName;
|
|
71
|
+
if (publicName !== undefined) {
|
|
72
|
+
if (!/^--[a-z][a-z0-9-]*$/.test(publicName))
|
|
73
|
+
throw new Error(`Invalid public CSS name: ${publicName}`);
|
|
74
|
+
return publicName;
|
|
75
|
+
}
|
|
76
|
+
if (tokenPath[0] === "legacy") {
|
|
77
|
+
return `--${tokenPath
|
|
78
|
+
.slice(1)
|
|
79
|
+
.map((segment) => LEGACY_ABBREVIATIONS[segment] ?? segment)
|
|
80
|
+
.join("-")}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tokenPath[0] === "css") return `--${tokenPath.slice(1).join("-")}`;
|
|
84
|
+
|
|
85
|
+
if (tokenPath[0] === "shadcn") {
|
|
86
|
+
const pathWithoutNamespace =
|
|
87
|
+
tokenPath[1] === "color" ? tokenPath.slice(2) : tokenPath.slice(1);
|
|
88
|
+
return `--${pathWithoutNamespace.join("-")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `--${tokenPath.map(kebabCase).join("-")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertNoCssVariableCollisions(tokens, label) {
|
|
95
|
+
const variables = new Map();
|
|
96
|
+
|
|
97
|
+
function visit(node, tokenPath = []) {
|
|
98
|
+
if (isToken(node)) {
|
|
99
|
+
const variable = cssVariableName(tokenPath, node);
|
|
100
|
+
const previous = variables.get(variable);
|
|
101
|
+
if (
|
|
102
|
+
previous &&
|
|
103
|
+
JSON.stringify(previous.value) !== JSON.stringify(node.$value)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${label}: CSS variable ${variable} is generated with conflicting values by both ${
|
|
107
|
+
previous.path
|
|
108
|
+
} and ${tokenPath.join(".")}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
variables.set(variable, {
|
|
112
|
+
path: tokenPath.join("."),
|
|
113
|
+
value: node.$value,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!isObject(node)) return;
|
|
119
|
+
for (const [key, value] of Object.entries(node)) {
|
|
120
|
+
if (!key.startsWith("$")) visit(value, [...tokenPath, key]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
visit(tokens);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The legacy layer reproduces the selectors `seeds-react-theme` shipped, because
|
|
128
|
+
// keeping existing consumers working is its entire purpose. These are historical
|
|
129
|
+
// facts rather than configuration, so a host does not get to choose them.
|
|
130
|
+
//
|
|
131
|
+
// `data-theme` meant color mode in that contract and identifies a brand in the
|
|
132
|
+
// current one, so the two vocabularies cannot share a selector. An app still on
|
|
133
|
+
// `data-theme="dark"` matches no current-contract block at all, which is why the
|
|
134
|
+
// legacy blocks keep a bare `:root` base. `[data-color-mode="dark"]` is included
|
|
135
|
+
// so an app can adopt the new mode mechanism before it migrates off these names.
|
|
136
|
+
const LEGACY_SELECTORS = Object.freeze({
|
|
137
|
+
base: ":root",
|
|
138
|
+
dark: '[data-theme="dark"], .dark, .lights-out, [data-color-mode="dark"]',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
function tailwindTheme(shadcn, darkVariant) {
|
|
142
|
+
const colorMappings = SHADCN_COLOR_SLOTS.filter((slot) => slot in shadcn).map(
|
|
143
|
+
(slot) => ` --color-${slot}: var(--${slot});`
|
|
144
|
+
);
|
|
145
|
+
const radiusMappings =
|
|
146
|
+
"radius" in shadcn
|
|
147
|
+
? [
|
|
148
|
+
" --radius-sm: calc(var(--radius) - 4px);",
|
|
149
|
+
" --radius-md: calc(var(--radius) - 2px);",
|
|
150
|
+
" --radius-lg: var(--radius);",
|
|
151
|
+
" --radius-xl: calc(var(--radius) + 4px);",
|
|
152
|
+
]
|
|
153
|
+
: [];
|
|
154
|
+
|
|
155
|
+
return `@custom-variant dark (${darkVariant});
|
|
156
|
+
|
|
157
|
+
@theme inline {
|
|
158
|
+
${[...colorMappings, ...radiusMappings].join("\n")}
|
|
159
|
+
}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function selectRoots(tokens, predicate) {
|
|
164
|
+
return Object.fromEntries(
|
|
165
|
+
Object.entries(tokens)
|
|
166
|
+
.filter(([key]) => !key.startsWith("$"))
|
|
167
|
+
.filter(([key]) => predicate({ path: [key] }))
|
|
168
|
+
.map(([key, value]) => [key, structuredClone(value)])
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function linesFor(dictionary, predicate, excludedNames = new Set()) {
|
|
173
|
+
const allTokens = dictionary.allTokens.filter(
|
|
174
|
+
(token) => predicate(token) && !excludedNames.has(`--${token.name}`)
|
|
175
|
+
);
|
|
176
|
+
if (allTokens.length === 0) return [];
|
|
177
|
+
|
|
178
|
+
return formattedVariables({
|
|
179
|
+
format: "css",
|
|
180
|
+
dictionary: { ...dictionary, allTokens },
|
|
181
|
+
outputReferences: true,
|
|
182
|
+
usesDtcg: true,
|
|
183
|
+
formatting: { indentation: " " },
|
|
184
|
+
}).split("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function variableName(line) {
|
|
188
|
+
return line.match(/^\s*(--[^:]+):/)?.[1];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function changedLines(lightLines, darkLines) {
|
|
192
|
+
const lightByName = new Map(
|
|
193
|
+
lightLines.map((line) => [variableName(line), line.trim()])
|
|
194
|
+
);
|
|
195
|
+
return darkLines.filter(
|
|
196
|
+
(line) => lightByName.get(variableName(line)) !== line.trim()
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function cssBlock(selector, lines) {
|
|
201
|
+
if (lines.length === 0) return "";
|
|
202
|
+
return `${selector} {\n${lines.join("\n")}\n}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function darkCssBlock(selectors, lines) {
|
|
206
|
+
const block = cssBlock(selectors.dark, lines);
|
|
207
|
+
if (!block) return block;
|
|
208
|
+
return selectors.darkMedia
|
|
209
|
+
? `@media ${selectors.darkMedia} {\n${block}}\n`
|
|
210
|
+
: block;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Compare declarations with references intact so inherited aliases remain live. */
|
|
214
|
+
export async function compileExtensionCss(product, baseline, selectors) {
|
|
215
|
+
const layers = {};
|
|
216
|
+
for (const [name, theme] of Object.entries({ product, baseline })) {
|
|
217
|
+
layers[name] = {};
|
|
218
|
+
for (const mode of themeModes) {
|
|
219
|
+
assertNoCssVariableCollisions(theme.modes[mode], `${theme.name}/${mode}`);
|
|
220
|
+
const dictionary = await styleDictionary(
|
|
221
|
+
theme,
|
|
222
|
+
mode,
|
|
223
|
+
"."
|
|
224
|
+
).getPlatformTokens("css");
|
|
225
|
+
layers[name][mode] = linesFor(dictionary, isThemeToken);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const light = changedLines(layers.baseline.light, layers.product.light);
|
|
229
|
+
// Product base outranks Seeds dark. Reset any light override that differs from
|
|
230
|
+
// the desired dark value, including when that value matches Seeds again.
|
|
231
|
+
const dark = changedLines(
|
|
232
|
+
[...layers.baseline.dark, ...light],
|
|
233
|
+
layers.product.dark
|
|
234
|
+
);
|
|
235
|
+
return `/* Generated extension of Seeds. Do not edit. */\n${cssBlock(
|
|
236
|
+
selectors.base,
|
|
237
|
+
light
|
|
238
|
+
)}${darkCssBlock(selectors, dark)}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function withImports(imports, blocks) {
|
|
242
|
+
const importsCss = imports
|
|
243
|
+
.map((filename) => `@import "${filename}";`)
|
|
244
|
+
.join("\n");
|
|
245
|
+
const blocksCss = blocks.filter(Boolean).join("\n");
|
|
246
|
+
return `${importsCss}${importsCss && blocksCss ? "\n\n" : ""}${blocksCss}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function shadcnValues(dictionary) {
|
|
250
|
+
const result = {};
|
|
251
|
+
for (const token of dictionary.allTokens.filter(
|
|
252
|
+
(token) => isShadcnToken(token) || token.path[0] === "css"
|
|
253
|
+
)) {
|
|
254
|
+
const pathWithoutNamespace =
|
|
255
|
+
token.path[1] === "color" ? token.path.slice(2) : token.path.slice(1);
|
|
256
|
+
const slot = pathWithoutNamespace.join("-");
|
|
257
|
+
if (SHADCN_COLOR_SLOTS.includes(slot) || slot === "radius") {
|
|
258
|
+
result[slot] = token.$value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function styleDictionary(theme, mode, outputDirectory) {
|
|
265
|
+
const jsonFiles = [
|
|
266
|
+
{
|
|
267
|
+
destination: `theme-${mode}.json`,
|
|
268
|
+
format: "json/nested",
|
|
269
|
+
filter: isThemeToken,
|
|
270
|
+
options: { showFileHeader: false },
|
|
271
|
+
},
|
|
272
|
+
];
|
|
273
|
+
const javascriptFiles = [
|
|
274
|
+
{
|
|
275
|
+
destination: `theme-${mode}.js`,
|
|
276
|
+
format: "javascript/es6",
|
|
277
|
+
filter: isThemeToken,
|
|
278
|
+
options: { showFileHeader: false },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
destination: `theme-${mode}.d.ts`,
|
|
282
|
+
format: "typescript/es6-declarations",
|
|
283
|
+
filter: isThemeToken,
|
|
284
|
+
options: { showFileHeader: false },
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
return new StyleDictionary({
|
|
289
|
+
tokens: theme.modes[mode],
|
|
290
|
+
log: { verbosity: "silent" },
|
|
291
|
+
platforms: {
|
|
292
|
+
css: {
|
|
293
|
+
transforms: [
|
|
294
|
+
// Avoid color/css first converting wide-gamut inputs to sRGB.
|
|
295
|
+
...StyleDictionary.hooks.transformGroups.css.filter(
|
|
296
|
+
(name) => name !== "color/css"
|
|
297
|
+
),
|
|
298
|
+
"seeds/theme/css-name",
|
|
299
|
+
"seeds/theme/css-value",
|
|
300
|
+
"seeds/theme/css-color",
|
|
301
|
+
"seeds/theme/legacy-value",
|
|
302
|
+
"seeds/theme/primitive-value",
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
json: {
|
|
306
|
+
transformGroup: "js",
|
|
307
|
+
buildPath: `${outputDirectory}${path.sep}`,
|
|
308
|
+
files: jsonFiles,
|
|
309
|
+
},
|
|
310
|
+
javascript: {
|
|
311
|
+
transformGroup: "js",
|
|
312
|
+
buildPath: `${outputDirectory}${path.sep}`,
|
|
313
|
+
files: javascriptFiles,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
StyleDictionary.registerTransform({
|
|
320
|
+
name: "seeds/theme/css-name",
|
|
321
|
+
type: "name",
|
|
322
|
+
transform: (token) => cssVariableName(token.path, token.original).slice(2),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
StyleDictionary.registerTransform({
|
|
326
|
+
name: "seeds/theme/css-value",
|
|
327
|
+
type: "value",
|
|
328
|
+
filter: (token) =>
|
|
329
|
+
Boolean(token.original.$extensions?.["com.sproutsocial.theme"]?.cssName),
|
|
330
|
+
transform: (token) => token.original.$value,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Style Dictionary owns color conversion. Keep runtime CSS expressions and
|
|
334
|
+
// references intact; those are evaluated by CSS after theme selection.
|
|
335
|
+
StyleDictionary.registerTransform({
|
|
336
|
+
name: "seeds/theme/css-color",
|
|
337
|
+
type: "value",
|
|
338
|
+
filter: (token) =>
|
|
339
|
+
token.$type === "color" &&
|
|
340
|
+
!isLegacyToken(token) &&
|
|
341
|
+
!isPrimitiveToken(token),
|
|
342
|
+
transform: (token, platform, options) => {
|
|
343
|
+
const value = token.original.$value;
|
|
344
|
+
// Already-OKLCH source values retain their authored precision.
|
|
345
|
+
if (typeof value === "string" && /^oklch\(/i.test(value)) return value;
|
|
346
|
+
if (
|
|
347
|
+
typeof value === "string" &&
|
|
348
|
+
/\{|\b(?:var|color-mix|light-dark|(?:repeating-)?(?:linear|radial|conic)-gradient)\s*\(|\(\s*from\b|^currentcolor$/i.test(
|
|
349
|
+
value
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return value;
|
|
353
|
+
return StyleDictionary.hooks.transforms["color/oklch"].transform(
|
|
354
|
+
{ ...token, $value: value },
|
|
355
|
+
platform,
|
|
356
|
+
options
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Migration data preserves the exact strings emitted by seeds-react-theme,
|
|
362
|
+
// including gradients, var() references, casing, and transparent.
|
|
363
|
+
StyleDictionary.registerTransform({
|
|
364
|
+
name: "seeds/theme/legacy-value",
|
|
365
|
+
type: "value",
|
|
366
|
+
filter: isLegacyToken,
|
|
367
|
+
transform: (token) => token.original.$value,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Primitive values remain an input so semantic references resolve and the
|
|
371
|
+
// migration layer can avoid redeclaring names owned by seeds-color.
|
|
372
|
+
StyleDictionary.registerTransform({
|
|
373
|
+
name: "seeds/theme/primitive-value",
|
|
374
|
+
type: "value",
|
|
375
|
+
filter: isPrimitiveToken,
|
|
376
|
+
transform: (token) => token.original.$value,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
export async function buildThemeArtifacts(
|
|
380
|
+
theme,
|
|
381
|
+
{ outputDirectory, selectors = selectorsForTheme(theme.name) }
|
|
382
|
+
) {
|
|
383
|
+
if (!outputDirectory) {
|
|
384
|
+
throw new TypeError("buildThemeArtifacts requires an outputDirectory.");
|
|
385
|
+
}
|
|
386
|
+
assertThemeSelectors(selectors);
|
|
387
|
+
|
|
388
|
+
await fs.mkdir(path.join(outputDirectory, "source"), { recursive: true });
|
|
389
|
+
const dictionaries = {};
|
|
390
|
+
|
|
391
|
+
for (const mode of themeModes) {
|
|
392
|
+
assertNoCssVariableCollisions(theme.modes[mode], `${theme.name}/${mode}`);
|
|
393
|
+
const dictionary = styleDictionary(theme, mode, outputDirectory);
|
|
394
|
+
await dictionary.buildPlatform("json");
|
|
395
|
+
await dictionary.buildPlatform("javascript");
|
|
396
|
+
dictionaries[mode] = await dictionary.getPlatformTokens("css");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const primitiveLight = linesFor(dictionaries.light, isPrimitiveToken);
|
|
400
|
+
const primitiveDark = linesFor(dictionaries.dark, isPrimitiveToken);
|
|
401
|
+
if (changedLines(primitiveLight, primitiveDark).length > 0) {
|
|
402
|
+
throw new Error("Primitive tokens must not change between color modes.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Legacy CSS has always supplied its border/spacing aliases itself. Only the
|
|
406
|
+
// historical color palette is external to that compatibility entry point.
|
|
407
|
+
const primitiveNames = new Set(
|
|
408
|
+
linesFor(dictionaries.light, (token) => token.path[0] === "color").map(
|
|
409
|
+
variableName
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
const layers = Object.fromEntries(
|
|
413
|
+
themeModes.map((mode) => {
|
|
414
|
+
const dictionary = dictionaries[mode];
|
|
415
|
+
return [
|
|
416
|
+
mode,
|
|
417
|
+
{
|
|
418
|
+
theme: linesFor(dictionary, isThemeToken),
|
|
419
|
+
shadcn: linesFor(dictionary, isShadcnToken),
|
|
420
|
+
legacy: linesFor(dictionary, isLegacyToken, primitiveNames),
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const files = {
|
|
427
|
+
"theme-light.css": cssBlock(selectors.base, layers.light.theme),
|
|
428
|
+
"theme-dark.css": darkCssBlock(selectors, layers.dark.theme),
|
|
429
|
+
"theme.css": withImports(
|
|
430
|
+
[],
|
|
431
|
+
[
|
|
432
|
+
cssBlock(selectors.base, layers.light.theme),
|
|
433
|
+
darkCssBlock(
|
|
434
|
+
selectors,
|
|
435
|
+
changedLines(layers.light.theme, layers.dark.theme)
|
|
436
|
+
),
|
|
437
|
+
]
|
|
438
|
+
),
|
|
439
|
+
"shadcn-light.css": withImports(
|
|
440
|
+
["./theme-light.css", "./tailwind.css"],
|
|
441
|
+
[cssBlock(selectors.base, layers.light.shadcn)]
|
|
442
|
+
),
|
|
443
|
+
"shadcn-dark.css": withImports(
|
|
444
|
+
["./theme-dark.css", "./tailwind.css"],
|
|
445
|
+
[darkCssBlock(selectors, layers.dark.shadcn)]
|
|
446
|
+
),
|
|
447
|
+
"shadcn.css": withImports(
|
|
448
|
+
["./theme.css", "./tailwind.css"],
|
|
449
|
+
[
|
|
450
|
+
cssBlock(selectors.base, layers.light.shadcn),
|
|
451
|
+
darkCssBlock(
|
|
452
|
+
selectors,
|
|
453
|
+
changedLines(layers.light.shadcn, layers.dark.shadcn)
|
|
454
|
+
),
|
|
455
|
+
]
|
|
456
|
+
),
|
|
457
|
+
"legacy-light.css": withImports(
|
|
458
|
+
["./theme-light.css"],
|
|
459
|
+
[cssBlock(LEGACY_SELECTORS.base, layers.light.legacy)]
|
|
460
|
+
),
|
|
461
|
+
"legacy-dark.css": withImports(
|
|
462
|
+
["./theme-dark.css"],
|
|
463
|
+
[cssBlock(LEGACY_SELECTORS.dark, layers.dark.legacy)]
|
|
464
|
+
),
|
|
465
|
+
"legacy.css": withImports(
|
|
466
|
+
["./theme.css"],
|
|
467
|
+
[
|
|
468
|
+
cssBlock(LEGACY_SELECTORS.base, layers.light.legacy),
|
|
469
|
+
cssBlock(
|
|
470
|
+
LEGACY_SELECTORS.dark,
|
|
471
|
+
changedLines(layers.light.legacy, layers.dark.legacy)
|
|
472
|
+
),
|
|
473
|
+
]
|
|
474
|
+
),
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const rawLayers = {
|
|
478
|
+
"theme-light": selectRoots(theme.modes.light, isThemeToken),
|
|
479
|
+
"theme-dark": selectRoots(theme.modes.dark, isThemeToken),
|
|
480
|
+
"shadcn-light": selectRoots(
|
|
481
|
+
theme.modes.light,
|
|
482
|
+
(token) => isShadcnToken(token) || token.path[0] === "css"
|
|
483
|
+
),
|
|
484
|
+
"shadcn-dark": selectRoots(
|
|
485
|
+
theme.modes.dark,
|
|
486
|
+
(token) => isShadcnToken(token) || token.path[0] === "css"
|
|
487
|
+
),
|
|
488
|
+
};
|
|
489
|
+
const shadcn = Object.fromEntries(
|
|
490
|
+
themeModes.map((mode) => [mode, shadcnValues(dictionaries[mode])])
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
await Promise.all([
|
|
494
|
+
...Object.entries(files).map(([filename, content]) =>
|
|
495
|
+
fs.writeFile(path.join(outputDirectory, filename), content)
|
|
496
|
+
),
|
|
497
|
+
...Object.entries(rawLayers).map(([name, tokens]) =>
|
|
498
|
+
fs.writeFile(
|
|
499
|
+
path.join(outputDirectory, "source", `${name}.tokens.json`),
|
|
500
|
+
`${JSON.stringify(tokens, null, 2)}\n`
|
|
501
|
+
)
|
|
502
|
+
),
|
|
503
|
+
fs.writeFile(
|
|
504
|
+
path.join(outputDirectory, "tailwind.css"),
|
|
505
|
+
tailwindTheme(shadcn.light, selectors.darkVariant)
|
|
506
|
+
),
|
|
507
|
+
fs.writeFile(
|
|
508
|
+
path.join(outputDirectory, "shadcn.registry.json"),
|
|
509
|
+
`${JSON.stringify(
|
|
510
|
+
{
|
|
511
|
+
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
|
512
|
+
name: theme.name,
|
|
513
|
+
type: "registry:theme",
|
|
514
|
+
cssVars: shadcn,
|
|
515
|
+
},
|
|
516
|
+
null,
|
|
517
|
+
2
|
|
518
|
+
)}\n`
|
|
519
|
+
),
|
|
520
|
+
]);
|
|
521
|
+
|
|
522
|
+
return [
|
|
523
|
+
...Object.keys(files),
|
|
524
|
+
...Object.keys(rawLayers).map((name) => `source/${name}.tokens.json`),
|
|
525
|
+
...themeModes.flatMap((mode) => [
|
|
526
|
+
`theme-${mode}.json`,
|
|
527
|
+
`theme-${mode}.js`,
|
|
528
|
+
`theme-${mode}.d.ts`,
|
|
529
|
+
]),
|
|
530
|
+
"tailwind.css",
|
|
531
|
+
"shadcn.registry.json",
|
|
532
|
+
].sort();
|
|
533
|
+
}
|
package/src/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DtcgTokenTree, ThemeSelectors } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export type ProductThemeTokenSource = DtcgTokenTree | string;
|
|
4
|
+
|
|
5
|
+
export type ProductThemeInput = {
|
|
6
|
+
name: string;
|
|
7
|
+
extensionRoots?: string[];
|
|
8
|
+
common?: ProductThemeTokenSource;
|
|
9
|
+
light: ProductThemeTokenSource;
|
|
10
|
+
dark: ProductThemeTokenSource;
|
|
11
|
+
outputDirectory?: string;
|
|
12
|
+
selectors?: ThemeSelectors;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ProductThemeConfig = {
|
|
16
|
+
kind: "product-theme-config";
|
|
17
|
+
name: string;
|
|
18
|
+
extensionRoots: string[];
|
|
19
|
+
sources: {
|
|
20
|
+
common: ProductThemeTokenSource;
|
|
21
|
+
light: ProductThemeTokenSource;
|
|
22
|
+
dark: ProductThemeTokenSource;
|
|
23
|
+
};
|
|
24
|
+
outputDirectory: string;
|
|
25
|
+
selectors?: ThemeSelectors;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function defineProductTheme(
|
|
29
|
+
input: ProductThemeInput
|
|
30
|
+
): ProductThemeConfig;
|
|
31
|
+
|
|
32
|
+
export function buildProductTheme(
|
|
33
|
+
config: ProductThemeConfig | ProductThemeInput,
|
|
34
|
+
options?: {
|
|
35
|
+
configDirectory?: string;
|
|
36
|
+
baseTokenDirectory?: string;
|
|
37
|
+
primitiveTokens?: DtcgTokenTree;
|
|
38
|
+
}
|
|
39
|
+
): Promise<string>;
|