@scattered-light/loomiere 0.3.0 → 0.4.1
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 +6 -1
- package/bin/loomiere.mjs +24 -0
- package/build/build-tokens.mjs +93 -49
- package/exporters/README.md +1 -1
- package/exporters/ag-grid-settings.mjs +7 -7
- package/exporters/ag-grid.mjs +6 -6
- package/exporters/chakra.mjs +43 -43
- package/exporters/component-styles.mjs +4 -4
- package/exporters/design-system.mjs +17 -12
- package/exporters/figma.mjs +774 -68
- package/exporters/mui.mjs +40 -40
- package/exporters/shadcn.mjs +5 -13
- package/exporters/tailwind.mjs +14 -5
- package/figma/README.md +136 -0
- package/figma/build-plugin.mjs +25 -0
- package/figma/engine.js +730 -0
- 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/figma/plugin/code.js +746 -0
- package/figma/plugin/manifest.json +14 -0
- package/figma/plugin/ui.html +237 -0
- package/figma/plugin-main.js +14 -0
- package/generator/brand.mjs +16 -14
- package/generator/brand.schema.json +60 -0
- package/generator/generate.mjs +2 -2
- package/generator/pairs.mjs +6 -1
- package/generator/roles.mjs +55 -15
- package/generator/vision.mjs +7 -2
- package/lib/config.mjs +2 -0
- package/lib/contrast.mjs +8 -3
- package/lib/figma.mjs +338 -0
- package/lib/studio.mjs +4 -0
- package/lib/tool-server.mjs +7 -4
- package/package.json +5 -1
- package/tokens/README.md +10 -5
- package/tokens/component/avatar.json +41 -6
- package/tokens/semantic/color.json +1 -1
- package/tokens/semantic/elevation.json +8 -43
- package/tokens/semantic/typography.json +61 -94
- package/tools/brand-builder/builder.js +33 -4
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ Every value is a `--sl-` variable, so your own CSS, and component packages such
|
|
|
29
29
|
| `loomiere check` | Checks contrast of the theme already built. |
|
|
30
30
|
| `loomiere studio` | Opens the brand studio at `http://localhost:4322`. `--export page.html` writes a standalone copy (needs esbuild). |
|
|
31
31
|
| `loomiere lab` | Opens the pair lab at `http://localhost:4321` to pick brand pairs (a background, its text and an accent). |
|
|
32
|
+
| `loomiere figma push` | Builds, then pushes variables and text styles to a Figma file. `--dry-run` reports only. See [figma/README.md](figma/README.md). |
|
|
33
|
+
| `loomiere figma serve` | Serves the build for the Loomiere Figma plugin's Pull from project. |
|
|
32
34
|
|
|
33
35
|
`--brand <file>`, `--out <dir>` and `--prefix <name>` override the config for one run. `--port <n>` moves studio or lab.
|
|
34
36
|
|
|
@@ -46,16 +48,19 @@ Every value is a `--sl-` variable, so your own CSS, and component packages such
|
|
|
46
48
|
- `out`: where the theme goes. Default `theme`.
|
|
47
49
|
- `prefix`: the CSS variable prefix. Default `sl`, which is what loomiere-base-ui reads.
|
|
48
50
|
- `exports`: which framework files to write. Default all of them.
|
|
51
|
+
- `figma`: `{ "file": "<Figma URL>" }`, the file `loomiere figma push` writes to.
|
|
49
52
|
|
|
50
53
|
## What a build writes
|
|
51
54
|
|
|
52
55
|
| File | For |
|
|
53
56
|
| --- | --- |
|
|
54
|
-
| `tokens.css` | Every token as a `--sl-` variable, inside `@layer sl.tokens` so your own styles win. Dark mode
|
|
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. |
|
|
55
58
|
| `utilities.css` | `.text-style-*`, `.icon-*`, `.focus-ring`, `.hide-on-*`. |
|
|
56
59
|
| `tokens.js`, `tokens.d.ts` | Resolved values for JavaScript. |
|
|
57
60
|
| `tokens.json`, `pairs.json` | The token catalog and every palette color against every other, for docs and agents. |
|
|
58
61
|
| `exports/` | DTCG JSON, Figma variables, Tailwind v4, shadcn/ui, MUI, Chakra v3, AG Grid 31–32, 33–35 and 36+, a Claude Design System and `AGENTS.md` (the brand book for agents). See [exporters/README.md](exporters/README.md). |
|
|
59
62
|
| `generated/` | The brand's primitive tokens. Rebuilt every time; no need to commit. |
|
|
60
63
|
|
|
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
|
+
|
|
61
66
|
How the layers work, the brand file's fields, roles, concepts and pairs: [tokens/README.md](tokens/README.md).
|
package/bin/loomiere.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* loomiere check check contrast of the theme already built
|
|
8
8
|
* loomiere studio edit the brand with live previews (--export page.html for a static copy)
|
|
9
9
|
* loomiere lab pick brand pairs
|
|
10
|
+
* loomiere figma push build, then push the theme to Figma (lib/figma.mjs)
|
|
11
|
+
* loomiere figma serve serve the build for the Loomiere Figma plugin
|
|
10
12
|
*
|
|
11
13
|
* Flags: --brand <file> --out <dir> --prefix <name> --port <n>
|
|
12
14
|
*/
|
|
@@ -26,6 +28,8 @@ Usage
|
|
|
26
28
|
loomiere check Check contrast of the built theme
|
|
27
29
|
loomiere studio Edit the brand with live previews
|
|
28
30
|
loomiere lab Pick brand pairs (background + text)
|
|
31
|
+
loomiere figma push Build, then push variables and text styles to Figma
|
|
32
|
+
loomiere figma serve Serve the build for the Loomiere Figma plugin
|
|
29
33
|
|
|
30
34
|
Options
|
|
31
35
|
--brand <file> Brand file (default: the only one in brand/)
|
|
@@ -33,6 +37,11 @@ Options
|
|
|
33
37
|
--prefix <name> CSS variable prefix (default: sl)
|
|
34
38
|
--port <n> Port for studio and lab
|
|
35
39
|
--export <file> studio only: write one self-contained page instead of serving
|
|
40
|
+
--file <url> figma: the Figma file (or "figma": { "file" } in the config)
|
|
41
|
+
--via <way> figma push: claude, api, plugin or scripts (default: the first that works)
|
|
42
|
+
--since <file> figma push --via scripts: the snapshot, to send only what changed
|
|
43
|
+
--remove figma push: delete what the build no longer has
|
|
44
|
+
--dry-run figma push: report what would change, change nothing
|
|
36
45
|
--help Show this help
|
|
37
46
|
`;
|
|
38
47
|
|
|
@@ -44,6 +53,11 @@ const { values: flags, positionals } = parseArgs({
|
|
|
44
53
|
prefix: { type: 'string' },
|
|
45
54
|
port: { type: 'string' },
|
|
46
55
|
export: { type: 'string' },
|
|
56
|
+
file: { type: 'string' },
|
|
57
|
+
via: { type: 'string' },
|
|
58
|
+
since: { type: 'string' },
|
|
59
|
+
remove: { type: 'boolean' },
|
|
60
|
+
'dry-run': { type: 'boolean' },
|
|
47
61
|
help: { type: 'boolean', short: 'h' },
|
|
48
62
|
},
|
|
49
63
|
});
|
|
@@ -101,6 +115,16 @@ async function main() {
|
|
|
101
115
|
const { lab } = await import('../lib/lab.mjs');
|
|
102
116
|
return lab(resolveConfig(configFlags), { port });
|
|
103
117
|
}
|
|
118
|
+
case 'figma': {
|
|
119
|
+
const { figmaCommand } = await import('../lib/figma.mjs');
|
|
120
|
+
const config = resolveConfig(configFlags);
|
|
121
|
+
const sub = rest[0] ?? 'push';
|
|
122
|
+
// Push what the brand says now, the way a designer would expect.
|
|
123
|
+
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 });
|
|
125
|
+
if (ok === false) process.exitCode = 1;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
104
128
|
default:
|
|
105
129
|
throw new Error(`Unknown command "${command}". Run loomiere --help.`);
|
|
106
130
|
}
|
package/build/build-tokens.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
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-
|
|
12
|
+
* Color vision themes (vision.* primitives) become [data-theme='<theme>'] blocks.
|
|
13
13
|
*
|
|
14
14
|
* Run it through the command line (loomiere build), which reads loomiere.config.json.
|
|
15
15
|
*/
|
|
@@ -22,28 +22,45 @@ import { fileHeader, formattedVariables } from 'style-dictionary/utils';
|
|
|
22
22
|
import { exportAll } from '../exporters/index.mjs';
|
|
23
23
|
import { generate } from '../generator/generate.mjs';
|
|
24
24
|
import { LEVELS, PAIR_ROLES, pairTable, paletteColors } from '../generator/pairs.mjs';
|
|
25
|
-
import { resolveRoles } from '../generator/roles.mjs';
|
|
25
|
+
import { resolveRoles, STYLES } from '../generator/roles.mjs';
|
|
26
26
|
|
|
27
27
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
-
// Set by buildTokens() from the config; the Style Dictionary formats below read them.
|
|
28
|
+
// Set by buildTokens() from the config and brand; the Style Dictionary formats below read them.
|
|
29
29
|
let prefix = 'sl';
|
|
30
30
|
let brandName = '';
|
|
31
|
+
let roles;
|
|
31
32
|
|
|
32
33
|
const LAYERS = ['foundation', 'primitive', 'semantic', 'component'];
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Themes, picked with data-theme on <html>: light, dark, light-high-contrast,
|
|
37
|
+
* dark-high-contrast, and one per color vision theme (deuteranopia…). With no data-theme the
|
|
38
|
+
* system decides light or dark and high contrast. Color vision themes stand on their own:
|
|
39
|
+
* they are light, and neither the system's dark mode nor its contrast setting changes them.
|
|
40
|
+
*/
|
|
41
|
+
const theme = (names) => names.map((n) => `[data-theme='${n}']`).join(', ');
|
|
42
|
+
|
|
34
43
|
/** Selectors for each mode, in cascade order: later blocks win. */
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[
|
|
39
|
-
'dark-
|
|
44
|
+
function modes(visions) {
|
|
45
|
+
const notLight = `:root:not(${theme(['light', 'light-high-contrast', ...visions])})`;
|
|
46
|
+
const notVision = visions.length ? `:root:not(${theme(visions)})` : ':root';
|
|
47
|
+
return [
|
|
48
|
+
['dark', [['@media (prefers-color-scheme: dark)', notLight], [null, theme(['dark', 'dark-high-contrast'])]]],
|
|
49
|
+
['contrast-more', [['@media (prefers-contrast: more)', notVision], [null, theme(['light-high-contrast', 'dark-high-contrast'])]]],
|
|
40
50
|
[
|
|
41
|
-
|
|
42
|
-
[
|
|
51
|
+
'dark-contrast-more',
|
|
52
|
+
[
|
|
53
|
+
['@media (prefers-contrast: more) and (prefers-color-scheme: dark)', notLight],
|
|
54
|
+
['@media (prefers-contrast: more)', theme(['dark'])],
|
|
55
|
+
[null, theme(['dark-high-contrast'])],
|
|
56
|
+
],
|
|
43
57
|
],
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
['reduced-motion', [['@media (prefers-reduced-motion: reduce)', ':root']]],
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The color vision themes the build has (vision.<theme>.* tokens). */
|
|
63
|
+
const visionsIn = (dictionary) => [...new Set(dictionary.allTokens.filter((t) => t.path[0] === 'vision').map((t) => t.path[1]))];
|
|
47
64
|
|
|
48
65
|
const varName = (tokenPath) => `--${[prefix, ...tokenPath].join('-')}`;
|
|
49
66
|
const refsToVars = (value) => String(value).replace(/\{([^}]+)\}/g, (_, ref) => `var(${varName(ref.split('.'))})`);
|
|
@@ -61,7 +78,7 @@ const modeValue = (token, mode) => token.original.$extensions?.['sl.modes']?.[mo
|
|
|
61
78
|
|
|
62
79
|
function modeBlocks(dictionary) {
|
|
63
80
|
const blocks = [];
|
|
64
|
-
for (const [mode, targets] of
|
|
81
|
+
for (const [mode, targets] of modes(visionsIn(dictionary))) {
|
|
65
82
|
const declarations = dictionary.allTokens
|
|
66
83
|
.filter((token) => modeValue(token, mode) !== undefined)
|
|
67
84
|
.map((token) => `${varName(token.path)}: ${refsToVars(modeValue(token, mode))};`)
|
|
@@ -76,7 +93,7 @@ function modeBlocks(dictionary) {
|
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
79
|
-
* [data-
|
|
96
|
+
* [data-theme="<vision theme>"] re-declares the brand's palette and data variables with the
|
|
80
97
|
* theme's (vision.<theme>.* tokens, generated in generator/vision.mjs). Set it on the root
|
|
81
98
|
* element: variables declared at :root resolve there, so everything that reads them follows.
|
|
82
99
|
*/
|
|
@@ -84,66 +101,92 @@ function visionBlocks(dictionary) {
|
|
|
84
101
|
const byTheme = new Map();
|
|
85
102
|
for (const token of dictionary.allTokens) {
|
|
86
103
|
if (token.path[0] !== 'vision') continue;
|
|
87
|
-
const [,
|
|
88
|
-
if (!byTheme.has(
|
|
89
|
-
byTheme.get(
|
|
104
|
+
const [, name, ...rest] = token.path;
|
|
105
|
+
if (!byTheme.has(name)) byTheme.set(name, []);
|
|
106
|
+
byTheme.get(name).push(`${varName(rest)}: var(${varName(token.path)});`);
|
|
90
107
|
}
|
|
91
|
-
return [...byTheme].map(([
|
|
108
|
+
return [...byTheme].map(([name, lines]) => `${theme([name])} {\n${indent(lines.join('\n'), 2)}\n}`).join('\n\n');
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
/**
|
|
95
|
-
* Color scopes and emphasis (generator/roles.mjs). Every palette's slots are tokens
|
|
112
|
+
* Color scopes, statuses and emphasis (generator/roles.mjs). Every palette's slots are tokens
|
|
96
113
|
* (scope.<palette>.<slot>); these blocks point the generic --sl-scope-<slot> at one of them:
|
|
97
|
-
* the accent by default, any palette under [data-color], and the level's own
|
|
98
|
-
* [data-emphasis] when that color is chrome or a named palette, unless the element
|
|
99
|
-
* component paints with: the level's
|
|
114
|
+
* the accent by default, any palette under [data-color] or [data-status], and the level's own
|
|
115
|
+
* color under [data-emphasis] when that color is chrome or a named palette, unless the element
|
|
116
|
+
* names a color or sits in a status. --sl-e-<part> is what a component paints with: the level's
|
|
117
|
+
* style read from whatever scope is in effect, or the brand pair the brand file picked for that
|
|
118
|
+
* level in that status (roles.status).
|
|
100
119
|
*/
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
// Each component that paints with emphasis starts at a level of its own; data-emphasis on it, or
|
|
121
|
+
// on a group around a toggle, picks another. Button's data-variant names are aliases for levels,
|
|
122
|
+
// so the brand's roles drive every button; a button with neither is bold.
|
|
123
|
+
const DEFAULT_LEVELS = {
|
|
124
|
+
bold: [".Button[data-variant='solid']:not([data-emphasis])", '.Button:not([data-variant]):not([data-emphasis])', '.AvatarRoot:not([data-emphasis])'],
|
|
125
|
+
standard: [".Button[data-variant='outline']:not([data-emphasis])", '.Toggle:not([data-emphasis], [data-emphasis] *)'],
|
|
106
126
|
quiet: [".Button[data-variant='ghost']:not([data-emphasis])"],
|
|
107
127
|
};
|
|
108
128
|
|
|
109
|
-
// A toast's type (toastManager.add({ type: 'success' }))
|
|
110
|
-
//
|
|
129
|
+
// A toast's type (toastManager.add({ type: 'success' })) is its status, so success, warning,
|
|
130
|
+
// error and info toasts tint themselves. data-color or data-status wins when one is set.
|
|
111
131
|
const TOAST_TYPES = ['success', 'warning', 'error', 'info'];
|
|
112
|
-
const
|
|
132
|
+
const statusSelectors = (name) => [`[data-status='${name}']`, ...(TOAST_TYPES.includes(name) ? [`.Toast[data-type='${name}']:not([data-color], [data-status])`] : [])];
|
|
133
|
+
const IN_STATUS = '[data-status], [data-status] *, .Toast[data-type], .Toast[data-type] *';
|
|
113
134
|
|
|
114
135
|
function roleBlocks(dictionary) {
|
|
115
136
|
const scopes = new Map();
|
|
116
|
-
const
|
|
137
|
+
const statuses = new Map();
|
|
117
138
|
for (const token of dictionary.allTokens) {
|
|
118
139
|
if (token.path[0] === 'scope') {
|
|
119
140
|
const [, name, slot] = token.path;
|
|
120
141
|
if (!scopes.has(name)) scopes.set(name, []);
|
|
121
142
|
scopes.get(name).push(slot);
|
|
122
143
|
}
|
|
123
|
-
if (token.path[0] === '
|
|
124
|
-
const [, level, part] = token.path;
|
|
144
|
+
if (token.path[0] === 'status-level') {
|
|
145
|
+
const [, name, level, part] = token.path;
|
|
146
|
+
if (!statuses.has(name)) statuses.set(name, new Map());
|
|
147
|
+
const levels = statuses.get(name);
|
|
125
148
|
if (!levels.has(level)) levels.set(level, []);
|
|
126
149
|
levels.get(level).push([part, String(token.original.$value)]);
|
|
127
150
|
}
|
|
128
151
|
}
|
|
129
152
|
if (!scopes.size) return '';
|
|
130
153
|
const point = (name) => scopes.get(name).map((slot) => `${varName(['scope', slot])}: var(${varName(['scope', name, slot])});`).join('\n');
|
|
131
|
-
const rule = (
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const color = parts.map(([, v]) => v.match(/^\{scope\.([^.]+)\./)?.[1]).find(Boolean);
|
|
136
|
-
const paint = parts
|
|
154
|
+
const rule = (selectors, body) => `${selectors.join(',\n')} {\n${indent(body, 2)}\n}`;
|
|
155
|
+
const pairOf = (parts) => parts.map(([, v]) => v.match(/^\{pair\.([^.]+)\./)?.[1]).find(Boolean);
|
|
156
|
+
const paint = (parts) =>
|
|
157
|
+
parts
|
|
137
158
|
.map(([part, v]) => {
|
|
138
159
|
const slot = v.match(/^\{scope\.[^.]+\.([^}]+)\}$/)?.[1];
|
|
139
|
-
return `${varName(['e', part])}: ${slot ? `var(${varName(['scope', slot])})` : v};`;
|
|
160
|
+
return `${varName(['e', part])}: ${slot ? `var(${varName(['scope', slot])})` : refsToVars(v)};`;
|
|
140
161
|
})
|
|
141
162
|
.join('\n');
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
|
|
164
|
+
const blocks = [rule([':root'], point('accent'))];
|
|
165
|
+
for (const name of scopes.keys()) blocks.push(rule([`[data-color='${name}']`, ...(statuses.has(name) && name !== 'default' ? statusSelectors(name) : [])], point(name)));
|
|
166
|
+
|
|
167
|
+
// Every level's style, read from whatever scope is in effect: a status's palette, data-color's, or the accent.
|
|
168
|
+
for (const [level, parts] of statuses.get('default') ?? []) {
|
|
169
|
+
const selectors = [`[data-emphasis='${level}']`, ...(DEFAULT_LEVELS[level] ?? [])];
|
|
170
|
+
const style = STYLES[roles.emphasis[level].style];
|
|
171
|
+
blocks.push(rule(selectors, paint(parts.map(([part]) => [part, style[part] === 'transparent' ? 'transparent' : `{scope.any.${style[part]}}`]))));
|
|
172
|
+
// Default status: the pair the brand picked, or the level's own color. Accent levels follow
|
|
173
|
+
// the scope around them, so data-color on a section re-colors them; chrome or a named palette
|
|
174
|
+
// stays put unless the element names a color or sits in a status.
|
|
175
|
+
if (pairOf(parts)) {
|
|
176
|
+
blocks.push(rule([`:is(${selectors.join(', ')}):not(${IN_STATUS})`], paint(parts)));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const color = parts.map(([, v]) => v.match(/^\{scope\.([^.]+)\./)?.[1]).find(Boolean);
|
|
180
|
+
if (color && color !== 'accent') blocks.push(rule(selectors.map((sel) => `${sel}:not([data-color], ${IN_STATUS})`), point(color)));
|
|
181
|
+
}
|
|
182
|
+
// Other statuses: the scope already holds the status's palette; only a picked pair needs its own rule.
|
|
183
|
+
for (const [name, levels] of statuses) {
|
|
184
|
+
if (name === 'default') continue;
|
|
185
|
+
for (const [level, parts] of levels) {
|
|
186
|
+
if (!pairOf(parts)) continue;
|
|
187
|
+
const selectors = `:is(${[`[data-emphasis='${level}']`, ...(DEFAULT_LEVELS[level] ?? [])].join(', ')})`;
|
|
188
|
+
blocks.push(rule(statusSelectors(name).flatMap((s) => [`${s} ${selectors}`, `${s}${selectors}`]), paint(parts)));
|
|
189
|
+
}
|
|
147
190
|
}
|
|
148
191
|
return blocks.join('\n\n');
|
|
149
192
|
}
|
|
@@ -284,10 +327,10 @@ StyleDictionary.registerFormat({
|
|
|
284
327
|
/**
|
|
285
328
|
* Text styles are written as one DTCG typography token each (tokens/semantic/typography.json).
|
|
286
329
|
* CSS can't use a composite, so this expands each into its parts plus a `font` shorthand:
|
|
287
|
-
* font.base → font.base.font, .font-family, .font-size, .font-weight, .line-height,
|
|
330
|
+
* font.body-base → font.body-base.font, .font-family, .font-size, .font-weight, .line-height,
|
|
288
331
|
* .letter-spacing (and .text-transform when $extensions["sl.text-transform"] is set)
|
|
289
|
-
* so --sl-font-base-font-size and friends exist for components. A text style whose value is
|
|
290
|
-
* another text style ("{font.base}") takes that style's parts.
|
|
332
|
+
* so --sl-font-body-base-font-size and friends exist for components. A text style whose value is
|
|
333
|
+
* another text style ("{font.body-base}") takes that style's parts.
|
|
291
334
|
*/
|
|
292
335
|
const TEXT_PARTS = { fontFamily: 'font-family', fontSize: 'font-size', fontWeight: 'font-weight', lineHeight: 'line-height', letterSpacing: 'letter-spacing' };
|
|
293
336
|
export const TEXT_STYLE_TYPE = 'typography';
|
|
@@ -422,6 +465,7 @@ export async function buildTokens(config) {
|
|
|
422
465
|
prefix = config.prefix;
|
|
423
466
|
const { brand, applied, files } = generate(config.brandFile, config.generatedDir);
|
|
424
467
|
brandName = brand.name;
|
|
468
|
+
roles = resolveRoles(brand);
|
|
425
469
|
const out = config.outDir + path.sep;
|
|
426
470
|
|
|
427
471
|
const sd = new StyleDictionary({
|
package/exporters/README.md
CHANGED
|
@@ -15,7 +15,7 @@ adapter in this folder, and each one only renames. No adapter does color math.
|
|
|
15
15
|
| `ag-grid-v33` | `ag-grid-v33.js`, `.d.ts`, `.css` | AG Grid 33 to 35 (Theming API). |
|
|
16
16
|
| `ag-grid-v32` | `ag-grid-v32.css` | AG Grid 31 and 32 (CSS themes). All three come from `ag-grid-settings.mjs`. |
|
|
17
17
|
| `design-system` | `design-system/`, `AGENTS.md` | A Claude Design System (claude.ai), so Claude designs, makes decks and builds UI with the brand. `AGENTS.md` is the same brand book for coding agents, plus how to load the theme. See below. |
|
|
18
|
-
| `figma` | `figma.json` | Figma variables
|
|
18
|
+
| `figma` | `figma.json` | Figma variables, text styles and effect styles (elevation/xs to 2xl, focus/ring, focus/field, focus/field-error): hidden Brand and Scale collections, and Size, Theme, Status and Emphasis with modes for desktop, tablet and mobile sizes, light, dark, high contrast and color vision, `data-status` and `data-emphasis`, plus a one-mode layout for Figma Starter. Read by `loomiere figma push` and the Loomiere plugin; see [../figma/README.md](../figma/README.md). |
|
|
19
19
|
|
|
20
20
|
`loomiere build` writes them all to the theme's `exports/` folder, with `catalog.json` listing what's there.
|
|
21
21
|
`"exports": ["ag-grid", "tailwind"]` in `loomiere.config.json` writes only those (an id prefix picks a
|
|
@@ -45,12 +45,12 @@ export const SETTINGS = [
|
|
|
45
45
|
['cellEditingShadow', '{field.shadow-focus}', null],
|
|
46
46
|
|
|
47
47
|
// Type: cells use the condense text style, the header uses condense-subtitle, whole.
|
|
48
|
-
['fontFamily', '{font.condense.font-family}', '--ag-font-family'],
|
|
49
|
-
['fontSize', '{font.condense.font-size}', '--ag-font-size'],
|
|
50
|
-
['fontWeight', '{font.condense.font-weight}', null],
|
|
51
|
-
['headerFontFamily', '{font.condense
|
|
52
|
-
['headerFontSize', '{font.condense
|
|
53
|
-
['headerFontWeight', '{font.condense
|
|
48
|
+
['fontFamily', '{font.body-condense.font-family}', '--ag-font-family'],
|
|
49
|
+
['fontSize', '{font.body-condense.font-size}', '--ag-font-size'],
|
|
50
|
+
['fontWeight', '{font.body-condense.font-weight}', null],
|
|
51
|
+
['headerFontFamily', '{font.subtitle-condense.font-family}', null],
|
|
52
|
+
['headerFontSize', '{font.subtitle-condense.font-size}', null],
|
|
53
|
+
['headerFontWeight', '{font.subtitle-condense.font-weight}', null],
|
|
54
54
|
|
|
55
55
|
// Sizes
|
|
56
56
|
['spacing', '{space.2}', '--ag-grid-size'],
|
|
@@ -142,7 +142,7 @@ export const SETTINGS = [
|
|
|
142
142
|
['buttonHoverBorder', '{stroke.default} solid {emphasis.standard.border-hover}', null],
|
|
143
143
|
['buttonActiveBackgroundColor', '{emphasis.standard.bg-active}', null],
|
|
144
144
|
['buttonBorderRadius', '{radius.control}', null],
|
|
145
|
-
['buttonFontWeight', '{font.condense
|
|
145
|
+
['buttonFontWeight', '{font.trigger-condense.font-weight}', null],
|
|
146
146
|
['iconButtonColor', '{icon-button.color}', null],
|
|
147
147
|
['iconButtonHoverColor', '{icon-button.color-hover}', null],
|
|
148
148
|
['iconButtonHoverBackgroundColor', '{icon-button.background-hover}', null],
|
package/exporters/ag-grid.mjs
CHANGED
|
@@ -21,11 +21,11 @@ function resolver(theme) {
|
|
|
21
21
|
const extraCss = (fixes = '') => `/* Letter spacing comes from the condense and condense-subtitle text styles. */
|
|
22
22
|
.ag-root-wrapper,
|
|
23
23
|
.ag-popup {
|
|
24
|
-
letter-spacing: var(--sl-font-condense-letter-spacing);
|
|
24
|
+
letter-spacing: var(--sl-font-body-condense-letter-spacing);
|
|
25
25
|
}
|
|
26
26
|
.ag-header-cell-text {
|
|
27
|
-
letter-spacing: var(--sl-font-condense-
|
|
28
|
-
line-height: var(--sl-font-condense-
|
|
27
|
+
letter-spacing: var(--sl-font-subtitle-condense-letter-spacing);
|
|
28
|
+
line-height: var(--sl-font-subtitle-condense-line-height);
|
|
29
29
|
}
|
|
30
30
|
${fixes}`;
|
|
31
31
|
|
|
@@ -99,9 +99,9 @@ ${lines.join('\n')}
|
|
|
99
99
|
|
|
100
100
|
/* The old themes have no header type settings: the header uses the condense-subtitle text style. */
|
|
101
101
|
.ag-theme-loomiere .ag-header-cell-text {
|
|
102
|
-
font-family: var(--${theme.prefix}-font-condense-
|
|
103
|
-
font-size: var(--${theme.prefix}-font-condense-
|
|
104
|
-
font-weight: var(--${theme.prefix}-font-condense-
|
|
102
|
+
font-family: var(--${theme.prefix}-font-subtitle-condense-font-family);
|
|
103
|
+
font-size: var(--${theme.prefix}-font-subtitle-condense-font-size);
|
|
104
|
+
font-weight: var(--${theme.prefix}-font-subtitle-condense-font-weight);
|
|
105
105
|
}
|
|
106
106
|
.ag-theme-loomiere .ag-header-cell:hover {
|
|
107
107
|
background-color: var(--${theme.prefix}-table-header-background-hover);
|