react-mcu 1.2.0 → 1.3.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 CHANGED
@@ -35,6 +35,8 @@ Extra:
35
35
 
36
36
  - [x] `contrastAllColors`: contrast also applies to custom-colors and shades
37
37
  (not only the core-colors)
38
+ - [x] `adaptiveShades`: shades adapt to the light/dark mode (instead of being
39
+ fixed)
38
40
 
39
41
  # Usage
40
42
 
@@ -173,6 +175,37 @@ Simply override/remap
173
175
  > Make sure `:root, .dark { ... }` comes AFTER `.root { ... } .dark { ... }` to
174
176
  > take precedence.
175
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
+
176
209
  # Dev
177
210
 
178
211
  ## INSTALL
@@ -198,6 +231,12 @@ Pre-requisites:
198
231
  $ pnpm i
199
232
  ```
200
233
 
234
+ ## Validation
235
+
236
+ ```sh
237
+ $ pnpm run lgtm
238
+ ```
239
+
201
240
  ## CONTRIBUTING
202
241
 
203
242
  When submitting a pull request, please include a changeset to document your
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { CustomColor, TonalPalette } from '@material/material-color-utilities';
1
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { SchemeTonalSpot, SchemeMonochrome, SchemeNeutral, SchemeVibrant, SchemeExpressive, SchemeFidelity, SchemeContent, CustomColor } from '@material/material-color-utilities';
3
3
 
4
4
  type HexCustomColor = Omit<CustomColor, "value"> & {
5
5
  hex: string;
@@ -29,52 +29,136 @@ 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;
36
35
  /**
37
36
  * Array of custom colors to include in the generated palette.
38
- *
39
- * @example
40
- * ```ts
41
- * customColors={[
42
- * { name: "brand", hex: "#FF5733", blend: true },
43
- * { name: "success", hex: "#28A745", blend: false }
44
- * ]}
45
- * ```
37
+ * Each custom color can be blended with the source color for harmonization.
46
38
  */
47
39
  customColors?: HexCustomColor[];
48
40
  /**
49
- * When true, applies the contrast level to all colors including custom colors and tonal palette shades.
50
- * When false (default), only core colors are affected by the contrast level.
51
- *
52
- * @default false
41
+ * Apply contrast adjustment to tonal palette shades when true.
42
+ * When true, all tonal palette shades are adjusted based on the contrast level.
43
+ * When false (default), contrast adjustments only apply to core Material Design tokens.
53
44
  */
54
45
  contrastAllColors?: boolean;
46
+ /**
47
+ * Adapt tonal palette shades for dark mode.
48
+ * When true, tonal palette shades automatically invert for dark mode
49
+ * (high values become dark, low values become light).
50
+ * When false (default), tonal palette values remain constant across light/dark mode.
51
+ */
52
+ adaptiveShades?: boolean;
55
53
  };
56
- declare const schemesMap: {
57
- readonly tonalSpot: typeof SchemeTonalSpot;
58
- readonly monochrome: typeof SchemeMonochrome;
59
- readonly neutral: typeof SchemeNeutral;
60
- readonly vibrant: typeof SchemeVibrant;
61
- readonly expressive: typeof SchemeExpressive;
62
- readonly fidelity: typeof SchemeFidelity;
63
- readonly content: typeof SchemeContent;
64
- };
65
- declare const schemeNames: (keyof typeof schemesMap)[];
54
+ declare const schemeNames: readonly ["tonalSpot", "monochrome", "neutral", "vibrant", "expressive", "fidelity", "content"];
66
55
  type SchemeName = (typeof schemeNames)[number];
67
- declare function Mcu({ source, scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, colorMatch, customColors, contrastAllColors, children, }: McuConfig & {
56
+ declare const tokenNames: readonly ["background", "error", "errorContainer", "inverseOnSurface", "inversePrimary", "inverseSurface", "onBackground", "onError", "onErrorContainer", "onPrimary", "onPrimaryContainer", "onPrimaryFixed", "onPrimaryFixedVariant", "onSecondary", "onSecondaryContainer", "onSecondaryFixed", "onSecondaryFixedVariant", "onSurface", "onSurfaceVariant", "onTertiary", "onTertiaryContainer", "onTertiaryFixed", "onTertiaryFixedVariant", "outline", "outlineVariant", "primary", "primaryContainer", "primaryFixed", "primaryFixedDim", "scrim", "secondary", "secondaryContainer", "secondaryFixed", "secondaryFixedDim", "shadow", "surface", "surfaceBright", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "surfaceDim", "surfaceTint", "surfaceVariant", "tertiary", "tertiaryContainer", "tertiaryFixed", "tertiaryFixedDim"];
57
+ type TokenName = (typeof tokenNames)[number];
58
+ declare function builder(hexSource: McuConfig["source"], { scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, customColors: hexCustomColors, contrastAllColors, adaptiveShades, }?: Omit<McuConfig, "source">): {
59
+ toCss(): string;
60
+ toJson(): {
61
+ seed: string;
62
+ coreColors: Record<string, string>;
63
+ extendedColors: {
64
+ name: string;
65
+ color: string;
66
+ description: string;
67
+ harmonized: boolean;
68
+ }[];
69
+ schemes: Record<string, Record<string, string>>;
70
+ palettes: Record<string, Record<string, string>>;
71
+ };
72
+ toFigmaTokens(): {
73
+ "Light.tokens.json": {
74
+ Schemes: Record<string, {
75
+ $type: "color";
76
+ $value: {
77
+ colorSpace: "srgb";
78
+ components: number[];
79
+ alpha: number;
80
+ hex: string;
81
+ };
82
+ $extensions: {
83
+ "com.figma.scopes": string[];
84
+ "com.figma.isOverride": boolean;
85
+ };
86
+ }>;
87
+ Palettes: Record<string, 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
+ $extensions: {
101
+ "com.figma.modeName": string;
102
+ };
103
+ };
104
+ "Dark.tokens.json": {
105
+ Schemes: Record<string, {
106
+ $type: "color";
107
+ $value: {
108
+ colorSpace: "srgb";
109
+ components: number[];
110
+ alpha: number;
111
+ hex: string;
112
+ };
113
+ $extensions: {
114
+ "com.figma.scopes": string[];
115
+ "com.figma.isOverride": boolean;
116
+ };
117
+ }>;
118
+ Palettes: Record<string, 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
+ $extensions: {
132
+ "com.figma.modeName": string;
133
+ };
134
+ };
135
+ };
136
+ mergedColorsLight: {
137
+ [x: string]: number;
138
+ };
139
+ mergedColorsDark: {
140
+ [x: string]: number;
141
+ };
142
+ allPalettes: {
143
+ primary: TonalPalette;
144
+ secondary: TonalPalette;
145
+ tertiary: TonalPalette;
146
+ error: TonalPalette;
147
+ neutral: TonalPalette;
148
+ "neutral-variant": TonalPalette;
149
+ };
150
+ };
151
+
152
+ declare function Mcu({ source, scheme, contrast, primary, secondary, tertiary, neutral, neutralVariant, error, colorMatch, customColors, contrastAllColors, adaptiveShades, children, }: McuConfig & {
68
153
  children?: React.ReactNode;
69
154
  }): react_jsx_runtime.JSX.Element;
70
- 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"];
71
- type TokenName = (typeof tokenNames)[number];
72
155
 
73
156
  type Api = {
74
157
  initials: McuConfig;
75
158
  setMcuConfig: (config: McuConfig) => void;
76
159
  getMcuColor: (colorName: TokenName, theme?: string) => string;
160
+ allPalettes: Record<string, TonalPalette>;
77
161
  };
78
162
  declare const useMcu: () => Api;
79
163
 
80
- export { Mcu, useMcu };
164
+ export { Mcu, builder, useMcu };
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  "use client";
2
2
 
3
- // src/Mcu.tsx
3
+ // src/lib/builder.ts
4
4
  import {
5
5
  argbFromHex,
6
6
  Blend,
7
+ blueFromArgb,
7
8
  DynamicColor,
8
9
  DynamicScheme,
10
+ greenFromArgb,
9
11
  Hct,
10
- hexFromArgb as hexFromArgb2,
12
+ hexFromArgb,
11
13
  MaterialDynamicColors,
14
+ redFromArgb,
12
15
  SchemeContent,
13
16
  SchemeExpressive,
14
17
  SchemeFidelity,
@@ -18,94 +21,12 @@ import {
18
21
  SchemeVibrant,
19
22
  TonalPalette
20
23
  } from "@material/material-color-utilities";
21
- import { kebabCase, upperFirst } from "lodash-es";
22
- import { useMemo as useMemo2 } from "react";
23
-
24
- // src/Mcu.context.tsx
25
- import { hexFromArgb } from "@material/material-color-utilities";
26
- import {
27
- useCallback,
28
- useInsertionEffect,
29
- useMemo,
30
- useState
31
- } from "react";
32
-
33
- // src/lib/createRequiredContext.ts
34
- import { createContext, useContext } from "react";
35
- var createRequiredContext = () => {
36
- const Ctx = createContext(null);
37
- const useCtx = () => {
38
- const contextValue = useContext(Ctx);
39
- if (contextValue === null) {
40
- throw new Error("Context value is null");
41
- }
42
- return contextValue;
43
- };
44
- return [useCtx, Ctx.Provider, Ctx];
45
- };
46
-
47
- // src/Mcu.context.tsx
48
- import { jsx } from "react/jsx-runtime";
49
- var [useMcu, Provider, McuContext] = createRequiredContext();
50
- var McuProvider = ({
51
- source: initialSource,
52
- scheme: initialScheme,
53
- contrast: initialContrast,
54
- customColors: initialCustomColors,
55
- contrastAllColors: initialContrastAllColors,
56
- styleId,
57
- children
58
- }) => {
59
- const [initials] = useState(() => ({
60
- source: initialSource,
61
- scheme: initialScheme,
62
- contrast: initialContrast,
63
- customColors: initialCustomColors,
64
- contrastAllColors: initialContrastAllColors
65
- }));
66
- const [mcuConfig, setMcuConfig] = useState(initials);
67
- const { css, mergedColorsLight, mergedColorsDark } = useMemo(
68
- () => generateCss(mcuConfig),
69
- [mcuConfig]
70
- );
71
- useInsertionEffect(() => {
72
- let tag = document.getElementById(styleId);
73
- if (!tag) {
74
- tag = document.createElement("style");
75
- tag.id = styleId;
76
- document.head.appendChild(tag);
77
- }
78
- tag.textContent = css;
79
- }, [css, styleId]);
80
- const getMcuColor = useCallback(
81
- (colorName, theme) => {
82
- return hexFromArgb(
83
- (theme === "light" ? mergedColorsLight : mergedColorsDark)[colorName]
84
- );
85
- },
86
- [mergedColorsDark, mergedColorsLight]
87
- );
88
- const value = useMemo(
89
- () => ({
90
- initials,
91
- setMcuConfig,
92
- getMcuColor
93
- }),
94
- [getMcuColor, initials]
95
- );
96
- return /* @__PURE__ */ jsx(Provider, { value, children });
97
- };
98
-
99
- // src/Mcu.tsx
100
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
101
- function adjustToneForContrast(baseTone, contrastLevel, isDark, adjustmentFactor = DEFAULT_CONTRAST_ADJUSTMENT_FACTOR) {
24
+ import { kebabCase, startCase, upperFirst } from "lodash-es";
25
+ function adjustToneForContrast(baseTone, contrastLevel, adjustmentFactor = DEFAULT_CONTRAST_ADJUSTMENT_FACTOR) {
102
26
  if (contrastLevel === 0) return baseTone;
103
- let adjustedTone;
104
- if (isDark) {
105
- adjustedTone = baseTone + contrastLevel * (100 - baseTone) * adjustmentFactor;
106
- } else {
107
- adjustedTone = baseTone - contrastLevel * baseTone * adjustmentFactor;
108
- }
27
+ const distanceToCenter = baseTone - 50;
28
+ const delta = distanceToCenter * contrastLevel * adjustmentFactor;
29
+ const adjustedTone = baseTone + delta;
109
30
  return Math.max(0, Math.min(100, adjustedTone));
110
31
  }
111
32
  var schemesMap = {
@@ -117,14 +38,11 @@ var schemesMap = {
117
38
  fidelity: SchemeFidelity,
118
39
  content: SchemeContent
119
40
  };
120
- var schemeNames = Object.keys(
121
- schemesMap
122
- );
123
41
  var DEFAULT_SCHEME = "tonalSpot";
124
42
  var DEFAULT_CONTRAST = 0;
125
- var DEFAULT_COLOR_MATCH = false;
126
43
  var DEFAULT_CUSTOM_COLORS = [];
127
44
  var DEFAULT_CONTRAST_ALL_COLORS = false;
45
+ var DEFAULT_ADAPTIVE_SHADES = false;
128
46
  var DEFAULT_BLEND = true;
129
47
  var DEFAULT_CONTRAST_ADJUSTMENT_FACTOR = 0.2;
130
48
  var STANDARD_TONES = [
@@ -159,135 +77,67 @@ var Variant = {
159
77
  FRUIT_SALAD: 8
160
78
  };
161
79
  var schemeToVariant = {
162
- tonalSpot: Variant.TONAL_SPOT,
163
80
  monochrome: Variant.MONOCHROME,
164
81
  neutral: Variant.NEUTRAL,
82
+ tonalSpot: Variant.TONAL_SPOT,
165
83
  vibrant: Variant.VIBRANT,
166
84
  expressive: Variant.EXPRESSIVE,
167
85
  fidelity: Variant.FIDELITY,
168
86
  content: Variant.CONTENT
169
87
  };
170
- var mcuStyleId = "mcu-styles";
171
- function Mcu({
172
- source,
173
- scheme = DEFAULT_SCHEME,
174
- contrast = DEFAULT_CONTRAST,
175
- primary,
176
- secondary,
177
- tertiary,
178
- neutral,
179
- neutralVariant,
180
- error,
181
- colorMatch = DEFAULT_COLOR_MATCH,
182
- customColors = DEFAULT_CUSTOM_COLORS,
183
- contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS,
184
- children
185
- }) {
186
- const config = useMemo2(
187
- () => ({
188
- source,
189
- scheme,
190
- contrast,
191
- primary,
192
- secondary,
193
- tertiary,
194
- neutral,
195
- neutralVariant,
196
- error,
197
- colorMatch,
198
- customColors,
199
- // extras features
200
- contrastAllColors
201
- }),
202
- [
203
- contrast,
204
- customColors,
205
- scheme,
206
- source,
207
- primary,
208
- secondary,
209
- tertiary,
210
- neutral,
211
- neutralVariant,
212
- error,
213
- colorMatch,
214
- contrastAllColors
215
- ]
216
- );
217
- const { css } = useMemo2(() => generateCss(config), [config]);
218
- return /* @__PURE__ */ jsxs(Fragment, { children: [
219
- /* @__PURE__ */ jsx2("style", { id: mcuStyleId, children: css }),
220
- /* @__PURE__ */ jsx2(McuProvider, { ...config, styleId: mcuStyleId, children })
221
- ] });
222
- }
223
88
  var tokenNames = [
224
89
  "background",
90
+ "error",
91
+ "errorContainer",
92
+ "inverseOnSurface",
93
+ "inversePrimary",
94
+ "inverseSurface",
225
95
  "onBackground",
226
- "surface",
227
- "surfaceDim",
228
- "surfaceBright",
229
- "surfaceContainerLowest",
230
- "surfaceContainerLow",
231
- "surfaceContainer",
232
- "surfaceContainerHigh",
233
- "surfaceContainerHighest",
96
+ "onError",
97
+ "onErrorContainer",
98
+ "onPrimary",
99
+ "onPrimaryContainer",
100
+ "onPrimaryFixed",
101
+ "onPrimaryFixedVariant",
102
+ "onSecondary",
103
+ "onSecondaryContainer",
104
+ "onSecondaryFixed",
105
+ "onSecondaryFixedVariant",
234
106
  "onSurface",
235
107
  "onSurfaceVariant",
108
+ "onTertiary",
109
+ "onTertiaryContainer",
110
+ "onTertiaryFixed",
111
+ "onTertiaryFixedVariant",
236
112
  "outline",
237
113
  "outlineVariant",
238
- "inverseSurface",
239
- "inverseOnSurface",
240
114
  "primary",
241
- // "primaryDim",
242
- "onPrimary",
243
115
  "primaryContainer",
244
- "onPrimaryContainer",
245
116
  "primaryFixed",
246
117
  "primaryFixedDim",
247
- "onPrimaryFixed",
248
- "onPrimaryFixedVariant",
249
- "inversePrimary",
250
- "primaryFixed",
251
- "primaryFixedDim",
252
- "onPrimaryFixed",
253
- "onPrimaryFixedVariant",
118
+ "scrim",
254
119
  "secondary",
255
- // "secondaryDim",
256
- "onSecondary",
257
120
  "secondaryContainer",
258
- "onSecondaryContainer",
259
121
  "secondaryFixed",
260
122
  "secondaryFixedDim",
261
- "onSecondaryFixed",
262
- "onSecondaryFixedVariant",
123
+ "shadow",
124
+ "surface",
125
+ "surfaceBright",
126
+ "surfaceContainer",
127
+ "surfaceContainerHigh",
128
+ "surfaceContainerHighest",
129
+ "surfaceContainerLow",
130
+ "surfaceContainerLowest",
131
+ "surfaceDim",
132
+ "surfaceTint",
133
+ "surfaceVariant",
263
134
  "tertiary",
264
- // "tertiaryDim",
265
- "onTertiary",
266
135
  "tertiaryContainer",
267
- "onTertiaryContainer",
268
136
  "tertiaryFixed",
269
- "tertiaryFixedDim",
270
- "onTertiaryFixed",
271
- "onTertiaryFixedVariant",
272
- "error",
273
- // "errorDim",
274
- "onError",
275
- "errorContainer",
276
- "onErrorContainer",
277
- "scrim",
278
- // added manually, was missing
279
- "shadow"
280
- // added manually, was missing
137
+ "tertiaryFixedDim"
281
138
  ];
282
139
  function toRecord(arr, getEntry) {
283
- return arr.reduce(
284
- (acc, item) => {
285
- const [key, value] = getEntry(item);
286
- acc[key] = value;
287
- return acc;
288
- },
289
- {}
290
- );
140
+ return Object.fromEntries(arr.map(getEntry));
291
141
  }
292
142
  function getPalette(palettes, colorName) {
293
143
  const palette = palettes[colorName];
@@ -310,7 +160,7 @@ function mergeBaseAndCustomColors(scheme, customColors, colorPalettes, contrastA
310
160
  const getPaletteForColor = (s) => getPalette(colorPalettes, colorname);
311
161
  const getTone = (baseTone) => (s) => {
312
162
  if (!contrastAllColors) return baseTone;
313
- return adjustToneForContrast(baseTone, s.contrastLevel, s.isDark);
163
+ return adjustToneForContrast(baseTone, s.contrastLevel);
314
164
  };
315
165
  const colorDynamicColor = new DynamicColor(
316
166
  colorname,
@@ -349,25 +199,6 @@ function mergeBaseAndCustomColors(scheme, customColors, colorPalettes, contrastA
349
199
  });
350
200
  return { ...baseVars, ...customVars };
351
201
  }
352
- var cssVar = (colorName, colorValue) => {
353
- const name = `--mcu-${kebabCase(colorName)}`;
354
- const value = hexFromArgb2(colorValue);
355
- return `${name}:${value};`;
356
- };
357
- var generateTonalPaletteVars = (paletteName, palette, scheme, applyContrast = false) => {
358
- return STANDARD_TONES.map((tone) => {
359
- let toneToUse = tone;
360
- if (applyContrast && scheme) {
361
- toneToUse = adjustToneForContrast(
362
- tone,
363
- scheme.contrastLevel,
364
- scheme.isDark
365
- );
366
- }
367
- const color = palette.tone(toneToUse);
368
- return cssVar(`${paletteName}-${tone}`, color);
369
- }).join(" ");
370
- };
371
202
  function createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonization) {
372
203
  const colorArgb = argbFromHex(colorDef.hex);
373
204
  const harmonizedArgb = colorDef.blend ? Blend.harmonize(colorArgb, effectiveSourceForHarmonization) : colorArgb;
@@ -386,11 +217,7 @@ function createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonizatio
386
217
  }
387
218
  return TonalPalette.fromHueAndChroma(hct.hue, targetChroma);
388
219
  }
389
- var toCssVars = (mergedColors) => {
390
- return Object.entries(mergedColors).map(([name, value]) => cssVar(name, value)).join(" ");
391
- };
392
- function generateCss({
393
- source: hexSource,
220
+ function builder(hexSource, {
394
221
  scheme = DEFAULT_SCHEME,
395
222
  contrast = DEFAULT_CONTRAST,
396
223
  primary,
@@ -399,11 +226,12 @@ function generateCss({
399
226
  neutral,
400
227
  neutralVariant,
401
228
  error,
402
- colorMatch = DEFAULT_COLOR_MATCH,
403
229
  customColors: hexCustomColors = DEFAULT_CUSTOM_COLORS,
404
- contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS
405
- }) {
230
+ contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS,
231
+ adaptiveShades = DEFAULT_ADAPTIVE_SHADES
232
+ } = {}) {
406
233
  const sourceArgb = argbFromHex(hexSource);
234
+ const sourceHct = Hct.fromInt(sourceArgb);
407
235
  const effectiveSource = primary || hexSource;
408
236
  const effectiveSourceArgb = argbFromHex(effectiveSource);
409
237
  const effectiveSourceForHarmonization = primary ? argbFromHex(primary) : sourceArgb;
@@ -462,12 +290,8 @@ function generateCss({
462
290
  createColorPalette(colorDef, baseScheme, effectiveSourceForHarmonization)
463
291
  ])
464
292
  );
465
- const createSchemes = (baseConfig) => [
466
- new DynamicScheme({ ...baseConfig, isDark: false }),
467
- new DynamicScheme({ ...baseConfig, isDark: true })
468
- ];
469
293
  const variant = schemeToVariant[scheme];
470
- const [lightScheme, darkScheme] = createSchemes({
294
+ const schemeConfig = {
471
295
  sourceColorArgb: effectiveSourceArgb,
472
296
  variant,
473
297
  contrastLevel: contrast,
@@ -476,12 +300,26 @@ function generateCss({
476
300
  tertiaryPalette: colorPalettes["tertiary"] || baseScheme.tertiaryPalette,
477
301
  neutralPalette: colorPalettes["neutral"] || baseScheme.neutralPalette,
478
302
  neutralVariantPalette: colorPalettes["neutralVariant"] || baseScheme.neutralVariantPalette
479
- });
303
+ };
304
+ const lightScheme = new DynamicScheme({ ...schemeConfig, isDark: false });
305
+ const darkScheme = new DynamicScheme({ ...schemeConfig, isDark: true });
480
306
  const errorPalette = colorPalettes["error"];
481
307
  if (errorPalette) {
482
308
  lightScheme.errorPalette = errorPalette;
483
309
  darkScheme.errorPalette = errorPalette;
484
310
  }
311
+ const allPalettes = {
312
+ primary: lightScheme.primaryPalette,
313
+ secondary: lightScheme.secondaryPalette,
314
+ tertiary: lightScheme.tertiaryPalette,
315
+ error: lightScheme.errorPalette,
316
+ neutral: lightScheme.neutralPalette,
317
+ "neutral-variant": lightScheme.neutralVariantPalette,
318
+ // Add custom color palettes
319
+ ...Object.fromEntries(
320
+ definedColors.filter((c) => !c.core).map((colorDef) => [colorDef.name, colorPalettes[colorDef.name]])
321
+ )
322
+ };
485
323
  const customColors = definedColors.filter((c) => !c.core).map((c) => ({
486
324
  name: c.name,
487
325
  blend: c.blend ?? DEFAULT_BLEND,
@@ -499,67 +337,452 @@ function generateCss({
499
337
  colorPalettes,
500
338
  contrastAllColors
501
339
  );
502
- const lightVars = toCssVars(mergedColorsLight);
503
- const darkVars = toCssVars(mergedColorsDark);
504
- const allTonalVars = [
505
- // Core colors from the scheme
506
- generateTonalPaletteVars(
507
- "primary",
508
- lightScheme.primaryPalette,
509
- lightScheme,
510
- contrastAllColors
511
- ),
512
- generateTonalPaletteVars(
513
- "secondary",
514
- lightScheme.secondaryPalette,
515
- lightScheme,
516
- contrastAllColors
517
- ),
518
- generateTonalPaletteVars(
519
- "tertiary",
520
- lightScheme.tertiaryPalette,
521
- lightScheme,
522
- contrastAllColors
523
- ),
524
- generateTonalPaletteVars(
525
- "error",
526
- lightScheme.errorPalette,
527
- lightScheme,
528
- contrastAllColors
529
- ),
530
- generateTonalPaletteVars(
531
- "neutral",
532
- lightScheme.neutralPalette,
533
- lightScheme,
534
- contrastAllColors
535
- ),
536
- generateTonalPaletteVars(
537
- "neutral-variant",
538
- lightScheme.neutralVariantPalette,
539
- lightScheme,
540
- contrastAllColors
541
- ),
542
- // Custom colors from our unified palette map
543
- ...customColors.map((customColorObj) => {
544
- const palette = getPalette(colorPalettes, customColorObj.name);
545
- return generateTonalPaletteVars(
546
- kebabCase(customColorObj.name),
547
- palette,
548
- lightScheme,
549
- contrastAllColors
550
- );
551
- })
552
- ].join(" ");
553
340
  return {
554
- css: `
555
- :root { ${lightVars} ${allTonalVars} }
556
- .dark { ${darkVars} }
557
- `,
341
+ //
342
+ // ██████ ███████ ███████
343
+ // ██ ██ ██
344
+ // ██ ███████ ███████
345
+ // ██ ██ ██
346
+ // ██████ ███████ ███████
347
+ //
348
+ toCss() {
349
+ function cssVar(colorName, colorValue) {
350
+ const name = `--mcu-${kebabCase(colorName)}`;
351
+ const value = hexFromArgb(colorValue);
352
+ return `${name}:${value};`;
353
+ }
354
+ function toCssVars(mergedColors) {
355
+ return Object.entries(mergedColors).map(([name, value]) => cssVar(name, value)).join(" ");
356
+ }
357
+ function generateTonalPaletteVars(paletteName, palette, scheme2, applyContrast, adaptiveShades2) {
358
+ return STANDARD_TONES.map((tone) => {
359
+ let toneToUse = tone;
360
+ if (adaptiveShades2 && scheme2.isDark) {
361
+ toneToUse = 100 - tone;
362
+ }
363
+ if (applyContrast) {
364
+ toneToUse = adjustToneForContrast(toneToUse, scheme2.contrastLevel);
365
+ }
366
+ const color = palette.tone(toneToUse);
367
+ return cssVar(`${paletteName}-${tone}`, color);
368
+ }).join(" ");
369
+ }
370
+ function generateTonalVars(s) {
371
+ return Object.entries(allPalettes).map(
372
+ ([name, palette]) => generateTonalPaletteVars(
373
+ kebabCase(name),
374
+ palette,
375
+ s,
376
+ contrastAllColors,
377
+ adaptiveShades
378
+ )
379
+ ).join(" ");
380
+ }
381
+ const lightVars = toCssVars(mergedColorsLight);
382
+ const darkVars = toCssVars(mergedColorsDark);
383
+ const lightTonalVars = generateTonalVars(lightScheme);
384
+ const darkTonalVars = generateTonalVars(darkScheme);
385
+ return `
386
+ :root { ${lightVars} ${lightTonalVars} }
387
+ .dark { ${darkVars} ${adaptiveShades ? darkTonalVars : lightTonalVars} }
388
+ `;
389
+ },
390
+ //
391
+ // ██ ███████ ██████ ███ ██
392
+ // ██ ██ ██ ██ ████ ██
393
+ // ██ ███████ ██ ██ ██ ██ ██
394
+ // ██ ██ ██ ██ ██ ██ ██ ██
395
+ // █████ ███████ ██████ ██ ████
396
+ //
397
+ toJson() {
398
+ const fixtureTokenOrder = [
399
+ "primary",
400
+ "surfaceTint",
401
+ "onPrimary",
402
+ "primaryContainer",
403
+ "onPrimaryContainer",
404
+ "secondary",
405
+ "onSecondary",
406
+ "secondaryContainer",
407
+ "onSecondaryContainer",
408
+ "tertiary",
409
+ "onTertiary",
410
+ "tertiaryContainer",
411
+ "onTertiaryContainer",
412
+ "error",
413
+ "onError",
414
+ "errorContainer",
415
+ "onErrorContainer",
416
+ "background",
417
+ "onBackground",
418
+ "surface",
419
+ "onSurface",
420
+ "surfaceVariant",
421
+ "onSurfaceVariant",
422
+ "outline",
423
+ "outlineVariant",
424
+ "shadow",
425
+ "scrim",
426
+ "inverseSurface",
427
+ "inverseOnSurface",
428
+ "inversePrimary",
429
+ "primaryFixed",
430
+ "onPrimaryFixed",
431
+ "primaryFixedDim",
432
+ "onPrimaryFixedVariant",
433
+ "secondaryFixed",
434
+ "onSecondaryFixed",
435
+ "secondaryFixedDim",
436
+ "onSecondaryFixedVariant",
437
+ "tertiaryFixed",
438
+ "onTertiaryFixed",
439
+ "tertiaryFixedDim",
440
+ "onTertiaryFixedVariant",
441
+ "surfaceDim",
442
+ "surfaceBright",
443
+ "surfaceContainerLowest",
444
+ "surfaceContainerLow",
445
+ "surfaceContainer",
446
+ "surfaceContainerHigh",
447
+ "surfaceContainerHighest"
448
+ ];
449
+ const neuHct = neutral ? Hct.fromInt(argbFromHex(neutral)) : sourceHct;
450
+ const nvHct = neutralVariant ? Hct.fromInt(argbFromHex(neutralVariant)) : sourceHct;
451
+ const rawPalettes = {
452
+ primary: TonalPalette.fromInt(effectiveSourceArgb),
453
+ secondary: secondary ? TonalPalette.fromInt(argbFromHex(secondary)) : TonalPalette.fromHueAndChroma(sourceHct.hue, sourceHct.chroma / 3),
454
+ tertiary: tertiary ? TonalPalette.fromInt(argbFromHex(tertiary)) : TonalPalette.fromHueAndChroma(
455
+ (sourceHct.hue + 60) % 360,
456
+ sourceHct.chroma / 2
457
+ ),
458
+ neutral: TonalPalette.fromHueAndChroma(
459
+ neuHct.hue,
460
+ Math.min(neuHct.chroma / 12, 4)
461
+ ),
462
+ "neutral-variant": TonalPalette.fromHueAndChroma(
463
+ nvHct.hue,
464
+ Math.min(nvHct.chroma / 6, 8)
465
+ )
466
+ };
467
+ function buildJsonSchemes() {
468
+ function extractSchemeColors(scheme2, backgroundScheme) {
469
+ const colors = {};
470
+ for (const tokenName of fixtureTokenOrder) {
471
+ const dynamicColor = MaterialDynamicColors[tokenName];
472
+ const useScheme = backgroundScheme && (tokenName === "background" || tokenName === "onBackground") ? backgroundScheme : scheme2;
473
+ colors[tokenName] = hexFromArgb(
474
+ dynamicColor.getArgb(useScheme)
475
+ ).toUpperCase();
476
+ }
477
+ return colors;
478
+ }
479
+ function resolveOverridePalette(hex, role) {
480
+ if (!hex) return null;
481
+ return new SchemeClass(Hct.fromInt(argbFromHex(hex)), false, 0)[role];
482
+ }
483
+ const secPalette = resolveOverridePalette(secondary, "primaryPalette");
484
+ const terPalette = resolveOverridePalette(tertiary, "primaryPalette");
485
+ const errPalette = resolveOverridePalette(error, "primaryPalette");
486
+ const neuPalette = resolveOverridePalette(neutral, "neutralPalette");
487
+ const nvPalette = resolveOverridePalette(
488
+ neutralVariant,
489
+ "neutralVariantPalette"
490
+ );
491
+ const jsonSchemes = {};
492
+ const jsonContrastLevels = [
493
+ { name: "light", isDark: false, contrast: 0 },
494
+ { name: "light-medium-contrast", isDark: false, contrast: 0.5 },
495
+ { name: "light-high-contrast", isDark: false, contrast: 1 },
496
+ { name: "dark", isDark: true, contrast: 0 },
497
+ { name: "dark-medium-contrast", isDark: true, contrast: 0.5 },
498
+ { name: "dark-high-contrast", isDark: true, contrast: 1 }
499
+ ];
500
+ for (const { name, isDark, contrast: contrast2 } of jsonContrastLevels) {
501
+ const baseScheme2 = new SchemeClass(primaryHct, isDark, contrast2);
502
+ const composedScheme = new DynamicScheme({
503
+ sourceColorArgb: effectiveSourceArgb,
504
+ variant: schemeToVariant[scheme],
505
+ contrastLevel: contrast2,
506
+ isDark,
507
+ primaryPalette: baseScheme2.primaryPalette,
508
+ secondaryPalette: secPalette || baseScheme2.secondaryPalette,
509
+ tertiaryPalette: terPalette || baseScheme2.tertiaryPalette,
510
+ neutralPalette: neuPalette || baseScheme2.neutralPalette,
511
+ neutralVariantPalette: nvPalette || baseScheme2.neutralVariantPalette
512
+ });
513
+ if (errPalette) composedScheme.errorPalette = errPalette;
514
+ jsonSchemes[name] = extractSchemeColors(composedScheme, baseScheme2);
515
+ }
516
+ return jsonSchemes;
517
+ }
518
+ function rawPalettesToJson() {
519
+ const jsonPalettes = {};
520
+ const RAW_PALETTE_NAMES = [
521
+ "primary",
522
+ "secondary",
523
+ "tertiary",
524
+ "neutral",
525
+ "neutral-variant"
526
+ ];
527
+ for (const name of RAW_PALETTE_NAMES) {
528
+ const palette = rawPalettes[name];
529
+ const tones = {};
530
+ for (const tone of STANDARD_TONES) {
531
+ tones[tone.toString()] = hexFromArgb(
532
+ palette.tone(tone)
533
+ ).toUpperCase();
534
+ }
535
+ jsonPalettes[name] = tones;
536
+ }
537
+ return jsonPalettes;
538
+ }
539
+ function buildCoreColors(opts) {
540
+ const colors = { primary: opts.primary };
541
+ if (opts.secondary) colors.secondary = opts.secondary.toUpperCase();
542
+ if (opts.tertiary) colors.tertiary = opts.tertiary.toUpperCase();
543
+ if (opts.error) colors.error = opts.error.toUpperCase();
544
+ if (opts.neutral) colors.neutral = opts.neutral.toUpperCase();
545
+ if (opts.neutralVariant)
546
+ colors.neutralVariant = opts.neutralVariant.toUpperCase();
547
+ return colors;
548
+ }
549
+ const seed = hexSource.toUpperCase();
550
+ const coreColors = buildCoreColors({
551
+ primary: (primary || hexSource).toUpperCase(),
552
+ secondary,
553
+ tertiary,
554
+ error,
555
+ neutral,
556
+ neutralVariant
557
+ });
558
+ const extendedColors = hexCustomColors.map((c) => ({
559
+ name: c.name,
560
+ color: c.hex.toUpperCase(),
561
+ description: "",
562
+ harmonized: c.blend ?? DEFAULT_BLEND
563
+ }));
564
+ return {
565
+ seed,
566
+ coreColors,
567
+ extendedColors,
568
+ schemes: buildJsonSchemes(),
569
+ palettes: rawPalettesToJson()
570
+ };
571
+ },
572
+ //
573
+ // ███████ ██ ██████ ███ ███ █████
574
+ // ██ ██ ██ ████ ████ ██ ██
575
+ // █████ ██ ██ ███ ██ ████ ██ ███████
576
+ // ██ ██ ██ ██ ██ ██ ██ ██ ██
577
+ // ██ ██ ██████ ██ ██ ██ ██
578
+ //
579
+ toFigmaTokens() {
580
+ function argbToFigmaColorValue(argb) {
581
+ return {
582
+ colorSpace: "srgb",
583
+ components: [
584
+ redFromArgb(argb) / 255,
585
+ greenFromArgb(argb) / 255,
586
+ blueFromArgb(argb) / 255
587
+ ],
588
+ alpha: 1,
589
+ hex: hexFromArgb(argb).toUpperCase()
590
+ };
591
+ }
592
+ function figmaToken(argb) {
593
+ return {
594
+ $type: "color",
595
+ $value: argbToFigmaColorValue(argb),
596
+ $extensions: {
597
+ "com.figma.scopes": ["ALL_SCOPES"],
598
+ "com.figma.isOverride": true
599
+ }
600
+ };
601
+ }
602
+ function buildFigmaSchemeTokens(mergedColors) {
603
+ const tokens = {};
604
+ for (const [name, argb] of Object.entries(mergedColors)) {
605
+ tokens[startCase(name)] = figmaToken(argb);
606
+ }
607
+ return tokens;
608
+ }
609
+ function buildFigmaPaletteTokens(isDark) {
610
+ const palettes = {};
611
+ for (const [name, palette] of Object.entries(allPalettes)) {
612
+ const tones = {};
613
+ for (const tone of STANDARD_TONES) {
614
+ let toneToUse = tone;
615
+ if (adaptiveShades && isDark) {
616
+ toneToUse = 100 - tone;
617
+ }
618
+ if (contrastAllColors) {
619
+ toneToUse = adjustToneForContrast(toneToUse, contrast);
620
+ }
621
+ const argb = palette.tone(toneToUse);
622
+ tones[tone.toString()] = figmaToken(argb);
623
+ }
624
+ palettes[startCase(name)] = tones;
625
+ }
626
+ return palettes;
627
+ }
628
+ function buildModeFile(modeName, mergedColors, isDark) {
629
+ return {
630
+ Schemes: buildFigmaSchemeTokens(mergedColors),
631
+ Palettes: buildFigmaPaletteTokens(isDark),
632
+ $extensions: {
633
+ "com.figma.modeName": modeName
634
+ }
635
+ };
636
+ }
637
+ return {
638
+ "Light.tokens.json": buildModeFile("Light", mergedColorsLight, false),
639
+ "Dark.tokens.json": buildModeFile("Dark", mergedColorsDark, true)
640
+ };
641
+ },
642
+ //
643
+ // API
644
+ //
558
645
  mergedColorsLight,
559
- mergedColorsDark
646
+ mergedColorsDark,
647
+ allPalettes
560
648
  };
561
649
  }
650
+
651
+ // src/Mcu.tsx
652
+ import { useMemo as useMemo2 } from "react";
653
+
654
+ // src/Mcu.context.tsx
655
+ import {
656
+ hexFromArgb as hexFromArgb2
657
+ } from "@material/material-color-utilities";
658
+ import React, {
659
+ useCallback,
660
+ useInsertionEffect,
661
+ useMemo,
662
+ useState
663
+ } from "react";
664
+
665
+ // src/lib/createRequiredContext.ts
666
+ import { createContext, useContext } from "react";
667
+ var createRequiredContext = () => {
668
+ const Ctx = createContext(null);
669
+ const useCtx = () => {
670
+ const contextValue = useContext(Ctx);
671
+ if (contextValue === null) {
672
+ throw new Error("Context value is null");
673
+ }
674
+ return contextValue;
675
+ };
676
+ return [useCtx, Ctx.Provider, Ctx];
677
+ };
678
+
679
+ // src/Mcu.context.tsx
680
+ import { jsx } from "react/jsx-runtime";
681
+ var [useMcu, Provider, McuContext] = createRequiredContext();
682
+ var McuProvider = ({
683
+ styleId,
684
+ children,
685
+ ...configProps
686
+ }) => {
687
+ const [initials] = useState(() => configProps);
688
+ const [mcuConfig, setMcuConfig] = useState(initials);
689
+ const configKey = JSON.stringify(configProps);
690
+ React.useEffect(() => {
691
+ setMcuConfig(configProps);
692
+ }, [configKey]);
693
+ const { css, mergedColorsLight, mergedColorsDark, allPalettes } = useMemo(() => {
694
+ const { toCss, ...rest } = builder(mcuConfig.source, mcuConfig);
695
+ return { css: toCss(), ...rest };
696
+ }, [mcuConfig]);
697
+ useInsertionEffect(() => {
698
+ let tag = document.getElementById(styleId);
699
+ if (!tag) {
700
+ tag = document.createElement("style");
701
+ tag.id = styleId;
702
+ document.head.appendChild(tag);
703
+ }
704
+ tag.textContent = css;
705
+ }, [css, styleId]);
706
+ const getMcuColor = useCallback(
707
+ (colorName, theme) => {
708
+ const mergedColors = theme === "light" ? mergedColorsLight : mergedColorsDark;
709
+ const colorValue = mergedColors[colorName];
710
+ if (colorValue === void 0) {
711
+ throw new Error(`Unknown MCU token '${colorName}'`);
712
+ }
713
+ return hexFromArgb2(colorValue);
714
+ },
715
+ [mergedColorsDark, mergedColorsLight]
716
+ );
717
+ const value = useMemo(
718
+ () => ({
719
+ initials,
720
+ setMcuConfig,
721
+ getMcuColor,
722
+ allPalettes
723
+ }),
724
+ [getMcuColor, initials, allPalettes]
725
+ );
726
+ return /* @__PURE__ */ jsx(Provider, { value, children });
727
+ };
728
+
729
+ // src/Mcu.tsx
730
+ import { jsx as jsx2 } from "react/jsx-runtime";
731
+ var mcuStyleId = "mcu-styles";
732
+ var DEFAULT_COLOR_MATCH = false;
733
+ function Mcu({
734
+ source,
735
+ scheme = DEFAULT_SCHEME,
736
+ contrast = DEFAULT_CONTRAST,
737
+ primary,
738
+ secondary,
739
+ tertiary,
740
+ neutral,
741
+ neutralVariant,
742
+ error,
743
+ colorMatch = DEFAULT_COLOR_MATCH,
744
+ customColors = DEFAULT_CUSTOM_COLORS,
745
+ contrastAllColors = DEFAULT_CONTRAST_ALL_COLORS,
746
+ adaptiveShades = DEFAULT_ADAPTIVE_SHADES,
747
+ children
748
+ }) {
749
+ const config = useMemo2(
750
+ () => ({
751
+ source,
752
+ scheme,
753
+ contrast,
754
+ primary,
755
+ secondary,
756
+ tertiary,
757
+ neutral,
758
+ neutralVariant,
759
+ error,
760
+ colorMatch,
761
+ customColors,
762
+ // extras features
763
+ contrastAllColors,
764
+ adaptiveShades
765
+ }),
766
+ [
767
+ contrast,
768
+ customColors,
769
+ scheme,
770
+ source,
771
+ primary,
772
+ secondary,
773
+ tertiary,
774
+ neutral,
775
+ neutralVariant,
776
+ error,
777
+ colorMatch,
778
+ contrastAllColors,
779
+ adaptiveShades
780
+ ]
781
+ );
782
+ return /* @__PURE__ */ jsx2(McuProvider, { ...config, styleId: mcuStyleId, children });
783
+ }
562
784
  export {
563
785
  Mcu,
786
+ builder,
564
787
  useMcu
565
788
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mcu",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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 "./lib/builder";
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();