material-theme-builder 3.2.0 → 3.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 +284 -236
- package/dist/cli.js +520 -105
- package/dist/index.d.ts +30 -1
- package/dist/index.js +120 -67
- package/dist/react.d.ts +69 -49
- package/dist/react.js +120 -67
- 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 +31 -10
- package/src/tailwind.css +0 -179
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import { Command
|
|
4
|
+
import * as fs2 from "fs";
|
|
5
|
+
import * as path2 from "path";
|
|
6
|
+
import { Command as Command2 } from "commander";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
|
+
// src/cli.options.ts
|
|
10
|
+
import {
|
|
11
|
+
Command,
|
|
12
|
+
InvalidArgumentError,
|
|
13
|
+
Option
|
|
14
|
+
} from "commander";
|
|
15
|
+
|
|
9
16
|
// src/lib/builder.ts
|
|
10
17
|
import {
|
|
11
18
|
argbFromHex as argbFromHex2,
|
|
@@ -116,9 +123,9 @@ function buildFigmaVariables(ctx) {
|
|
|
116
123
|
paletteHexMap[paletteName] = {};
|
|
117
124
|
for (const tone of STANDARD_TONES) {
|
|
118
125
|
const argb = palette.tone(tone);
|
|
119
|
-
const
|
|
126
|
+
const path3 = `ref/palette/${paletteName}/${tone}`;
|
|
120
127
|
const hex = hexFromArgb2(argb).toUpperCase();
|
|
121
|
-
paletteHexMap[paletteName][hex] =
|
|
128
|
+
paletteHexMap[paletteName][hex] = path3;
|
|
122
129
|
const color = {
|
|
123
130
|
r: redFromArgb(argb) / 255,
|
|
124
131
|
g: greenFromArgb(argb) / 255,
|
|
@@ -126,7 +133,7 @@ function buildFigmaVariables(ctx) {
|
|
|
126
133
|
a: 1
|
|
127
134
|
};
|
|
128
135
|
variables.push({
|
|
129
|
-
path:
|
|
136
|
+
path: path3,
|
|
130
137
|
scopes: ["ALL_SCOPES"],
|
|
131
138
|
values: { Light: color, Dark: color }
|
|
132
139
|
});
|
|
@@ -210,8 +217,8 @@ function buildFigmaTokens(ctx) {
|
|
|
210
217
|
$extensions: toDtcgExtensions(v)
|
|
211
218
|
};
|
|
212
219
|
}
|
|
213
|
-
function parsePath(
|
|
214
|
-
const parts =
|
|
220
|
+
function parsePath(path3) {
|
|
221
|
+
const parts = path3.split("/");
|
|
215
222
|
if (parts[0] === "ref" && parts[1] === "palette" && parts[2] && parts[3]) {
|
|
216
223
|
return {
|
|
217
224
|
kind: "palette",
|
|
@@ -661,6 +668,39 @@ function toShadcnVars(mergedColors) {
|
|
|
661
668
|
});
|
|
662
669
|
return Object.fromEntries(entries);
|
|
663
670
|
}
|
|
671
|
+
function toShadcnAliasVars(prefix, fallbacks) {
|
|
672
|
+
const entries = SHADCN_MAPPING.map(([cssVar, m3Token]) => {
|
|
673
|
+
const bare = cssVar.slice(2);
|
|
674
|
+
const property = `--${prefix}-sys-color-${m3Token}`;
|
|
675
|
+
return [
|
|
676
|
+
bare,
|
|
677
|
+
fallbacks ? `var(${property}, ${fallbacks[bare]})` : `var(${property})`
|
|
678
|
+
];
|
|
679
|
+
});
|
|
680
|
+
return Object.fromEntries(entries);
|
|
681
|
+
}
|
|
682
|
+
function buildShadcnAliases(ctx) {
|
|
683
|
+
const lines = Object.entries(toShadcnAliasVars(ctx.prefix)).map(
|
|
684
|
+
([name, value]) => `--${name}: ${value};`
|
|
685
|
+
);
|
|
686
|
+
return `:root,
|
|
687
|
+
.dark {
|
|
688
|
+
${lines.join("\n ")}
|
|
689
|
+
}
|
|
690
|
+
`;
|
|
691
|
+
}
|
|
692
|
+
function buildShadcnRegistryItem(ctx, { fallback = false } = {}) {
|
|
693
|
+
const concrete = fallback ? buildShadcn(ctx) : void 0;
|
|
694
|
+
const vars = (mode) => toShadcnAliasVars(ctx.prefix, concrete?.[mode]);
|
|
695
|
+
return {
|
|
696
|
+
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
|
697
|
+
name: "material-theme-builder",
|
|
698
|
+
type: "registry:theme",
|
|
699
|
+
title: "Material Theme Builder",
|
|
700
|
+
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." : ""}`,
|
|
701
|
+
cssVars: { light: vars("light"), dark: vars("dark") }
|
|
702
|
+
};
|
|
703
|
+
}
|
|
664
704
|
function buildShadcn(ctx) {
|
|
665
705
|
const { mergedColorsLight, mergedColorsDark } = ctx;
|
|
666
706
|
return {
|
|
@@ -671,6 +711,64 @@ function buildShadcn(ctx) {
|
|
|
671
711
|
|
|
672
712
|
// src/lib/builder.tailwind.ts
|
|
673
713
|
import { kebabCase as kebabCase3 } from "lodash-es";
|
|
714
|
+
|
|
715
|
+
// src/lib/tokens.ts
|
|
716
|
+
var DEFAULT_PREFIX = "md";
|
|
717
|
+
var tokenDescriptions = {
|
|
718
|
+
background: "Default background color for screens and large surfaces.",
|
|
719
|
+
error: "Color for error states, used on elements like error text and icons.",
|
|
720
|
+
errorContainer: "Fill color for error container elements like error banners.",
|
|
721
|
+
inverseOnSurface: "Color for text and icons on inverse surface backgrounds.",
|
|
722
|
+
inversePrimary: "Primary color used on inverse surface, e.g. buttons on snackbars.",
|
|
723
|
+
inverseSurface: "Background for elements that require reverse contrast, such as snackbars.",
|
|
724
|
+
onBackground: "Color for text and icons displayed on the background.",
|
|
725
|
+
onError: "Color for text and icons on error-colored elements.",
|
|
726
|
+
onErrorContainer: "Color for text and icons on error container elements.",
|
|
727
|
+
onPrimary: "Color for text and icons on primary-colored elements like filled buttons.",
|
|
728
|
+
onPrimaryContainer: "Color for text and icons on primary container elements like tonal buttons.",
|
|
729
|
+
onPrimaryFixed: "Color for text and icons on primary fixed elements, constant across themes.",
|
|
730
|
+
onPrimaryFixedVariant: "Lower-emphasis color for text and icons on primary fixed elements.",
|
|
731
|
+
onSecondary: "Color for text and icons on secondary-colored elements.",
|
|
732
|
+
onSecondaryContainer: "Color for text and icons on secondary container elements.",
|
|
733
|
+
onSecondaryFixed: "Color for text and icons on secondary fixed elements, constant across themes.",
|
|
734
|
+
onSecondaryFixedVariant: "Lower-emphasis color for text and icons on secondary fixed elements.",
|
|
735
|
+
onSurface: "High-emphasis color for text and icons on surface backgrounds.",
|
|
736
|
+
onSurfaceVariant: "Medium-emphasis color for text and icons on surface variant backgrounds.",
|
|
737
|
+
onTertiary: "Color for text and icons on tertiary-colored elements.",
|
|
738
|
+
onTertiaryContainer: "Color for text and icons on tertiary container elements.",
|
|
739
|
+
onTertiaryFixed: "Color for text and icons on tertiary fixed elements, constant across themes.",
|
|
740
|
+
onTertiaryFixedVariant: "Lower-emphasis color for text and icons on tertiary fixed elements.",
|
|
741
|
+
outline: "Subtle color for borders and dividers to create visual separation.",
|
|
742
|
+
outlineVariant: "Lower-emphasis border color used for decorative dividers.",
|
|
743
|
+
primary: "Main brand color, used for key components like filled buttons and active states.",
|
|
744
|
+
primaryContainer: "Fill color for large primary elements like cards and tonal buttons.",
|
|
745
|
+
primaryFixed: "Fixed primary color that stays the same in light and dark themes.",
|
|
746
|
+
primaryFixedDim: "Dimmed variant of the fixed primary color for lower emphasis.",
|
|
747
|
+
scrim: "Color overlay for modals and dialogs to obscure background content.",
|
|
748
|
+
secondary: "Accent color for less prominent elements like filter chips and selections.",
|
|
749
|
+
secondaryContainer: "Fill color for secondary container elements like tonal buttons and input fields.",
|
|
750
|
+
secondaryFixed: "Fixed secondary color that stays the same in light and dark themes.",
|
|
751
|
+
secondaryFixedDim: "Dimmed variant of the fixed secondary color for lower emphasis.",
|
|
752
|
+
shadow: "Color for elevation shadows applied to surfaces and components.",
|
|
753
|
+
surface: "Default surface color for cards, sheets, and dialogs.",
|
|
754
|
+
surfaceBright: "Brightest surface variant, used for elevated surfaces in dark themes.",
|
|
755
|
+
surfaceContainer: "Middle-emphasis container color for grouping related content.",
|
|
756
|
+
surfaceContainerHigh: "Higher-emphasis container color for elements like cards.",
|
|
757
|
+
surfaceContainerHighest: "Highest-emphasis container color for text fields and other input areas.",
|
|
758
|
+
surfaceContainerLow: "Lower-emphasis container color for subtle surface groupings.",
|
|
759
|
+
surfaceContainerLowest: "Lowest-emphasis container, typically the lightest surface in light theme.",
|
|
760
|
+
surfaceDim: "Dimmest surface variant, used for recessed areas or dark theme backgrounds.",
|
|
761
|
+
surfaceTint: "Tint color applied to surfaces for subtle primary color elevation overlay.",
|
|
762
|
+
surfaceVariant: "Alternative surface color for differentiated areas like sidebar backgrounds.",
|
|
763
|
+
tertiary: "Third accent color for complementary elements that balance primary and secondary.",
|
|
764
|
+
tertiaryContainer: "Fill color for tertiary container elements like complementary cards.",
|
|
765
|
+
tertiaryFixed: "Fixed tertiary color that stays the same in light and dark themes.",
|
|
766
|
+
tertiaryFixedDim: "Dimmed variant of the fixed tertiary color for lower emphasis."
|
|
767
|
+
};
|
|
768
|
+
function isTokenName(key) {
|
|
769
|
+
return key in tokenDescriptions;
|
|
770
|
+
}
|
|
771
|
+
var tokenNames = Object.keys(tokenDescriptions).filter(isTokenName);
|
|
674
772
|
var SHADE_TO_TONE = [
|
|
675
773
|
[50, 95],
|
|
676
774
|
[100, 90],
|
|
@@ -692,6 +790,8 @@ var CORE_PALETTES = [
|
|
|
692
790
|
"neutral",
|
|
693
791
|
"neutral-variant"
|
|
694
792
|
];
|
|
793
|
+
|
|
794
|
+
// src/lib/builder.tailwind.ts
|
|
695
795
|
function buildTailwind(ctx, options) {
|
|
696
796
|
const { prefix, mergedColorsLight, hexCustomColors } = ctx;
|
|
697
797
|
const lines = [];
|
|
@@ -724,17 +824,8 @@ function buildTailwind(ctx, options) {
|
|
|
724
824
|
${lines.join("\n ")}
|
|
725
825
|
}
|
|
726
826
|
`;
|
|
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
|
-
}
|
|
827
|
+
if (options?.shadcn) output += `
|
|
828
|
+
${buildShadcnAliases(ctx)}`;
|
|
738
829
|
return output;
|
|
739
830
|
}
|
|
740
831
|
|
|
@@ -761,7 +852,24 @@ var DEFAULT_SCHEME = "tonalSpot";
|
|
|
761
852
|
var DEFAULT_CONTRAST = 0;
|
|
762
853
|
var DEFAULT_CUSTOM_COLORS = [];
|
|
763
854
|
var DEFAULT_BLEND = true;
|
|
764
|
-
var
|
|
855
|
+
var HEX_COLOR = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
856
|
+
function isHexColor(value) {
|
|
857
|
+
return HEX_COLOR.test(value);
|
|
858
|
+
}
|
|
859
|
+
function assertHexColor(label, value) {
|
|
860
|
+
if (!isHexColor(value))
|
|
861
|
+
throw new Error(
|
|
862
|
+
`Invalid ${label}: '${value}'. Expected a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #6750A4).`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
function assertHexInputs(source, cores, customColors) {
|
|
866
|
+
assertHexColor("source", source);
|
|
867
|
+
for (const [name, hex] of Object.entries(cores))
|
|
868
|
+
if (hex !== void 0) assertHexColor(name, hex);
|
|
869
|
+
customColors.forEach(
|
|
870
|
+
(color, i) => assertHexColor(`customColors[${i}].hex`, color.hex)
|
|
871
|
+
);
|
|
872
|
+
}
|
|
765
873
|
var STANDARD_TONES = [
|
|
766
874
|
0,
|
|
767
875
|
4,
|
|
@@ -812,61 +920,6 @@ var schemeToVariant = {
|
|
|
812
920
|
fidelity: Variant.FIDELITY,
|
|
813
921
|
content: Variant.CONTENT
|
|
814
922
|
};
|
|
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
923
|
function deriveCustomPaletteName(tokenName, allPaletteNamesKebab) {
|
|
871
924
|
let baseName = tokenName;
|
|
872
925
|
if (/^on[A-Z]/.test(baseName) && baseName.length > 2) {
|
|
@@ -985,6 +1038,11 @@ function builder(hexSource, {
|
|
|
985
1038
|
customColors: hexCustomColors = DEFAULT_CUSTOM_COLORS,
|
|
986
1039
|
prefix = DEFAULT_PREFIX
|
|
987
1040
|
} = {}) {
|
|
1041
|
+
assertHexInputs(
|
|
1042
|
+
hexSource,
|
|
1043
|
+
{ primary, secondary, tertiary, error, neutral, neutralVariant },
|
|
1044
|
+
hexCustomColors
|
|
1045
|
+
);
|
|
988
1046
|
const sourceArgb = argbFromHex2(hexSource);
|
|
989
1047
|
const sourceHct = Hct2.fromInt(sourceArgb);
|
|
990
1048
|
const effectiveSource = primary || hexSource;
|
|
@@ -1128,6 +1186,8 @@ function builder(hexSource, {
|
|
|
1128
1186
|
toFigmaTokens: () => buildFigmaTokens(ctx),
|
|
1129
1187
|
toTailwind: (options) => buildTailwind(ctx, options),
|
|
1130
1188
|
toShadcn: () => buildShadcn(ctx),
|
|
1189
|
+
toShadcnAliases: () => buildShadcnAliases(ctx),
|
|
1190
|
+
toShadcnRegistryItem: (options) => buildShadcnRegistryItem(ctx, options),
|
|
1131
1191
|
toFlutter: () => buildFlutter(ctx),
|
|
1132
1192
|
mergedColorsLight,
|
|
1133
1193
|
mergedColorsDark,
|
|
@@ -1135,19 +1195,342 @@ function builder(hexSource, {
|
|
|
1135
1195
|
};
|
|
1136
1196
|
}
|
|
1137
1197
|
|
|
1198
|
+
// src/cli.options.ts
|
|
1199
|
+
function parseHexColor(value) {
|
|
1200
|
+
if (!isHexColor(value))
|
|
1201
|
+
throw new InvalidArgumentError(
|
|
1202
|
+
"Expected a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #6750A4)."
|
|
1203
|
+
);
|
|
1204
|
+
return value;
|
|
1205
|
+
}
|
|
1206
|
+
function addSourceArgument(command) {
|
|
1207
|
+
return command.argument(
|
|
1208
|
+
"<source>",
|
|
1209
|
+
"Source color in hex format (e.g. #6750A4)",
|
|
1210
|
+
parseHexColor
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
function addThemeOptions(command) {
|
|
1214
|
+
return command.addOption(
|
|
1215
|
+
new Option("--scheme <name>", "Color scheme variant").choices(schemeNames).default(DEFAULT_SCHEME)
|
|
1216
|
+
).option(
|
|
1217
|
+
"--contrast <number>",
|
|
1218
|
+
"Contrast level from -1.0 to 1.0",
|
|
1219
|
+
parseFloat,
|
|
1220
|
+
DEFAULT_CONTRAST
|
|
1221
|
+
).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(
|
|
1222
|
+
"--neutral-variant <hex>",
|
|
1223
|
+
"Neutral variant color override",
|
|
1224
|
+
parseHexColor
|
|
1225
|
+
).option(
|
|
1226
|
+
"--no-fallback",
|
|
1227
|
+
"Omit this theme's colors as the var() fallbacks in the registry item, so it renders nothing without an <Mtb> above it"
|
|
1228
|
+
).option(
|
|
1229
|
+
"--prefix <string>",
|
|
1230
|
+
"CSS variable prefix (e.g. md \u2192 --md-sys-color-*, --md-ref-palette-*)",
|
|
1231
|
+
DEFAULT_PREFIX
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
var THEME_OPTIONS = addThemeOptions(new Command()).options;
|
|
1235
|
+
function builderOptions(opts) {
|
|
1236
|
+
return {
|
|
1237
|
+
scheme: opts.scheme,
|
|
1238
|
+
contrast: opts.contrast,
|
|
1239
|
+
primary: opts.primary,
|
|
1240
|
+
secondary: opts.secondary,
|
|
1241
|
+
tertiary: opts.tertiary,
|
|
1242
|
+
error: opts.error,
|
|
1243
|
+
neutral: opts.neutral,
|
|
1244
|
+
neutralVariant: opts.neutralVariant,
|
|
1245
|
+
prefix: opts.prefix
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function specifiedThemeArgs(command) {
|
|
1249
|
+
const args = [];
|
|
1250
|
+
for (const option of THEME_OPTIONS) {
|
|
1251
|
+
const name = option.attributeName();
|
|
1252
|
+
if (command.getOptionValueSource(name) !== "cli") continue;
|
|
1253
|
+
if (option.long) args.push(option.long);
|
|
1254
|
+
if (!option.negate) args.push(String(command.opts()[name]));
|
|
1255
|
+
}
|
|
1256
|
+
return args;
|
|
1257
|
+
}
|
|
1258
|
+
function themeFrom(command) {
|
|
1259
|
+
const opts = command.opts();
|
|
1260
|
+
return {
|
|
1261
|
+
options: builderOptions(opts),
|
|
1262
|
+
// Declared as a negation, so commander's own default here is `true` -- the
|
|
1263
|
+
// opposite of the API's, and deliberately so: a CLI knows the source color,
|
|
1264
|
+
// which is what makes baking its colors in as the fallbacks free. See
|
|
1265
|
+
// `buildShadcnRegistryItem()`.
|
|
1266
|
+
fallback: opts.fallback ?? true,
|
|
1267
|
+
args: specifiedThemeArgs(command)
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/cli.shadcn.ts
|
|
1272
|
+
import { spawnSync } from "child_process";
|
|
1273
|
+
import * as fs from "fs";
|
|
1274
|
+
import * as os from "os";
|
|
1275
|
+
import * as path from "path";
|
|
1276
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1277
|
+
var NPX = ["npx", "--yes"];
|
|
1278
|
+
var DEFAULT_SHADCN = "shadcn@latest";
|
|
1279
|
+
var SELF = "material-theme-builder";
|
|
1280
|
+
var INIT_TARGET = "shadcn init";
|
|
1281
|
+
var ADD_TARGET = "shadcn add";
|
|
1282
|
+
var ITEM = "mtb.json";
|
|
1283
|
+
var APP_NAME = "material-theme-app";
|
|
1284
|
+
var NAME = { long: "--name", short: "-n", value: APP_NAME };
|
|
1285
|
+
var YES = { long: "--yes", short: "-y" };
|
|
1286
|
+
var INIT_DEFAULTS = [
|
|
1287
|
+
{ long: "--preset", short: "-p", value: "b0" },
|
|
1288
|
+
{ long: "--template", short: "-t", value: "vite" },
|
|
1289
|
+
NAME,
|
|
1290
|
+
YES
|
|
1291
|
+
];
|
|
1292
|
+
var ADD_DEFAULTS = [YES];
|
|
1293
|
+
function parseShadcnSpec(value) {
|
|
1294
|
+
if (value.startsWith("-"))
|
|
1295
|
+
throw new InvalidArgumentError2(
|
|
1296
|
+
"A package spec cannot start with '-': npx would read it as one of its own options."
|
|
1297
|
+
);
|
|
1298
|
+
if (/^\d/.test(value))
|
|
1299
|
+
throw new InvalidArgumentError2(
|
|
1300
|
+
`That is a version, not a package spec -- write 'shadcn@${value}'.`
|
|
1301
|
+
);
|
|
1302
|
+
return value;
|
|
1303
|
+
}
|
|
1304
|
+
function addChainOptions(command) {
|
|
1305
|
+
return command.option(
|
|
1306
|
+
"--shadcn-cli <spec>",
|
|
1307
|
+
"npx package spec for the shadcn CLI to run (a version, tag, fork or tarball \u2014 anything npx resolves)",
|
|
1308
|
+
parseShadcnSpec,
|
|
1309
|
+
DEFAULT_SHADCN
|
|
1310
|
+
).option(
|
|
1311
|
+
"--print",
|
|
1312
|
+
"Print the equivalent shell chain instead of running it"
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
function mentions(args, { long, short }) {
|
|
1316
|
+
return args.some(
|
|
1317
|
+
(arg) => arg === long || arg.startsWith(`${long}=`) || arg.startsWith(short)
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
function mergeDefaults(defaults, forwarded) {
|
|
1321
|
+
const injected = defaults.filter((option) => !mentions(forwarded, option)).flatMap(
|
|
1322
|
+
({ long, value }) => value === void 0 ? [long] : [long, value]
|
|
1323
|
+
);
|
|
1324
|
+
return [...injected, ...forwarded];
|
|
1325
|
+
}
|
|
1326
|
+
function lastName(args) {
|
|
1327
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
1328
|
+
const arg = args[i] ?? "";
|
|
1329
|
+
if (arg === NAME.long || arg === NAME.short) return args[i + 1];
|
|
1330
|
+
if (arg.startsWith(`${NAME.long}=`)) return arg.slice(NAME.long.length + 1);
|
|
1331
|
+
if (arg.length > NAME.short.length && arg.startsWith(NAME.short))
|
|
1332
|
+
return arg.slice(NAME.short.length);
|
|
1333
|
+
}
|
|
1334
|
+
return void 0;
|
|
1335
|
+
}
|
|
1336
|
+
function ownOptionIn(declared, forwarded) {
|
|
1337
|
+
return forwarded.find(
|
|
1338
|
+
(arg) => declared.some(
|
|
1339
|
+
(option) => arg === option.long || arg === option.short || option.long !== void 0 && arg.startsWith(`${option.long}=`)
|
|
1340
|
+
)
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
function refuseOwnOptions(command, forwarded, target) {
|
|
1344
|
+
const misplaced = ownOptionIn(command.options, forwarded);
|
|
1345
|
+
if (misplaced !== void 0)
|
|
1346
|
+
fail(
|
|
1347
|
+
`${misplaced} is ours, not shadcn's, so it belongs before the \`--\`. Everything after the separator is handed to \`${target}\` untouched.`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
var DEFAULT_THEME = { options: {}, fallback: true, args: [] };
|
|
1351
|
+
function chainOptionsFrom(command) {
|
|
1352
|
+
return { theme: themeFrom(command), shadcn: command.opts().shadcnCli };
|
|
1353
|
+
}
|
|
1354
|
+
function initPlan(source, forwarded = [], {
|
|
1355
|
+
theme = DEFAULT_THEME,
|
|
1356
|
+
shadcn = DEFAULT_SHADCN
|
|
1357
|
+
} = {}) {
|
|
1358
|
+
const args = mergeDefaults(INIT_DEFAULTS, forwarded);
|
|
1359
|
+
const dir = lastName(args) ?? APP_NAME;
|
|
1360
|
+
return {
|
|
1361
|
+
source,
|
|
1362
|
+
theme,
|
|
1363
|
+
dir,
|
|
1364
|
+
steps: [
|
|
1365
|
+
{
|
|
1366
|
+
kind: "spawn",
|
|
1367
|
+
label: INIT_TARGET,
|
|
1368
|
+
argv: [...NPX, shadcn, "init", ...args],
|
|
1369
|
+
where: "cwd"
|
|
1370
|
+
},
|
|
1371
|
+
{ kind: "cd", dir },
|
|
1372
|
+
{ kind: "item", file: ITEM },
|
|
1373
|
+
// The same spec as the step above, deliberately: a chain pinned for the
|
|
1374
|
+
// scaffold and floating for the install is worse than one that floats
|
|
1375
|
+
// throughout, since the two would disagree about the registry format
|
|
1376
|
+
// without saying so.
|
|
1377
|
+
{
|
|
1378
|
+
kind: "spawn",
|
|
1379
|
+
label: ADD_TARGET,
|
|
1380
|
+
argv: [
|
|
1381
|
+
...NPX,
|
|
1382
|
+
shadcn,
|
|
1383
|
+
"add",
|
|
1384
|
+
`./${ITEM}`,
|
|
1385
|
+
...mergeDefaults(ADD_DEFAULTS, [])
|
|
1386
|
+
],
|
|
1387
|
+
where: "project"
|
|
1388
|
+
},
|
|
1389
|
+
{ kind: "rm", file: ITEM },
|
|
1390
|
+
// The handover: long-running by design, stdio inherited, ctrl-C reaching
|
|
1391
|
+
// it. Package-manager detection is out of scope -- `shadcn init` under
|
|
1392
|
+
// `npx` writes a `package-lock.json` and the `dev` script npm will run.
|
|
1393
|
+
{
|
|
1394
|
+
kind: "spawn",
|
|
1395
|
+
label: "npm run dev",
|
|
1396
|
+
argv: ["npm", "run", "dev"],
|
|
1397
|
+
where: "project"
|
|
1398
|
+
}
|
|
1399
|
+
]
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function applyPlan(source, forwarded = [], {
|
|
1403
|
+
theme = DEFAULT_THEME,
|
|
1404
|
+
shadcn = DEFAULT_SHADCN
|
|
1405
|
+
} = {}) {
|
|
1406
|
+
return {
|
|
1407
|
+
source,
|
|
1408
|
+
theme,
|
|
1409
|
+
steps: [
|
|
1410
|
+
{ kind: "item", file: ITEM },
|
|
1411
|
+
{
|
|
1412
|
+
kind: "spawn",
|
|
1413
|
+
label: ADD_TARGET,
|
|
1414
|
+
argv: [
|
|
1415
|
+
...NPX,
|
|
1416
|
+
shadcn,
|
|
1417
|
+
"add",
|
|
1418
|
+
`./${ITEM}`,
|
|
1419
|
+
...mergeDefaults(ADD_DEFAULTS, forwarded)
|
|
1420
|
+
],
|
|
1421
|
+
where: "cwd"
|
|
1422
|
+
},
|
|
1423
|
+
{ kind: "rm", file: ITEM }
|
|
1424
|
+
]
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
var SHELL_SAFE = /^[\w@%+=:,./-]+$/;
|
|
1428
|
+
function quote(argv) {
|
|
1429
|
+
return argv.map(
|
|
1430
|
+
(arg) => SHELL_SAFE.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`
|
|
1431
|
+
).join(" ");
|
|
1432
|
+
}
|
|
1433
|
+
function renderStep(plan, step) {
|
|
1434
|
+
switch (step.kind) {
|
|
1435
|
+
case "spawn":
|
|
1436
|
+
return quote(step.argv);
|
|
1437
|
+
case "cd":
|
|
1438
|
+
return `cd ${step.dir}`;
|
|
1439
|
+
// Rendered as the command someone could have run themselves -- theme options
|
|
1440
|
+
// included, or the line would describe a different theme from the one the
|
|
1441
|
+
// runner generates. The runner does the same thing in-process, being the very
|
|
1442
|
+
// binary this spawns.
|
|
1443
|
+
case "item":
|
|
1444
|
+
return `${quote([...NPX, SELF, plan.source, ...plan.theme.args, "--format", "registry-item"])} > ${step.file}`;
|
|
1445
|
+
case "rm":
|
|
1446
|
+
return `rm ${step.file}`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function renderChain(plan) {
|
|
1450
|
+
return plan.steps.map((step) => renderStep(plan, step)).join(" && ");
|
|
1451
|
+
}
|
|
1452
|
+
function fail(message, code = 1) {
|
|
1453
|
+
console.error(`Error: ${message}`);
|
|
1454
|
+
process.exit(code);
|
|
1455
|
+
}
|
|
1456
|
+
function run({ label, argv }, cwd) {
|
|
1457
|
+
const [command = "", ...args] = argv;
|
|
1458
|
+
const { error, status, signal } = spawnSync(command, args, {
|
|
1459
|
+
cwd,
|
|
1460
|
+
stdio: "inherit",
|
|
1461
|
+
shell: false
|
|
1462
|
+
});
|
|
1463
|
+
if (error) fail(`${label} could not be started: ${error.message}`);
|
|
1464
|
+
if (signal) process.exit(128 + (os.constants.signals[signal] ?? 0));
|
|
1465
|
+
if (status !== 0) fail(`${label} failed`, status ?? 1);
|
|
1466
|
+
}
|
|
1467
|
+
function writeItem({ source, theme }, file) {
|
|
1468
|
+
if (fs.existsSync(file))
|
|
1469
|
+
fail(
|
|
1470
|
+
`${path.basename(file)} already exists and would be overwritten, then deleted. Move it aside first.`
|
|
1471
|
+
);
|
|
1472
|
+
process.on("exit", () => fs.rmSync(file, { force: true }));
|
|
1473
|
+
const item = builder(source, theme.options).toShadcnRegistryItem({
|
|
1474
|
+
fallback: theme.fallback
|
|
1475
|
+
});
|
|
1476
|
+
fs.writeFileSync(file, `${JSON.stringify(item, null, 2)}
|
|
1477
|
+
`);
|
|
1478
|
+
}
|
|
1479
|
+
function runStep(plan, step, projectDir) {
|
|
1480
|
+
switch (step.kind) {
|
|
1481
|
+
case "spawn":
|
|
1482
|
+
return run(step, step.where === "project" ? projectDir : process.cwd());
|
|
1483
|
+
case "item":
|
|
1484
|
+
return writeItem(plan, path.join(projectDir, step.file));
|
|
1485
|
+
case "rm":
|
|
1486
|
+
return fs.rmSync(path.join(projectDir, step.file), { force: true });
|
|
1487
|
+
// Nothing to do: the runner resolved the directory up front, where a shell
|
|
1488
|
+
// would have changed into it. The step exists so that `--print` can say so.
|
|
1489
|
+
case "cd":
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function runChain(plan) {
|
|
1494
|
+
const projectDir = plan.dir ? path.resolve(plan.dir) : process.cwd();
|
|
1495
|
+
if (plan.dir && fs.existsSync(projectDir))
|
|
1496
|
+
fail(
|
|
1497
|
+
`./${plan.dir} already exists. Pick another name with \`-- -n other-name\`, or remove it first.`
|
|
1498
|
+
);
|
|
1499
|
+
for (const step of plan.steps) runStep(plan, step, projectDir);
|
|
1500
|
+
}
|
|
1501
|
+
function execute(plan, command) {
|
|
1502
|
+
if (command.opts().print === true) {
|
|
1503
|
+
process.stdout.write(`${renderChain(plan)}
|
|
1504
|
+
`);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
runChain(plan);
|
|
1508
|
+
}
|
|
1509
|
+
function runInit(source, forwarded, command) {
|
|
1510
|
+
refuseOwnOptions(command, forwarded, INIT_TARGET);
|
|
1511
|
+
execute(initPlan(source, forwarded, chainOptionsFrom(command)), command);
|
|
1512
|
+
}
|
|
1513
|
+
function runApply(source, forwarded, command) {
|
|
1514
|
+
refuseOwnOptions(command, forwarded, ADD_TARGET);
|
|
1515
|
+
execute(applyPlan(source, forwarded, chainOptionsFrom(command)), command);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1138
1518
|
// src/cli.ts
|
|
1139
1519
|
var customColorSchema = z.array(
|
|
1140
1520
|
z.object({
|
|
1141
1521
|
name: z.string(),
|
|
1142
|
-
hex: z.string()
|
|
1522
|
+
hex: z.string().refine(
|
|
1523
|
+
isHexColor,
|
|
1524
|
+
"must be a hex color \u2014 3, 6 or 8 hex digits, with or without '#' (e.g. #FF5733)"
|
|
1525
|
+
),
|
|
1143
1526
|
blend: z.boolean().default(DEFAULT_BLEND)
|
|
1144
1527
|
})
|
|
1145
1528
|
);
|
|
1146
1529
|
function writeFigmaTokens(theme, outputDir) {
|
|
1147
|
-
|
|
1530
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
1148
1531
|
for (const [filename, content] of Object.entries(theme.toFigmaTokens())) {
|
|
1149
|
-
const filePath =
|
|
1150
|
-
|
|
1532
|
+
const filePath = path2.join(outputDir, filename);
|
|
1533
|
+
fs2.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
|
|
1151
1534
|
console.error(`wrote ${filePath}`);
|
|
1152
1535
|
}
|
|
1153
1536
|
}
|
|
@@ -1160,6 +1543,10 @@ function writeOutput(theme, opts) {
|
|
|
1160
1543
|
return process.stdout.write(theme.toTailwind({ shadcn: opts.shadcn }));
|
|
1161
1544
|
case "shadcn":
|
|
1162
1545
|
return process.stdout.write(json(theme.toShadcn()));
|
|
1546
|
+
case "registry-item":
|
|
1547
|
+
return process.stdout.write(
|
|
1548
|
+
json(theme.toShadcnRegistryItem({ fallback: opts.fallback }))
|
|
1549
|
+
);
|
|
1163
1550
|
case "flutter":
|
|
1164
1551
|
return process.stdout.write(theme.toFlutter());
|
|
1165
1552
|
case "figma":
|
|
@@ -1168,56 +1555,84 @@ function writeOutput(theme, opts) {
|
|
|
1168
1555
|
return process.stdout.write(json(theme.toJson()));
|
|
1169
1556
|
}
|
|
1170
1557
|
}
|
|
1171
|
-
var program = new
|
|
1172
|
-
|
|
1173
|
-
|
|
1558
|
+
var program = new Command2();
|
|
1559
|
+
addThemeOptions(
|
|
1560
|
+
addSourceArgument(
|
|
1561
|
+
program.name("material-theme-builder").description("Generate a color theme from a source color")
|
|
1562
|
+
).enablePositionalOptions()
|
|
1174
1563
|
).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
1564
|
"--custom-colors <json>",
|
|
1181
1565
|
`Custom colors as JSON array (e.g. '[{"name":"brand","hex":"#FF5733","blend":true}]')`
|
|
1182
1566
|
).option(
|
|
1183
1567
|
"--format <type>",
|
|
1184
|
-
"Output format: json, css, figma, tailwind, shadcn, or flutter",
|
|
1568
|
+
"Output format: json, css, figma, tailwind, shadcn, registry-item, or flutter",
|
|
1185
1569
|
"figma"
|
|
1186
1570
|
).option("--output <dir>", "Output directory (required for figma format)").option(
|
|
1187
1571
|
"--shadcn",
|
|
1188
1572
|
"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) => {
|
|
1573
|
+
).action((source, opts, command) => {
|
|
1194
1574
|
if (opts.shadcn && opts.format !== "tailwind") {
|
|
1195
1575
|
console.error(
|
|
1196
1576
|
"Error: --shadcn only applies to --format tailwind. For concrete color values, use --format shadcn."
|
|
1197
1577
|
);
|
|
1198
1578
|
process.exit(1);
|
|
1199
1579
|
}
|
|
1580
|
+
if (command.getOptionValueSource("fallback") === "cli" && opts.format !== "registry-item") {
|
|
1581
|
+
console.error(
|
|
1582
|
+
"Error: --no-fallback only applies to --format registry-item."
|
|
1583
|
+
);
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1200
1586
|
let customColors = [];
|
|
1201
1587
|
if (opts.customColors) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1588
|
+
let parsed;
|
|
1589
|
+
try {
|
|
1590
|
+
parsed = JSON.parse(opts.customColors);
|
|
1591
|
+
} catch {
|
|
1204
1592
|
console.error("Error: --custom-colors must be valid JSON");
|
|
1205
1593
|
process.exit(1);
|
|
1206
1594
|
}
|
|
1595
|
+
const result2 = customColorSchema.safeParse(parsed);
|
|
1596
|
+
if (!result2.success) {
|
|
1597
|
+
const issue = result2.error.issues[0];
|
|
1598
|
+
const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
|
|
1599
|
+
console.error(
|
|
1600
|
+
`Error: --custom-colors${where}: ${issue?.message ?? "is invalid"}`
|
|
1601
|
+
);
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1207
1604
|
customColors = result2.data;
|
|
1208
1605
|
}
|
|
1209
1606
|
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
|
|
1607
|
+
...builderOptions(opts),
|
|
1608
|
+
customColors
|
|
1220
1609
|
});
|
|
1221
1610
|
writeOutput(result, opts);
|
|
1222
1611
|
});
|
|
1612
|
+
addChainOptions(
|
|
1613
|
+
addThemeOptions(
|
|
1614
|
+
addSourceArgument(
|
|
1615
|
+
program.command("shadcn-init").description(
|
|
1616
|
+
"Scaffold a new shadcn app themed from a source color, and start it"
|
|
1617
|
+
)
|
|
1618
|
+
).argument(
|
|
1619
|
+
"[shadcn-args...]",
|
|
1620
|
+
"Options after a `--`, forwarded verbatim to `shadcn init`"
|
|
1621
|
+
)
|
|
1622
|
+
)
|
|
1623
|
+
).action(
|
|
1624
|
+
(source, shadcnArgs, _opts, command) => runInit(source, shadcnArgs, command)
|
|
1625
|
+
);
|
|
1626
|
+
addChainOptions(
|
|
1627
|
+
addThemeOptions(
|
|
1628
|
+
addSourceArgument(
|
|
1629
|
+
program.command("shadcn-apply").description("Theme the shadcn project in the current directory")
|
|
1630
|
+
).argument(
|
|
1631
|
+
"[shadcn-args...]",
|
|
1632
|
+
"Options after a `--`, forwarded verbatim to `shadcn add`"
|
|
1633
|
+
)
|
|
1634
|
+
)
|
|
1635
|
+
).action(
|
|
1636
|
+
(source, shadcnArgs, _opts, command) => runApply(source, shadcnArgs, command)
|
|
1637
|
+
);
|
|
1223
1638
|
program.parse();
|