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/dist/cli.js CHANGED
@@ -1,11 +1,18 @@
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 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 path2 = `ref/palette/${paletteName}/${tone}`;
126
+ const path3 = `ref/palette/${paletteName}/${tone}`;
120
127
  const hex = hexFromArgb2(argb).toUpperCase();
121
- paletteHexMap[paletteName][hex] = path2;
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: path2,
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(path2) {
214
- const parts = path2.split("/");
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
- 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
- }
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 DEFAULT_PREFIX = "md";
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
- fs.mkdirSync(outputDir, { recursive: true });
1530
+ fs2.mkdirSync(outputDir, { recursive: true });
1148
1531
  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");
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 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)
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
- ).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) => {
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
- const result2 = customColorSchema.safeParse(JSON.parse(opts.customColors));
1203
- if (!result2.success) {
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
- 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
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();