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 +60 -0
- package/dist/index.d.ts +115 -15
- package/dist/index.js +423 -118
- package/package.json +25 -3
- package/src/cli.ts +116 -0
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 {
|
|
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
|
|
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
|
|
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", "
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
294
|
-
(s) => s.isDark ? 80 : 40,
|
|
295
|
-
//
|
|
296
|
-
|
|
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
|
-
|
|
301
|
-
(s) => s.isDark ? 20 : 100,
|
|
302
|
-
//
|
|
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
|
-
|
|
308
|
-
(s) => s.isDark ? 30 : 90,
|
|
309
|
-
//
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
(s) => s.isDark ? 90 : 30,
|
|
316
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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();
|