@scattered-light/loomiere 0.0.0-stage → 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 +60 -2
- package/bin/loomiere.mjs +112 -0
- package/build/build-tokens.mjs +485 -0
- package/exporters/README.md +45 -0
- package/exporters/ag-grid-settings.mjs +177 -0
- package/exporters/ag-grid.mjs +112 -0
- package/exporters/chakra.mjs +630 -0
- package/exporters/component-styles.mjs +207 -0
- package/exporters/dtcg.mjs +31 -0
- package/exporters/figma.mjs +100 -0
- package/exporters/index.mjs +38 -0
- package/exporters/mui.mjs +666 -0
- package/exporters/tailwind.mjs +35 -0
- package/exporters/theme.mjs +93 -0
- package/generator/brand.mjs +328 -0
- package/generator/brand.schema.json +657 -0
- package/generator/color.mjs +130 -0
- package/generator/data-colors.mjs +104 -0
- package/generator/format-brand.mjs +60 -0
- package/generator/generate.mjs +43 -0
- package/generator/pairs.mjs +236 -0
- package/generator/roles.mjs +258 -0
- package/generator/vision.mjs +167 -0
- package/lib/config.mjs +71 -0
- package/lib/contrast.mjs +134 -0
- package/lib/lab.mjs +108 -0
- package/lib/studio.mjs +127 -0
- package/lib/tool-server.mjs +96 -0
- package/package.json +51 -4
- package/templates/brand.json +104 -0
- package/tokens/README.md +219 -0
- package/tokens/component/avatar.json +10 -0
- package/tokens/component/button.json +12 -0
- package/tokens/component/drawer.json +26 -0
- package/tokens/component/navigation-menu.json +13 -0
- package/tokens/component/popover.json +13 -0
- package/tokens/component/preview-card.json +13 -0
- package/tokens/component/slider.json +13 -0
- package/tokens/component/switch.json +41 -0
- package/tokens/component/toast.json +21 -0
- package/tokens/component/toggle.json +9 -0
- package/tokens/component/toolbar.json +9 -0
- package/tokens/component/tooltip.json +9 -0
- package/tokens/foundation/layering.json +57 -0
- package/tokens/foundation/layout.json +75 -0
- package/tokens/foundation/motion.json +41 -0
- package/tokens/foundation/type.json +92 -0
- package/tokens/semantic/accessibility.json +39 -0
- package/tokens/semantic/bar.json +45 -0
- package/tokens/semantic/check.json +57 -0
- package/tokens/semantic/color.json +270 -0
- package/tokens/semantic/content.json +124 -0
- package/tokens/semantic/disclosure.json +49 -0
- package/tokens/semantic/elevation.json +72 -0
- package/tokens/semantic/field.json +77 -0
- package/tokens/semantic/focus.json +36 -0
- package/tokens/semantic/gauge.json +45 -0
- package/tokens/semantic/icon-button.json +33 -0
- package/tokens/semantic/icon.json +30 -0
- package/tokens/semantic/item.json +65 -0
- package/tokens/semantic/modal.json +86 -0
- package/tokens/semantic/motion.json +20 -0
- package/tokens/semantic/notice.json +37 -0
- package/tokens/semantic/popup.json +89 -0
- package/tokens/semantic/scheme.json +11 -0
- package/tokens/semantic/shape.json +54 -0
- package/tokens/semantic/spacing.json +110 -0
- package/tokens/semantic/table.json +73 -0
- package/tokens/semantic/typography.json +312 -0
- package/tools/brand-builder/builder.js +1119 -0
- package/tools/brand-builder/index.html +329 -0
- package/tools/pair-lab/index.html +119 -0
- package/tools/pair-lab/lab.js +249 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Base UI styles' shared patterns as style objects, for frameworks that style their own
|
|
3
|
+
* components (MUI, Chakra). Each piece reads the same concept variables the patterns in
|
|
4
|
+
* loomiere-base-ui/styles/patterns/shared.css read, so a MUI TextField, a Chakra Input and a
|
|
5
|
+
* Base UI Input are one control with three APIs. Values are var(--<prefix>-…) strings: the
|
|
6
|
+
* page must load the theme's tokens.css, and light, dark and data-color follow from there.
|
|
7
|
+
*/
|
|
8
|
+
import { LEVELS, STYLES } from '../generator/roles.mjs';
|
|
9
|
+
|
|
10
|
+
const TEXT_PROPS = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing'];
|
|
11
|
+
const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
12
|
+
|
|
13
|
+
export function styleKit(prefix, roles) {
|
|
14
|
+
const v = (name) => `var(--${prefix}-${name})`;
|
|
15
|
+
|
|
16
|
+
/** One whole text style, as longhands (the shorthand would reset kerning). */
|
|
17
|
+
const text = (style) => ({
|
|
18
|
+
...Object.fromEntries(TEXT_PROPS.map((p) => [camel(p), v(`font-${style}-${p}`)])),
|
|
19
|
+
...(style.endsWith('subtitle-label') && { textTransform: v(`font-${style}-text-transform`) }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** A palette's slot, through the scope variables: `scope('error', 'solid')`. */
|
|
23
|
+
const scope = (palette, slot) => (slot === 'transparent' ? 'transparent' : v(`scope-${palette}-${slot}`));
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An emphasis level's colors. Without `palette` the level uses its own color from the
|
|
27
|
+
* brand's roles (bold is the accent, quiet is chrome in Mojave); with one, the level's
|
|
28
|
+
* style in that palette, the way data-color re-colors a Base UI Button.
|
|
29
|
+
*/
|
|
30
|
+
const emphasis = (level, palette) => {
|
|
31
|
+
const { style, color } = roles.emphasis[level];
|
|
32
|
+
const p = palette ?? color;
|
|
33
|
+
const parts = Object.fromEntries(Object.entries(STYLES[style]).map(([part, slot]) => [part, scope(p, slot)]));
|
|
34
|
+
return { ...parts, focus: scope(p, 'focus'), halo: scope(p, 'soft-hover'), palette: p };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const focusRing = {
|
|
38
|
+
outline: v('focus-outline'),
|
|
39
|
+
outlineOffset: v('focus-outline-offset'),
|
|
40
|
+
boxShadow: `0 0 0 ${v('focus-halo-spread')} ${v('focus-halo-color')}`,
|
|
41
|
+
};
|
|
42
|
+
const focusRingInset = { outline: v('focus-outline'), outlineOffset: v('focus-outline-offset-inset') };
|
|
43
|
+
|
|
44
|
+
const field = {
|
|
45
|
+
boxSizing: 'border-box',
|
|
46
|
+
color: v('field-color'),
|
|
47
|
+
background: v('field-background'),
|
|
48
|
+
border: `${v('field-border-width')} solid ${v('field-border-color')}`,
|
|
49
|
+
borderRadius: v('field-radius'),
|
|
50
|
+
transition: v('field-transition'),
|
|
51
|
+
outline: 'none',
|
|
52
|
+
};
|
|
53
|
+
const fieldStates = {
|
|
54
|
+
hover: { borderColor: v('field-border-color-hover') },
|
|
55
|
+
focus: { borderColor: v('field-border-color-focus'), boxShadow: v('field-shadow-focus') },
|
|
56
|
+
error: { borderColor: v('field-border-color-error') },
|
|
57
|
+
errorFocus: { borderColor: v('field-border-color-error'), boxShadow: v('field-shadow-focus-error') },
|
|
58
|
+
disabled: { background: v('field-background-disabled'), color: v('field-color-disabled'), cursor: 'not-allowed' },
|
|
59
|
+
placeholder: { color: v('field-placeholder-color'), opacity: 1 },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Control sizes: height, padding and the text style of each tier. */
|
|
63
|
+
const SIZES = {
|
|
64
|
+
xs: { suffix: '-xs', text: 'minimize', icon: 'condense' },
|
|
65
|
+
sm: { suffix: '-sm', text: 'condense', icon: 'base' },
|
|
66
|
+
md: { suffix: '', text: 'base', icon: 'magnify' },
|
|
67
|
+
lg: { suffix: '-lg', text: 'magnify', icon: 'maximize' },
|
|
68
|
+
};
|
|
69
|
+
const control = (size, { trigger = false } = {}) => {
|
|
70
|
+
const s = SIZES[size];
|
|
71
|
+
return {
|
|
72
|
+
...text(trigger ? `${s.text}-trigger` : s.text),
|
|
73
|
+
height: v(`control-height${s.suffix || '-md'}`),
|
|
74
|
+
paddingInline: v(`control-padding-inline${s.suffix}`),
|
|
75
|
+
gap: v(size === 'xs' ? 'gap-control-xs' : size === 'sm' ? 'gap-control-sm' : 'gap-control'),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const iconSize = (size) => v(`icon-size-${SIZES[size].icon}`);
|
|
79
|
+
|
|
80
|
+
const popupSurface = {
|
|
81
|
+
boxSizing: 'border-box',
|
|
82
|
+
background: v('popup-background'),
|
|
83
|
+
border: `${v('popup-border-width')} solid ${v('popup-border-color')}`,
|
|
84
|
+
borderRadius: v('popup-radius'),
|
|
85
|
+
boxShadow: v('popup-shadow'),
|
|
86
|
+
backgroundImage: 'none',
|
|
87
|
+
color: v('text-primary'),
|
|
88
|
+
};
|
|
89
|
+
const modalSurface = {
|
|
90
|
+
boxSizing: 'border-box',
|
|
91
|
+
gap: v('modal-gap'),
|
|
92
|
+
padding: v('modal-padding'),
|
|
93
|
+
background: v('modal-background'),
|
|
94
|
+
border: `${v('modal-border-width')} solid ${v('modal-border-color')}`,
|
|
95
|
+
borderRadius: v('modal-radius'),
|
|
96
|
+
boxShadow: v('modal-shadow'),
|
|
97
|
+
backgroundImage: 'none',
|
|
98
|
+
color: v('text-primary'),
|
|
99
|
+
};
|
|
100
|
+
const barSurface = {
|
|
101
|
+
boxSizing: 'border-box',
|
|
102
|
+
padding: v('bar-padding'),
|
|
103
|
+
gap: v('bar-gap'),
|
|
104
|
+
background: v('bar-background'),
|
|
105
|
+
border: `${v('bar-border-width')} solid ${v('bar-border-color')}`,
|
|
106
|
+
borderRadius: v('bar-radius'),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const itemRow = {
|
|
110
|
+
...text('base'),
|
|
111
|
+
display: 'flex',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
gap: v('item-gap'),
|
|
114
|
+
minHeight: 0,
|
|
115
|
+
paddingBlock: v('item-padding-block'),
|
|
116
|
+
paddingInline: v('item-padding-inline'),
|
|
117
|
+
borderRadius: v('item-radius'),
|
|
118
|
+
color: v('item-color'),
|
|
119
|
+
transition: v('item-transition'),
|
|
120
|
+
};
|
|
121
|
+
const itemStates = {
|
|
122
|
+
hover: { background: v('item-background-hover'), color: v('item-color-hover') },
|
|
123
|
+
selected: { ...text('base-selected'), background: v('item-background-selected'), color: v('item-color-selected') },
|
|
124
|
+
disabled: { color: v('item-color-disabled'), opacity: v('item-opacity-disabled') },
|
|
125
|
+
};
|
|
126
|
+
const groupLabel = {
|
|
127
|
+
...text('condense-subtitle-label'),
|
|
128
|
+
paddingBlock: `${v('popup-group-label-padding-top')} ${v('popup-group-label-padding-bottom')}`,
|
|
129
|
+
paddingInline: v('item-padding-inline'),
|
|
130
|
+
color: v('text-secondary'),
|
|
131
|
+
background: 'transparent',
|
|
132
|
+
};
|
|
133
|
+
const separator = { height: v('stroke-default'), marginBlock: v('popup-separator-spacing'), border: 'none', background: v('popup-separator-color') };
|
|
134
|
+
|
|
135
|
+
const checkBox = {
|
|
136
|
+
boxSizing: 'border-box',
|
|
137
|
+
display: 'inline-flex',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
justifyContent: 'center',
|
|
140
|
+
flexShrink: 0,
|
|
141
|
+
border: `${v('check-border-width')} solid ${v('check-border-color')}`,
|
|
142
|
+
background: v('check-background'),
|
|
143
|
+
color: v('check-mark-color'),
|
|
144
|
+
transition: v('check-transition'),
|
|
145
|
+
};
|
|
146
|
+
const checkStates = {
|
|
147
|
+
hover: { borderColor: v('check-border-color-hover') },
|
|
148
|
+
checked: { background: v('check-background-checked'), borderColor: v('check-border-color-checked') },
|
|
149
|
+
disabled: { opacity: v('check-opacity-disabled') },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const iconButton = {
|
|
153
|
+
width: v('icon-button-size'),
|
|
154
|
+
height: v('icon-button-size'),
|
|
155
|
+
padding: 0,
|
|
156
|
+
borderRadius: v('icon-button-radius'),
|
|
157
|
+
color: v('icon-button-color'),
|
|
158
|
+
transition: v('icon-button-transition'),
|
|
159
|
+
};
|
|
160
|
+
const iconButtonHover = { background: v('icon-button-background-hover'), color: v('icon-button-color-hover') };
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
v,
|
|
164
|
+
text,
|
|
165
|
+
scope,
|
|
166
|
+
emphasis,
|
|
167
|
+
levels: LEVELS,
|
|
168
|
+
focusRing,
|
|
169
|
+
focusRingInset,
|
|
170
|
+
field,
|
|
171
|
+
fieldStates,
|
|
172
|
+
control,
|
|
173
|
+
iconSize,
|
|
174
|
+
sizes: Object.keys(SIZES),
|
|
175
|
+
popupSurface,
|
|
176
|
+
modalSurface,
|
|
177
|
+
barSurface,
|
|
178
|
+
itemRow,
|
|
179
|
+
itemStates,
|
|
180
|
+
groupLabel,
|
|
181
|
+
separator,
|
|
182
|
+
checkBox,
|
|
183
|
+
checkStates,
|
|
184
|
+
iconButton,
|
|
185
|
+
iconButtonHover,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Marks a piece of JavaScript source to paste into a generated file as is. */
|
|
190
|
+
export const raw = (code) => ({ __raw: code });
|
|
191
|
+
|
|
192
|
+
/** A value as JavaScript source: JSON, except raw() pieces are pasted in. */
|
|
193
|
+
export function toSource(value, indent = '') {
|
|
194
|
+
if (value && typeof value === 'object' && '__raw' in value) return value.__raw;
|
|
195
|
+
const inner = `${indent} `;
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
if (!value.length) return '[]';
|
|
198
|
+
return `[\n${value.map((x) => inner + toSource(x, inner)).join(',\n')}\n${indent}]`;
|
|
199
|
+
}
|
|
200
|
+
if (value && typeof value === 'object') {
|
|
201
|
+
const entries = Object.entries(value).filter(([, x]) => x !== undefined);
|
|
202
|
+
if (!entries.length) return '{}';
|
|
203
|
+
const key = (k) => (/^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k));
|
|
204
|
+
return `{\n${entries.map(([k, x]) => `${inner}${key(k)}: ${toSource(x, inner)}`).join(',\n')}\n${indent}}`;
|
|
205
|
+
}
|
|
206
|
+
return JSON.stringify(value);
|
|
207
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTCG (Design Tokens Community Group) JSON: every token, nested by path, keeping its
|
|
3
|
+
* references, with dark and other modes under $extensions. Tokens Studio, Figma variable
|
|
4
|
+
* importers, Style Dictionary and Terrazzo all read it, so it's the universal export.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
id: 'dtcg',
|
|
8
|
+
name: 'Design tokens (DTCG)',
|
|
9
|
+
file: 'tokens.dtcg.json',
|
|
10
|
+
for: 'Any tool that reads the W3C design tokens format: Figma (Tokens Studio, variable importers), Style Dictionary, Terrazzo, Penpot.',
|
|
11
|
+
build(theme) {
|
|
12
|
+
const root = {};
|
|
13
|
+
for (const t of theme.tokens) {
|
|
14
|
+
const keys = t.path.split('.');
|
|
15
|
+
let node = root;
|
|
16
|
+
for (const k of keys.slice(0, -1)) {
|
|
17
|
+
// A path that is both a token and a group (rare) keeps the token under "DEFAULT".
|
|
18
|
+
if (node[k]?.$value !== undefined) node[k] = { DEFAULT: node[k] };
|
|
19
|
+
node = node[k] ??= {};
|
|
20
|
+
}
|
|
21
|
+
const modes = t.modeReferences ?? t.modes;
|
|
22
|
+
node[keys.at(-1)] = {
|
|
23
|
+
...(t.type && { $type: t.type }),
|
|
24
|
+
$value: t.references ?? t.value,
|
|
25
|
+
...(t.description && { $description: t.description }),
|
|
26
|
+
...(modes && { $extensions: { 'sl.modes': modes } }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return `${JSON.stringify({ $description: `${theme.brand} theme, prefix --${theme.prefix}-`, ...root }, null, 2)}\n`;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma variables, one collection per builder step so each step can sync on its own:
|
|
3
|
+
* Brand palettes, data colors, space, radius, stroke, type (step 1)
|
|
4
|
+
* Pairs each pair's colors, light and dark (step 2)
|
|
5
|
+
* Theme surfaces, text, borders, scopes and emphasis levels (step 3)
|
|
6
|
+
* Pairs and Theme alias Brand, and Theme aliases itself the way the tokens do
|
|
7
|
+
* (emphasis → scope → palette step), so changing a role in Figma moves everything that uses it.
|
|
8
|
+
* Values that are math in CSS (color-mix) are resolved to colors. Every variable carries its
|
|
9
|
+
* CSS variable as WEB code syntax. The file is data for a sync script that writes the
|
|
10
|
+
* variables with the Figma plugin API; it isn't a Figma import format.
|
|
11
|
+
*/
|
|
12
|
+
import { formatHex, formatHex8, interpolate, parse } from 'culori';
|
|
13
|
+
|
|
14
|
+
/** #rrggbb, or #rrggbbaa when the color is see-through. */
|
|
15
|
+
const hex = (c) => ((c.alpha ?? 1) < 1 ? formatHex8(c) : formatHex(c));
|
|
16
|
+
|
|
17
|
+
import { px, valueIn } from './theme.mjs';
|
|
18
|
+
|
|
19
|
+
/** color-mix(in oklab|srgb, A p%, B) → a hex; anything else parse() understands → a hex. */
|
|
20
|
+
function solve(value) {
|
|
21
|
+
const mix = String(value).match(/^color-mix\(in (\w+),\s*(.+?)\s+([\d.]+)%,\s*(.+)\)$/);
|
|
22
|
+
if (mix) return hex(interpolate([solve(mix[4]), solve(mix[2])], mix[1] === 'srgb' ? 'rgb' : mix[1])(Number(mix[3]) / 100));
|
|
23
|
+
if (value === 'transparent') return '#00000000';
|
|
24
|
+
const c = parse(value);
|
|
25
|
+
return c ? hex(c) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const COLLECTION_OF = (t) => {
|
|
29
|
+
if (t.layer === 'primitive' && !t.path.startsWith('vision.') && !/^color\.(accent|chrome)\./.test(t.path)) return 'Brand';
|
|
30
|
+
if (t.layer === 'semantic' && t.type === 'color') return t.path.startsWith('pair.') ? 'Pairs' : 'Theme';
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SCOPES = {
|
|
35
|
+
surface: ['FRAME_FILL', 'SHAPE_FILL'],
|
|
36
|
+
text: ['TEXT_FILL'],
|
|
37
|
+
border: ['STROKE_COLOR'],
|
|
38
|
+
color: ['ALL_FILLS', 'STROKE_COLOR'],
|
|
39
|
+
space: ['GAP', 'WIDTH_HEIGHT'],
|
|
40
|
+
radius: ['CORNER_RADIUS'],
|
|
41
|
+
stroke: ['STROKE_FLOAT'],
|
|
42
|
+
'font-size': ['FONT_SIZE'],
|
|
43
|
+
'font-family': ['FONT_FAMILY'],
|
|
44
|
+
};
|
|
45
|
+
const scopesOf = (path) => SCOPES[path.split('.')[0]] ?? ['ALL_FILLS', 'STROKE_COLOR', 'EFFECT_COLOR'];
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
id: 'figma',
|
|
49
|
+
name: 'Figma variables',
|
|
50
|
+
file: 'figma.json',
|
|
51
|
+
for: 'Syncing to Figma variables: Brand, Pairs and Theme collections with light and dark modes. Ask Claude to sync a step.',
|
|
52
|
+
build(theme) {
|
|
53
|
+
const byPath = new Map(theme.tokens.map((t) => [t.path, t]));
|
|
54
|
+
const figmaName = (path) => path.replace(/\./g, '/');
|
|
55
|
+
const refOf = (r) => r?.match(/^\{([^}]+)\}$/)?.[1];
|
|
56
|
+
// Follow accent/chrome aliases (they aren't variables in Figma) to the palette step.
|
|
57
|
+
const target = (path) => {
|
|
58
|
+
let t = byPath.get(path);
|
|
59
|
+
while (t && /^color\.(accent|chrome)\./.test(t.path)) t = byPath.get(refOf(t.references));
|
|
60
|
+
return t && COLLECTION_OF(t) ? `${COLLECTION_OF(t)}:${figmaName(t.path)}` : null;
|
|
61
|
+
};
|
|
62
|
+
const valueFor = (t, mode) => {
|
|
63
|
+
const ref = mode === 'Dark' ? refOf(t.modeReferences?.dark) ?? (t.modes?.dark === undefined ? refOf(t.references) : null) : refOf(t.references);
|
|
64
|
+
const alias = ref && target(ref);
|
|
65
|
+
if (alias) return { alias };
|
|
66
|
+
const raw = mode === 'Dark' ? valueIn(byPath, t.path, 'dark') : t.value;
|
|
67
|
+
return t.type === 'color' ? solve(raw) : raw;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const collections = { Brand: [], Pairs: [], Theme: [] };
|
|
71
|
+
for (const t of theme.tokens) {
|
|
72
|
+
const c = COLLECTION_OF(t);
|
|
73
|
+
if (!c) continue;
|
|
74
|
+
const top = t.path.split('.')[0];
|
|
75
|
+
let type, value;
|
|
76
|
+
if (t.type === 'color' || top === 'color') type = 'COLOR';
|
|
77
|
+
else if (['space', 'radius', 'stroke', 'font-size'].includes(top)) {
|
|
78
|
+
// Fluid sizes go in at their large-screen size; Figma variables hold one number.
|
|
79
|
+
const m = String(t.value).match(/,\s*([\d.]+rem)\)$/);
|
|
80
|
+
value = px(m ? m[1] : t.value);
|
|
81
|
+
if (value === undefined) continue;
|
|
82
|
+
type = 'FLOAT';
|
|
83
|
+
} else if (top === 'font-family') {
|
|
84
|
+
type = 'STRING';
|
|
85
|
+
value = String(t.value).split(',')[0].replace(/['"]/g, '').trim();
|
|
86
|
+
} else continue;
|
|
87
|
+
const modes = c === 'Brand' ? { Value: value ?? valueFor(t, 'Light') } : { Light: valueFor(t, 'Light'), Dark: valueFor(t, 'Dark') };
|
|
88
|
+
collections[c].push({ name: figmaName(t.path), type, modes, scopes: scopesOf(t.path), code: `var(${t.name})`, ...(t.description && { description: t.description }) });
|
|
89
|
+
}
|
|
90
|
+
const out = {
|
|
91
|
+
brand: theme.brand,
|
|
92
|
+
collections: [
|
|
93
|
+
{ name: 'Brand', step: 'brand', modes: ['Value'], variables: collections.Brand },
|
|
94
|
+
{ name: 'Pairs', step: 'pairs', modes: ['Light', 'Dark'], variables: collections.Pairs },
|
|
95
|
+
{ name: 'Theme', step: 'roles', modes: ['Light', 'Dark'], variables: collections.Theme },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
return `${JSON.stringify(out)}\n`;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The export catalog. The universal outputs are the CSS variables (dist/tokens/tokens.css)
|
|
3
|
+
* and DTCG JSON; every framework adapter reads the same neutral theme (theme.mjs) and only
|
|
4
|
+
* renames. To add a framework, add a file like mui.mjs and list it here.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
import chakra from './chakra.mjs';
|
|
10
|
+
import dtcg from './dtcg.mjs';
|
|
11
|
+
import figma from './figma.mjs';
|
|
12
|
+
import mui from './mui.mjs';
|
|
13
|
+
import tailwind from './tailwind.mjs';
|
|
14
|
+
import { agGridV32, agGridV33, agGridV36 } from './ag-grid.mjs';
|
|
15
|
+
import { readTheme } from './theme.mjs';
|
|
16
|
+
|
|
17
|
+
export const EXPORTERS = [dtcg, figma, tailwind, mui, chakra, agGridV36, agGridV33, agGridV32];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether an exporter is wanted: `only` is "all" or a list of ids, where an id may name a
|
|
21
|
+
* family by its start ("ag-grid" picks ag-grid-v36, ag-grid-v33 and ag-grid-v32).
|
|
22
|
+
*/
|
|
23
|
+
const wanted = (id, only) => only === 'all' || only.some((pick) => id === pick || id.startsWith(`${pick}-`));
|
|
24
|
+
|
|
25
|
+
/** Writes the chosen exports into outDir and a catalog.json that lists them. */
|
|
26
|
+
export function exportAll(catalog, roles, outDir, only = 'all') {
|
|
27
|
+
const theme = readTheme(catalog, roles);
|
|
28
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
29
|
+
const written = EXPORTERS.filter((exporter) => wanted(exporter.id, only)).map((exporter) => {
|
|
30
|
+
// An exporter returns one file's text, or { fileName: text } when it writes several.
|
|
31
|
+
const out = exporter.build(theme);
|
|
32
|
+
const files = typeof out === 'string' ? { [exporter.file]: out } : out;
|
|
33
|
+
for (const [file, text] of Object.entries(files)) fs.writeFileSync(path.join(outDir, file), text);
|
|
34
|
+
return { id: exporter.id, name: exporter.name, file: exporter.file, files: Object.keys(files), for: exporter.for };
|
|
35
|
+
});
|
|
36
|
+
fs.writeFileSync(path.join(outDir, 'catalog.json'), `${JSON.stringify({ brand: theme.brand, exports: written }, null, 2)}\n`);
|
|
37
|
+
return written;
|
|
38
|
+
}
|