bdsg-cli 0.1.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.
@@ -0,0 +1,132 @@
1
+ import {
2
+ generatePalette,
3
+ generateShadows,
4
+ generateSpacingScale,
5
+ generateTypographyScale,
6
+ } from "bdsg";
7
+ import chalk from "chalk";
8
+ import { Command } from "commander";
9
+ import inquirer from "inquirer";
10
+ import ora from "ora";
11
+ import { writeTokensToFile } from "../utils/files.js";
12
+
13
+ export const initCommand = new Command("init")
14
+ .description("Initialize a new design system")
15
+ .option("-o, --output <dir>", "Output directory", "./tokens")
16
+ .action(async (options) => {
17
+ console.log(chalk.bold("\n BDSG - Design System Generator\n"));
18
+
19
+ const answers = await inquirer.prompt([
20
+ {
21
+ type: "input",
22
+ name: "projectName",
23
+ message: "Project name:",
24
+ default: "my-design-system",
25
+ },
26
+ {
27
+ type: "input",
28
+ name: "primaryColor",
29
+ message: "Primary color (hex):",
30
+ default: "#3B82F6",
31
+ validate: (input: string) => {
32
+ if (/^#[0-9A-Fa-f]{6}$/.test(input)) return true;
33
+ return "Please enter a valid hex color (e.g., #3B82F6)";
34
+ },
35
+ },
36
+ {
37
+ type: "list",
38
+ name: "typographyRatio",
39
+ message: "Typography scale ratio:",
40
+ choices: [
41
+ { name: "Minor Second (1.067)", value: "minor-second" },
42
+ { name: "Major Second (1.125)", value: "major-second" },
43
+ { name: "Minor Third (1.2)", value: "minor-third" },
44
+ { name: "Major Third (1.25)", value: "major-third" },
45
+ { name: "Perfect Fourth (1.333)", value: "perfect-fourth" },
46
+ { name: "Perfect Fifth (1.5)", value: "perfect-fifth" },
47
+ { name: "Golden Ratio (1.618)", value: "golden-ratio" },
48
+ ],
49
+ default: "perfect-fourth",
50
+ },
51
+ {
52
+ type: "list",
53
+ name: "spacingMethod",
54
+ message: "Spacing method:",
55
+ choices: [
56
+ { name: "Fibonacci (natural progression)", value: "fibonacci" },
57
+ { name: "Linear (consistent increments)", value: "linear" },
58
+ { name: "T-shirt sizes (semantic)", value: "t-shirt" },
59
+ ],
60
+ default: "fibonacci",
61
+ },
62
+ {
63
+ type: "list",
64
+ name: "shadowStyle",
65
+ message: "Shadow style:",
66
+ choices: [
67
+ { name: "Material Design", value: "material" },
68
+ { name: "Soft", value: "soft" },
69
+ { name: "Hard", value: "hard" },
70
+ ],
71
+ default: "material",
72
+ },
73
+ {
74
+ type: "list",
75
+ name: "outputFormat",
76
+ message: "Output format:",
77
+ choices: [
78
+ { name: "CSS Variables", value: "css" },
79
+ { name: "JSON Tokens", value: "json" },
80
+ { name: "Tailwind v4 (@theme)", value: "tailwind-v4" },
81
+ { name: "Shadcn/ui (with dark mode)", value: "shadcn" },
82
+ ],
83
+ default: "css",
84
+ },
85
+ ]);
86
+
87
+ const spinner = ora("Generating design tokens...").start();
88
+
89
+ try {
90
+ // Generate tokens using bdsg
91
+ const palette = generatePalette(answers.primaryColor, "primary");
92
+ const typography = generateTypographyScale({
93
+ ratio: answers.typographyRatio,
94
+ });
95
+ const spacing = generateSpacingScale({
96
+ method: answers.spacingMethod,
97
+ });
98
+ const shadows = generateShadows({
99
+ style: answers.shadowStyle,
100
+ });
101
+
102
+ // Write to files
103
+ await writeTokensToFile({
104
+ palette,
105
+ typography,
106
+ spacing,
107
+ shadows,
108
+ format: answers.outputFormat,
109
+ outputDir: options.output,
110
+ });
111
+
112
+ spinner.succeed(chalk.green("Design tokens generated!"));
113
+
114
+ console.log(chalk.dim("\nFiles created:"));
115
+ if (answers.outputFormat === "shadcn") {
116
+ console.log(chalk.dim(` ${options.output}/globals.css`));
117
+ } else if (answers.outputFormat === "tailwind-v4") {
118
+ console.log(chalk.dim(` ${options.output}/theme.css`));
119
+ } else {
120
+ const ext = answers.outputFormat === "json" ? "json" : "css";
121
+ console.log(chalk.dim(` ${options.output}/colors.${ext}`));
122
+ console.log(chalk.dim(` ${options.output}/typography.${ext}`));
123
+ console.log(chalk.dim(` ${options.output}/spacing.${ext}`));
124
+ console.log(chalk.dim(` ${options.output}/shadows.${ext}`));
125
+ }
126
+ console.log();
127
+ } catch (error) {
128
+ spinner.fail(chalk.red("Failed to generate tokens"));
129
+ console.error(error);
130
+ process.exit(1);
131
+ }
132
+ });
@@ -0,0 +1,89 @@
1
+ import { adjustColorForContrast, calculateContrast, meetsWCAG } from "bdsg";
2
+ import chalk from "chalk";
3
+ import { Command } from "commander";
4
+
5
+ export const validateCommand = new Command("validate")
6
+ .description("Validate WCAG contrast between colors")
7
+ .argument("<foreground>", "Foreground color in hex format")
8
+ .argument("<background>", "Background color in hex format")
9
+ .option("-l, --level <level>", "Target WCAG level (AA, AAA)", "AA")
10
+ .option("-s, --size <size>", "Text size (normal, large)", "normal")
11
+ .action((foreground, background, options) => {
12
+ console.log(chalk.bold("\n Contrast Analysis\n"));
13
+
14
+ // Calculate contrast
15
+ const ratio = calculateContrast(foreground, background);
16
+
17
+ // Check each combination
18
+ const AANormal = meetsWCAG(ratio, "AA", "normal");
19
+ const AALarge = meetsWCAG(ratio, "AA", "large");
20
+ const AAANormal = meetsWCAG(ratio, "AAA", "normal");
21
+ const AAALarge = meetsWCAG(ratio, "AAA", "large");
22
+
23
+ // Display colors
24
+ console.log(chalk.dim("Colors"));
25
+ console.log(chalk.dim("──────────────────"));
26
+ console.log(` Foreground: ${chalk.hex(foreground)(foreground)}`);
27
+ console.log(` Background: ${chalk.hex(background)("████")} ${background}`);
28
+ console.log(` Ratio: ${chalk.bold(ratio.toFixed(2))}:1`);
29
+ console.log();
30
+
31
+ // WCAG Results
32
+ console.log(chalk.dim("WCAG Results"));
33
+ console.log(chalk.dim("──────────────────"));
34
+
35
+ const passSymbol = chalk.green("✓ Pass");
36
+ const failSymbol = chalk.red("✗ Fail");
37
+
38
+ console.log(
39
+ ` AA Normal Text: ${AANormal ? passSymbol : failSymbol} (needs 4.5:1)`,
40
+ );
41
+ console.log(
42
+ ` AA Large Text: ${AALarge ? passSymbol : failSymbol} (needs 3.0:1)`,
43
+ );
44
+ console.log(
45
+ ` AAA Normal Text: ${AAANormal ? passSymbol : failSymbol} (needs 7.0:1)`,
46
+ );
47
+ console.log(
48
+ ` AAA Large Text: ${AAALarge ? passSymbol : failSymbol} (needs 4.5:1)`,
49
+ );
50
+ console.log();
51
+
52
+ // Suggest fix if failing target level
53
+ const targetLevel = options.level as "AA" | "AAA";
54
+ const textSize = options.size as "normal" | "large";
55
+ const targetMet = meetsWCAG(ratio, targetLevel, textSize);
56
+
57
+ if (!targetMet) {
58
+ console.log(chalk.dim("Suggestion"));
59
+ console.log(chalk.dim("──────────────────"));
60
+
61
+ try {
62
+ const adjusted = adjustColorForContrast(
63
+ foreground,
64
+ background,
65
+ targetLevel,
66
+ textSize,
67
+ );
68
+
69
+ if (adjusted.adjusted !== foreground) {
70
+ console.log(` For ${targetLevel} ${textSize} text compliance:`);
71
+ console.log(
72
+ ` Try: ${chalk.hex(adjusted.adjusted)(adjusted.adjusted)} (ratio: ${adjusted.ratio.toFixed(2)}:1)`,
73
+ );
74
+ console.log(` Strategy: ${adjusted.strategy}`);
75
+ } else {
76
+ console.log(chalk.yellow(" Could not find a suitable adjustment."));
77
+ }
78
+ } catch {
79
+ console.log(chalk.yellow(" Could not calculate adjustment."));
80
+ }
81
+ console.log();
82
+ } else {
83
+ console.log(
84
+ chalk.green(
85
+ `✓ Colors meet ${targetLevel} ${textSize} text requirements!\n`,
86
+ ),
87
+ );
88
+ }
89
+ });
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { generateCommand } from "./commands/generate.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { validateCommand } from "./commands/validate.js";
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name("bdsg")
11
+ .description("Design system generation CLI")
12
+ .version("0.1.0");
13
+
14
+ program.addCommand(initCommand);
15
+ program.addCommand(generateCommand);
16
+ program.addCommand(validateCommand);
17
+
18
+ program.parse();
@@ -0,0 +1,249 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type {
4
+ ColorPalette,
5
+ ShadowScale,
6
+ SpacingScale,
7
+ TypographyScale,
8
+ } from "bdsg";
9
+
10
+ interface TokensToWrite {
11
+ palette: ColorPalette;
12
+ typography: TypographyScale;
13
+ spacing: SpacingScale;
14
+ shadows: ShadowScale;
15
+ format: "css" | "json" | "tailwind-v4" | "shadcn";
16
+ outputDir: string;
17
+ }
18
+
19
+ export async function writeTokensToFile(tokens: TokensToWrite): Promise<void> {
20
+ await mkdir(tokens.outputDir, { recursive: true });
21
+
22
+ if (tokens.format === "json") {
23
+ await writeJsonTokens(tokens);
24
+ } else if (tokens.format === "tailwind-v4") {
25
+ await writeTailwindV4Tokens(tokens);
26
+ } else if (tokens.format === "shadcn") {
27
+ await writeShadcnTokens(tokens);
28
+ } else {
29
+ await writeCssTokens(tokens);
30
+ }
31
+ }
32
+
33
+ async function writeCssTokens(tokens: TokensToWrite): Promise<void> {
34
+ // Colors CSS
35
+ const colorsContent = generateColorsCss(tokens.palette);
36
+ await writeFile(join(tokens.outputDir, "colors.css"), colorsContent);
37
+
38
+ // Typography CSS
39
+ await writeFile(
40
+ join(tokens.outputDir, "typography.css"),
41
+ tokens.typography.cssVariables,
42
+ );
43
+
44
+ // Spacing CSS
45
+ await writeFile(
46
+ join(tokens.outputDir, "spacing.css"),
47
+ tokens.spacing.cssVariables,
48
+ );
49
+
50
+ // Shadows CSS
51
+ await writeFile(
52
+ join(tokens.outputDir, "shadows.css"),
53
+ tokens.shadows.cssVariables,
54
+ );
55
+ }
56
+
57
+ async function writeJsonTokens(tokens: TokensToWrite): Promise<void> {
58
+ // Colors JSON
59
+ const colors: Record<string, string> = {};
60
+ for (const [shade, data] of Object.entries(tokens.palette.shades)) {
61
+ colors[`primary-${shade}`] = data.value;
62
+ }
63
+ await writeFile(
64
+ join(tokens.outputDir, "colors.json"),
65
+ JSON.stringify({ colors }, null, 2),
66
+ );
67
+
68
+ // Typography JSON
69
+ const typography: Record<string, unknown> = {};
70
+ for (const token of tokens.typography.tokens) {
71
+ typography[token.name] = {
72
+ fontSize: token.fontSize,
73
+ lineHeight: token.lineHeight,
74
+ letterSpacing: token.letterSpacing,
75
+ };
76
+ }
77
+ await writeFile(
78
+ join(tokens.outputDir, "typography.json"),
79
+ JSON.stringify({ typography }, null, 2),
80
+ );
81
+
82
+ // Spacing JSON
83
+ const spacing: Record<string, unknown> = {};
84
+ for (const token of tokens.spacing.tokens) {
85
+ spacing[token.name] = {
86
+ value: token.formatted,
87
+ px: token.value,
88
+ };
89
+ }
90
+ await writeFile(
91
+ join(tokens.outputDir, "spacing.json"),
92
+ JSON.stringify({ spacing }, null, 2),
93
+ );
94
+
95
+ // Shadows JSON
96
+ const shadows: Record<string, string> = {};
97
+ for (const token of tokens.shadows.tokens) {
98
+ shadows[token.name] = token.value;
99
+ }
100
+ await writeFile(
101
+ join(tokens.outputDir, "shadows.json"),
102
+ JSON.stringify({ shadows }, null, 2),
103
+ );
104
+ }
105
+
106
+ async function writeTailwindV4Tokens(tokens: TokensToWrite): Promise<void> {
107
+ let content = '@import "tailwindcss";\n\n@theme {\n';
108
+
109
+ // Colors
110
+ for (const [shade, data] of Object.entries(tokens.palette.shades)) {
111
+ content += ` --color-primary-${shade}: ${data.value};\n`;
112
+ }
113
+ content += "\n";
114
+
115
+ // Typography
116
+ for (const token of tokens.typography.tokens) {
117
+ const remValue = token.fontSize / 16;
118
+ content += ` --font-size-${token.name}: ${remValue}rem;\n`;
119
+ }
120
+ content += "\n";
121
+
122
+ // Spacing
123
+ for (const token of tokens.spacing.tokens) {
124
+ content += ` --spacing-${token.name}: ${token.formatted};\n`;
125
+ }
126
+ content += "\n";
127
+
128
+ // Shadows
129
+ for (const token of tokens.shadows.tokens) {
130
+ content += ` --shadow-${token.name}: ${token.value};\n`;
131
+ }
132
+
133
+ content += "}\n";
134
+
135
+ await writeFile(join(tokens.outputDir, "theme.css"), content);
136
+ }
137
+
138
+ function generateColorsCss(palette: ColorPalette): string {
139
+ let css = ":root {\n";
140
+ for (const [shade, data] of Object.entries(palette.shades)) {
141
+ css += ` --color-primary-${shade}: ${data.value};\n`;
142
+ }
143
+ css += "}\n";
144
+ return css;
145
+ }
146
+
147
+ async function writeShadcnTokens(tokens: TokensToWrite): Promise<void> {
148
+ const shades = tokens.palette.shades;
149
+
150
+ // Light mode tokens (using palette shades)
151
+ const lightTokens = {
152
+ background: "#ffffff",
153
+ foreground: shades[900].value,
154
+ card: "#ffffff",
155
+ "card-foreground": shades[900].value,
156
+ popover: "#ffffff",
157
+ "popover-foreground": shades[900].value,
158
+ primary: shades[500].value,
159
+ "primary-foreground": shades[50].value,
160
+ secondary: shades[100].value,
161
+ "secondary-foreground": shades[900].value,
162
+ muted: shades[100].value,
163
+ "muted-foreground": shades[500].value,
164
+ accent: shades[100].value,
165
+ "accent-foreground": shades[900].value,
166
+ destructive: "#ef4444",
167
+ border: shades[200].value,
168
+ input: shades[200].value,
169
+ ring: shades[500].value,
170
+ };
171
+
172
+ // Dark mode tokens (inverted)
173
+ const darkTokens = {
174
+ background: shades[900].value,
175
+ foreground: shades[50].value,
176
+ card: shades[800].value,
177
+ "card-foreground": shades[50].value,
178
+ popover: shades[800].value,
179
+ "popover-foreground": shades[50].value,
180
+ primary: shades[400].value,
181
+ "primary-foreground": shades[900].value,
182
+ secondary: shades[700].value,
183
+ "secondary-foreground": shades[50].value,
184
+ muted: shades[700].value,
185
+ "muted-foreground": shades[400].value,
186
+ accent: shades[700].value,
187
+ "accent-foreground": shades[50].value,
188
+ destructive: "#dc2626",
189
+ border: shades[700].value,
190
+ input: shades[700].value,
191
+ ring: shades[400].value,
192
+ };
193
+
194
+ let content = '@import "tailwindcss";\n\n';
195
+
196
+ // @theme block for Tailwind v4 mapping
197
+ content += "@custom-variant dark (&:is(.dark *));\n\n";
198
+ content += "@theme inline {\n";
199
+ content += " --color-background: var(--background);\n";
200
+ content += " --color-foreground: var(--foreground);\n";
201
+ content += " --color-card: var(--card);\n";
202
+ content += " --color-card-foreground: var(--card-foreground);\n";
203
+ content += " --color-popover: var(--popover);\n";
204
+ content += " --color-popover-foreground: var(--popover-foreground);\n";
205
+ content += " --color-primary: var(--primary);\n";
206
+ content += " --color-primary-foreground: var(--primary-foreground);\n";
207
+ content += " --color-secondary: var(--secondary);\n";
208
+ content += " --color-secondary-foreground: var(--secondary-foreground);\n";
209
+ content += " --color-muted: var(--muted);\n";
210
+ content += " --color-muted-foreground: var(--muted-foreground);\n";
211
+ content += " --color-accent: var(--accent);\n";
212
+ content += " --color-accent-foreground: var(--accent-foreground);\n";
213
+ content += " --color-destructive: var(--destructive);\n";
214
+ content += " --color-border: var(--border);\n";
215
+ content += " --color-input: var(--input);\n";
216
+ content += " --color-ring: var(--ring);\n";
217
+ content += " --radius-sm: calc(var(--radius) - 4px);\n";
218
+ content += " --radius-md: calc(var(--radius) - 2px);\n";
219
+ content += " --radius-lg: var(--radius);\n";
220
+ content += " --radius-xl: calc(var(--radius) + 4px);\n";
221
+ content += "}\n\n";
222
+
223
+ // :root with light mode tokens
224
+ content += ":root {\n";
225
+ content += " --radius: 0.625rem;\n";
226
+ for (const [name, value] of Object.entries(lightTokens)) {
227
+ content += ` --${name}: ${value};\n`;
228
+ }
229
+ content += "}\n\n";
230
+
231
+ // .dark with dark mode tokens
232
+ content += ".dark {\n";
233
+ for (const [name, value] of Object.entries(darkTokens)) {
234
+ content += ` --${name}: ${value};\n`;
235
+ }
236
+ content += "}\n\n";
237
+
238
+ // Base layer
239
+ content += "@layer base {\n";
240
+ content += " * {\n";
241
+ content += " @apply border-border outline-ring/50;\n";
242
+ content += " }\n";
243
+ content += " body {\n";
244
+ content += " @apply bg-background text-foreground;\n";
245
+ content += " }\n";
246
+ content += "}\n";
247
+
248
+ await writeFile(join(tokens.outputDir, "globals.css"), content);
249
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@bdsg/config/tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "sourceMap": true,
7
+ "outDir": "dist",
8
+ "composite": true,
9
+ "rootDir": "src"
10
+ },
11
+ "include": ["src"]
12
+ }