react-mcu 1.1.1 → 1.3.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 CHANGED
@@ -15,6 +15,29 @@ m3 references:
15
15
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
16
  | [<img width="2836" height="2266" alt="CleanShot 2026-01-14 at 08 58 40@2x" src="https://github.com/user-attachments/assets/e4b47c00-716f-4b08-b393-de306d5ce302" />](https://material-foundation.github.io/material-theme-builder/) | [<img width="2836" height="2266" alt="CleanShot 2026-01-14 at 09 01 23@2x" src="https://github.com/user-attachments/assets/826e502d-e173-43c4-807a-53d0ba075a88" />](https://m3.material.io/styles/color/roles) |
17
17
 
18
+ Support for:
19
+
20
+ Base (like in the Builder):
21
+
22
+ - [x] light/dark mode
23
+ - [x] source color
24
+ - [x] scheme
25
+ - [x] contrast
26
+ - [x] core-colors overrides: primary, secondary, tertiary, error, neutral,
27
+ neutralVariant
28
+ - [x] custom-colors (aka. "Extended colors")
29
+ - [x] Harmonization (aka. `blend`) -- with effective color: `source` or
30
+ `primary` if defined
31
+ - [x] Shades (aka. "tonals")
32
+ - [ ] colorMatch
33
+
34
+ Extra:
35
+
36
+ - [x] `contrastAllColors`: contrast also applies to custom-colors and shades
37
+ (not only the core-colors)
38
+ - [x] `adaptiveShades`: shades adapt to the light/dark mode (instead of being
39
+ fixed)
40
+
18
41
  # Usage
19
42
 
20
43
  ```tsx
@@ -152,6 +175,37 @@ Simply override/remap
152
175
  > Make sure `:root, .dark { ... }` comes AFTER `.root { ... } .dark { ... }` to
153
176
  > take precedence.
154
177
 
178
+ ## Programmatic API
179
+
180
+ ```ts
181
+ import { builder } from "react-mcu";
182
+
183
+ const theme = builder("#6750A4", {
184
+ scheme: "vibrant",
185
+ contrast: 0.5,
186
+ primary: "#FF0000",
187
+ secondary: "#00FF00",
188
+ customColors: [
189
+ { name: "brand", hex: "#FF5733", blend: true },
190
+ { name: "success", hex: "#28A745", blend: false },
191
+ ],
192
+ contrastAllColors: true,
193
+ });
194
+
195
+ theme.toJson();
196
+ theme.toCss();
197
+ ```
198
+
199
+ ## CLI
200
+
201
+ ```sh
202
+ $ npx react-mcu builder "#6750A4"
203
+ ```
204
+
205
+ will generate a `mcu-theme` folder with: `Light.tokens.json` and `Dark.tokens.json` [design-tokens](https://www.designtokens.org/tr/2025.10/) files, you can (both) import into Figma.
206
+
207
+ See `npx react-mcu builder --help` for all available options.
208
+
155
209
  # Dev
156
210
 
157
211
  ## INSTALL
@@ -177,6 +231,12 @@ Pre-requisites:
177
231
  $ pnpm i
178
232
  ```
179
233
 
234
+ ## Validation
235
+
236
+ ```sh
237
+ $ pnpm run lgtm
238
+ ```
239
+
180
240
  ## CONTRIBUTING
181
241
 
182
242
  When submitting a pull request, please include a changeset to document your
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { SchemeTonalSpot, SchemeMonochrome, SchemeNeutral, SchemeVibrant, SchemeExpressive, SchemeFidelity, SchemeContent, CustomColor } from '@material/material-color-utilities';
2
+ import { CustomColor, TonalPalette } from '@material/material-color-utilities';
3
3
 
4
4
  type HexCustomColor = Omit<CustomColor, "value"> & {
5
5
  hex: string;
@@ -29,7 +29,6 @@ type McuConfig = {
29
29
  * When false (default), colors may be adjusted for better harmonization.
30
30
  * Corresponds to "Color match - Stay true to my color inputs" in Material Theme Builder.
31
31
  *
32
- * @default false
33
32
  * @deprecated Not yet implemented. This prop is currently ignored.
34
33
  */
35
34
  colorMatch?: boolean;
@@ -45,29 +44,130 @@ type McuConfig = {
45
44
  * ```
46
45
  */
47
46
  customColors?: HexCustomColor[];
47
+ /**
48
+ * When true, applies the contrast level to all colors including custom colors and tonal palette shades.
49
+ * When false (default), only core colors are affected by the contrast level.
50
+ */
51
+ contrastAllColors?: boolean;
52
+ /**
53
+ * When true (default), tonal palette shades adapt to the theme (light/dark) with inverted tone values.
54
+ * In dark mode, high tones (light colors) map to low tones (dark colors) and vice versa.
55
+ * When false, shades remain the same across themes.
56
+ */
57
+ adaptiveShades?: boolean;
48
58
  };
49
- declare const schemesMap: {
50
- readonly tonalSpot: typeof SchemeTonalSpot;
51
- readonly monochrome: typeof SchemeMonochrome;
52
- readonly neutral: typeof SchemeNeutral;
53
- readonly vibrant: typeof SchemeVibrant;
54
- readonly expressive: typeof SchemeExpressive;
55
- readonly fidelity: typeof SchemeFidelity;
56
- readonly content: typeof SchemeContent;
57
- };
58
- declare const schemeNames: (keyof typeof schemesMap)[];
59
+ declare const schemeNames: readonly ["tonalSpot", "monochrome", "neutral", "vibrant", "expressive", "fidelity", "content"];
59
60
  type SchemeName = (typeof schemeNames)[number];
60
- declare function Mcu({ source, scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, colorMatch, customColors, children, }: McuConfig & {
61
+ declare const DEFAULT_SCHEME: SchemeName;
62
+ declare const DEFAULT_CONTRAST = 0;
63
+ declare const DEFAULT_CONTRAST_ALL_COLORS = false;
64
+ declare const DEFAULT_ADAPTIVE_SHADES = false;
65
+ declare const DEFAULT_BLEND = true;
66
+ declare function Mcu({ source, scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, colorMatch, customColors, contrastAllColors, adaptiveShades, children, }: McuConfig & {
61
67
  children?: React.ReactNode;
62
68
  }): react_jsx_runtime.JSX.Element;
63
- declare const tokenNames: readonly ["background", "onBackground", "surface", "surfaceDim", "surfaceBright", "surfaceContainerLowest", "surfaceContainerLow", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "onSurface", "onSurfaceVariant", "outline", "outlineVariant", "inverseSurface", "inverseOnSurface", "primary", "onPrimary", "primaryContainer", "onPrimaryContainer", "primaryFixed", "primaryFixedDim", "onPrimaryFixed", "onPrimaryFixedVariant", "inversePrimary", "primaryFixed", "primaryFixedDim", "onPrimaryFixed", "onPrimaryFixedVariant", "secondary", "onSecondary", "secondaryContainer", "onSecondaryContainer", "secondaryFixed", "secondaryFixedDim", "onSecondaryFixed", "onSecondaryFixedVariant", "tertiary", "onTertiary", "tertiaryContainer", "onTertiaryContainer", "tertiaryFixed", "tertiaryFixedDim", "onTertiaryFixed", "onTertiaryFixedVariant", "error", "onError", "errorContainer", "onErrorContainer", "scrim", "shadow"];
69
+ declare const tokenNames: readonly ["background", "onBackground", "surface", "surfaceDim", "surfaceBright", "surfaceContainerLowest", "surfaceContainerLow", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "onSurface", "surfaceVariant", "onSurfaceVariant", "outline", "outlineVariant", "inverseSurface", "inverseOnSurface", "primary", "surfaceTint", "onPrimary", "primaryContainer", "onPrimaryContainer", "primaryFixed", "primaryFixedDim", "onPrimaryFixed", "onPrimaryFixedVariant", "inversePrimary", "secondary", "onSecondary", "secondaryContainer", "onSecondaryContainer", "secondaryFixed", "secondaryFixedDim", "onSecondaryFixed", "onSecondaryFixedVariant", "tertiary", "onTertiary", "tertiaryContainer", "onTertiaryContainer", "tertiaryFixed", "tertiaryFixedDim", "onTertiaryFixed", "onTertiaryFixedVariant", "error", "onError", "errorContainer", "onErrorContainer", "scrim", "shadow"];
64
70
  type TokenName = (typeof tokenNames)[number];
71
+ declare function builder(hexSource: McuConfig["source"], { scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, customColors: hexCustomColors, contrastAllColors, adaptiveShades, }?: Omit<McuConfig, "source">): {
72
+ toCss(): string;
73
+ toJson(): {
74
+ seed: string;
75
+ coreColors: Record<string, string>;
76
+ extendedColors: {
77
+ name: string;
78
+ color: string;
79
+ description: string;
80
+ harmonized: boolean;
81
+ }[];
82
+ schemes: Record<string, Record<string, string>>;
83
+ palettes: Record<string, Record<string, string>>;
84
+ };
85
+ toFigmaTokens(): {
86
+ "Light.tokens.json": {
87
+ Schemes: Record<string, {
88
+ $type: "color";
89
+ $value: {
90
+ colorSpace: "srgb";
91
+ components: number[];
92
+ alpha: number;
93
+ hex: string;
94
+ };
95
+ $extensions: {
96
+ "com.figma.scopes": string[];
97
+ "com.figma.isOverride": boolean;
98
+ };
99
+ }>;
100
+ Palettes: Record<string, Record<string, {
101
+ $type: "color";
102
+ $value: {
103
+ colorSpace: "srgb";
104
+ components: number[];
105
+ alpha: number;
106
+ hex: string;
107
+ };
108
+ $extensions: {
109
+ "com.figma.scopes": string[];
110
+ "com.figma.isOverride": boolean;
111
+ };
112
+ }>>;
113
+ $extensions: {
114
+ "com.figma.modeName": string;
115
+ };
116
+ };
117
+ "Dark.tokens.json": {
118
+ Schemes: Record<string, {
119
+ $type: "color";
120
+ $value: {
121
+ colorSpace: "srgb";
122
+ components: number[];
123
+ alpha: number;
124
+ hex: string;
125
+ };
126
+ $extensions: {
127
+ "com.figma.scopes": string[];
128
+ "com.figma.isOverride": boolean;
129
+ };
130
+ }>;
131
+ Palettes: Record<string, Record<string, {
132
+ $type: "color";
133
+ $value: {
134
+ colorSpace: "srgb";
135
+ components: number[];
136
+ alpha: number;
137
+ hex: string;
138
+ };
139
+ $extensions: {
140
+ "com.figma.scopes": string[];
141
+ "com.figma.isOverride": boolean;
142
+ };
143
+ }>>;
144
+ $extensions: {
145
+ "com.figma.modeName": string;
146
+ };
147
+ };
148
+ };
149
+ mergedColorsLight: {
150
+ [x: string]: number;
151
+ };
152
+ mergedColorsDark: {
153
+ [x: string]: number;
154
+ };
155
+ allPalettes: {
156
+ primary: TonalPalette;
157
+ secondary: TonalPalette;
158
+ tertiary: TonalPalette;
159
+ error: TonalPalette;
160
+ neutral: TonalPalette;
161
+ "neutral-variant": TonalPalette;
162
+ };
163
+ };
65
164
 
66
165
  type Api = {
67
166
  initials: McuConfig;
68
167
  setMcuConfig: (config: McuConfig) => void;
69
168
  getMcuColor: (colorName: TokenName, theme?: string) => string;
169
+ allPalettes: Record<string, TonalPalette>;
70
170
  };
71
171
  declare const useMcu: () => Api;
72
172
 
73
- export { Mcu, useMcu };
173
+ export { DEFAULT_ADAPTIVE_SHADES, DEFAULT_BLEND, DEFAULT_CONTRAST, DEFAULT_CONTRAST_ALL_COLORS, DEFAULT_SCHEME, Mcu, builder, schemeNames, useMcu };
package/dist/index.js CHANGED
@@ -4,11 +4,14 @@
4
4
  import {
5
5
  argbFromHex,
6
6
  Blend,
7
+ blueFromArgb,
7
8
  DynamicColor,
8
9
  DynamicScheme,
10
+ greenFromArgb,
9
11
  Hct,
10
12
  hexFromArgb as hexFromArgb2,
11
13
  MaterialDynamicColors,
14
+ redFromArgb,
12
15
  SchemeContent,
13
16
  SchemeExpressive,
14
17
  SchemeFidelity,
@@ -18,12 +21,14 @@ import {
18
21
  SchemeVibrant,
19
22
  TonalPalette
20
23
  } from "@material/material-color-utilities";
21
- import { kebabCase, upperFirst } from "lodash-es";
24
+ import { kebabCase, startCase, upperFirst } from "lodash-es";
22
25
  import { useMemo as useMemo2 } from "react";
23
26
 
24
27
  // src/Mcu.context.tsx
25
- import { hexFromArgb } from "@material/material-color-utilities";
26
28
  import {
29
+ hexFromArgb
30
+ } from "@material/material-color-utilities";
31
+ import React, {
27
32
  useCallback,
28
33
  useInsertionEffect,
29
34
  useMemo,
@@ -48,24 +53,20 @@ var createRequiredContext = () => {
48
53
  import { jsx } from "react/jsx-runtime";
49
54
  var [useMcu, Provider, McuContext] = createRequiredContext();
50
55
  var McuProvider = ({
51
- source: initialSource,
52
- scheme: initialScheme,
53
- contrast: initialContrast,
54
- customColors: initialCustomColors,
55
56
  styleId,
56
- children
57
+ children,
58
+ ...configProps
57
59
  }) => {
58
- const [initials] = useState(() => ({
59
- source: initialSource,
60
- scheme: initialScheme,
61
- contrast: initialContrast,
62
- customColors: initialCustomColors
63
- }));
60
+ const [initials] = useState(() => configProps);
64
61
  const [mcuConfig, setMcuConfig] = useState(initials);
65
- const { css, mergedColorsLight, mergedColorsDark } = useMemo(
66
- () => generateCss(mcuConfig),
67
- [mcuConfig]
68
- );
62
+ const configKey = JSON.stringify(configProps);
63
+ React.useEffect(() => {
64
+ setMcuConfig(configProps);
65
+ }, [configKey]);
66
+ const { css, mergedColorsLight, mergedColorsDark, allPalettes } = useMemo(() => {
67
+ const { toCss, ...rest } = builder(mcuConfig.source, mcuConfig);
68
+ return { css: toCss(), ...rest };
69
+ }, [mcuConfig]);
69
70
  useInsertionEffect(() => {
70
71
  let tag = document.getElementById(styleId);
71
72
  if (!tag) {
@@ -77,9 +78,12 @@ var McuProvider = ({
77
78
  }, [css, styleId]);
78
79
  const getMcuColor = useCallback(
79
80
  (colorName, theme) => {
80
- return hexFromArgb(
81
- (theme === "light" ? mergedColorsLight : mergedColorsDark)[colorName]
82
- );
81
+ const mergedColors = theme === "light" ? mergedColorsLight : mergedColorsDark;
82
+ const colorValue = mergedColors[colorName];
83
+ if (colorValue === void 0) {
84
+ throw new Error(`Unknown MCU token '${colorName}'`);
85
+ }
86
+ return hexFromArgb(colorValue);
83
87
  },
84
88
  [mergedColorsDark, mergedColorsLight]
85
89
  );
@@ -87,15 +91,32 @@ var McuProvider = ({
87
91
  () => ({
88
92
  initials,
89
93
  setMcuConfig,
90
- getMcuColor
94
+ getMcuColor,
95
+ allPalettes
91
96
  }),
92
- [getMcuColor, initials]
97
+ [getMcuColor, initials, allPalettes]
93
98
  );
94
99
  return /* @__PURE__ */ jsx(Provider, { value, children });
95
100
  };
96
101
 
97
102
  // src/Mcu.tsx
98
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
103
+ import { jsx as jsx2 } from "react/jsx-runtime";
104
+ function adjustToneForContrast(baseTone, contrastLevel, adjustmentFactor = DEFAULT_CONTRAST_ADJUSTMENT_FACTOR) {
105
+ if (contrastLevel === 0) return baseTone;
106
+ const distanceToCenter = baseTone - 50;
107
+ const delta = distanceToCenter * contrastLevel * adjustmentFactor;
108
+ const adjustedTone = baseTone + delta;
109
+ return Math.max(0, Math.min(100, adjustedTone));
110
+ }
111
+ var schemeNames = [
112
+ "tonalSpot",
113
+ "monochrome",
114
+ "neutral",
115
+ "vibrant",
116
+ "expressive",
117
+ "fidelity",
118
+ "content"
119
+ ];
99
120
  var schemesMap = {
100
121
  tonalSpot: SchemeTonalSpot,
101
122
  monochrome: SchemeMonochrome,
@@ -105,13 +126,14 @@ var schemesMap = {
105
126
  fidelity: SchemeFidelity,
106
127
  content: SchemeContent
107
128
  };
108
- var schemeNames = Object.keys(
109
- schemesMap
110
- );
111
129
  var DEFAULT_SCHEME = "tonalSpot";
112
130
  var DEFAULT_CONTRAST = 0;
113
131
  var DEFAULT_COLOR_MATCH = false;
114
132
  var DEFAULT_CUSTOM_COLORS = [];
133
+ var DEFAULT_CONTRAST_ALL_COLORS = false;
134
+ var DEFAULT_ADAPTIVE_SHADES = false;
135
+ var DEFAULT_BLEND = true;
136
+ var DEFAULT_CONTRAST_ADJUSTMENT_FACTOR = 0.2;
115
137
  var STANDARD_TONES = [
116
138
  0,
117
139
  5,
@@ -165,6 +187,8 @@ function Mcu({
165
187
  error,
166
188
  colorMatch = DEFAULT_COLOR_MATCH,
167
189
  customColors = DEFAULT_CUSTOM_COLORS,
190
+ contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS,
191
+ adaptiveShades = DEFAULT_ADAPTIVE_SHADES,
168
192
  children
169
193
  }) {
170
194
  const config = useMemo2(
@@ -179,7 +203,10 @@ function Mcu({
179
203
  neutralVariant,
180
204
  error,
181
205
  colorMatch,
182
- customColors
206
+ customColors,
207
+ // extras features
208
+ contrastAllColors,
209
+ adaptiveShades
183
210
  }),
184
211
  [
185
212
  contrast,
@@ -192,14 +219,12 @@ function Mcu({
192
219
  neutral,
193
220
  neutralVariant,
194
221
  error,
195
- colorMatch
222
+ colorMatch,
223
+ contrastAllColors,
224
+ adaptiveShades
196
225
  ]
197
226
  );
198
- const { css } = useMemo2(() => generateCss(config), [config]);
199
- return /* @__PURE__ */ jsxs(Fragment, { children: [
200
- /* @__PURE__ */ jsx2("style", { id: mcuStyleId, children: css }),
201
- /* @__PURE__ */ jsx2(McuProvider, { ...config, styleId: mcuStyleId, children })
202
- ] });
227
+ return /* @__PURE__ */ jsx2(McuProvider, { ...config, styleId: mcuStyleId, children });
203
228
  }
204
229
  var tokenNames = [
205
230
  "background",
@@ -213,13 +238,14 @@ var tokenNames = [
213
238
  "surfaceContainerHigh",
214
239
  "surfaceContainerHighest",
215
240
  "onSurface",
241
+ "surfaceVariant",
216
242
  "onSurfaceVariant",
217
243
  "outline",
218
244
  "outlineVariant",
219
245
  "inverseSurface",
220
246
  "inverseOnSurface",
221
247
  "primary",
222
- // "primaryDim",
248
+ "surfaceTint",
223
249
  "onPrimary",
224
250
  "primaryContainer",
225
251
  "onPrimaryContainer",
@@ -228,12 +254,7 @@ var tokenNames = [
228
254
  "onPrimaryFixed",
229
255
  "onPrimaryFixedVariant",
230
256
  "inversePrimary",
231
- "primaryFixed",
232
- "primaryFixedDim",
233
- "onPrimaryFixed",
234
- "onPrimaryFixedVariant",
235
257
  "secondary",
236
- // "secondaryDim",
237
258
  "onSecondary",
238
259
  "secondaryContainer",
239
260
  "onSecondaryContainer",
@@ -242,7 +263,6 @@ var tokenNames = [
242
263
  "onSecondaryFixed",
243
264
  "onSecondaryFixedVariant",
244
265
  "tertiary",
245
- // "tertiaryDim",
246
266
  "onTertiary",
247
267
  "tertiaryContainer",
248
268
  "onTertiaryContainer",
@@ -251,24 +271,14 @@ var tokenNames = [
251
271
  "onTertiaryFixed",
252
272
  "onTertiaryFixedVariant",
253
273
  "error",
254
- // "errorDim",
255
274
  "onError",
256
275
  "errorContainer",
257
276
  "onErrorContainer",
258
277
  "scrim",
259
- // added manually, was missing
260
278
  "shadow"
261
- // added manually, was missing
262
279
  ];
263
280
  function toRecord(arr, getEntry) {
264
- return arr.reduce(
265
- (acc, item) => {
266
- const [key, value] = getEntry(item);
267
- acc[key] = value;
268
- return acc;
269
- },
270
- {}
271
- );
281
+ return Object.fromEntries(arr.map(getEntry));
272
282
  }
273
283
  function getPalette(palettes, colorName) {
274
284
  const palette = palettes[colorName];
@@ -279,7 +289,7 @@ function getPalette(palettes, colorName) {
279
289
  }
280
290
  return palette;
281
291
  }
282
- function mergeBaseAndCustomColors(scheme, customColors, colorPalettes) {
292
+ function mergeBaseAndCustomColors(scheme, customColors, colorPalettes, contrastAllColors) {
283
293
  const baseVars = toRecord(tokenNames, (tokenName) => {
284
294
  const dynamicColor = MaterialDynamicColors[tokenName];
285
295
  const argb = dynamicColor.getArgb(scheme);
@@ -288,32 +298,39 @@ function mergeBaseAndCustomColors(scheme, customColors, colorPalettes) {
288
298
  const customVars = {};
289
299
  customColors.forEach((color) => {
290
300
  const colorname = color.name;
301
+ const getPaletteForColor = (s) => getPalette(colorPalettes, colorname);
302
+ const getTone = (baseTone) => (s) => {
303
+ if (!contrastAllColors) return baseTone;
304
+ return adjustToneForContrast(baseTone, s.contrastLevel);
305
+ };
291
306
  const colorDynamicColor = new DynamicColor(
292
307
  colorname,
293
- (s) => getPalette(colorPalettes, colorname),
294
- (s) => s.isDark ? 80 : 40,
295
- // Same as primary
296
- false
308
+ getPaletteForColor,
309
+ (s) => getTone(s.isDark ? 80 : 40)(s),
310
+ // Main color: lighter in dark mode, darker in light mode
311
+ true
312
+ // background
297
313
  );
298
314
  const onColorDynamicColor = new DynamicColor(
299
315
  `on${upperFirst(colorname)}`,
300
- (s) => getPalette(colorPalettes, colorname),
301
- (s) => s.isDark ? 20 : 100,
302
- // Same as onPrimary
316
+ getPaletteForColor,
317
+ (s) => getTone(s.isDark ? 20 : 100)(s),
318
+ // Text on main color: high contrast (dark on light, light on dark)
303
319
  false
304
320
  );
305
321
  const containerDynamicColor = new DynamicColor(
306
322
  `${colorname}Container`,
307
- (s) => getPalette(colorPalettes, colorname),
308
- (s) => s.isDark ? 30 : 90,
309
- // Same as primaryContainer
310
- false
323
+ getPaletteForColor,
324
+ (s) => getTone(s.isDark ? 30 : 90)(s),
325
+ // Container: subtle variant (darker in dark mode, lighter in light mode)
326
+ true
327
+ // background
311
328
  );
312
329
  const onContainerDynamicColor = new DynamicColor(
313
330
  `on${upperFirst(colorname)}Container`,
314
- (s) => getPalette(colorPalettes, colorname),
315
- (s) => s.isDark ? 90 : 30,
316
- // Same as onPrimaryContainer
331
+ getPaletteForColor,
332
+ (s) => getTone(s.isDark ? 90 : 30)(s),
333
+ // Text on container: high contrast against container background
317
334
  false
318
335
  );
319
336
  customVars[colorname] = colorDynamicColor.getArgb(scheme);
@@ -323,17 +340,6 @@ function mergeBaseAndCustomColors(scheme, customColors, colorPalettes) {
323
340
  });
324
341
  return { ...baseVars, ...customVars };
325
342
  }
326
- var cssVar = (colorName, colorValue) => {
327
- const name = `--mcu-${kebabCase(colorName)}`;
328
- const value = hexFromArgb2(colorValue);
329
- return `${name}:${value};`;
330
- };
331
- var generateTonalPaletteVars = (paletteName, palette) => {
332
- return STANDARD_TONES.map((tone) => {
333
- const color = palette.tone(tone);
334
- return cssVar(`${paletteName}-${tone}`, color);
335
- }).join(" ");
336
- };
337
343
  function createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonization) {
338
344
  const colorArgb = argbFromHex(colorDef.hex);
339
345
  const harmonizedArgb = colorDef.blend ? Blend.harmonize(colorArgb, effectiveSourceForHarmonization) : colorArgb;
@@ -352,11 +358,7 @@ function createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonizatio
352
358
  }
353
359
  return TonalPalette.fromHueAndChroma(hct.hue, targetChroma);
354
360
  }
355
- var toCssVars = (mergedColors) => {
356
- return Object.entries(mergedColors).map(([name, value]) => cssVar(name, value)).join(" ");
357
- };
358
- function generateCss({
359
- source: hexSource,
361
+ function builder(hexSource, {
360
362
  scheme = DEFAULT_SCHEME,
361
363
  contrast = DEFAULT_CONTRAST,
362
364
  primary,
@@ -365,10 +367,12 @@ function generateCss({
365
367
  neutral,
366
368
  neutralVariant,
367
369
  error,
368
- colorMatch = DEFAULT_COLOR_MATCH,
369
- customColors: hexCustomColors = DEFAULT_CUSTOM_COLORS
370
- }) {
370
+ customColors: hexCustomColors = DEFAULT_CUSTOM_COLORS,
371
+ contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS,
372
+ adaptiveShades = DEFAULT_ADAPTIVE_SHADES
373
+ } = {}) {
371
374
  const sourceArgb = argbFromHex(hexSource);
375
+ const sourceHct = Hct.fromInt(sourceArgb);
372
376
  const effectiveSource = primary || hexSource;
373
377
  const effectiveSourceArgb = argbFromHex(effectiveSource);
374
378
  const effectiveSourceForHarmonization = primary ? argbFromHex(primary) : sourceArgb;
@@ -427,12 +431,8 @@ function generateCss({
427
431
  createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonization)
428
432
  ])
429
433
  );
430
- const createSchemes = (baseConfig) => [
431
- new DynamicScheme({ ...baseConfig, isDark: false }),
432
- new DynamicScheme({ ...baseConfig, isDark: true })
433
- ];
434
434
  const variant = schemeToVariant[scheme];
435
- const [lightScheme, darkScheme] = createSchemes({
435
+ const schemeConfig = {
436
436
  sourceColorArgb: effectiveSourceArgb,
437
437
  variant,
438
438
  contrastLevel: contrast,
@@ -441,56 +441,361 @@ function generateCss({
441
441
  tertiaryPalette: colorPalettes["tertiary"] || baseScheme.tertiaryPalette,
442
442
  neutralPalette: colorPalettes["neutral"] || baseScheme.neutralPalette,
443
443
  neutralVariantPalette: colorPalettes["neutralVariant"] || baseScheme.neutralVariantPalette
444
- });
444
+ };
445
+ const lightScheme = new DynamicScheme({ ...schemeConfig, isDark: false });
446
+ const darkScheme = new DynamicScheme({ ...schemeConfig, isDark: true });
445
447
  const errorPalette = colorPalettes["error"];
446
448
  if (errorPalette) {
447
449
  lightScheme.errorPalette = errorPalette;
448
450
  darkScheme.errorPalette = errorPalette;
449
451
  }
452
+ const allPalettes = {
453
+ primary: lightScheme.primaryPalette,
454
+ secondary: lightScheme.secondaryPalette,
455
+ tertiary: lightScheme.tertiaryPalette,
456
+ error: lightScheme.errorPalette,
457
+ neutral: lightScheme.neutralPalette,
458
+ "neutral-variant": lightScheme.neutralVariantPalette,
459
+ // Add custom color palettes
460
+ ...Object.fromEntries(
461
+ definedColors.filter((c) => !c.core).map((colorDef) => [colorDef.name, colorPalettes[colorDef.name]])
462
+ )
463
+ };
450
464
  const customColors = definedColors.filter((c) => !c.core).map((c) => ({
451
465
  name: c.name,
452
- blend: c.blend ?? false,
466
+ blend: c.blend ?? DEFAULT_BLEND,
453
467
  value: argbFromHex(c.hex)
454
468
  }));
455
469
  const mergedColorsLight = mergeBaseAndCustomColors(
456
470
  lightScheme,
457
471
  customColors,
458
- colorPalettes
472
+ colorPalettes,
473
+ contrastAllColors
459
474
  );
460
475
  const mergedColorsDark = mergeBaseAndCustomColors(
461
476
  darkScheme,
462
477
  customColors,
463
- colorPalettes
478
+ colorPalettes,
479
+ contrastAllColors
464
480
  );
465
- const lightVars = toCssVars(mergedColorsLight);
466
- const darkVars = toCssVars(mergedColorsDark);
467
- const allTonalVars = [
468
- // Core colors from the scheme
469
- generateTonalPaletteVars("primary", lightScheme.primaryPalette),
470
- generateTonalPaletteVars("secondary", lightScheme.secondaryPalette),
471
- generateTonalPaletteVars("tertiary", lightScheme.tertiaryPalette),
472
- generateTonalPaletteVars("error", lightScheme.errorPalette),
473
- generateTonalPaletteVars("neutral", lightScheme.neutralPalette),
474
- generateTonalPaletteVars(
475
- "neutral-variant",
476
- lightScheme.neutralVariantPalette
477
- ),
478
- // Custom colors from our unified palette map
479
- ...customColors.map((customColorObj) => {
480
- const palette = getPalette(colorPalettes, customColorObj.name);
481
- return generateTonalPaletteVars(kebabCase(customColorObj.name), palette);
482
- })
483
- ].join(" ");
484
481
  return {
485
- css: `
486
- :root { ${lightVars} ${allTonalVars} }
487
- .dark { ${darkVars} }
488
- `,
482
+ //
483
+ // ██████ ███████ ███████
484
+ // ██ ██ ██
485
+ // ██ ███████ ███████
486
+ // ██ ██ ██
487
+ // ██████ ███████ ███████
488
+ //
489
+ toCss() {
490
+ function cssVar(colorName, colorValue) {
491
+ const name = `--mcu-${kebabCase(colorName)}`;
492
+ const value = hexFromArgb2(colorValue);
493
+ return `${name}:${value};`;
494
+ }
495
+ function toCssVars(mergedColors) {
496
+ return Object.entries(mergedColors).map(([name, value]) => cssVar(name, value)).join(" ");
497
+ }
498
+ function generateTonalPaletteVars(paletteName, palette, scheme2, applyContrast, adaptiveShades2) {
499
+ return STANDARD_TONES.map((tone) => {
500
+ let toneToUse = tone;
501
+ if (adaptiveShades2 && scheme2.isDark) {
502
+ toneToUse = 100 - tone;
503
+ }
504
+ if (applyContrast) {
505
+ toneToUse = adjustToneForContrast(toneToUse, scheme2.contrastLevel);
506
+ }
507
+ const color = palette.tone(toneToUse);
508
+ return cssVar(`${paletteName}-${tone}`, color);
509
+ }).join(" ");
510
+ }
511
+ function generateTonalVars(s) {
512
+ return Object.entries(allPalettes).map(
513
+ ([name, palette]) => generateTonalPaletteVars(
514
+ kebabCase(name),
515
+ palette,
516
+ s,
517
+ contrastAllColors,
518
+ adaptiveShades
519
+ )
520
+ ).join(" ");
521
+ }
522
+ const lightVars = toCssVars(mergedColorsLight);
523
+ const darkVars = toCssVars(mergedColorsDark);
524
+ const lightTonalVars = generateTonalVars(lightScheme);
525
+ const darkTonalVars = generateTonalVars(darkScheme);
526
+ return `
527
+ :root { ${lightVars} ${lightTonalVars} }
528
+ .dark { ${darkVars} ${adaptiveShades ? darkTonalVars : lightTonalVars} }
529
+ `;
530
+ },
531
+ //
532
+ // ██ ███████ ██████ ███ ██
533
+ // ██ ██ ██ ██ ████ ██
534
+ // ██ ███████ ██ ██ ██ ██ ██
535
+ // ██ ██ ██ ██ ██ ██ ██ ██
536
+ // █████ ███████ ██████ ██ ████
537
+ //
538
+ toJson() {
539
+ const fixtureTokenOrder = [
540
+ "primary",
541
+ "surfaceTint",
542
+ "onPrimary",
543
+ "primaryContainer",
544
+ "onPrimaryContainer",
545
+ "secondary",
546
+ "onSecondary",
547
+ "secondaryContainer",
548
+ "onSecondaryContainer",
549
+ "tertiary",
550
+ "onTertiary",
551
+ "tertiaryContainer",
552
+ "onTertiaryContainer",
553
+ "error",
554
+ "onError",
555
+ "errorContainer",
556
+ "onErrorContainer",
557
+ "background",
558
+ "onBackground",
559
+ "surface",
560
+ "onSurface",
561
+ "surfaceVariant",
562
+ "onSurfaceVariant",
563
+ "outline",
564
+ "outlineVariant",
565
+ "shadow",
566
+ "scrim",
567
+ "inverseSurface",
568
+ "inverseOnSurface",
569
+ "inversePrimary",
570
+ "primaryFixed",
571
+ "onPrimaryFixed",
572
+ "primaryFixedDim",
573
+ "onPrimaryFixedVariant",
574
+ "secondaryFixed",
575
+ "onSecondaryFixed",
576
+ "secondaryFixedDim",
577
+ "onSecondaryFixedVariant",
578
+ "tertiaryFixed",
579
+ "onTertiaryFixed",
580
+ "tertiaryFixedDim",
581
+ "onTertiaryFixedVariant",
582
+ "surfaceDim",
583
+ "surfaceBright",
584
+ "surfaceContainerLowest",
585
+ "surfaceContainerLow",
586
+ "surfaceContainer",
587
+ "surfaceContainerHigh",
588
+ "surfaceContainerHighest"
589
+ ];
590
+ const neuHct = neutral ? Hct.fromInt(argbFromHex(neutral)) : sourceHct;
591
+ const nvHct = neutralVariant ? Hct.fromInt(argbFromHex(neutralVariant)) : sourceHct;
592
+ const rawPalettes = {
593
+ primary: TonalPalette.fromInt(effectiveSourceArgb),
594
+ secondary: secondary ? TonalPalette.fromInt(argbFromHex(secondary)) : TonalPalette.fromHueAndChroma(sourceHct.hue, sourceHct.chroma / 3),
595
+ tertiary: tertiary ? TonalPalette.fromInt(argbFromHex(tertiary)) : TonalPalette.fromHueAndChroma(
596
+ (sourceHct.hue + 60) % 360,
597
+ sourceHct.chroma / 2
598
+ ),
599
+ neutral: TonalPalette.fromHueAndChroma(
600
+ neuHct.hue,
601
+ Math.min(neuHct.chroma / 12, 4)
602
+ ),
603
+ "neutral-variant": TonalPalette.fromHueAndChroma(
604
+ nvHct.hue,
605
+ Math.min(nvHct.chroma / 6, 8)
606
+ )
607
+ };
608
+ function buildJsonSchemes() {
609
+ function extractSchemeColors(scheme2, backgroundScheme) {
610
+ const colors = {};
611
+ for (const tokenName of fixtureTokenOrder) {
612
+ const dynamicColor = MaterialDynamicColors[tokenName];
613
+ const useScheme = backgroundScheme && (tokenName === "background" || tokenName === "onBackground") ? backgroundScheme : scheme2;
614
+ colors[tokenName] = hexFromArgb2(
615
+ dynamicColor.getArgb(useScheme)
616
+ ).toUpperCase();
617
+ }
618
+ return colors;
619
+ }
620
+ function resolveOverridePalette(hex, role) {
621
+ if (!hex) return null;
622
+ return new SchemeClass(Hct.fromInt(argbFromHex(hex)), false, 0)[role];
623
+ }
624
+ const secPalette = resolveOverridePalette(secondary, "primaryPalette");
625
+ const terPalette = resolveOverridePalette(tertiary, "primaryPalette");
626
+ const errPalette = resolveOverridePalette(error, "primaryPalette");
627
+ const neuPalette = resolveOverridePalette(neutral, "neutralPalette");
628
+ const nvPalette = resolveOverridePalette(
629
+ neutralVariant,
630
+ "neutralVariantPalette"
631
+ );
632
+ const jsonSchemes = {};
633
+ const jsonContrastLevels = [
634
+ { name: "light", isDark: false, contrast: 0 },
635
+ { name: "light-medium-contrast", isDark: false, contrast: 0.5 },
636
+ { name: "light-high-contrast", isDark: false, contrast: 1 },
637
+ { name: "dark", isDark: true, contrast: 0 },
638
+ { name: "dark-medium-contrast", isDark: true, contrast: 0.5 },
639
+ { name: "dark-high-contrast", isDark: true, contrast: 1 }
640
+ ];
641
+ for (const { name, isDark, contrast: contrast2 } of jsonContrastLevels) {
642
+ const baseScheme2 = new SchemeClass(primaryHct, isDark, contrast2);
643
+ const composedScheme = new DynamicScheme({
644
+ sourceColorArgb: effectiveSourceArgb,
645
+ variant: schemeToVariant[scheme],
646
+ contrastLevel: contrast2,
647
+ isDark,
648
+ primaryPalette: baseScheme2.primaryPalette,
649
+ secondaryPalette: secPalette || baseScheme2.secondaryPalette,
650
+ tertiaryPalette: terPalette || baseScheme2.tertiaryPalette,
651
+ neutralPalette: neuPalette || baseScheme2.neutralPalette,
652
+ neutralVariantPalette: nvPalette || baseScheme2.neutralVariantPalette
653
+ });
654
+ if (errPalette) composedScheme.errorPalette = errPalette;
655
+ jsonSchemes[name] = extractSchemeColors(composedScheme, baseScheme2);
656
+ }
657
+ return jsonSchemes;
658
+ }
659
+ function rawPalettesToJson() {
660
+ const jsonPalettes = {};
661
+ const RAW_PALETTE_NAMES = [
662
+ "primary",
663
+ "secondary",
664
+ "tertiary",
665
+ "neutral",
666
+ "neutral-variant"
667
+ ];
668
+ for (const name of RAW_PALETTE_NAMES) {
669
+ const palette = rawPalettes[name];
670
+ const tones = {};
671
+ for (const tone of STANDARD_TONES) {
672
+ tones[tone.toString()] = hexFromArgb2(
673
+ palette.tone(tone)
674
+ ).toUpperCase();
675
+ }
676
+ jsonPalettes[name] = tones;
677
+ }
678
+ return jsonPalettes;
679
+ }
680
+ function buildCoreColors(opts) {
681
+ const colors = { primary: opts.primary };
682
+ if (opts.secondary) colors.secondary = opts.secondary.toUpperCase();
683
+ if (opts.tertiary) colors.tertiary = opts.tertiary.toUpperCase();
684
+ if (opts.error) colors.error = opts.error.toUpperCase();
685
+ if (opts.neutral) colors.neutral = opts.neutral.toUpperCase();
686
+ if (opts.neutralVariant)
687
+ colors.neutralVariant = opts.neutralVariant.toUpperCase();
688
+ return colors;
689
+ }
690
+ const seed = hexSource.toUpperCase();
691
+ const coreColors = buildCoreColors({
692
+ primary: (primary || hexSource).toUpperCase(),
693
+ secondary,
694
+ tertiary,
695
+ error,
696
+ neutral,
697
+ neutralVariant
698
+ });
699
+ const extendedColors = hexCustomColors.map((c) => ({
700
+ name: c.name,
701
+ color: c.hex.toUpperCase(),
702
+ description: "",
703
+ harmonized: c.blend ?? DEFAULT_BLEND
704
+ }));
705
+ return {
706
+ seed,
707
+ coreColors,
708
+ extendedColors,
709
+ schemes: buildJsonSchemes(),
710
+ palettes: rawPalettesToJson()
711
+ };
712
+ },
713
+ //
714
+ // ███████ ██ ██████ ███ ███ █████
715
+ // ██ ██ ██ ████ ████ ██ ██
716
+ // █████ ██ ██ ███ ██ ████ ██ ███████
717
+ // ██ ██ ██ ██ ██ ██ ██ ██ ██
718
+ // ██ ██ ██████ ██ ██ ██ ██
719
+ //
720
+ toFigmaTokens() {
721
+ function argbToFigmaColorValue(argb) {
722
+ return {
723
+ colorSpace: "srgb",
724
+ components: [
725
+ redFromArgb(argb) / 255,
726
+ greenFromArgb(argb) / 255,
727
+ blueFromArgb(argb) / 255
728
+ ],
729
+ alpha: 1,
730
+ hex: hexFromArgb2(argb).toUpperCase()
731
+ };
732
+ }
733
+ function figmaToken(argb) {
734
+ return {
735
+ $type: "color",
736
+ $value: argbToFigmaColorValue(argb),
737
+ $extensions: {
738
+ "com.figma.scopes": ["ALL_SCOPES"],
739
+ "com.figma.isOverride": true
740
+ }
741
+ };
742
+ }
743
+ function buildFigmaSchemeTokens(mergedColors) {
744
+ const tokens = {};
745
+ for (const [name, argb] of Object.entries(mergedColors)) {
746
+ tokens[startCase(name)] = figmaToken(argb);
747
+ }
748
+ return tokens;
749
+ }
750
+ function buildFigmaPaletteTokens(isDark) {
751
+ const palettes = {};
752
+ for (const [name, palette] of Object.entries(allPalettes)) {
753
+ const tones = {};
754
+ for (const tone of STANDARD_TONES) {
755
+ let toneToUse = tone;
756
+ if (adaptiveShades && isDark) {
757
+ toneToUse = 100 - tone;
758
+ }
759
+ if (contrastAllColors) {
760
+ toneToUse = adjustToneForContrast(toneToUse, contrast);
761
+ }
762
+ const argb = palette.tone(toneToUse);
763
+ tones[tone.toString()] = figmaToken(argb);
764
+ }
765
+ palettes[startCase(name)] = tones;
766
+ }
767
+ return palettes;
768
+ }
769
+ function buildModeFile(modeName, mergedColors, isDark) {
770
+ return {
771
+ Schemes: buildFigmaSchemeTokens(mergedColors),
772
+ Palettes: buildFigmaPaletteTokens(isDark),
773
+ $extensions: {
774
+ "com.figma.modeName": modeName
775
+ }
776
+ };
777
+ }
778
+ return {
779
+ "Light.tokens.json": buildModeFile("Light", mergedColorsLight, false),
780
+ "Dark.tokens.json": buildModeFile("Dark", mergedColorsDark, true)
781
+ };
782
+ },
783
+ //
784
+ // API
785
+ //
489
786
  mergedColorsLight,
490
- mergedColorsDark
787
+ mergedColorsDark,
788
+ allPalettes
491
789
  };
492
790
  }
493
791
  export {
792
+ DEFAULT_ADAPTIVE_SHADES,
793
+ DEFAULT_BLEND,
794
+ DEFAULT_CONTRAST,
795
+ DEFAULT_CONTRAST_ALL_COLORS,
796
+ DEFAULT_SCHEME,
494
797
  Mcu,
798
+ builder,
799
+ schemeNames,
495
800
  useMcu
496
801
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mcu",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A React component library",
5
5
  "keywords": [
6
6
  "react",
@@ -28,32 +28,50 @@
28
28
  "dist",
29
29
  "src/tailwind.css"
30
30
  ],
31
+ "bin": {
32
+ "react-mcu": "./src/cli.ts"
33
+ },
31
34
  "type": "module",
32
35
  "devDependencies": {
33
36
  "@arethetypeswrong/cli": "^0.18.2",
34
37
  "@changesets/cli": "^2.27.7",
38
+ "@chromatic-com/storybook": "^5.0.0",
39
+ "@eslint/js": "^10.0.1",
35
40
  "@storybook/addon-docs": "^10.1.11",
36
41
  "@storybook/addon-themes": "^10.1.11",
37
42
  "@storybook/react-vite": "^10.1.11",
38
43
  "@tailwindcss/postcss": "^4.1.18",
39
44
  "@testing-library/dom": "^10.4.1",
40
45
  "@testing-library/react": "^16.3.1",
46
+ "@types/culori": "^4.0.1",
41
47
  "@types/lodash-es": "^4.17.12",
42
48
  "@types/react": "^19.2.7",
43
49
  "@types/react-dom": "^19.2.3",
44
50
  "@vitejs/plugin-react": "^5.1.2",
51
+ "ajv": "^8.18.0",
52
+ "ajv-formats": "^3.0.1",
45
53
  "chromatic": "^13.3.5",
54
+ "class-variance-authority": "^0.7.1",
55
+ "clsx": "^2.1.1",
56
+ "culori": "^4.0.2",
57
+ "eslint": "^10.0.0",
58
+ "eslint-plugin-react-hooks": "^7.0.1",
59
+ "eslint-plugin-sonarjs": "^3.0.7",
60
+ "globals": "^17.3.0",
46
61
  "husky": "^9.1.7",
47
62
  "jsdom": "^27.4.0",
48
63
  "lint-staged": "^16.2.7",
49
64
  "postcss": "^8.5.6",
50
65
  "prettier": "^3.3.3",
66
+ "prettier-plugin-organize-imports": "^4.3.0",
51
67
  "react": "^19.2.3",
52
68
  "react-dom": "^19.2.3",
53
69
  "storybook": "^10.1.11",
70
+ "tailwind-merge": "^3.4.0",
54
71
  "tailwindcss": "^4.1.18",
55
72
  "tsup": "^8.2.4",
56
73
  "typescript": "^5.5.4",
74
+ "typescript-eslint": "^8.55.0",
57
75
  "vitest": "^4.0.16"
58
76
  },
59
77
  "peerDependencies": {
@@ -62,13 +80,17 @@
62
80
  },
63
81
  "dependencies": {
64
82
  "@material/material-color-utilities": "^0.3.0",
65
- "lodash-es": "^4.17.22"
83
+ "commander": "^14.0.3",
84
+ "lodash-es": "^4.17.22",
85
+ "tsx": "^4.21.0"
66
86
  },
67
87
  "scripts": {
68
88
  "build": "tsup",
69
- "lgtm": "pnpm run build && pnpm run check-format && pnpm run check-exports && pnpm run typecheck && pnpm run test",
89
+ "lint": "eslint .",
90
+ "lgtm": "pnpm run build && pnpm run lint && pnpm run check-format && pnpm run check-exports && pnpm run typecheck && pnpm run test",
70
91
  "typecheck": "tsc",
71
92
  "test": "vitest run",
93
+ "pretest": "bash scripts/download-dtcg-schemas.sh",
72
94
  "format": "prettier --write .",
73
95
  "check-format": "prettier --check .",
74
96
  "check-exports": "attw --pack . --ignore-rules cjs-resolves-to-esm no-resolution",
package/src/cli.ts ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // @example
4
+
5
+ // ```sh
6
+ // $ npx tsx src/cli.ts builder '#6750A4'
7
+ // $ npx tsx src/cli.ts builder '#6750A4' --format css
8
+ // $ npx tsx src/cli.ts builder '#6750A4' --adaptive-shades --format figma
9
+ // ```
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ import { Command, Option } from "commander";
15
+ import {
16
+ builder,
17
+ DEFAULT_ADAPTIVE_SHADES,
18
+ DEFAULT_BLEND,
19
+ DEFAULT_CONTRAST,
20
+ DEFAULT_CONTRAST_ALL_COLORS,
21
+ DEFAULT_SCHEME,
22
+ schemeNames,
23
+ } from "react-mcu";
24
+
25
+ const program = new Command();
26
+
27
+ program.name("react-mcu").description("m3 color system for react");
28
+
29
+ program
30
+ .command("builder")
31
+ .description("Generate a color theme from a source color")
32
+ .argument("<source>", "Source color in hex format (e.g. #6750A4)")
33
+ .addOption(
34
+ new Option("--scheme <name>", "Color scheme variant")
35
+ .choices(schemeNames)
36
+ .default(DEFAULT_SCHEME),
37
+ )
38
+ .option(
39
+ "--contrast <number>",
40
+ "Contrast level from -1.0 to 1.0",
41
+ parseFloat,
42
+ DEFAULT_CONTRAST,
43
+ )
44
+ .option("--primary <hex>", "Primary color override")
45
+ .option("--secondary <hex>", "Secondary color override")
46
+ .option("--tertiary <hex>", "Tertiary color override")
47
+ .option("--error <hex>", "Error color override")
48
+ .option("--neutral <hex>", "Neutral color override")
49
+ .option("--neutral-variant <hex>", "Neutral variant color override")
50
+ .option(
51
+ "--custom-colors <json>",
52
+ 'Custom colors as JSON array (e.g. \'[{"name":"brand","hex":"#FF5733","blend":true}]\')',
53
+ )
54
+ .option("--format <type>", "Output format: json, css, or figma", "figma")
55
+ .option("--output <dir>", "Output directory (required for figma format)")
56
+ .option(
57
+ "--adaptive-shades",
58
+ "Adapt tonal palette shades for dark mode",
59
+ DEFAULT_ADAPTIVE_SHADES,
60
+ )
61
+ .option(
62
+ "--contrast-all-colors",
63
+ "Apply contrast adjustment to tonal palette shades",
64
+ DEFAULT_CONTRAST_ALL_COLORS,
65
+ )
66
+ .action((source: string, opts) => {
67
+ let customColors: { name: string; hex: string; blend: boolean }[] = [];
68
+ if (opts.customColors) {
69
+ try {
70
+ const parsed = JSON.parse(opts.customColors) as {
71
+ name: string;
72
+ hex: string;
73
+ blend?: boolean;
74
+ }[];
75
+ customColors = parsed.map((c) => ({
76
+ name: c.name,
77
+ hex: c.hex,
78
+ blend: c.blend ?? DEFAULT_BLEND,
79
+ }));
80
+ } catch {
81
+ console.error("Error: --custom-colors must be valid JSON");
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ const result = builder(source, {
87
+ scheme: opts.scheme,
88
+ contrast: opts.contrast,
89
+ primary: opts.primary,
90
+ secondary: opts.secondary,
91
+ tertiary: opts.tertiary,
92
+ error: opts.error,
93
+ neutral: opts.neutral,
94
+ neutralVariant: opts.neutralVariant,
95
+ customColors,
96
+ adaptiveShades: opts.adaptiveShades,
97
+ contrastAllColors: opts.contrastAllColors,
98
+ });
99
+
100
+ if (opts.format === "css") {
101
+ process.stdout.write(result.toCss());
102
+ } else if (opts.format === "figma") {
103
+ const outputDir = opts.output ?? "mcu-theme";
104
+ fs.mkdirSync(outputDir, { recursive: true });
105
+ const files = result.toFigmaTokens();
106
+ for (const [filename, content] of Object.entries(files)) {
107
+ const filePath = path.join(outputDir, filename);
108
+ fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
109
+ console.error(`wrote ${filePath}`);
110
+ }
111
+ } else {
112
+ process.stdout.write(JSON.stringify(result.toJson(), null, 2) + "\n");
113
+ }
114
+ });
115
+
116
+ program.parse();