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/dist/cli.js CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import * as fs from "fs";
5
- import * as path from "path";
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 path2 = `ref/palette/${paletteName}/${tone}`;
125
+ const path3 = `ref/palette/${paletteName}/${tone}`;
120
126
  const hex = hexFromArgb2(argb).toUpperCase();
121
- paletteHexMap[paletteName][hex] = path2;
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: path2,
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(path2) {
214
- const parts = path2.split("/");
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
- const shadcnLines = SHADCN_MAPPING.map(
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 DEFAULT_PREFIX = "md";
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
- fs.mkdirSync(outputDir, { recursive: true });
1317
+ fs2.mkdirSync(outputDir, { recursive: true });
1148
1318
  for (const [filename, content] of Object.entries(theme.toFigmaTokens())) {
1149
- const filePath = path.join(outputDir, filename);
1150
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
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
- program.name("material-theme-builder").description("Generate a color theme from a source color").argument("<source>", "Source color in hex format (e.g. #6750A4)").addOption(
1173
- new Option("--scheme <name>", "Color scheme variant").choices(schemeNames).default(DEFAULT_SCHEME)
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
- ).option(
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
- ).option(
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
- const result2 = customColorSchema.safeParse(JSON.parse(opts.customColors));
1203
- if (!result2.success) {
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
- scheme: opts.scheme,
1211
- contrast: opts.contrast,
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 };