@scattered-light/loomiere 0.4.0 → 0.5.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 +3 -1
- package/bin/loomiere.mjs +3 -1
- package/build/build-tokens.mjs +180 -35
- package/exporters/design-system.mjs +7 -0
- package/exporters/figma.mjs +545 -478
- package/exporters/index.mjs +4 -0
- package/figma/README.md +24 -8
- package/figma/library/README.md +92 -0
- package/figma/library/check/known-differences.md +20 -0
- package/figma/library/check/mojave.json +246 -0
- package/figma/library/kit/base.js +89 -0
- package/figma/library/kit/components.js +95 -0
- package/figma/library/kit/docs.js +37 -0
- package/figma/library/kit/fingerprint.js +143 -0
- package/figma/library/pages/02-palettes.js +40 -0
- package/figma/library/pages/03-color.js +126 -0
- package/figma/library/pages/04-type.js +56 -0
- package/figma/library/pages/05-scale.js +58 -0
- package/figma/library/pages/06-size.js +111 -0
- package/figma/library/pages/07-elevation-focus-motion.js +108 -0
- package/figma/library/pages/08-themes.js +36 -0
- package/figma/library/pages/09-fields/01-input.js +55 -0
- package/figma/library/pages/09-fields/02-checks.js +82 -0
- package/figma/library/pages/09-fields/03-field.js +32 -0
- package/figma/library/pages/09-fields/04-more-fields.js +144 -0
- package/figma/library/pages/09-fields/05-labels.js +14 -0
- package/figma/library/pages/09-fields/06-doc.js +75 -0
- package/figma/library/pages/10-buttons/01-buttons.js +97 -0
- package/figma/library/pages/10-buttons/02-doc.js +55 -0
- package/figma/library/pages/11-popups/01-popups.js +166 -0
- package/figma/library/pages/11-popups/02-more-popups.js +90 -0
- package/figma/library/pages/11-popups/03-labels.js +14 -0
- package/figma/library/pages/11-popups/04-doc.js +89 -0
- package/figma/library/pages/12-overlays/01-overlays.js +135 -0
- package/figma/library/pages/12-overlays/02-labels.js +10 -0
- package/figma/library/pages/12-overlays/03-doc.js +80 -0
- package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
- package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
- package/figma/library/pages/13-disclosure/03-labels.js +16 -0
- package/figma/library/pages/13-disclosure/04-doc.js +91 -0
- package/figma/library/pages/14-display/01-display.js +165 -0
- package/figma/library/pages/14-display/02-more-display.js +55 -0
- package/figma/library/pages/14-display/03-labels.js +14 -0
- package/figma/library/pages/14-display/04-doc.js +96 -0
- package/figma/library/pages/15-cover.js +87 -0
- package/figma/library/run-order.json +21 -0
- package/figma/library/tools/compare.mjs +11 -0
- package/figma/library/tools/diff-snippet.js +15 -0
- package/figma/library/tools/kit-script.mjs +21 -0
- package/figma/library/tools/run.mjs +27 -0
- package/figma/library/tools/runs.mjs +34 -0
- package/generator/brand.schema.json +83 -0
- package/generator/generate.mjs +26 -6
- package/generator/looks.mjs +62 -0
- package/lib/contrast.mjs +24 -6
- package/lib/figma.mjs +58 -23
- package/package.json +2 -1
- package/tokens/README.md +26 -0
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Every value is a `--sl-` variable, so your own CSS, and component packages such
|
|
|
54
54
|
|
|
55
55
|
| File | For |
|
|
56
56
|
| --- | --- |
|
|
57
|
-
| `tokens.css` | Every token as a `--sl-` variable, inside `@layer sl.tokens` so your own styles win. Dark mode and high contrast follow the system, or `data-theme` on `<html>`: `light`, `dark`, `light-high-contrast`, `dark-high-contrast`, or a color vision theme (`deuteranopia`, `protanopia`, `tritanopia`, `achromatopsia`). Also holds the `.sl-pair-<name>` classes, `data-color` scopes, `data-status` (info, success, warning, error) and `data-emphasis` levels. |
|
|
57
|
+
| `tokens.css` | Every token as a `--sl-` variable, inside `@layer sl.tokens` so your own styles win. Dark mode and high contrast follow the system, or `data-theme` on `<html>`: `light`, `dark`, `light-high-contrast`, `dark-high-contrast`, or a color vision theme (`deuteranopia`, `protanopia`, `tritanopia`, `achromatopsia`). A brand with looks also has `data-look`. Also holds the `.sl-pair-<name>` classes, `data-color` scopes, `data-status` (info, success, warning, error) and `data-emphasis` levels. |
|
|
58
58
|
| `utilities.css` | `.text-style-*`, `.icon-*`, `.focus-ring`, `.hide-on-*`. |
|
|
59
59
|
| `tokens.js`, `tokens.d.ts` | Resolved values for JavaScript. |
|
|
60
60
|
| `tokens.json`, `pairs.json` | The token catalog and every palette color against every other, for docs and agents. |
|
|
@@ -63,4 +63,6 @@ Every value is a `--sl-` variable, so your own CSS, and component packages such
|
|
|
63
63
|
|
|
64
64
|
Figma: three ways to send the theme into a Figma file (a Claude chat, `loomiere figma push`, or the Loomiere plugin for designers without the repo) in [figma/README.md](figma/README.md).
|
|
65
65
|
|
|
66
|
+
Several looks for one brand (`data-look`, Brand modes in Figma): [tokens/README.md#looks](tokens/README.md#looks).
|
|
67
|
+
|
|
66
68
|
How the layers work, the brand file's fields, roles, concepts and pairs: [tokens/README.md](tokens/README.md).
|
package/bin/loomiere.mjs
CHANGED
|
@@ -40,6 +40,7 @@ Options
|
|
|
40
40
|
--file <url> figma: the Figma file (or "figma": { "file" } in the config)
|
|
41
41
|
--via <way> figma push: claude, api, plugin or scripts (default: the first that works)
|
|
42
42
|
--since <file> figma push --via scripts: the snapshot, to send only what changed
|
|
43
|
+
--plain figma push --via scripts: write the data as plain JSON, not compressed
|
|
43
44
|
--remove figma push: delete what the build no longer has
|
|
44
45
|
--dry-run figma push: report what would change, change nothing
|
|
45
46
|
--help Show this help
|
|
@@ -56,6 +57,7 @@ const { values: flags, positionals } = parseArgs({
|
|
|
56
57
|
file: { type: 'string' },
|
|
57
58
|
via: { type: 'string' },
|
|
58
59
|
since: { type: 'string' },
|
|
60
|
+
plain: { type: 'boolean' },
|
|
59
61
|
remove: { type: 'boolean' },
|
|
60
62
|
'dry-run': { type: 'boolean' },
|
|
61
63
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -121,7 +123,7 @@ async function main() {
|
|
|
121
123
|
const sub = rest[0] ?? 'push';
|
|
122
124
|
// Push what the brand says now, the way a designer would expect.
|
|
123
125
|
if (sub === 'push' && flags.via !== 'scripts' && !(await build(config))) return void (process.exitCode = 1);
|
|
124
|
-
const ok = await figmaCommand(config, sub, { via: flags.via, file: flags.file, since: flags.since, remove: flags.remove, dryRun: flags['dry-run'], port });
|
|
126
|
+
const ok = await figmaCommand(config, sub, { via: flags.via, file: flags.file, since: flags.since, plain: flags.plain, remove: flags.remove, dryRun: flags['dry-run'], port });
|
|
125
127
|
if (ok === false) process.exitCode = 1;
|
|
126
128
|
return;
|
|
127
129
|
}
|
package/build/build-tokens.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Tokens can carry mode values in $extensions["sl.modes"] (dark, contrast-more,
|
|
11
11
|
* dark-contrast-more, reduced-motion); the CSS output emits each mode as its own block.
|
|
12
|
-
* Color vision themes (vision.* primitives) become [data-theme='<theme>'] blocks
|
|
12
|
+
* Color vision themes (vision.* primitives) become [data-theme='<theme>'] blocks, and the
|
|
13
|
+
* brand's other looks (generator/looks.mjs) [data-look='<name>'] blocks.
|
|
13
14
|
*
|
|
14
15
|
* Run it through the command line (loomiere build), which reads loomiere.config.json.
|
|
15
16
|
*/
|
|
@@ -242,6 +243,81 @@ function pairBlocks(dictionary) {
|
|
|
242
243
|
return `/* Brand pairs: generated from color.pairs in the brand file. */\n${blocks.join('\n\n')}`;
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Looks (generator/looks.mjs). Each look is built on its own, and what it changes becomes
|
|
248
|
+
* [data-look='<name>'] blocks: the variables whose value differs from the brand's own look, then
|
|
249
|
+
* each mode's values where they differ, so a look's dark mode is its own. Every look has the
|
|
250
|
+
* same tokens, so the class rules (roles, pairs, color vision) are the brand's, shared; a look
|
|
251
|
+
* whose rules come out different is an error.
|
|
252
|
+
*/
|
|
253
|
+
let lookParts = [];
|
|
254
|
+
|
|
255
|
+
const declarationsIn = (text) =>
|
|
256
|
+
new Map(
|
|
257
|
+
text
|
|
258
|
+
.split('\n')
|
|
259
|
+
.map((line) => line.match(/^\s*(--[\w-]+):\s*(.*?);(?:\s*\/\*\*.*\*\/)?$/))
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.map((m) => [m[1], m[2]]),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
/** What a look's CSS is made of: its :root values, its mode values, and its class rules. */
|
|
265
|
+
function partsOf(dictionary, options) {
|
|
266
|
+
const root = declarationsIn(formattedVariables({ format: 'css', dictionary, outputReferences: options.outputReferences, usesDtcg: true }));
|
|
267
|
+
const modeValues = {};
|
|
268
|
+
for (const [mode] of modes(visionsIn(dictionary))) {
|
|
269
|
+
modeValues[mode] = Object.fromEntries(
|
|
270
|
+
dictionary.allTokens.filter((token) => modeValue(token, mode) !== undefined).map((token) => [varName(token.path), refsToVars(modeValue(token, mode))]),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return { root: Object.fromEntries(root), modes: modeValues, rules: { roles: roleBlocks(dictionary), pairs: pairBlocks(dictionary), vision: visionBlocks(dictionary) } };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
StyleDictionary.registerFormat({
|
|
277
|
+
name: 'json/sl-look-parts',
|
|
278
|
+
format: ({ dictionary, options }) => `${JSON.stringify(partsOf(dictionary, options))}\n`,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/** Adds [data-look='<name>'] to each selector in a list (commas inside :not() stay). */
|
|
282
|
+
const inLook = (selectors, name) =>
|
|
283
|
+
selectors
|
|
284
|
+
.split(/,(?![^(]*\))/)
|
|
285
|
+
.map((s) => `${s.trim()}[data-look='${name}']`)
|
|
286
|
+
.join(', ');
|
|
287
|
+
|
|
288
|
+
/** [rootBlocks, modeBlocks] for every look against the brand's own parts. */
|
|
289
|
+
function lookBlocks(dictionary, base) {
|
|
290
|
+
const roots = [];
|
|
291
|
+
const modeRules = [];
|
|
292
|
+
for (const { name, label, parts } of lookParts) {
|
|
293
|
+
for (const [rule, text] of Object.entries(parts.rules)) {
|
|
294
|
+
// Color vision blocks list the data colors by name, so a look with its own names has its own.
|
|
295
|
+
if (rule === 'vision') continue;
|
|
296
|
+
if (text !== base.rules[rule]) throw new Error(`Look "${name}" changes the ${rule} rules, not only values. A look can change colors, fonts and sizes, but it has to keep the brand's palettes, pairs, statuses and emphasis levels.`);
|
|
297
|
+
}
|
|
298
|
+
const changed = Object.entries(parts.root).filter(([v, value]) => base.root[v] !== value);
|
|
299
|
+
if (changed.length) roots.push(`/* Look: ${label} (data-look='${name}'). */\n[data-look='${name}'] {\n${indent(changed.map(([v, value]) => `${v}: ${value};`).join('\n'), 2)}\n}`);
|
|
300
|
+
if (parts.rules.vision !== base.rules.vision) {
|
|
301
|
+
roots.push(parts.rules.vision.replace(/^(\[data-theme='[\w-]+'\]) \{/gm, (_, sel) => `${inLook(sel, name)} {`));
|
|
302
|
+
}
|
|
303
|
+
// A mode value the look's own root value or mode value would lose to: the brand's mode
|
|
304
|
+
// blocks come after the look's root block, so restate the look's value in each mode.
|
|
305
|
+
for (const [mode, targets] of modes(visionsIn(dictionary))) {
|
|
306
|
+
const lines = [];
|
|
307
|
+
for (const v of Object.keys(parts.root)) {
|
|
308
|
+
const own = parts.modes[mode][v] ?? (base.modes[mode][v] !== undefined ? parts.root[v] : undefined);
|
|
309
|
+
if (own !== undefined && own !== base.modes[mode][v]) lines.push(`${v}: ${own};`);
|
|
310
|
+
}
|
|
311
|
+
if (!lines.length) continue;
|
|
312
|
+
for (const [media, selector] of targets) {
|
|
313
|
+
const rule = `${inLook(selector, name)} {\n${indent(lines.join('\n'), 2)}\n}`;
|
|
314
|
+
modeRules.push(media ? `${media} {\n${indent(rule, 2)}\n}` : rule);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return [roots.join('\n\n'), modeRules.join('\n\n')];
|
|
319
|
+
}
|
|
320
|
+
|
|
245
321
|
StyleDictionary.registerTransform({
|
|
246
322
|
name: 'name/sl-path',
|
|
247
323
|
type: 'name',
|
|
@@ -260,7 +336,9 @@ StyleDictionary.registerFormat({
|
|
|
260
336
|
usesDtcg: true,
|
|
261
337
|
});
|
|
262
338
|
const root = `:root {\n${variables}\n}`;
|
|
263
|
-
|
|
339
|
+
// Look roots come before the mode and color vision blocks, so those still win in their themes.
|
|
340
|
+
const [lookRoots, lookModes] = lookParts.length ? lookBlocks(dictionary, partsOf(dictionary, options)) : ['', ''];
|
|
341
|
+
const body = [root, lookRoots, modeBlocks(dictionary), visionBlocks(dictionary), lookModes, roleBlocks(dictionary), pairBlocks(dictionary)].filter(Boolean).join('\n\n');
|
|
264
342
|
return `${header}@layer ${prefix}.tokens {\n${indent(body, 2)}\n}\n`;
|
|
265
343
|
},
|
|
266
344
|
});
|
|
@@ -379,6 +457,24 @@ export function expandTextStyles(tokens) {
|
|
|
379
457
|
|
|
380
458
|
StyleDictionary.registerPreprocessor({ name: 'sl/text-styles', preprocessor: (tokens) => expandTextStyles(tokens) });
|
|
381
459
|
|
|
460
|
+
/**
|
|
461
|
+
* `tokens` in the brand file or a look: { "button.radius": "{radius.control}" } replaces a
|
|
462
|
+
* semantic or component token's value, for what the brand inputs can't say. Set per build.
|
|
463
|
+
*/
|
|
464
|
+
let picks = {};
|
|
465
|
+
StyleDictionary.registerPreprocessor({
|
|
466
|
+
name: 'sl/picks',
|
|
467
|
+
preprocessor: (tokens) => {
|
|
468
|
+
for (const [tokenPath, value] of Object.entries(picks)) {
|
|
469
|
+
const token = tokenPath.split('.').reduce((node, key) => node?.[key], tokens);
|
|
470
|
+
if (token?.$value === undefined) throw new Error(`tokens."${tokenPath}" in the brand file is not a token`);
|
|
471
|
+
token.$extensions = { ...token.$extensions, 'sl.pick': { was: token.$value } };
|
|
472
|
+
token.$value = value;
|
|
473
|
+
}
|
|
474
|
+
return tokens;
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
382
478
|
/**
|
|
383
479
|
* Classes made from tokens, so they can't drift from them. All go in <out>/utilities.css:
|
|
384
480
|
* .text-style-<name> every text style in tokens/semantic/typography.json
|
|
@@ -457,51 +553,75 @@ function utilitiesCss() {
|
|
|
457
553
|
return `/* Utility classes for "${brandName}". Generated by loomiere build; do not edit. */\n\n${body}\n`;
|
|
458
554
|
}
|
|
459
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Builds one theme. `config` comes from resolveConfig() (lib/config.mjs):
|
|
558
|
+
* { brandFile, outDir, generatedDir, prefix, exports }.
|
|
559
|
+
*/
|
|
560
|
+
/** Style Dictionary over this package's tokens and one look's generated ones. */
|
|
561
|
+
const sourcesFor = (generatedDir) => [
|
|
562
|
+
// Layer order, so tokens.css reads foundation → primitive → semantic → component.
|
|
563
|
+
path.join(root, 'tokens', 'foundation', '**', '*.json'),
|
|
564
|
+
path.join(generatedDir, 'tokens', 'primitive', '**', '*.json'),
|
|
565
|
+
path.join(root, 'tokens', 'semantic', '**', '*.json'),
|
|
566
|
+
path.join(generatedDir, 'tokens', 'semantic', '**', '*.json'),
|
|
567
|
+
path.join(root, 'tokens', 'component', '**', '*.json'),
|
|
568
|
+
];
|
|
569
|
+
const dictionary = (generatedDir, platforms) =>
|
|
570
|
+
new StyleDictionary({ usesDtcg: true, preprocessors: ['sl/picks', 'sl/text-styles'], log: { verbosity: 'default', warnings: 'warn' }, source: sourcesFor(generatedDir), platforms });
|
|
571
|
+
const CSS_TRANSFORMS = ['name/sl-path', 'fontFamily/css', 'cubicBezier/css'];
|
|
572
|
+
|
|
460
573
|
/**
|
|
461
574
|
* Builds one theme. `config` comes from resolveConfig() (lib/config.mjs):
|
|
462
575
|
* { brandFile, outDir, generatedDir, prefix, exports }.
|
|
463
576
|
*/
|
|
464
577
|
export async function buildTokens(config) {
|
|
465
578
|
prefix = config.prefix;
|
|
466
|
-
const { brand, applied, files } = generate(config.brandFile, config.generatedDir);
|
|
579
|
+
const { brand, applied, files, looks } = generate(config.brandFile, config.generatedDir);
|
|
467
580
|
brandName = brand.name;
|
|
468
|
-
roles = resolveRoles(brand);
|
|
469
581
|
const out = config.outDir + path.sep;
|
|
470
582
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
path.join(root, 'tokens', 'foundation', '**', '*.json'),
|
|
478
|
-
path.join(config.generatedDir, 'tokens', 'primitive', '**', '*.json'),
|
|
479
|
-
path.join(root, 'tokens', 'semantic', '**', '*.json'),
|
|
480
|
-
path.join(config.generatedDir, 'tokens', 'semantic', '**', '*.json'),
|
|
481
|
-
path.join(root, 'tokens', 'component', '**', '*.json'),
|
|
482
|
-
],
|
|
483
|
-
platforms: {
|
|
583
|
+
// Every other look first: its parts for tokens.css, and its catalog for the exports.
|
|
584
|
+
lookParts = [];
|
|
585
|
+
for (const look of looks.filter((l) => !l.base)) {
|
|
586
|
+
roles = resolveRoles(look.brand);
|
|
587
|
+
picks = look.brand.tokens ?? {};
|
|
588
|
+
await dictionary(look.dir, {
|
|
484
589
|
css: {
|
|
485
590
|
prefix,
|
|
486
|
-
transforms:
|
|
487
|
-
buildPath:
|
|
591
|
+
transforms: CSS_TRANSFORMS,
|
|
592
|
+
buildPath: look.dir + path.sep,
|
|
488
593
|
files: [
|
|
489
|
-
{
|
|
490
|
-
destination: 'tokens.css',
|
|
491
|
-
format: 'css/sl-layered',
|
|
492
|
-
options: { outputReferences: true, showFileHeader: true },
|
|
493
|
-
},
|
|
594
|
+
{ destination: 'parts.json', format: 'json/sl-look-parts', options: { outputReferences: true } },
|
|
494
595
|
{ destination: 'tokens.json', format: 'json/sl-catalog' },
|
|
495
596
|
],
|
|
496
597
|
},
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
598
|
+
}).buildAllPlatforms();
|
|
599
|
+
lookParts.push({ ...look, parts: JSON.parse(fs.readFileSync(path.join(look.dir, 'parts.json'), 'utf8')) });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
roles = resolveRoles(brand);
|
|
603
|
+
picks = brand.tokens ?? {};
|
|
604
|
+
const sd = dictionary(config.generatedDir, {
|
|
605
|
+
css: {
|
|
606
|
+
prefix,
|
|
607
|
+
transforms: CSS_TRANSFORMS,
|
|
608
|
+
buildPath: out,
|
|
609
|
+
files: [
|
|
610
|
+
{
|
|
611
|
+
destination: 'tokens.css',
|
|
612
|
+
format: 'css/sl-layered',
|
|
613
|
+
options: { outputReferences: true, showFileHeader: true },
|
|
614
|
+
},
|
|
615
|
+
{ destination: 'tokens.json', format: 'json/sl-catalog' },
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
js: {
|
|
619
|
+
transforms: ['name/camel', 'fontFamily/css', 'cubicBezier/css'],
|
|
620
|
+
buildPath: out,
|
|
621
|
+
files: [
|
|
622
|
+
{ destination: 'tokens.js', format: 'javascript/sl-values' },
|
|
623
|
+
{ destination: 'tokens.d.ts', format: 'typescript/sl-values' },
|
|
624
|
+
],
|
|
505
625
|
},
|
|
506
626
|
});
|
|
507
627
|
|
|
@@ -522,8 +642,33 @@ export async function buildTokens(config) {
|
|
|
522
642
|
path.join(config.outDir, 'pairs.json'),
|
|
523
643
|
`${JSON.stringify({ brand: brandName, levels: Object.fromEntries(LEVELS), colors: Object.fromEntries(colors.map((c) => [c.path, c.hex])), pairs: compact })}\n`,
|
|
524
644
|
);
|
|
525
|
-
//
|
|
645
|
+
// The looks in tokens.json: each one's name and what it changes, by CSS variable.
|
|
526
646
|
const catalog = JSON.parse(fs.readFileSync(path.join(config.outDir, 'tokens.json'), 'utf8'));
|
|
527
|
-
const
|
|
528
|
-
|
|
647
|
+
const lookCatalogs = looks.map((look) => ({
|
|
648
|
+
...look,
|
|
649
|
+
catalog: look.base ? catalog : JSON.parse(fs.readFileSync(path.join(look.dir, 'tokens.json'), 'utf8')),
|
|
650
|
+
roles: resolveRoles(look.brand),
|
|
651
|
+
}));
|
|
652
|
+
if (looks.length > 1) {
|
|
653
|
+
const own = new Map(catalog.tokens.map((t) => [t.name, t]));
|
|
654
|
+
catalog.looks = lookCatalogs.map(({ name, label, description, base, catalog: c }) => ({
|
|
655
|
+
name,
|
|
656
|
+
label,
|
|
657
|
+
...(description && { description }),
|
|
658
|
+
...(base
|
|
659
|
+
? { base: true }
|
|
660
|
+
: {
|
|
661
|
+
css: `[data-look='${name}']`,
|
|
662
|
+
changes: Object.fromEntries(
|
|
663
|
+
c.tokens
|
|
664
|
+
.filter((t) => JSON.stringify(t.value) !== JSON.stringify(own.get(t.name)?.value) || JSON.stringify(t.modes) !== JSON.stringify(own.get(t.name)?.modes))
|
|
665
|
+
.map((t) => [t.name, t.modes ? { value: t.value, modes: t.modes } : t.value]),
|
|
666
|
+
),
|
|
667
|
+
}),
|
|
668
|
+
}));
|
|
669
|
+
fs.writeFileSync(path.join(config.outDir, 'tokens.json'), `${JSON.stringify(catalog, null, 2)}\n`);
|
|
670
|
+
}
|
|
671
|
+
// Other frameworks: DTCG JSON plus one adapter per framework (exporters/).
|
|
672
|
+
const written = exportAll(catalog, resolveRoles(brand), path.join(config.outDir, 'exports'), config.exports, { themeDir: config.outDir, brand, looks: lookCatalogs });
|
|
673
|
+
return { brand, applied, looks, exports: written };
|
|
529
674
|
}
|
|
@@ -318,6 +318,13 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
|
|
|
318
318
|
`Themes: \`data-theme="light|dark|light-high-contrast|dark-high-contrast"\` on the root, or the system setting (including \`prefers-contrast: more\`) when none is set. Color vision themes are themes of their own, each a light theme designed from this brand: \`data-theme="deuteranopia|protanopia|tritanopia|achromatopsia"\`. Never hand-pick dark colors; the tokens already flip.`,
|
|
319
319
|
'',
|
|
320
320
|
);
|
|
321
|
+
if (theme.looks?.length > 1) {
|
|
322
|
+
const others = theme.looks.filter((l) => !l.base);
|
|
323
|
+
out.push(
|
|
324
|
+
`Looks: this brand can look ${theme.looks.length} ways. With no \`data-look\` it is ${theme.looks[0].label}; ${others.map((l) => `\`data-look="${l.name}"\` on the root is ${l.label}${l.description ? ` (${l.description.replace(/\.$/, '')})` : ''}`).join('; ')}. A look changes token values, never names, so design with the tokens and every look follows. The values listed here are ${theme.looks[0].label}'s; \`bundle.css\` holds every look.`,
|
|
325
|
+
'',
|
|
326
|
+
);
|
|
327
|
+
}
|
|
321
328
|
if (componentNames.length) {
|
|
322
329
|
const has = (n) => componentNames.includes(n);
|
|
323
330
|
const pick = (list) => list.filter(([n]) => has(n)).map(([n, use]) => `| \`${n}\` | ${use} |`);
|