material-theme-builder 3.2.0 → 4.0.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 +346 -252
- package/dist/cli.js +314 -117
- package/dist/index.d.ts +30 -1
- package/dist/index.js +135 -75
- package/dist/react.d.ts +69 -49
- package/dist/react.js +142 -90
- package/dist/registry-item.json +75 -0
- package/dist/shadcn.css +43 -0
- package/dist/tailwind-plugin.d.ts +73 -0
- package/dist/tailwind-plugin.js +135 -0
- package/dist/tailwind.css +47 -81
- package/package.json +34 -11
- package/src/tailwind.css +0 -179
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import { Command, Option } from "commander";
|
|
4
|
+
import * as fs2 from "fs";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import { Command, Option as Option2 } from "commander";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
|
+
// src/cli.options.ts
|
|
10
|
+
import {
|
|
11
|
+
InvalidArgumentError,
|
|
12
|
+
Option
|
|
13
|
+
} from "commander";
|
|
14
|
+
|
|
9
15
|
// src/lib/builder.ts
|
|
10
16
|
import {
|
|
11
17
|
argbFromHex as argbFromHex2,
|
|
@@ -116,9 +122,9 @@ function buildFigmaVariables(ctx) {
|
|
|
116
122
|
paletteHexMap[paletteName] = {};
|
|
117
123
|
for (const tone of STANDARD_TONES) {
|
|
118
124
|
const argb = palette.tone(tone);
|
|
119
|
-
const
|
|
125
|
+
const path3 = `ref/palette/${paletteName}/${tone}`;
|
|
120
126
|
const hex = hexFromArgb2(argb).toUpperCase();
|
|
121
|
-
paletteHexMap[paletteName][hex] =
|
|
127
|
+
paletteHexMap[paletteName][hex] = path3;
|
|
122
128
|
const color = {
|
|
123
129
|
r: redFromArgb(argb) / 255,
|
|
124
130
|
g: greenFromArgb(argb) / 255,
|
|
@@ -126,7 +132,7 @@ function buildFigmaVariables(ctx) {
|
|
|
126
132
|
a: 1
|
|
127
133
|
};
|
|
128
134
|
variables.push({
|
|
129
|
-
path:
|
|
135
|
+
path: path3,
|
|
130
136
|
scopes: ["ALL_SCOPES"],
|
|
131
137
|
values: { Light: color, Dark: color }
|
|
132
138
|
});
|
|
@@ -210,8 +216,8 @@ function buildFigmaTokens(ctx) {
|
|
|
210
216
|
$extensions: toDtcgExtensions(v)
|
|
211
217
|
};
|
|
212
218
|
}
|
|
213
|
-
function parsePath(
|
|
214
|
-
const parts =
|
|
219
|
+
function parsePath(path3) {
|
|
220
|
+
const parts = path3.split("/");
|
|
215
221
|
if (parts[0] === "ref" && parts[1] === "palette" && parts[2] && parts[3]) {
|
|
216
222
|
return {
|
|
217
223
|
kind: "palette",
|
|
@@ -661,6 +667,39 @@ function toShadcnVars(mergedColors) {
|
|
|
661
667
|
});
|
|
662
668
|
return Object.fromEntries(entries);
|
|
663
669
|
}
|
|
670
|
+
function toShadcnAliasVars(prefix, fallbacks) {
|
|
671
|
+
const entries = SHADCN_MAPPING.map(([cssVar, m3Token]) => {
|
|
672
|
+
const bare = cssVar.slice(2);
|
|
673
|
+
const property = `--${prefix}-sys-color-${m3Token}`;
|
|
674
|
+
return [
|
|
675
|
+
bare,
|
|
676
|
+
fallbacks ? `var(${property}, ${fallbacks[bare]})` : `var(${property})`
|
|
677
|
+
];
|
|
678
|
+
});
|
|
679
|
+
return Object.fromEntries(entries);
|
|
680
|
+
}
|
|
681
|
+
function buildShadcnAliases(ctx) {
|
|
682
|
+
const lines = Object.entries(toShadcnAliasVars(ctx.prefix)).map(
|
|
683
|
+
([name, value]) => `--${name}: ${value};`
|
|
684
|
+
);
|
|
685
|
+
return `:root:root,
|
|
686
|
+
.dark.dark {
|
|
687
|
+
${lines.join("\n ")}
|
|
688
|
+
}
|
|
689
|
+
`;
|
|
690
|
+
}
|
|
691
|
+
function buildShadcnRegistryItem(ctx, { fallback = false } = {}) {
|
|
692
|
+
const concrete = fallback ? buildShadcn(ctx) : void 0;
|
|
693
|
+
const vars = (mode) => toShadcnAliasVars(ctx.prefix, concrete?.[mode]);
|
|
694
|
+
return {
|
|
695
|
+
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
|
696
|
+
name: "material-theme-builder",
|
|
697
|
+
type: "registry:theme",
|
|
698
|
+
title: "Material Theme Builder",
|
|
699
|
+
description: `Points shadcn's CSS variables at the M3 custom properties \`<Mtb>\` emits, so every shadcn component follows whichever theme is above it in the tree.${fallback ? " Falls back to this theme's own colors where no `<Mtb>` is mounted." : ""}`,
|
|
700
|
+
cssVars: { light: vars("light"), dark: vars("dark") }
|
|
701
|
+
};
|
|
702
|
+
}
|
|
664
703
|
function buildShadcn(ctx) {
|
|
665
704
|
const { mergedColorsLight, mergedColorsDark } = ctx;
|
|
666
705
|
return {
|
|
@@ -671,6 +710,64 @@ function buildShadcn(ctx) {
|
|
|
671
710
|
|
|
672
711
|
// src/lib/builder.tailwind.ts
|
|
673
712
|
import { kebabCase as kebabCase3 } from "lodash-es";
|
|
713
|
+
|
|
714
|
+
// src/lib/tokens.ts
|
|
715
|
+
var DEFAULT_PREFIX = "md";
|
|
716
|
+
var tokenDescriptions = {
|
|
717
|
+
background: "Default background color for screens and large surfaces.",
|
|
718
|
+
error: "Color for error states, used on elements like error text and icons.",
|
|
719
|
+
errorContainer: "Fill color for error container elements like error banners.",
|
|
720
|
+
inverseOnSurface: "Color for text and icons on inverse surface backgrounds.",
|
|
721
|
+
inversePrimary: "Primary color used on inverse surface, e.g. buttons on snackbars.",
|
|
722
|
+
inverseSurface: "Background for elements that require reverse contrast, such as snackbars.",
|
|
723
|
+
onBackground: "Color for text and icons displayed on the background.",
|
|
724
|
+
onError: "Color for text and icons on error-colored elements.",
|
|
725
|
+
onErrorContainer: "Color for text and icons on error container elements.",
|
|
726
|
+
onPrimary: "Color for text and icons on primary-colored elements like filled buttons.",
|
|
727
|
+
onPrimaryContainer: "Color for text and icons on primary container elements like tonal buttons.",
|
|
728
|
+
onPrimaryFixed: "Color for text and icons on primary fixed elements, constant across themes.",
|
|
729
|
+
onPrimaryFixedVariant: "Lower-emphasis color for text and icons on primary fixed elements.",
|
|
730
|
+
onSecondary: "Color for text and icons on secondary-colored elements.",
|
|
731
|
+
onSecondaryContainer: "Color for text and icons on secondary container elements.",
|
|
732
|
+
onSecondaryFixed: "Color for text and icons on secondary fixed elements, constant across themes.",
|
|
733
|
+
onSecondaryFixedVariant: "Lower-emphasis color for text and icons on secondary fixed elements.",
|
|
734
|
+
onSurface: "High-emphasis color for text and icons on surface backgrounds.",
|
|
735
|
+
onSurfaceVariant: "Medium-emphasis color for text and icons on surface variant backgrounds.",
|
|
736
|
+
onTertiary: "Color for text and icons on tertiary-colored elements.",
|
|
737
|
+
onTertiaryContainer: "Color for text and icons on tertiary container elements.",
|
|
738
|
+
onTertiaryFixed: "Color for text and icons on tertiary fixed elements, constant across themes.",
|
|
739
|
+
onTertiaryFixedVariant: "Lower-emphasis color for text and icons on tertiary fixed elements.",
|
|
740
|
+
outline: "Subtle color for borders and dividers to create visual separation.",
|
|
741
|
+
outlineVariant: "Lower-emphasis border color used for decorative dividers.",
|
|
742
|
+
primary: "Main brand color, used for key components like filled buttons and active states.",
|
|
743
|
+
primaryContainer: "Fill color for large primary elements like cards and tonal buttons.",
|
|
744
|
+
primaryFixed: "Fixed primary color that stays the same in light and dark themes.",
|
|
745
|
+
primaryFixedDim: "Dimmed variant of the fixed primary color for lower emphasis.",
|
|
746
|
+
scrim: "Color overlay for modals and dialogs to obscure background content.",
|
|
747
|
+
secondary: "Accent color for less prominent elements like filter chips and selections.",
|
|
748
|
+
secondaryContainer: "Fill color for secondary container elements like tonal buttons and input fields.",
|
|
749
|
+
secondaryFixed: "Fixed secondary color that stays the same in light and dark themes.",
|
|
750
|
+
secondaryFixedDim: "Dimmed variant of the fixed secondary color for lower emphasis.",
|
|
751
|
+
shadow: "Color for elevation shadows applied to surfaces and components.",
|
|
752
|
+
surface: "Default surface color for cards, sheets, and dialogs.",
|
|
753
|
+
surfaceBright: "Brightest surface variant, used for elevated surfaces in dark themes.",
|
|
754
|
+
surfaceContainer: "Middle-emphasis container color for grouping related content.",
|
|
755
|
+
surfaceContainerHigh: "Higher-emphasis container color for elements like cards.",
|
|
756
|
+
surfaceContainerHighest: "Highest-emphasis container color for text fields and other input areas.",
|
|
757
|
+
surfaceContainerLow: "Lower-emphasis container color for subtle surface groupings.",
|
|
758
|
+
surfaceContainerLowest: "Lowest-emphasis container, typically the lightest surface in light theme.",
|
|
759
|
+
surfaceDim: "Dimmest surface variant, used for recessed areas or dark theme backgrounds.",
|
|
760
|
+
surfaceTint: "Tint color applied to surfaces for subtle primary color elevation overlay.",
|
|
761
|
+
surfaceVariant: "Alternative surface color for differentiated areas like sidebar backgrounds.",
|
|
762
|
+
tertiary: "Third accent color for complementary elements that balance primary and secondary.",
|
|
763
|
+
tertiaryContainer: "Fill color for tertiary container elements like complementary cards.",
|
|
764
|
+
tertiaryFixed: "Fixed tertiary color that stays the same in light and dark themes.",
|
|
765
|
+
tertiaryFixedDim: "Dimmed variant of the fixed tertiary color for lower emphasis."
|
|
766
|
+
};
|
|
767
|
+
function isTokenName(key) {
|
|
768
|
+
return key in tokenDescriptions;
|
|
769
|
+
}
|
|
770
|
+
var tokenNames = Object.keys(tokenDescriptions).filter(isTokenName);
|
|
674
771
|
var SHADE_TO_TONE = [
|
|
675
772
|
[50, 95],
|
|
676
773
|
[100, 90],
|
|
@@ -692,6 +789,8 @@ var CORE_PALETTES = [
|
|
|
692
789
|
"neutral",
|
|
693
790
|
"neutral-variant"
|
|
694
791
|
];
|
|
792
|
+
|
|
793
|
+
// src/lib/builder.tailwind.ts
|
|
695
794
|
function buildTailwind(ctx, options) {
|
|
696
795
|
const { prefix, mergedColorsLight, hexCustomColors } = ctx;
|
|
697
796
|
const lines = [];
|
|
@@ -724,17 +823,8 @@ function buildTailwind(ctx, options) {
|
|
|
724
823
|
${lines.join("\n ")}
|
|
725
824
|
}
|
|
726
825
|
`;
|
|
727
|
-
if (options?.shadcn)
|
|
728
|
-
|
|
729
|
-
([shadcnVar, m3Token]) => `${shadcnVar}: var(--${prefix}-sys-color-${m3Token});`
|
|
730
|
-
);
|
|
731
|
-
output += `
|
|
732
|
-
:root,
|
|
733
|
-
.dark {
|
|
734
|
-
${shadcnLines.join("\n ")}
|
|
735
|
-
}
|
|
736
|
-
`;
|
|
737
|
-
}
|
|
826
|
+
if (options?.shadcn) output += `
|
|
827
|
+
${buildShadcnAliases(ctx)}`;
|
|
738
828
|
return output;
|
|
739
829
|
}
|
|
740
830
|
|
|
@@ -761,7 +851,27 @@ var DEFAULT_SCHEME = "tonalSpot";
|
|
|
761
851
|
var DEFAULT_CONTRAST = 0;
|
|
762
852
|
var DEFAULT_CUSTOM_COLORS = [];
|
|
763
853
|
var DEFAULT_BLEND = true;
|
|
764
|
-
var
|
|
854
|
+
var HEX_COLOR = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
855
|
+
function isHexColor(value) {
|
|
856
|
+
return HEX_COLOR.test(value);
|
|
857
|
+
}
|
|
858
|
+
function assertHexColor(label, value) {
|
|
859
|
+
if (!isHexColor(value))
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Invalid ${label}: '${value}'. Expected a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #6750A4).`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
function assertHexInputs(source, cores, customColors) {
|
|
865
|
+
assertHexColor("source", source);
|
|
866
|
+
for (const [name, hex] of Object.entries(cores))
|
|
867
|
+
if (hex !== void 0) assertHexColor(name, hex);
|
|
868
|
+
customColors.forEach(
|
|
869
|
+
(color, i) => assertHexColor(`customColors[${i}].hex`, color.hex)
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
function optionalHex(hex) {
|
|
873
|
+
return hex?.trim() || void 0;
|
|
874
|
+
}
|
|
765
875
|
var STANDARD_TONES = [
|
|
766
876
|
0,
|
|
767
877
|
4,
|
|
@@ -812,61 +922,6 @@ var schemeToVariant = {
|
|
|
812
922
|
fidelity: Variant.FIDELITY,
|
|
813
923
|
content: Variant.CONTENT
|
|
814
924
|
};
|
|
815
|
-
var tokenDescriptions = {
|
|
816
|
-
background: "Default background color for screens and large surfaces.",
|
|
817
|
-
error: "Color for error states, used on elements like error text and icons.",
|
|
818
|
-
errorContainer: "Fill color for error container elements like error banners.",
|
|
819
|
-
inverseOnSurface: "Color for text and icons on inverse surface backgrounds.",
|
|
820
|
-
inversePrimary: "Primary color used on inverse surface, e.g. buttons on snackbars.",
|
|
821
|
-
inverseSurface: "Background for elements that require reverse contrast, such as snackbars.",
|
|
822
|
-
onBackground: "Color for text and icons displayed on the background.",
|
|
823
|
-
onError: "Color for text and icons on error-colored elements.",
|
|
824
|
-
onErrorContainer: "Color for text and icons on error container elements.",
|
|
825
|
-
onPrimary: "Color for text and icons on primary-colored elements like filled buttons.",
|
|
826
|
-
onPrimaryContainer: "Color for text and icons on primary container elements like tonal buttons.",
|
|
827
|
-
onPrimaryFixed: "Color for text and icons on primary fixed elements, constant across themes.",
|
|
828
|
-
onPrimaryFixedVariant: "Lower-emphasis color for text and icons on primary fixed elements.",
|
|
829
|
-
onSecondary: "Color for text and icons on secondary-colored elements.",
|
|
830
|
-
onSecondaryContainer: "Color for text and icons on secondary container elements.",
|
|
831
|
-
onSecondaryFixed: "Color for text and icons on secondary fixed elements, constant across themes.",
|
|
832
|
-
onSecondaryFixedVariant: "Lower-emphasis color for text and icons on secondary fixed elements.",
|
|
833
|
-
onSurface: "High-emphasis color for text and icons on surface backgrounds.",
|
|
834
|
-
onSurfaceVariant: "Medium-emphasis color for text and icons on surface variant backgrounds.",
|
|
835
|
-
onTertiary: "Color for text and icons on tertiary-colored elements.",
|
|
836
|
-
onTertiaryContainer: "Color for text and icons on tertiary container elements.",
|
|
837
|
-
onTertiaryFixed: "Color for text and icons on tertiary fixed elements, constant across themes.",
|
|
838
|
-
onTertiaryFixedVariant: "Lower-emphasis color for text and icons on tertiary fixed elements.",
|
|
839
|
-
outline: "Subtle color for borders and dividers to create visual separation.",
|
|
840
|
-
outlineVariant: "Lower-emphasis border color used for decorative dividers.",
|
|
841
|
-
primary: "Main brand color, used for key components like filled buttons and active states.",
|
|
842
|
-
primaryContainer: "Fill color for large primary elements like cards and tonal buttons.",
|
|
843
|
-
primaryFixed: "Fixed primary color that stays the same in light and dark themes.",
|
|
844
|
-
primaryFixedDim: "Dimmed variant of the fixed primary color for lower emphasis.",
|
|
845
|
-
scrim: "Color overlay for modals and dialogs to obscure background content.",
|
|
846
|
-
secondary: "Accent color for less prominent elements like filter chips and selections.",
|
|
847
|
-
secondaryContainer: "Fill color for secondary container elements like tonal buttons and input fields.",
|
|
848
|
-
secondaryFixed: "Fixed secondary color that stays the same in light and dark themes.",
|
|
849
|
-
secondaryFixedDim: "Dimmed variant of the fixed secondary color for lower emphasis.",
|
|
850
|
-
shadow: "Color for elevation shadows applied to surfaces and components.",
|
|
851
|
-
surface: "Default surface color for cards, sheets, and dialogs.",
|
|
852
|
-
surfaceBright: "Brightest surface variant, used for elevated surfaces in dark themes.",
|
|
853
|
-
surfaceContainer: "Middle-emphasis container color for grouping related content.",
|
|
854
|
-
surfaceContainerHigh: "Higher-emphasis container color for elements like cards.",
|
|
855
|
-
surfaceContainerHighest: "Highest-emphasis container color for text fields and other input areas.",
|
|
856
|
-
surfaceContainerLow: "Lower-emphasis container color for subtle surface groupings.",
|
|
857
|
-
surfaceContainerLowest: "Lowest-emphasis container, typically the lightest surface in light theme.",
|
|
858
|
-
surfaceDim: "Dimmest surface variant, used for recessed areas or dark theme backgrounds.",
|
|
859
|
-
surfaceTint: "Tint color applied to surfaces for subtle primary color elevation overlay.",
|
|
860
|
-
surfaceVariant: "Alternative surface color for differentiated areas like sidebar backgrounds.",
|
|
861
|
-
tertiary: "Third accent color for complementary elements that balance primary and secondary.",
|
|
862
|
-
tertiaryContainer: "Fill color for tertiary container elements like complementary cards.",
|
|
863
|
-
tertiaryFixed: "Fixed tertiary color that stays the same in light and dark themes.",
|
|
864
|
-
tertiaryFixedDim: "Dimmed variant of the fixed tertiary color for lower emphasis."
|
|
865
|
-
};
|
|
866
|
-
function isTokenName(key) {
|
|
867
|
-
return key in tokenDescriptions;
|
|
868
|
-
}
|
|
869
|
-
var tokenNames = Object.keys(tokenDescriptions).filter(isTokenName);
|
|
870
925
|
function deriveCustomPaletteName(tokenName, allPaletteNamesKebab) {
|
|
871
926
|
let baseName = tokenName;
|
|
872
927
|
if (/^on[A-Z]/.test(baseName) && baseName.length > 2) {
|
|
@@ -985,11 +1040,20 @@ function builder(hexSource, {
|
|
|
985
1040
|
customColors: hexCustomColors = DEFAULT_CUSTOM_COLORS,
|
|
986
1041
|
prefix = DEFAULT_PREFIX
|
|
987
1042
|
} = {}) {
|
|
1043
|
+
const cores = {
|
|
1044
|
+
primary: optionalHex(primary),
|
|
1045
|
+
secondary: optionalHex(secondary),
|
|
1046
|
+
tertiary: optionalHex(tertiary),
|
|
1047
|
+
error: optionalHex(error),
|
|
1048
|
+
neutral: optionalHex(neutral),
|
|
1049
|
+
neutralVariant: optionalHex(neutralVariant)
|
|
1050
|
+
};
|
|
1051
|
+
assertHexInputs(hexSource, cores, hexCustomColors);
|
|
988
1052
|
const sourceArgb = argbFromHex2(hexSource);
|
|
989
1053
|
const sourceHct = Hct2.fromInt(sourceArgb);
|
|
990
|
-
const effectiveSource = primary || hexSource;
|
|
1054
|
+
const effectiveSource = cores.primary || hexSource;
|
|
991
1055
|
const effectiveSourceArgb = argbFromHex2(effectiveSource);
|
|
992
|
-
const effectiveSourceForHarmonization = primary ? argbFromHex2(primary) : sourceArgb;
|
|
1056
|
+
const effectiveSourceForHarmonization = cores.primary ? argbFromHex2(cores.primary) : sourceArgb;
|
|
993
1057
|
const SchemeClass = schemesMap[scheme];
|
|
994
1058
|
const primaryHct = Hct2.fromInt(effectiveSourceArgb);
|
|
995
1059
|
const baseScheme = new SchemeClass(primaryHct, false, contrast);
|
|
@@ -997,32 +1061,32 @@ function builder(hexSource, {
|
|
|
997
1061
|
// Core colors (hex may be undefined)
|
|
998
1062
|
{
|
|
999
1063
|
name: "primary",
|
|
1000
|
-
hex: primary,
|
|
1064
|
+
hex: cores.primary,
|
|
1001
1065
|
core: true,
|
|
1002
1066
|
chromaSource: "primary"
|
|
1003
1067
|
},
|
|
1004
1068
|
{
|
|
1005
1069
|
name: "secondary",
|
|
1006
|
-
hex: secondary,
|
|
1070
|
+
hex: cores.secondary,
|
|
1007
1071
|
core: true,
|
|
1008
1072
|
chromaSource: "primary"
|
|
1009
1073
|
},
|
|
1010
1074
|
{
|
|
1011
1075
|
name: "tertiary",
|
|
1012
|
-
hex: tertiary,
|
|
1076
|
+
hex: cores.tertiary,
|
|
1013
1077
|
core: true,
|
|
1014
1078
|
chromaSource: "primary"
|
|
1015
1079
|
},
|
|
1016
|
-
{ name: "error", hex: error, core: true, chromaSource: "primary" },
|
|
1080
|
+
{ name: "error", hex: cores.error, core: true, chromaSource: "primary" },
|
|
1017
1081
|
{
|
|
1018
1082
|
name: "neutral",
|
|
1019
|
-
hex: neutral,
|
|
1083
|
+
hex: cores.neutral,
|
|
1020
1084
|
core: true,
|
|
1021
1085
|
chromaSource: "neutral"
|
|
1022
1086
|
},
|
|
1023
1087
|
{
|
|
1024
1088
|
name: "neutralVariant",
|
|
1025
|
-
hex: neutralVariant,
|
|
1089
|
+
hex: cores.neutralVariant,
|
|
1026
1090
|
core: true,
|
|
1027
1091
|
chromaSource: "neutralVariant"
|
|
1028
1092
|
},
|
|
@@ -1128,6 +1192,8 @@ function builder(hexSource, {
|
|
|
1128
1192
|
toFigmaTokens: () => buildFigmaTokens(ctx),
|
|
1129
1193
|
toTailwind: (options) => buildTailwind(ctx, options),
|
|
1130
1194
|
toShadcn: () => buildShadcn(ctx),
|
|
1195
|
+
toShadcnAliases: () => buildShadcnAliases(ctx),
|
|
1196
|
+
toShadcnRegistryItem: (options) => buildShadcnRegistryItem(ctx, options),
|
|
1131
1197
|
toFlutter: () => buildFlutter(ctx),
|
|
1132
1198
|
mergedColorsLight,
|
|
1133
1199
|
mergedColorsDark,
|
|
@@ -1135,89 +1201,220 @@ function builder(hexSource, {
|
|
|
1135
1201
|
};
|
|
1136
1202
|
}
|
|
1137
1203
|
|
|
1204
|
+
// src/cli.options.ts
|
|
1205
|
+
function parseHexColor(value) {
|
|
1206
|
+
if (!isHexColor(value))
|
|
1207
|
+
throw new InvalidArgumentError(
|
|
1208
|
+
"Expected a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #6750A4)."
|
|
1209
|
+
);
|
|
1210
|
+
return value;
|
|
1211
|
+
}
|
|
1212
|
+
function addSourceArgument(command) {
|
|
1213
|
+
return command.argument(
|
|
1214
|
+
"<source>",
|
|
1215
|
+
"Source color in hex format (e.g. #6750A4)",
|
|
1216
|
+
parseHexColor
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
function addThemeOptions(command) {
|
|
1220
|
+
return command.addOption(
|
|
1221
|
+
new Option("--scheme <name>", "Color scheme variant").choices(schemeNames).default(DEFAULT_SCHEME)
|
|
1222
|
+
).option(
|
|
1223
|
+
"--contrast <number>",
|
|
1224
|
+
"Contrast level from -1.0 to 1.0",
|
|
1225
|
+
parseFloat,
|
|
1226
|
+
DEFAULT_CONTRAST
|
|
1227
|
+
).option("--primary <hex>", "Primary color override", parseHexColor).option("--secondary <hex>", "Secondary color override", parseHexColor).option("--tertiary <hex>", "Tertiary color override", parseHexColor).option("--error <hex>", "Error color override", parseHexColor).option("--neutral <hex>", "Neutral color override", parseHexColor).option(
|
|
1228
|
+
"--neutral-variant <hex>",
|
|
1229
|
+
"Neutral variant color override",
|
|
1230
|
+
parseHexColor
|
|
1231
|
+
).option(
|
|
1232
|
+
"--no-fallback",
|
|
1233
|
+
"Omit this theme's colors as the var() fallbacks in the registry item, so it renders nothing without an <Mtb> above it"
|
|
1234
|
+
).option(
|
|
1235
|
+
"--prefix <string>",
|
|
1236
|
+
"CSS variable prefix (e.g. md \u2192 --md-sys-color-*, --md-ref-palette-*)",
|
|
1237
|
+
DEFAULT_PREFIX
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
function builderOptions(opts) {
|
|
1241
|
+
return {
|
|
1242
|
+
scheme: opts.scheme,
|
|
1243
|
+
contrast: opts.contrast,
|
|
1244
|
+
primary: opts.primary,
|
|
1245
|
+
secondary: opts.secondary,
|
|
1246
|
+
tertiary: opts.tertiary,
|
|
1247
|
+
error: opts.error,
|
|
1248
|
+
neutral: opts.neutral,
|
|
1249
|
+
neutralVariant: opts.neutralVariant,
|
|
1250
|
+
prefix: opts.prefix
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function themeFrom(command) {
|
|
1254
|
+
const opts = command.opts();
|
|
1255
|
+
return {
|
|
1256
|
+
options: builderOptions(opts),
|
|
1257
|
+
// Declared as a negation, so commander's own default here is `true` -- the
|
|
1258
|
+
// opposite of the API's, and deliberately so: a CLI knows the source color,
|
|
1259
|
+
// which is what makes baking its colors in as the fallbacks free. See
|
|
1260
|
+
// `buildShadcnRegistryItem()`.
|
|
1261
|
+
fallback: opts.fallback ?? true
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/cli.shadcn.ts
|
|
1266
|
+
import { spawnSync } from "child_process";
|
|
1267
|
+
import * as fs from "fs";
|
|
1268
|
+
import * as path from "path";
|
|
1269
|
+
import "commander";
|
|
1270
|
+
var NPX = ["npx", "--yes"];
|
|
1271
|
+
var DEFAULT_SHADCN = "shadcn@latest";
|
|
1272
|
+
var YES = "--yes";
|
|
1273
|
+
var ITEM = "mtb.json";
|
|
1274
|
+
function addArgv(shadcn, forwarded = []) {
|
|
1275
|
+
return [...NPX, shadcn, "add", `./${ITEM}`, YES, ...forwarded];
|
|
1276
|
+
}
|
|
1277
|
+
function fail(message, code = 1) {
|
|
1278
|
+
console.error(`Error: ${message}`);
|
|
1279
|
+
process.exit(code);
|
|
1280
|
+
}
|
|
1281
|
+
function writeItem(source, command, file) {
|
|
1282
|
+
if (fs.existsSync(file))
|
|
1283
|
+
fail(
|
|
1284
|
+
`${path.basename(file)} already exists and would be overwritten, then deleted. Move it aside first.`
|
|
1285
|
+
);
|
|
1286
|
+
process.on("exit", () => fs.rmSync(file, { force: true }));
|
|
1287
|
+
const { options, fallback } = themeFrom(command);
|
|
1288
|
+
const item = builder(source, options).toShadcnRegistryItem({ fallback });
|
|
1289
|
+
fs.writeFileSync(file, `${JSON.stringify(item, null, 2)}
|
|
1290
|
+
`);
|
|
1291
|
+
}
|
|
1292
|
+
function runApply(source, forwarded, command) {
|
|
1293
|
+
const file = path.resolve(ITEM);
|
|
1294
|
+
writeItem(source, command, file);
|
|
1295
|
+
const [bin = "", ...args] = addArgv(command.opts().shadcnCli, forwarded);
|
|
1296
|
+
const { error, status } = spawnSync(bin, args, {
|
|
1297
|
+
stdio: "inherit",
|
|
1298
|
+
shell: false
|
|
1299
|
+
});
|
|
1300
|
+
if (error) fail(`shadcn add could not be started: ${error.message}`);
|
|
1301
|
+
if (status !== 0) fail("shadcn add failed", status ?? 1);
|
|
1302
|
+
fs.rmSync(file, { force: true });
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1138
1305
|
// src/cli.ts
|
|
1139
1306
|
var customColorSchema = z.array(
|
|
1140
1307
|
z.object({
|
|
1141
1308
|
name: z.string(),
|
|
1142
|
-
hex: z.string()
|
|
1309
|
+
hex: z.string().refine(
|
|
1310
|
+
isHexColor,
|
|
1311
|
+
"must be a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #FF5733)"
|
|
1312
|
+
),
|
|
1143
1313
|
blend: z.boolean().default(DEFAULT_BLEND)
|
|
1144
1314
|
})
|
|
1145
1315
|
);
|
|
1146
1316
|
function writeFigmaTokens(theme, outputDir) {
|
|
1147
|
-
|
|
1317
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
1148
1318
|
for (const [filename, content] of Object.entries(theme.toFigmaTokens())) {
|
|
1149
|
-
const filePath =
|
|
1150
|
-
|
|
1319
|
+
const filePath = path2.join(outputDir, filename);
|
|
1320
|
+
fs2.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
|
|
1151
1321
|
console.error(`wrote ${filePath}`);
|
|
1152
1322
|
}
|
|
1153
1323
|
}
|
|
1324
|
+
var FORMATS = [
|
|
1325
|
+
"json",
|
|
1326
|
+
"css",
|
|
1327
|
+
"figma",
|
|
1328
|
+
"tailwind",
|
|
1329
|
+
"shadcn",
|
|
1330
|
+
"registry-item",
|
|
1331
|
+
"flutter"
|
|
1332
|
+
];
|
|
1154
1333
|
function writeOutput(theme, opts) {
|
|
1155
1334
|
const json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
1156
1335
|
switch (opts.format) {
|
|
1336
|
+
case "json":
|
|
1337
|
+
return process.stdout.write(json(theme.toJson()));
|
|
1157
1338
|
case "css":
|
|
1158
1339
|
return process.stdout.write(theme.toCss());
|
|
1159
1340
|
case "tailwind":
|
|
1160
1341
|
return process.stdout.write(theme.toTailwind({ shadcn: opts.shadcn }));
|
|
1161
1342
|
case "shadcn":
|
|
1162
1343
|
return process.stdout.write(json(theme.toShadcn()));
|
|
1344
|
+
case "registry-item":
|
|
1345
|
+
return process.stdout.write(
|
|
1346
|
+
json(theme.toShadcnRegistryItem({ fallback: opts.fallback }))
|
|
1347
|
+
);
|
|
1163
1348
|
case "flutter":
|
|
1164
1349
|
return process.stdout.write(theme.toFlutter());
|
|
1165
1350
|
case "figma":
|
|
1166
1351
|
return writeFigmaTokens(theme, opts.output ?? "material-theme");
|
|
1167
|
-
default:
|
|
1168
|
-
return process.stdout.write(json(theme.toJson()));
|
|
1169
1352
|
}
|
|
1170
1353
|
}
|
|
1171
1354
|
var program = new Command();
|
|
1172
|
-
|
|
1173
|
-
|
|
1355
|
+
addThemeOptions(
|
|
1356
|
+
addSourceArgument(
|
|
1357
|
+
program.name("material-theme-builder").description("Generate a color theme from a source color")
|
|
1358
|
+
).enablePositionalOptions()
|
|
1174
1359
|
).option(
|
|
1175
|
-
"--contrast <number>",
|
|
1176
|
-
"Contrast level from -1.0 to 1.0",
|
|
1177
|
-
parseFloat,
|
|
1178
|
-
DEFAULT_CONTRAST
|
|
1179
|
-
).option("--primary <hex>", "Primary color override").option("--secondary <hex>", "Secondary color override").option("--tertiary <hex>", "Tertiary color override").option("--error <hex>", "Error color override").option("--neutral <hex>", "Neutral color override").option("--neutral-variant <hex>", "Neutral variant color override").option(
|
|
1180
1360
|
"--custom-colors <json>",
|
|
1181
1361
|
`Custom colors as JSON array (e.g. '[{"name":"brand","hex":"#FF5733","blend":true}]')`
|
|
1182
|
-
).
|
|
1183
|
-
"--format <type>",
|
|
1184
|
-
"Output format: json, css, figma, tailwind, shadcn, or flutter",
|
|
1185
|
-
"figma"
|
|
1362
|
+
).addOption(
|
|
1363
|
+
new Option2("--format <type>", "Output format").choices(FORMATS).default("figma")
|
|
1186
1364
|
).option("--output <dir>", "Output directory (required for figma format)").option(
|
|
1187
1365
|
"--shadcn",
|
|
1188
1366
|
"Append the shadcn var() alias block to --format tailwind (for concrete values, use --format shadcn)"
|
|
1189
|
-
).
|
|
1190
|
-
"--prefix <string>",
|
|
1191
|
-
"CSS variable prefix (e.g. md \u2192 --md-sys-color-*, --md-ref-palette-*)",
|
|
1192
|
-
DEFAULT_PREFIX
|
|
1193
|
-
).action((source, opts) => {
|
|
1367
|
+
).action((source, opts, command) => {
|
|
1194
1368
|
if (opts.shadcn && opts.format !== "tailwind") {
|
|
1195
1369
|
console.error(
|
|
1196
1370
|
"Error: --shadcn only applies to --format tailwind. For concrete color values, use --format shadcn."
|
|
1197
1371
|
);
|
|
1198
1372
|
process.exit(1);
|
|
1199
1373
|
}
|
|
1374
|
+
if (command.getOptionValueSource("fallback") === "cli" && opts.format !== "registry-item") {
|
|
1375
|
+
console.error(
|
|
1376
|
+
"Error: --no-fallback only applies to --format registry-item."
|
|
1377
|
+
);
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1200
1380
|
let customColors = [];
|
|
1201
1381
|
if (opts.customColors) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1382
|
+
let parsed;
|
|
1383
|
+
try {
|
|
1384
|
+
parsed = JSON.parse(opts.customColors);
|
|
1385
|
+
} catch {
|
|
1204
1386
|
console.error("Error: --custom-colors must be valid JSON");
|
|
1205
1387
|
process.exit(1);
|
|
1206
1388
|
}
|
|
1389
|
+
const result2 = customColorSchema.safeParse(parsed);
|
|
1390
|
+
if (!result2.success) {
|
|
1391
|
+
const issue = result2.error.issues[0];
|
|
1392
|
+
const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
|
|
1393
|
+
console.error(
|
|
1394
|
+
`Error: --custom-colors${where}: ${issue?.message ?? "is invalid"}`
|
|
1395
|
+
);
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1207
1398
|
customColors = result2.data;
|
|
1208
1399
|
}
|
|
1209
1400
|
const result = builder(source, {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
primary: opts.primary,
|
|
1213
|
-
secondary: opts.secondary,
|
|
1214
|
-
tertiary: opts.tertiary,
|
|
1215
|
-
error: opts.error,
|
|
1216
|
-
neutral: opts.neutral,
|
|
1217
|
-
neutralVariant: opts.neutralVariant,
|
|
1218
|
-
customColors,
|
|
1219
|
-
prefix: opts.prefix
|
|
1401
|
+
...builderOptions(opts),
|
|
1402
|
+
customColors
|
|
1220
1403
|
});
|
|
1221
1404
|
writeOutput(result, opts);
|
|
1222
1405
|
});
|
|
1406
|
+
addThemeOptions(
|
|
1407
|
+
addSourceArgument(
|
|
1408
|
+
program.command("shadcn-apply").description("Theme the shadcn project in the current directory")
|
|
1409
|
+
).argument(
|
|
1410
|
+
"[shadcn-args...]",
|
|
1411
|
+
"Options after a `--`, forwarded verbatim to `shadcn add`"
|
|
1412
|
+
)
|
|
1413
|
+
).option(
|
|
1414
|
+
"--shadcn-cli <spec>",
|
|
1415
|
+
"npx package spec for the shadcn CLI to run (a version, tag, fork or tarball \u2014 anything npx resolves)",
|
|
1416
|
+
DEFAULT_SHADCN
|
|
1417
|
+
).action(
|
|
1418
|
+
(source, shadcnArgs, _opts, command) => runApply(source, shadcnArgs, command)
|
|
1419
|
+
);
|
|
1223
1420
|
program.parse();
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,31 @@ type ShadcnTheme = {
|
|
|
27
27
|
light: Record<ShadcnVarName, string>;
|
|
28
28
|
dark: Record<ShadcnVarName, string>;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* A shadcn registry item carrying the alias mapping, as
|
|
32
|
+
* `toShadcnRegistryItem()` returns it.
|
|
33
|
+
*
|
|
34
|
+
* @see https://ui.shadcn.com/schema/registry-item.json
|
|
35
|
+
*/
|
|
36
|
+
type ShadcnRegistryItem = {
|
|
37
|
+
$schema: string;
|
|
38
|
+
name: string;
|
|
39
|
+
type: "registry:theme";
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
cssVars: ShadcnTheme;
|
|
43
|
+
};
|
|
44
|
+
/** Options for `toShadcnRegistryItem()`. */
|
|
45
|
+
type ShadcnRegistryItemOptions = {
|
|
46
|
+
/**
|
|
47
|
+
* Embed this theme's concrete colors as the `var()` fallbacks, so the item
|
|
48
|
+
* also works where nothing declares the M3 custom properties. Off by default
|
|
49
|
+
* — see `buildShadcnRegistryItem()` for why.
|
|
50
|
+
*
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
fallback?: boolean;
|
|
54
|
+
};
|
|
30
55
|
|
|
31
56
|
/** DTCG color value (direct, non-alias) */
|
|
32
57
|
type DtcgColorValue = {
|
|
@@ -110,6 +135,8 @@ type MtbConfig = {
|
|
|
110
135
|
/**
|
|
111
136
|
* Array of custom colors to include in the generated palette.
|
|
112
137
|
* Each custom color can be blended with the source color for harmonization.
|
|
138
|
+
*
|
|
139
|
+
* @see https://m3.material.io/blog/dynamic-color-harmony
|
|
113
140
|
*/
|
|
114
141
|
customColors?: HexCustomColor[];
|
|
115
142
|
/**
|
|
@@ -178,6 +205,8 @@ declare function builder(hexSource: MtbConfig["source"], { scheme, contrast, pri
|
|
|
178
205
|
};
|
|
179
206
|
toTailwind: (options?: TailwindOptions) => string;
|
|
180
207
|
toShadcn: () => ShadcnTheme;
|
|
208
|
+
toShadcnAliases: () => string;
|
|
209
|
+
toShadcnRegistryItem: (options?: ShadcnRegistryItemOptions) => ShadcnRegistryItem;
|
|
181
210
|
toFlutter: () => string;
|
|
182
211
|
mergedColorsLight: {
|
|
183
212
|
[x: string]: number;
|
|
@@ -195,4 +224,4 @@ declare function builder(hexSource: MtbConfig["source"], { scheme, contrast, pri
|
|
|
195
224
|
};
|
|
196
225
|
};
|
|
197
226
|
|
|
198
|
-
export { type McuConfig, type MtbConfig, type ShadcnTheme, type ShadcnVarName, builder };
|
|
227
|
+
export { type McuConfig, type MtbConfig, type ShadcnRegistryItem, type ShadcnTheme, type ShadcnVarName, builder };
|