@advantacode/brander 0.2.0 → 0.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 +46 -8
- package/dist/adapters/css.js +19 -0
- package/dist/adapters/scss.js +19 -0
- package/dist/adapters/tailwind.js +23 -2
- package/dist/engine/themes.js +25 -2
- package/dist/generate-tokens.js +168 -5
- package/dist/index.js +21 -11
- package/dist/setup.js +84 -123
- package/dist/style-imports.js +94 -0
- package/docs/CONTRIBUTING.md +4 -0
- package/docs/TECH_OVERVIEW.md +105 -9
- package/package.json +64 -60
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# AdvantaCode Brander
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@advantacode/brander)
|
|
4
|
+
|
|
3
5
|
AdvantaCode Brander is a design token generator that produces consistent branding tokens for modern web applications.
|
|
4
6
|
|
|
5
7
|
It converts a simple configuration into reusable outputs for multiple platforms including:
|
|
@@ -16,13 +18,13 @@ AdvantaCode Brander uses OKLCH color space to generate perceptually consistent c
|
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
npm install -D @advantacode/brander
|
|
19
|
-
npx --package @advantacode/brander advantacode-brander setup --
|
|
21
|
+
npx --package @advantacode/brander advantacode-brander setup --style src/style.css
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
This creates `brand.config.ts`, adds a `brand:generate` script, patches your stylesheet imports, and prepares the token output folder.
|
|
23
25
|
|
|
24
26
|
During setup, Brander creates `brand.css` next to your main stylesheet, writes token/theme imports there, and adds a single `@import './brand.css';` to your main stylesheet.
|
|
25
|
-
|
|
27
|
+
Brander stores project paths like `project.outDir` and `project.styleFile` in `brand.config.*` so your `brand:generate` script can stay minimal.
|
|
26
28
|
|
|
27
29
|
AdvantaCode Brander generates design tokens and framework adapters from a single brand configuration file. It allows applications, design systems, and design tools to share a consistent source of truth for colors and semantic tokens.
|
|
28
30
|
|
|
@@ -77,16 +79,17 @@ advantacode-brander --out src/tokens
|
|
|
77
79
|
advantacode-brander --format css,tailwind,figma
|
|
78
80
|
advantacode-brander --theme dark
|
|
79
81
|
advantacode-brander --prefix ac
|
|
80
|
-
advantacode-brander setup --
|
|
82
|
+
advantacode-brander setup --style src/style.css
|
|
81
83
|
advantacode-brander init --out src/brander
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
Supported flags:
|
|
86
|
+
Supported flags (CLI overrides `brand.config.*`):
|
|
85
87
|
|
|
86
|
-
* `--out <dir>` writes generated files to a custom folder instead of `dist/brander`
|
|
88
|
+
* `--out <dir>` writes generated files to a custom folder instead of `dist/brander` (or `project.outDir`)
|
|
87
89
|
* `--format <list>` limits output to specific formats: `all`, `css`, `json`, `typescript` or `ts`, `scss`, `tailwind`, `bootstrap`, `figma`
|
|
88
|
-
* `--theme <value>` limits theme CSS output to `light`, `dark`, or `both`
|
|
89
|
-
* `--prefix <value>` applies a CSS variable prefix like `ac`, producing variables such as `--ac-primary`
|
|
90
|
+
* `--theme <value>` limits theme CSS output to `light`, `dark`, or `both` (or `theme`)
|
|
91
|
+
* `--prefix <value>` applies a CSS variable prefix like `ac`, producing variables such as `--ac-primary` (or `css.prefix`)
|
|
92
|
+
* `--style <path>` refreshes `brand.css` and the main stylesheet import during normal generation (or `project.styleFile`)
|
|
90
93
|
* `--version`, `-v` prints the installed package version
|
|
91
94
|
* `--help`, `-h` prints the CLI help text
|
|
92
95
|
|
|
@@ -104,6 +107,12 @@ Example:
|
|
|
104
107
|
```ts
|
|
105
108
|
export default {
|
|
106
109
|
name: process.env.COMPANY_NAME || "My Company",
|
|
110
|
+
project: {
|
|
111
|
+
outDir: "src/assets/brand",
|
|
112
|
+
styleFile: "src/styles.css"
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
adapters: ["tailwind"],
|
|
107
116
|
css: {
|
|
108
117
|
prefix: process.env.CSS_PREFIX ?? ""
|
|
109
118
|
},
|
|
@@ -117,6 +126,19 @@ export default {
|
|
|
117
126
|
success: process.env.SUCCESS_COLOR || "green-500",
|
|
118
127
|
warning: process.env.WARNING_COLOR || "yellow-500",
|
|
119
128
|
danger: process.env.DANGER_COLOR || "red-500"
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
typography: {
|
|
132
|
+
fontSans: "Inter",
|
|
133
|
+
fontMono: "JetBrains Mono"
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
spacing: {
|
|
137
|
+
xs: "0.25rem",
|
|
138
|
+
sm: "0.5rem",
|
|
139
|
+
md: "1rem",
|
|
140
|
+
lg: "1.5rem",
|
|
141
|
+
xl: "2rem"
|
|
120
142
|
}
|
|
121
143
|
};
|
|
122
144
|
```
|
|
@@ -148,7 +170,7 @@ DANGER_COLOR=red-500
|
|
|
148
170
|
|
|
149
171
|
## Generated Outputs
|
|
150
172
|
|
|
151
|
-
|
|
173
|
+
If neither CLI `--format` nor `formats` / `adapters` are set in `brand.config.*`, running the CLI with no flags generates all formats into `dist/brander` and writes both light and dark theme CSS.
|
|
152
174
|
|
|
153
175
|
```text
|
|
154
176
|
dist/
|
|
@@ -175,6 +197,8 @@ Example generated `tokens.css`:
|
|
|
175
197
|
:root {
|
|
176
198
|
--primary-500: oklch(0.65 0.2 45);
|
|
177
199
|
--neutral-50: oklch(0.97 0.02 95);
|
|
200
|
+
--space-md: 1rem;
|
|
201
|
+
--font-sans: "Inter", sans-serif;
|
|
178
202
|
}
|
|
179
203
|
```
|
|
180
204
|
|
|
@@ -280,6 +304,18 @@ text-danger
|
|
|
280
304
|
border-secondary
|
|
281
305
|
```
|
|
282
306
|
|
|
307
|
+
CommonJS Tailwind config:
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const brandPreset = require("./src/assets/brander/adapters/tailwind.preset");
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
presets: [brandPreset]
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This step should stay documented rather than auto-patched for now. Unlike stylesheet imports, Tailwind config shape varies across projects between CJS, ESM, TS, Vite plugins, and newer Tailwind entrypoints, so automatic mutation is riskier and easier to get wrong.
|
|
318
|
+
|
|
283
319
|
## Bootstrap / SCSS Frameworks
|
|
284
320
|
|
|
285
321
|
Generated file:
|
|
@@ -359,6 +395,8 @@ AdvantaCode Brander is maintained under a closed governance model.
|
|
|
359
395
|
|
|
360
396
|
Issues and feature requests are welcome, but pull requests may not be accepted.
|
|
361
397
|
|
|
398
|
+
Maintainer release and publishing workflow documentation is in [docs/TECH_OVERVIEW.md](docs/TECH_OVERVIEW.md).
|
|
399
|
+
|
|
362
400
|
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for details.
|
|
363
401
|
|
|
364
402
|
## Trademark Notice
|
package/dist/adapters/css.js
CHANGED
|
@@ -25,6 +25,25 @@ function renderPrimitiveTokens(tokenModel, variableOptions) {
|
|
|
25
25
|
css += ` ${getVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
|
|
29
|
+
css += "\n";
|
|
30
|
+
for (const [spaceName, spaceToken] of Object.entries(tokenModel.spacing)) {
|
|
31
|
+
css += ` ${getVariableName(`space-${spaceName}`, variableOptions)}: ${spaceToken.value};\n`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (tokenModel.typography) {
|
|
35
|
+
const fontSans = tokenModel.typography.fontSans?.value;
|
|
36
|
+
const fontMono = tokenModel.typography.fontMono?.value;
|
|
37
|
+
if (fontSans || fontMono) {
|
|
38
|
+
css += "\n";
|
|
39
|
+
}
|
|
40
|
+
if (fontSans) {
|
|
41
|
+
css += ` ${getVariableName("font-sans", variableOptions)}: ${fontSans};\n`;
|
|
42
|
+
}
|
|
43
|
+
if (fontMono) {
|
|
44
|
+
css += ` ${getVariableName("font-mono", variableOptions)}: ${fontMono};\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
28
47
|
css += "}\n";
|
|
29
48
|
return css;
|
|
30
49
|
}
|
package/dist/adapters/scss.js
CHANGED
|
@@ -38,6 +38,25 @@ function renderTokensScss(tokenModel, variableOptions) {
|
|
|
38
38
|
scss += `${getSassVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
|
|
42
|
+
scss += "\n";
|
|
43
|
+
for (const [spaceName, spaceToken] of Object.entries(tokenModel.spacing)) {
|
|
44
|
+
scss += `${getSassVariableName(`space-${spaceName}`, variableOptions)}: ${spaceToken.value};\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (tokenModel.typography) {
|
|
48
|
+
const fontSans = tokenModel.typography.fontSans?.value;
|
|
49
|
+
const fontMono = tokenModel.typography.fontMono?.value;
|
|
50
|
+
if (fontSans || fontMono) {
|
|
51
|
+
scss += "\n";
|
|
52
|
+
}
|
|
53
|
+
if (fontSans) {
|
|
54
|
+
scss += `${getSassVariableName("font-sans", variableOptions)}: ${fontSans};\n`;
|
|
55
|
+
}
|
|
56
|
+
if (fontMono) {
|
|
57
|
+
scss += `${getSassVariableName("font-mono", variableOptions)}: ${fontMono};\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
41
60
|
scss += "\n";
|
|
42
61
|
for (const semanticTokenName of semanticTokenNames) {
|
|
43
62
|
scss += `${getSassVariableName(semanticTokenName, variableOptions)}: ${tokenModel.color.semantic.light[semanticTokenName].value};\n`;
|
|
@@ -4,15 +4,36 @@ import { semanticTokenNames } from "../engine/semantics.js";
|
|
|
4
4
|
import { getVariableReference } from "./variables.js";
|
|
5
5
|
export function writeTailwindAdapter(outputDir, tokenModel, variableOptions) {
|
|
6
6
|
const adaptersDir = path.join(outputDir, "adapters");
|
|
7
|
-
let preset = `export default {\n theme: {\n extend: {\n
|
|
7
|
+
let preset = `export default {\n theme: {\n extend: {\n`;
|
|
8
8
|
fs.mkdirSync(adaptersDir, { recursive: true });
|
|
9
|
+
preset += " colors: {\n";
|
|
9
10
|
for (const semanticTokenName of semanticTokenNames) {
|
|
10
11
|
if (!tokenModel.color.semantic.light[semanticTokenName]) {
|
|
11
12
|
continue;
|
|
12
13
|
}
|
|
13
14
|
preset += ` "${semanticTokenName}": "${getVariableReference(semanticTokenName, variableOptions)}",\n`;
|
|
14
15
|
}
|
|
15
|
-
preset += " }
|
|
16
|
+
preset += " },\n";
|
|
17
|
+
if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
|
|
18
|
+
preset += " spacing: {\n";
|
|
19
|
+
for (const spaceName of Object.keys(tokenModel.spacing)) {
|
|
20
|
+
preset += ` "${spaceName}": "${getVariableReference(`space-${spaceName}`, variableOptions)}",\n`;
|
|
21
|
+
}
|
|
22
|
+
preset += " },\n";
|
|
23
|
+
}
|
|
24
|
+
const fontSans = tokenModel.typography?.fontSans?.value;
|
|
25
|
+
const fontMono = tokenModel.typography?.fontMono?.value;
|
|
26
|
+
if (fontSans || fontMono) {
|
|
27
|
+
preset += " fontFamily: {\n";
|
|
28
|
+
if (fontSans) {
|
|
29
|
+
preset += ` "sans": ["${getVariableReference("font-sans", variableOptions)}"],\n`;
|
|
30
|
+
}
|
|
31
|
+
if (fontMono) {
|
|
32
|
+
preset += ` "mono": ["${getVariableReference("font-mono", variableOptions)}"],\n`;
|
|
33
|
+
}
|
|
34
|
+
preset += " },\n";
|
|
35
|
+
}
|
|
36
|
+
preset += " }\n }\n};\n";
|
|
16
37
|
fs.writeFileSync(path.join(adaptersDir, "tailwind.preset.ts"), preset);
|
|
17
38
|
return ["adapters/tailwind.preset.ts"];
|
|
18
39
|
}
|
package/dist/engine/themes.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { generatePrimitivePalettes } from "./palette.js";
|
|
2
2
|
import { buildThemeReferences, resolveThemeValues } from "./semantics.js";
|
|
3
|
-
export function createTokenModel(baseColors) {
|
|
3
|
+
export function createTokenModel(baseColors, extras) {
|
|
4
4
|
const primitivePalettes = generatePrimitivePalettes(baseColors);
|
|
5
5
|
const themeReferences = buildThemeReferences(primitivePalettes);
|
|
6
6
|
const themeValues = resolveThemeValues(primitivePalettes, themeReferences);
|
|
7
|
-
|
|
7
|
+
const tokenModel = {
|
|
8
8
|
color: {
|
|
9
9
|
primitive: primitivePalettes,
|
|
10
10
|
semantic: {
|
|
@@ -13,6 +13,29 @@ export function createTokenModel(baseColors) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
+
if (extras?.typography) {
|
|
17
|
+
const typography = {};
|
|
18
|
+
const fontSans = extras.typography.fontSans;
|
|
19
|
+
const fontMono = extras.typography.fontMono;
|
|
20
|
+
if (typeof fontSans === "string" && fontSans.trim()) {
|
|
21
|
+
typography.fontSans = { value: fontSans.trim() };
|
|
22
|
+
}
|
|
23
|
+
if (typeof fontMono === "string" && fontMono.trim()) {
|
|
24
|
+
typography.fontMono = { value: fontMono.trim() };
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(typography).length > 0) {
|
|
27
|
+
tokenModel.typography = typography;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (extras?.spacing) {
|
|
31
|
+
const entries = Object.entries(extras.spacing).filter(([, value]) => typeof value === "string" && value.trim());
|
|
32
|
+
if (entries.length > 0) {
|
|
33
|
+
tokenModel.spacing = Object.fromEntries(entries
|
|
34
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
35
|
+
.map(([key, value]) => [key, { value: value.trim() }]));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return tokenModel;
|
|
16
39
|
}
|
|
17
40
|
function mapThemeTokens(references, values) {
|
|
18
41
|
return Object.fromEntries(Object.entries(references).map(([tokenName, reference]) => [
|
package/dist/generate-tokens.js
CHANGED
|
@@ -11,6 +11,7 @@ import { writeTypeScriptArtifacts } from "./adapters/typescript.js";
|
|
|
11
11
|
import { normalizeVariablePrefix } from "./adapters/variables.js";
|
|
12
12
|
import { baseColorNames, resolveBaseColors } from "./engine/color-parser.js";
|
|
13
13
|
import { createTokenModel } from "./engine/themes.js";
|
|
14
|
+
import { syncStyleImports } from "./style-imports.js";
|
|
14
15
|
export const supportedFormats = [
|
|
15
16
|
"all",
|
|
16
17
|
"css",
|
|
@@ -22,15 +23,18 @@ export const supportedFormats = [
|
|
|
22
23
|
"figma"
|
|
23
24
|
];
|
|
24
25
|
export async function generateTokens(options = {}) {
|
|
25
|
-
const defaultOutputDir = resolveDefaultOutputDir();
|
|
26
|
-
const outputDir = options.outputDir ? path.resolve(process.cwd(), options.outputDir) : defaultOutputDir;
|
|
27
|
-
const theme = options.theme ?? "both";
|
|
28
|
-
const formats = resolveFormats(options.formats);
|
|
29
26
|
loadDotEnv({ path: path.resolve(process.cwd(), ".env"), quiet: true });
|
|
30
27
|
const brandConfig = await loadBrandConfig();
|
|
28
|
+
const defaultOutputDir = resolveDefaultOutputDir();
|
|
29
|
+
const outputDir = resolveOutputDir(options.outputDir, brandConfig.project?.outDir, defaultOutputDir);
|
|
30
|
+
const theme = options.theme ?? brandConfig.theme ?? "both";
|
|
31
|
+
const formats = resolveFormats(options.formats ?? brandConfig.formats ?? resolveFormatsFromAdapters(brandConfig.adapters));
|
|
31
32
|
const prefix = resolveCssPrefix(options.prefix, brandConfig.css?.prefix, process.env.CSS_PREFIX);
|
|
32
33
|
const baseColors = resolveBaseColors(brandConfig.colors ?? {});
|
|
33
|
-
const tokenModel = createTokenModel(baseColors
|
|
34
|
+
const tokenModel = createTokenModel(baseColors, {
|
|
35
|
+
typography: resolveTypographyConfig(brandConfig.typography),
|
|
36
|
+
spacing: brandConfig.spacing
|
|
37
|
+
});
|
|
34
38
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
35
39
|
if (outputDir === defaultOutputDir) {
|
|
36
40
|
removeLegacyGeneratedFiles();
|
|
@@ -69,6 +73,10 @@ export async function generateTokens(options = {}) {
|
|
|
69
73
|
writtenArtifacts.push(...writeTypeScriptArtifacts(outputDir, tokenModel, metadata));
|
|
70
74
|
}
|
|
71
75
|
writtenArtifacts.push(...writeMetadataJson(outputDir, metadata));
|
|
76
|
+
const stylePath = options.stylePath ?? brandConfig.project?.styleFile;
|
|
77
|
+
if (stylePath && formats.has("css")) {
|
|
78
|
+
syncStyleImports(stylePath, outputDir, theme);
|
|
79
|
+
}
|
|
72
80
|
console.log(`✔ AdvantaCode tokens generated in ${path.relative(process.cwd(), outputDir) || "."}!`);
|
|
73
81
|
}
|
|
74
82
|
async function loadBrandConfig() {
|
|
@@ -117,6 +125,67 @@ function parseBrandConfig(rawConfig, configPath) {
|
|
|
117
125
|
}
|
|
118
126
|
parsedConfig.name = config.name;
|
|
119
127
|
}
|
|
128
|
+
if ("project" in config && config.project !== undefined) {
|
|
129
|
+
if (typeof config.project !== "object" || config.project === null || Array.isArray(config.project)) {
|
|
130
|
+
throw new Error(`Expected "project" in ${path.basename(configPath)} to be an object.`);
|
|
131
|
+
}
|
|
132
|
+
const projectConfig = config.project;
|
|
133
|
+
const parsedProjectConfig = {};
|
|
134
|
+
if ("outDir" in projectConfig && projectConfig.outDir !== undefined) {
|
|
135
|
+
if (typeof projectConfig.outDir !== "string") {
|
|
136
|
+
throw new Error(`Expected "project.outDir" in ${path.basename(configPath)} to be a string.`);
|
|
137
|
+
}
|
|
138
|
+
parsedProjectConfig.outDir = projectConfig.outDir;
|
|
139
|
+
}
|
|
140
|
+
if ("styleFile" in projectConfig && projectConfig.styleFile !== undefined) {
|
|
141
|
+
if (typeof projectConfig.styleFile !== "string") {
|
|
142
|
+
throw new Error(`Expected "project.styleFile" in ${path.basename(configPath)} to be a string.`);
|
|
143
|
+
}
|
|
144
|
+
parsedProjectConfig.styleFile = projectConfig.styleFile;
|
|
145
|
+
}
|
|
146
|
+
parsedConfig.project = parsedProjectConfig;
|
|
147
|
+
}
|
|
148
|
+
if ("adapters" in config && config.adapters !== undefined) {
|
|
149
|
+
if (!Array.isArray(config.adapters)) {
|
|
150
|
+
throw new Error(`Expected "adapters" in ${path.basename(configPath)} to be an array.`);
|
|
151
|
+
}
|
|
152
|
+
const parsedAdapters = [];
|
|
153
|
+
for (const adapter of config.adapters) {
|
|
154
|
+
if (typeof adapter !== "string") {
|
|
155
|
+
throw new Error(`Expected "adapters" entries in ${path.basename(configPath)} to be strings.`);
|
|
156
|
+
}
|
|
157
|
+
if (!["tailwind", "bootstrap", "figma"].includes(adapter)) {
|
|
158
|
+
throw new Error(`Unsupported adapter "${adapter}" in ${path.basename(configPath)}.`);
|
|
159
|
+
}
|
|
160
|
+
parsedAdapters.push(adapter);
|
|
161
|
+
}
|
|
162
|
+
parsedConfig.adapters = parsedAdapters;
|
|
163
|
+
}
|
|
164
|
+
if ("formats" in config && config.formats !== undefined) {
|
|
165
|
+
if (!Array.isArray(config.formats)) {
|
|
166
|
+
throw new Error(`Expected "formats" in ${path.basename(configPath)} to be an array.`);
|
|
167
|
+
}
|
|
168
|
+
const parsedFormats = [];
|
|
169
|
+
for (const format of config.formats) {
|
|
170
|
+
if (typeof format !== "string") {
|
|
171
|
+
throw new Error(`Expected "formats" entries in ${path.basename(configPath)} to be strings.`);
|
|
172
|
+
}
|
|
173
|
+
if (!supportedFormats.includes(format)) {
|
|
174
|
+
throw new Error(`Unknown format "${format}" in ${path.basename(configPath)}.`);
|
|
175
|
+
}
|
|
176
|
+
parsedFormats.push(format);
|
|
177
|
+
}
|
|
178
|
+
parsedConfig.formats = parsedFormats;
|
|
179
|
+
}
|
|
180
|
+
if ("theme" in config && config.theme !== undefined) {
|
|
181
|
+
if (typeof config.theme !== "string") {
|
|
182
|
+
throw new Error(`Expected "theme" in ${path.basename(configPath)} to be a string.`);
|
|
183
|
+
}
|
|
184
|
+
if (!["light", "dark", "both"].includes(config.theme)) {
|
|
185
|
+
throw new Error(`Invalid "theme" in ${path.basename(configPath)}. Use "light", "dark", or "both".`);
|
|
186
|
+
}
|
|
187
|
+
parsedConfig.theme = config.theme;
|
|
188
|
+
}
|
|
120
189
|
if ("colors" in config && config.colors !== undefined) {
|
|
121
190
|
if (typeof config.colors !== "object" || config.colors === null || Array.isArray(config.colors)) {
|
|
122
191
|
throw new Error(`Expected "colors" in ${path.basename(configPath)} to be an object.`);
|
|
@@ -148,8 +217,92 @@ function parseBrandConfig(rawConfig, configPath) {
|
|
|
148
217
|
}
|
|
149
218
|
parsedConfig.css = parsedCssConfig;
|
|
150
219
|
}
|
|
220
|
+
if ("typography" in config && config.typography !== undefined) {
|
|
221
|
+
if (typeof config.typography !== "object" || config.typography === null || Array.isArray(config.typography)) {
|
|
222
|
+
throw new Error(`Expected "typography" in ${path.basename(configPath)} to be an object.`);
|
|
223
|
+
}
|
|
224
|
+
const typographyConfig = config.typography;
|
|
225
|
+
const parsedTypographyConfig = {};
|
|
226
|
+
if ("fontSans" in typographyConfig && typographyConfig.fontSans !== undefined) {
|
|
227
|
+
if (typeof typographyConfig.fontSans !== "string") {
|
|
228
|
+
throw new Error(`Expected "typography.fontSans" in ${path.basename(configPath)} to be a string.`);
|
|
229
|
+
}
|
|
230
|
+
parsedTypographyConfig.fontSans = typographyConfig.fontSans;
|
|
231
|
+
}
|
|
232
|
+
if ("fontMono" in typographyConfig && typographyConfig.fontMono !== undefined) {
|
|
233
|
+
if (typeof typographyConfig.fontMono !== "string") {
|
|
234
|
+
throw new Error(`Expected "typography.fontMono" in ${path.basename(configPath)} to be a string.`);
|
|
235
|
+
}
|
|
236
|
+
parsedTypographyConfig.fontMono = typographyConfig.fontMono;
|
|
237
|
+
}
|
|
238
|
+
parsedConfig.typography = parsedTypographyConfig;
|
|
239
|
+
}
|
|
240
|
+
if ("spacing" in config && config.spacing !== undefined) {
|
|
241
|
+
if (typeof config.spacing !== "object" || config.spacing === null || Array.isArray(config.spacing)) {
|
|
242
|
+
throw new Error(`Expected "spacing" in ${path.basename(configPath)} to be an object.`);
|
|
243
|
+
}
|
|
244
|
+
const spacingEntries = Object.entries(config.spacing);
|
|
245
|
+
const parsedSpacing = {};
|
|
246
|
+
for (const [spaceName, spaceValue] of spacingEntries) {
|
|
247
|
+
if (!isSafeTokenKey(spaceName)) {
|
|
248
|
+
throw new Error(`Unsupported spacing token "${spaceName}" in ${path.basename(configPath)}. Use letters, numbers, ".", "_", or "-".`);
|
|
249
|
+
}
|
|
250
|
+
if (typeof spaceValue !== "string") {
|
|
251
|
+
throw new Error(`Expected spacing "${spaceName}" in ${path.basename(configPath)} to be a string.`);
|
|
252
|
+
}
|
|
253
|
+
parsedSpacing[spaceName] = spaceValue;
|
|
254
|
+
}
|
|
255
|
+
parsedConfig.spacing = parsedSpacing;
|
|
256
|
+
}
|
|
151
257
|
return parsedConfig;
|
|
152
258
|
}
|
|
259
|
+
function isSafeTokenKey(value) {
|
|
260
|
+
return /^[A-Za-z0-9._-]+$/.test(value);
|
|
261
|
+
}
|
|
262
|
+
const genericFontFamilyKeywords = new Set([
|
|
263
|
+
"serif",
|
|
264
|
+
"sans-serif",
|
|
265
|
+
"monospace",
|
|
266
|
+
"cursive",
|
|
267
|
+
"fantasy",
|
|
268
|
+
"system-ui",
|
|
269
|
+
"ui-serif",
|
|
270
|
+
"ui-sans-serif",
|
|
271
|
+
"ui-monospace",
|
|
272
|
+
"ui-rounded",
|
|
273
|
+
"emoji",
|
|
274
|
+
"math",
|
|
275
|
+
"fangsong"
|
|
276
|
+
]);
|
|
277
|
+
function resolveTypographyConfig(config) {
|
|
278
|
+
if (!config) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const resolved = {};
|
|
282
|
+
if (typeof config.fontSans === "string" && config.fontSans.trim()) {
|
|
283
|
+
resolved.fontSans = normalizeFontStack(config.fontSans, "sans-serif");
|
|
284
|
+
}
|
|
285
|
+
if (typeof config.fontMono === "string" && config.fontMono.trim()) {
|
|
286
|
+
resolved.fontMono = normalizeFontStack(config.fontMono, "monospace");
|
|
287
|
+
}
|
|
288
|
+
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
289
|
+
}
|
|
290
|
+
function normalizeFontStack(fontValue, fallback) {
|
|
291
|
+
const trimmed = fontValue.trim();
|
|
292
|
+
if (!trimmed) {
|
|
293
|
+
return trimmed;
|
|
294
|
+
}
|
|
295
|
+
if (trimmed.includes(",")) {
|
|
296
|
+
return trimmed;
|
|
297
|
+
}
|
|
298
|
+
if (genericFontFamilyKeywords.has(trimmed)) {
|
|
299
|
+
return trimmed;
|
|
300
|
+
}
|
|
301
|
+
return `"${escapeCssString(trimmed)}", ${fallback}`;
|
|
302
|
+
}
|
|
303
|
+
function escapeCssString(value) {
|
|
304
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
305
|
+
}
|
|
153
306
|
function resolveFormats(formats) {
|
|
154
307
|
const resolvedFormats = new Set();
|
|
155
308
|
if (!formats || formats.length === 0 || formats.includes("all")) {
|
|
@@ -170,6 +323,16 @@ function resolveFormats(formats) {
|
|
|
170
323
|
}
|
|
171
324
|
return resolvedFormats;
|
|
172
325
|
}
|
|
326
|
+
function resolveFormatsFromAdapters(adapters) {
|
|
327
|
+
if (!adapters || adapters.length === 0) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
return ["css", ...adapters];
|
|
331
|
+
}
|
|
332
|
+
function resolveOutputDir(cliOutDir, configOutDir, defaultOutputDir) {
|
|
333
|
+
const resolvedOutDir = cliOutDir ?? configOutDir;
|
|
334
|
+
return resolvedOutDir ? path.resolve(process.cwd(), resolvedOutDir) : defaultOutputDir;
|
|
335
|
+
}
|
|
173
336
|
function removeLegacyGeneratedFiles() {
|
|
174
337
|
const legacyFiles = [
|
|
175
338
|
path.resolve(process.cwd(), "dist", "tokens.css"),
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function runCli(args) {
|
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
|
16
16
|
if (command === 'generate') {
|
|
17
|
-
await
|
|
17
|
+
await runGenerateCommand(parseGenerateArgs(commandArgs));
|
|
18
18
|
return 0;
|
|
19
19
|
}
|
|
20
20
|
await setupProject(parseSetupArgs(command, commandArgs));
|
|
@@ -44,6 +44,9 @@ function resolveCommand(args) {
|
|
|
44
44
|
}
|
|
45
45
|
throw new Error(`Unknown command "${firstArg}". Use --help to see supported commands.`);
|
|
46
46
|
}
|
|
47
|
+
async function runGenerateCommand(options) {
|
|
48
|
+
await generateTokens(options);
|
|
49
|
+
}
|
|
47
50
|
function parseGenerateArgs(args) {
|
|
48
51
|
const options = {};
|
|
49
52
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -74,6 +77,11 @@ function parseGenerateArgs(args) {
|
|
|
74
77
|
index += 1;
|
|
75
78
|
continue;
|
|
76
79
|
}
|
|
80
|
+
if (arg === "--style") {
|
|
81
|
+
options.stylePath = getNextArgValue(arg, args, index);
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
77
85
|
if (arg.startsWith("-")) {
|
|
78
86
|
throw new Error(`Unknown option "${arg}". Use --help to see supported flags.`);
|
|
79
87
|
}
|
|
@@ -161,11 +169,11 @@ Generation options:
|
|
|
161
169
|
--theme <value> Theme CSS output: light, dark, or both (default: both)
|
|
162
170
|
--prefix <value> CSS variable prefix. Use "" or omit for no prefix
|
|
163
171
|
|
|
164
|
-
Examples:
|
|
165
|
-
advantacode-brander ${command}
|
|
166
|
-
advantacode-brander ${command} --out src/brander
|
|
167
|
-
advantacode-brander ${command} --style src/style.css
|
|
168
|
-
advantacode-brander ${command} --skip-imports --skip-generate
|
|
172
|
+
Examples:
|
|
173
|
+
advantacode-brander ${command}
|
|
174
|
+
advantacode-brander ${command} --out src/brander
|
|
175
|
+
advantacode-brander ${command} --style src/style.css
|
|
176
|
+
advantacode-brander ${command} --skip-imports --skip-generate
|
|
169
177
|
`;
|
|
170
178
|
}
|
|
171
179
|
return `AdvantaCode Brander
|
|
@@ -186,11 +194,13 @@ Options:
|
|
|
186
194
|
--format <list> Comma-separated formats: all, css, json, typescript|ts, scss, tailwind, bootstrap, figma
|
|
187
195
|
--theme <value> Theme CSS output: light, dark, or both (default: both)
|
|
188
196
|
--prefix <value> CSS variable prefix. Use "" or omit for no prefix
|
|
197
|
+
--style <path> Main stylesheet file to patch with a brand.css import
|
|
189
198
|
|
|
190
|
-
Examples:
|
|
191
|
-
advantacode-brander
|
|
192
|
-
advantacode-brander --out src/tokens
|
|
193
|
-
advantacode-brander
|
|
194
|
-
advantacode-brander
|
|
199
|
+
Examples:
|
|
200
|
+
advantacode-brander
|
|
201
|
+
advantacode-brander --out src/tokens
|
|
202
|
+
advantacode-brander --style src/style.css
|
|
203
|
+
advantacode-brander setup --style src/style.css
|
|
204
|
+
advantacode-brander init --out resources/brander --skip-imports
|
|
195
205
|
`;
|
|
196
206
|
}
|
package/dist/setup.js
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { generateTokens } from "./generate-tokens.js";
|
|
4
|
+
import { ensureStyleImports, resolveStylePath, syncStyleImports as syncStyleImportsImpl } from "./style-imports.js";
|
|
4
5
|
const defaultSetupOutputDir = path.join("src", "brander");
|
|
5
|
-
const brandStylesheetFileName = "brand.css";
|
|
6
|
-
const defaultStyleCandidates = [
|
|
7
|
-
path.join("src", "style.css"),
|
|
8
|
-
path.join("src", "main.css"),
|
|
9
|
-
path.join("src", "index.css"),
|
|
10
|
-
path.join("src", "app.css"),
|
|
11
|
-
path.join("resources", "css", "app.css")
|
|
12
|
-
];
|
|
13
6
|
export async function setupProject(options) {
|
|
14
7
|
const resolvedOutputDir = options.outputDir ?? defaultSetupOutputDir;
|
|
15
8
|
const scriptName = options.scriptName ?? "brand:generate";
|
|
16
9
|
const notes = [];
|
|
10
|
+
const resolvedStylePath = !options.skipImports ? resolveStylePath(options.stylePath) : undefined;
|
|
11
|
+
const configFormats = options.formats && options.formats.length > 0 ? options.formats : undefined;
|
|
17
12
|
if (!options.skipConfig) {
|
|
18
|
-
const configResult = ensureBrandConfig(
|
|
13
|
+
const configResult = ensureBrandConfig({
|
|
14
|
+
outputDir: resolvedOutputDir,
|
|
15
|
+
styleFile: resolvedStylePath ? normalizeConfigPath(path.relative(process.cwd(), resolvedStylePath)) : undefined,
|
|
16
|
+
formats: configFormats,
|
|
17
|
+
adapters: !configFormats ? ["tailwind"] : undefined,
|
|
18
|
+
prefix: options.prefix,
|
|
19
|
+
theme: options.theme
|
|
20
|
+
});
|
|
19
21
|
notes.push(configResult.message);
|
|
20
22
|
}
|
|
21
23
|
if (!options.skipScript) {
|
|
22
24
|
const scriptResult = ensurePackageScript(scriptName, buildGenerateCommand({
|
|
23
|
-
...options
|
|
24
|
-
outputDir: resolvedOutputDir
|
|
25
|
+
...options
|
|
25
26
|
}));
|
|
26
27
|
notes.push(scriptResult.message);
|
|
27
28
|
}
|
|
28
29
|
if (!options.skipImports) {
|
|
29
|
-
const styleResult = ensureStyleImports(
|
|
30
|
+
const styleResult = ensureStyleImports(resolvedStylePath ? normalizeConfigPath(path.relative(process.cwd(), resolvedStylePath)) : undefined, resolvedOutputDir, options.theme ?? "both");
|
|
30
31
|
notes.push(styleResult.message);
|
|
31
32
|
}
|
|
32
33
|
if (!options.skipGenerate) {
|
|
@@ -43,13 +44,8 @@ export async function setupProject(options) {
|
|
|
43
44
|
console.log(` - ${note}`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
if (fs.existsSync(configPath)) {
|
|
49
|
-
return { message: "Kept existing brand.config.js." };
|
|
50
|
-
}
|
|
51
|
-
fs.writeFileSync(configPath, getDefaultBrandConfigTemplate());
|
|
52
|
-
return { message: "Created brand.config.js." };
|
|
47
|
+
export function syncStyleImports(stylePath, outputDir) {
|
|
48
|
+
return syncStyleImportsImpl(stylePath, outputDir);
|
|
53
49
|
}
|
|
54
50
|
function ensurePackageScript(scriptName, command) {
|
|
55
51
|
const packageJsonPath = path.resolve(process.cwd(), "package.json");
|
|
@@ -71,114 +67,79 @@ function ensurePackageScript(scriptName, command) {
|
|
|
71
67
|
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
72
68
|
return { message: `Added "${scriptName}" script to package.json.` };
|
|
73
69
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
if (!resolvedStylePath) {
|
|
77
|
-
return {
|
|
78
|
-
message: "Skipped stylesheet imports because no stylesheet was found. Use --style <path> to target a file explicitly."
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const brandStylesheetPath = path.join(path.dirname(resolvedStylePath), brandStylesheetFileName);
|
|
82
|
-
const brandStylesheetImports = [
|
|
83
|
-
buildImportLine(brandStylesheetPath, path.join(outputDir, "tokens.css")),
|
|
84
|
-
buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "light.css")),
|
|
85
|
-
buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "dark.css"))
|
|
86
|
-
];
|
|
87
|
-
const brandStylesheetContents = `${brandStylesheetImports.join("\n")}\n`;
|
|
88
|
-
const hasBrandStylesheet = fs.existsSync(brandStylesheetPath);
|
|
89
|
-
const existingBrandStylesheet = hasBrandStylesheet ? fs.readFileSync(brandStylesheetPath, "utf8") : "";
|
|
90
|
-
if (existingBrandStylesheet !== brandStylesheetContents) {
|
|
91
|
-
fs.writeFileSync(brandStylesheetPath, brandStylesheetContents);
|
|
92
|
-
}
|
|
93
|
-
const styleFileContents = fs.readFileSync(resolvedStylePath, "utf8");
|
|
94
|
-
const legacyTokenImports = [
|
|
95
|
-
buildImportLine(resolvedStylePath, path.join(outputDir, "tokens.css")),
|
|
96
|
-
buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "light.css")),
|
|
97
|
-
buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "dark.css"))
|
|
98
|
-
];
|
|
99
|
-
const brandImportLine = buildImportLine(resolvedStylePath, brandStylesheetPath);
|
|
100
|
-
const styleLineEnding = styleFileContents.includes("\r\n") ? "\r\n" : "\n";
|
|
101
|
-
const styleLines = styleFileContents.split(/\r?\n/);
|
|
102
|
-
const legacyImportCandidates = new Set();
|
|
103
|
-
for (const importLine of legacyTokenImports) {
|
|
104
|
-
legacyImportCandidates.add(importLine);
|
|
105
|
-
legacyImportCandidates.add(importLine.replace(/'/g, '"'));
|
|
106
|
-
}
|
|
107
|
-
let nextStyleLines = styleLines.filter((line) => !legacyImportCandidates.has(line.trim()));
|
|
108
|
-
const hasBrandImport = nextStyleLines.some((line) => line.trim() === brandImportLine || line.trim() === brandImportLine.replace(/'/g, '"'));
|
|
109
|
-
if (!hasBrandImport) {
|
|
110
|
-
nextStyleLines = [brandImportLine, ...nextStyleLines];
|
|
111
|
-
}
|
|
112
|
-
while (nextStyleLines[0] === "") {
|
|
113
|
-
nextStyleLines = nextStyleLines.slice(1);
|
|
114
|
-
}
|
|
115
|
-
const nextStyleContents = `${nextStyleLines.join(styleLineEnding)}${styleLineEnding}`;
|
|
116
|
-
if (nextStyleContents !== styleFileContents) {
|
|
117
|
-
fs.writeFileSync(resolvedStylePath, nextStyleContents);
|
|
118
|
-
}
|
|
119
|
-
const brandStylesheetStatus = hasBrandStylesheet ? "Updated" : "Created";
|
|
120
|
-
const mainStylesheetStatus = nextStyleContents === styleFileContents ? "Kept" : "Updated";
|
|
121
|
-
return {
|
|
122
|
-
message: `${brandStylesheetStatus} ${path.relative(process.cwd(), brandStylesheetPath)} and ${mainStylesheetStatus.toLowerCase()} ${path.relative(process.cwd(), resolvedStylePath)} to import it.`
|
|
123
|
-
};
|
|
70
|
+
function normalizeConfigPath(value) {
|
|
71
|
+
return value.replace(/\\/g, "/");
|
|
124
72
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!fs.existsSync(candidatePath)) {
|
|
129
|
-
throw new Error(`Unable to find stylesheet "${stylePath}".`);
|
|
130
|
-
}
|
|
131
|
-
return candidatePath;
|
|
132
|
-
}
|
|
133
|
-
const existingCandidates = defaultStyleCandidates
|
|
134
|
-
.map((candidate) => path.resolve(process.cwd(), candidate))
|
|
135
|
-
.filter((candidatePath) => fs.existsSync(candidatePath));
|
|
136
|
-
if (existingCandidates.length === 1) {
|
|
137
|
-
return existingCandidates[0];
|
|
138
|
-
}
|
|
139
|
-
if (existingCandidates.length > 1) {
|
|
140
|
-
throw new Error(`Multiple stylesheet candidates were found: ${existingCandidates
|
|
141
|
-
.map((candidatePath) => path.relative(process.cwd(), candidatePath))
|
|
142
|
-
.join(", ")}. Use --style <path> to choose one.`);
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
73
|
+
function buildGenerateCommand(options) {
|
|
74
|
+
void options;
|
|
75
|
+
return "advantacode-brander";
|
|
145
76
|
}
|
|
146
|
-
function
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
77
|
+
function ensureBrandConfig(options) {
|
|
78
|
+
const existingConfigPath = findExistingBrandConfigPath();
|
|
79
|
+
if (existingConfigPath) {
|
|
80
|
+
return { message: `Kept existing ${path.basename(existingConfigPath)}.` };
|
|
81
|
+
}
|
|
82
|
+
const configPath = path.resolve(process.cwd(), "brand.config.ts");
|
|
83
|
+
fs.writeFileSync(configPath, getDefaultBrandConfigTemplate(options));
|
|
84
|
+
return { message: `Created ${path.basename(configPath)}.` };
|
|
150
85
|
}
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
86
|
+
function findExistingBrandConfigPath() {
|
|
87
|
+
const candidateFiles = [
|
|
88
|
+
"brand.config.ts",
|
|
89
|
+
"brand.config.mts",
|
|
90
|
+
"brand.config.cts",
|
|
91
|
+
"brand.config.js",
|
|
92
|
+
"brand.config.mjs",
|
|
93
|
+
"brand.config.cjs"
|
|
94
|
+
];
|
|
95
|
+
for (const candidateFile of candidateFiles) {
|
|
96
|
+
const candidatePath = path.resolve(process.cwd(), candidateFile);
|
|
97
|
+
if (fs.existsSync(candidatePath)) {
|
|
98
|
+
return candidatePath;
|
|
99
|
+
}
|
|
163
100
|
}
|
|
164
|
-
return
|
|
101
|
+
return undefined;
|
|
165
102
|
}
|
|
166
|
-
function getDefaultBrandConfigTemplate() {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
prefix: process.env.CSS_PREFIX ?? ""
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
103
|
+
function getDefaultBrandConfigTemplate(options) {
|
|
104
|
+
const outDir = options?.outputDir ?? defaultSetupOutputDir;
|
|
105
|
+
const adapters = options?.adapters && options.adapters.length > 0 ? options.adapters : undefined;
|
|
106
|
+
const formats = options?.formats && options.formats.length > 0 ? options.formats : undefined;
|
|
107
|
+
const prefix = options?.prefix ? JSON.stringify(options.prefix) : "process.env.CSS_PREFIX ?? \"\"";
|
|
108
|
+
const themeLine = options?.theme && options.theme !== "both" ? ` theme: ${JSON.stringify(options.theme)},\n` : "";
|
|
109
|
+
const projectLines = [
|
|
110
|
+
` project: {`,
|
|
111
|
+
` outDir: ${JSON.stringify(outDir)},`,
|
|
112
|
+
...(options?.styleFile ? [` styleFile: ${JSON.stringify(options.styleFile)},`] : []),
|
|
113
|
+
` },`
|
|
114
|
+
].join("\n");
|
|
115
|
+
return `export default {
|
|
116
|
+
name: process.env.COMPANY_NAME || "My Company",
|
|
117
|
+
${projectLines}
|
|
118
|
+
|
|
119
|
+
${themeLine}${formats ? ` formats: ${JSON.stringify(formats)},\n` : ""}${adapters ? ` adapters: ${JSON.stringify(adapters)},\n` : ""} css: {
|
|
120
|
+
prefix: ${prefix}
|
|
121
|
+
},
|
|
122
|
+
colors: {
|
|
123
|
+
primary: process.env.PRIMARY_COLOR || "amber-500",
|
|
124
|
+
secondary: process.env.SECONDARY_COLOR || "zinc-700",
|
|
125
|
+
neutral: process.env.NEUTRAL_COLOR || process.env.SECONDARY_COLOR || "zinc-700",
|
|
126
|
+
accent: process.env.ACCENT_COLOR || "amber-400",
|
|
127
|
+
info: process.env.INFO_COLOR || "sky-500",
|
|
128
|
+
success: process.env.SUCCESS_COLOR || "green-500",
|
|
129
|
+
warning: process.env.WARNING_COLOR || "yellow-500",
|
|
130
|
+
danger: process.env.DANGER_COLOR || "red-500"
|
|
131
|
+
},
|
|
132
|
+
typography: {
|
|
133
|
+
fontSans: "Inter",
|
|
134
|
+
fontMono: "JetBrains Mono"
|
|
135
|
+
},
|
|
136
|
+
spacing: {
|
|
137
|
+
xs: "0.25rem",
|
|
138
|
+
sm: "0.5rem",
|
|
139
|
+
md: "1rem",
|
|
140
|
+
lg: "1.5rem",
|
|
141
|
+
xl: "2rem"
|
|
142
|
+
}
|
|
143
|
+
};
|
|
183
144
|
`;
|
|
184
145
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const brandStylesheetFileName = "brand.css";
|
|
4
|
+
const defaultStyleCandidates = [
|
|
5
|
+
path.join("src", "style.css"),
|
|
6
|
+
path.join("src", "main.css"),
|
|
7
|
+
path.join("src", "index.css"),
|
|
8
|
+
path.join("src", "app.css"),
|
|
9
|
+
path.join("resources", "css", "app.css")
|
|
10
|
+
];
|
|
11
|
+
export function syncStyleImports(stylePath, outputDir, theme = "both") {
|
|
12
|
+
return ensureStyleImports(stylePath, outputDir, theme);
|
|
13
|
+
}
|
|
14
|
+
export function ensureStyleImports(stylePath, outputDir, theme = "both") {
|
|
15
|
+
const resolvedStylePath = resolveStylePath(stylePath);
|
|
16
|
+
if (!resolvedStylePath) {
|
|
17
|
+
return {
|
|
18
|
+
message: "Skipped stylesheet imports because no stylesheet was found. Use --style <path> to target a file explicitly."
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const brandStylesheetPath = path.join(path.dirname(resolvedStylePath), brandStylesheetFileName);
|
|
22
|
+
const brandStylesheetImports = [buildImportLine(brandStylesheetPath, path.join(outputDir, "tokens.css"))];
|
|
23
|
+
if (theme === "light" || theme === "both") {
|
|
24
|
+
brandStylesheetImports.push(buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "light.css")));
|
|
25
|
+
}
|
|
26
|
+
if (theme === "dark" || theme === "both") {
|
|
27
|
+
brandStylesheetImports.push(buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "dark.css")));
|
|
28
|
+
}
|
|
29
|
+
const brandStylesheetContents = `${brandStylesheetImports.join("\n")}\n`;
|
|
30
|
+
const hasBrandStylesheet = fs.existsSync(brandStylesheetPath);
|
|
31
|
+
const existingBrandStylesheet = hasBrandStylesheet ? fs.readFileSync(brandStylesheetPath, "utf8") : "";
|
|
32
|
+
if (existingBrandStylesheet !== brandStylesheetContents) {
|
|
33
|
+
fs.writeFileSync(brandStylesheetPath, brandStylesheetContents);
|
|
34
|
+
}
|
|
35
|
+
const styleFileContents = fs.readFileSync(resolvedStylePath, "utf8");
|
|
36
|
+
const legacyTokenImports = [
|
|
37
|
+
buildImportLine(resolvedStylePath, path.join(outputDir, "tokens.css")),
|
|
38
|
+
buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "light.css")),
|
|
39
|
+
buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "dark.css"))
|
|
40
|
+
];
|
|
41
|
+
const brandImportLine = buildImportLine(resolvedStylePath, brandStylesheetPath);
|
|
42
|
+
const styleLineEnding = styleFileContents.includes("\r\n") ? "\r\n" : "\n";
|
|
43
|
+
const styleLines = styleFileContents.split(/\r?\n/);
|
|
44
|
+
const legacyImportCandidates = new Set();
|
|
45
|
+
for (const importLine of legacyTokenImports) {
|
|
46
|
+
legacyImportCandidates.add(importLine);
|
|
47
|
+
legacyImportCandidates.add(importLine.replace(/'/g, '"'));
|
|
48
|
+
}
|
|
49
|
+
let nextStyleLines = styleLines.filter((line) => !legacyImportCandidates.has(line.trim()));
|
|
50
|
+
const hasBrandImport = nextStyleLines.some((line) => line.trim() === brandImportLine || line.trim() === brandImportLine.replace(/'/g, '"'));
|
|
51
|
+
if (!hasBrandImport) {
|
|
52
|
+
nextStyleLines = [brandImportLine, ...nextStyleLines];
|
|
53
|
+
}
|
|
54
|
+
while (nextStyleLines[0] === "") {
|
|
55
|
+
nextStyleLines = nextStyleLines.slice(1);
|
|
56
|
+
}
|
|
57
|
+
const nextStyleContents = `${nextStyleLines.join(styleLineEnding)}${styleLineEnding}`;
|
|
58
|
+
if (nextStyleContents !== styleFileContents) {
|
|
59
|
+
fs.writeFileSync(resolvedStylePath, nextStyleContents);
|
|
60
|
+
}
|
|
61
|
+
const brandStylesheetStatus = hasBrandStylesheet ? "Updated" : "Created";
|
|
62
|
+
const mainStylesheetStatus = nextStyleContents === styleFileContents ? "Kept" : "Updated";
|
|
63
|
+
return {
|
|
64
|
+
message: `${brandStylesheetStatus} ${path.relative(process.cwd(), brandStylesheetPath)} and ${mainStylesheetStatus.toLowerCase()} ${path.relative(process.cwd(), resolvedStylePath)} to import it.`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function resolveStylePath(stylePath) {
|
|
68
|
+
if (stylePath) {
|
|
69
|
+
const candidatePath = path.resolve(process.cwd(), stylePath);
|
|
70
|
+
if (!fs.existsSync(candidatePath)) {
|
|
71
|
+
throw new Error(`Unable to find stylesheet "${stylePath}".`);
|
|
72
|
+
}
|
|
73
|
+
return candidatePath;
|
|
74
|
+
}
|
|
75
|
+
const existingCandidates = defaultStyleCandidates
|
|
76
|
+
.map((candidate) => path.resolve(process.cwd(), candidate))
|
|
77
|
+
.filter((candidatePath) => fs.existsSync(candidatePath));
|
|
78
|
+
if (existingCandidates.length === 1) {
|
|
79
|
+
return existingCandidates[0];
|
|
80
|
+
}
|
|
81
|
+
if (existingCandidates.length > 1) {
|
|
82
|
+
throw new Error(`Multiple stylesheet candidates were found: ${existingCandidates
|
|
83
|
+
.map((candidatePath) => path.relative(process.cwd(), candidatePath))
|
|
84
|
+
.join(", ")}. Use --style <path> to choose one.`);
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function buildImportLine(stylePath, targetPath) {
|
|
89
|
+
const relativeImportPath = path
|
|
90
|
+
.relative(path.dirname(stylePath), path.resolve(process.cwd(), targetPath))
|
|
91
|
+
.replace(/\\/g, "/");
|
|
92
|
+
const normalizedImportPath = relativeImportPath.startsWith(".") ? relativeImportPath : `./${relativeImportPath}`;
|
|
93
|
+
return `@import '${normalizedImportPath}';`;
|
|
94
|
+
}
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -34,6 +34,10 @@ For internal architecture and technical details, see:
|
|
|
34
34
|
|
|
35
35
|
[TECH_OVERVIEW.md](TECH_OVERVIEW.md)
|
|
36
36
|
|
|
37
|
+
## Changelog
|
|
38
|
+
|
|
39
|
+
User-facing changes should be recorded in `CHANGELOG.md` under the `[Unreleased]` section.
|
|
40
|
+
|
|
37
41
|
## Branding and Trademarks
|
|
38
42
|
|
|
39
43
|
Use of the AdvantaCode name, logos, package names, domains, and other brand identifiers is governed by the trademark policy.
|
package/docs/TECH_OVERVIEW.md
CHANGED
|
@@ -308,12 +308,18 @@ Type definitions for Culori.
|
|
|
308
308
|
brand.config.ts
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
Defines the user-provided
|
|
311
|
+
Defines the user-provided project settings and design tokens.
|
|
312
|
+
Also supports `project` output settings, optional `adapters` / `formats`, and optional `typography` (fonts) + `spacing` scales.
|
|
312
313
|
|
|
313
314
|
Example:
|
|
314
315
|
|
|
315
316
|
```ts
|
|
316
317
|
export default {
|
|
318
|
+
project: {
|
|
319
|
+
outDir: "src/assets/brand",
|
|
320
|
+
styleFile: "src/styles.css"
|
|
321
|
+
},
|
|
322
|
+
adapters: ["tailwind"],
|
|
317
323
|
css: {
|
|
318
324
|
prefix: ""
|
|
319
325
|
},
|
|
@@ -321,6 +327,17 @@ export default {
|
|
|
321
327
|
primary: "amber-500",
|
|
322
328
|
secondary: "zinc-700",
|
|
323
329
|
info: "sky-500"
|
|
330
|
+
},
|
|
331
|
+
typography: {
|
|
332
|
+
fontSans: "Inter",
|
|
333
|
+
fontMono: "JetBrains Mono"
|
|
334
|
+
},
|
|
335
|
+
spacing: {
|
|
336
|
+
xs: "0.25rem",
|
|
337
|
+
sm: "0.5rem",
|
|
338
|
+
md: "1rem",
|
|
339
|
+
lg: "1.5rem",
|
|
340
|
+
xl: "2rem"
|
|
324
341
|
}
|
|
325
342
|
};
|
|
326
343
|
```
|
|
@@ -452,9 +469,25 @@ Example:
|
|
|
452
469
|
export default {
|
|
453
470
|
name: "My Company",
|
|
454
471
|
|
|
472
|
+
project: {
|
|
473
|
+
outDir: "src/assets/brand",
|
|
474
|
+
styleFile: "src/styles.css"
|
|
475
|
+
},
|
|
476
|
+
adapters: ["tailwind"],
|
|
455
477
|
colors: {
|
|
456
478
|
primary: "amber-500",
|
|
457
479
|
secondary: "zinc-700"
|
|
480
|
+
},
|
|
481
|
+
typography: {
|
|
482
|
+
fontSans: "Inter",
|
|
483
|
+
fontMono: "JetBrains Mono"
|
|
484
|
+
},
|
|
485
|
+
spacing: {
|
|
486
|
+
xs: "0.25rem",
|
|
487
|
+
sm: "0.5rem",
|
|
488
|
+
md: "1rem",
|
|
489
|
+
lg: "1.5rem",
|
|
490
|
+
xl: "2rem"
|
|
458
491
|
}
|
|
459
492
|
}
|
|
460
493
|
```
|
|
@@ -520,6 +553,27 @@ The current test coverage includes:
|
|
|
520
553
|
|
|
521
554
|
---
|
|
522
555
|
|
|
556
|
+
# Release and Publishing
|
|
557
|
+
|
|
558
|
+
For v1.0.0, keep release mechanics intentionally simple and rely on a short manual checklist.
|
|
559
|
+
|
|
560
|
+
Preflight checks:
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
npm run release:check
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Recommended flow:
|
|
567
|
+
|
|
568
|
+
* update `CHANGELOG.md`
|
|
569
|
+
* run `npm run release:check`
|
|
570
|
+
* bump version with `npm version patch|minor|major`
|
|
571
|
+
* cut the changelog section with `npm run changelog:cut` (uses the current `package.json` version)
|
|
572
|
+
* publish with `npm publish`
|
|
573
|
+
* push the commit + tag
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
523
577
|
# Run Token Generator
|
|
524
578
|
|
|
525
579
|
For local repository development, you can execute the generator directly with:
|
|
@@ -554,6 +608,40 @@ Why `devDependency`:
|
|
|
554
608
|
* Brander is a build-time code generation tool
|
|
555
609
|
* the consuming app needs the generated token files at runtime, not the generator itself
|
|
556
610
|
|
|
611
|
+
## Compatibility Test Matrix (v1.0.0)
|
|
612
|
+
|
|
613
|
+
Keep v1.0.0 validation lean and focus on the integration points Brander actually owns:
|
|
614
|
+
|
|
615
|
+
* config loading (`brand.config.ts` / `brand.config.js`)
|
|
616
|
+
* writing outputs to `project.outDir`
|
|
617
|
+
* CSS variable/theme correctness
|
|
618
|
+
* `brand.css` wiring via `project.styleFile` (when applicable)
|
|
619
|
+
* Tailwind preset adapter import + usage
|
|
620
|
+
|
|
621
|
+
**Minimal matrix (recommended for v1):**
|
|
622
|
+
|
|
623
|
+
1. Plain Vite + Tailwind (framework-agnostic baseline)
|
|
624
|
+
2. Next.js + Tailwind (SSR + App Router + CSS import realities)
|
|
625
|
+
3. Nuxt + Tailwind (Vue SSR ecosystem)
|
|
626
|
+
4. One Vite SPA framework: Vue + Tailwind *or* React + Tailwind (framework independence)
|
|
627
|
+
|
|
628
|
+
**Optional (adds credibility, not required for v1):**
|
|
629
|
+
|
|
630
|
+
* Astro + Tailwind (marketing sites)
|
|
631
|
+
* Laravel + Vite + Tailwind (monolith stack)
|
|
632
|
+
* SvelteKit + Tailwind (non-React/Vue framework)
|
|
633
|
+
|
|
634
|
+
**Notes:**
|
|
635
|
+
|
|
636
|
+
* For Next.js/Nuxt/Astro you may prefer to skip `setup` import patching and wire `brand.css` manually in the framework’s global stylesheet entrypoint; still validate `generate` and token consumption.
|
|
637
|
+
* Vite + Next.js covers the majority of real-world build tooling for Brander’s outputs.
|
|
638
|
+
|
|
639
|
+
Automation (optional):
|
|
640
|
+
|
|
641
|
+
* GitHub Actions workflow: `.github/workflows/consumer-matrix.yml`
|
|
642
|
+
* Script used by the workflow: `scripts/consumer-smoke.sh`
|
|
643
|
+
* The workflow packs the current commit (`npm pack`) and installs Brander from that tarball in each consumer project to avoid npm-registry drift.
|
|
644
|
+
|
|
557
645
|
Build and pack Brander:
|
|
558
646
|
|
|
559
647
|
```
|
|
@@ -573,7 +661,7 @@ Add a script to the app:
|
|
|
573
661
|
```json
|
|
574
662
|
{
|
|
575
663
|
"scripts": {
|
|
576
|
-
"brand:generate": "advantacode-brander
|
|
664
|
+
"brand:generate": "advantacode-brander"
|
|
577
665
|
}
|
|
578
666
|
}
|
|
579
667
|
```
|
|
@@ -581,7 +669,7 @@ Add a script to the app:
|
|
|
581
669
|
Or let Brander do that setup explicitly:
|
|
582
670
|
|
|
583
671
|
```
|
|
584
|
-
npx --package @advantacode/brander advantacode-brander setup --
|
|
672
|
+
npx --package @advantacode/brander advantacode-brander setup --style src/style.css
|
|
585
673
|
```
|
|
586
674
|
|
|
587
675
|
That command:
|
|
@@ -709,24 +797,32 @@ The project compiles TypeScript using:
|
|
|
709
797
|
|
|
710
798
|
# CLI Entry Point
|
|
711
799
|
|
|
712
|
-
The source
|
|
800
|
+
The source executable wrapper lives at:
|
|
713
801
|
|
|
714
802
|
```
|
|
715
|
-
src/
|
|
803
|
+
src/cli-wrapper.ts
|
|
716
804
|
```
|
|
717
805
|
|
|
718
|
-
It uses this executable header
|
|
806
|
+
It uses this executable header:
|
|
719
807
|
|
|
720
808
|
```ts
|
|
721
|
-
#!/usr/bin/env
|
|
809
|
+
#!/usr/bin/env node
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
CLI parsing and command behavior live in:
|
|
813
|
+
|
|
814
|
+
```
|
|
815
|
+
src/index.ts
|
|
722
816
|
```
|
|
723
817
|
|
|
724
|
-
|
|
818
|
+
TypeScript config support is enabled at runtime by dynamically loading `tsx/esm` when a `brand.config.ts` is detected.
|
|
819
|
+
|
|
820
|
+
The published binary entry in `package.json` points to the compiled wrapper:
|
|
725
821
|
|
|
726
822
|
```json
|
|
727
823
|
{
|
|
728
824
|
"bin": {
|
|
729
|
-
"advantacode-brander": "./dist/
|
|
825
|
+
"advantacode-brander": "./dist/cli-wrapper.js"
|
|
730
826
|
}
|
|
731
827
|
}
|
|
732
828
|
```
|
package/package.json
CHANGED
|
@@ -1,60 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@advantacode/brander",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AdvantaCode Design System Brand Generator",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist/**/*.js",
|
|
8
|
-
"docs/*.md",
|
|
9
|
-
"README.md",
|
|
10
|
-
"LICENSE"
|
|
11
|
-
],
|
|
12
|
-
"bin": {
|
|
13
|
-
"advantacode-brander": "./dist/cli-wrapper.js"
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
17
|
-
"build": "npm run clean && tsc && chmod +x dist/cli-wrapper.js",
|
|
18
|
-
"cli": "node --import tsx/esm dist/cli-wrapper.js",
|
|
19
|
-
"lint": "eslint --max-warnings=0 src test",
|
|
20
|
-
"pretest": "npm run build",
|
|
21
|
-
"test": "node --import tsx/esm --test",
|
|
22
|
-
"release:check": "npm run lint && npm test && npm pack --dry-run",
|
|
23
|
-
"tokens": "tsx src/generate-tokens.ts",
|
|
24
|
-
"prepack": "npm run build",
|
|
25
|
-
"
|
|
26
|
-
"brand:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"url": "https://github.com/advantacode/advantacode-brander
|
|
37
|
-
},
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@advantacode/brander",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AdvantaCode Design System Brand Generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**/*.js",
|
|
8
|
+
"docs/*.md",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"advantacode-brander": "./dist/cli-wrapper.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
17
|
+
"build": "npm run clean && tsc && chmod +x dist/cli-wrapper.js",
|
|
18
|
+
"cli": "node --import tsx/esm dist/cli-wrapper.js",
|
|
19
|
+
"lint": "eslint --max-warnings=0 src test",
|
|
20
|
+
"pretest": "npm run build",
|
|
21
|
+
"test": "node --import tsx/esm --test",
|
|
22
|
+
"release:check": "npm run lint && npm test && npm pack --dry-run",
|
|
23
|
+
"tokens": "tsx src/generate-tokens.ts",
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"changelog:cut": "node scripts/cut-changelog.mjs",
|
|
26
|
+
"brand:generate": "advantacode-brander",
|
|
27
|
+
"brand:test": "node ./dist/cli-wrapper.js"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"author": "Anthony Penn",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/advantacode/advantacode-brander.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/advantacode/advantacode-brander#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/advantacode/advantacode-brander/issues"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"design-tokens",
|
|
44
|
+
"branding",
|
|
45
|
+
"oklch",
|
|
46
|
+
"tailwind",
|
|
47
|
+
"cli"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^25.3.3",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
55
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
56
|
+
"eslint": "^8.0.0",
|
|
57
|
+
"typescript": "^5.3.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"culori": "^4.0.2",
|
|
61
|
+
"dotenv": "^17.3.1",
|
|
62
|
+
"tsx": "^4.21.0"
|
|
63
|
+
}
|
|
64
|
+
}
|