@stacklist-app/brandkit 1.1.1
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 +95 -0
- package/bin/brandkit.js +49 -0
- package/cli/build.js +35 -0
- package/cli/dev.js +129 -0
- package/cli/generate.js +272 -0
- package/cli/init.js +62 -0
- package/dist/engine.js +847 -0
- package/dist/index.html +173 -0
- package/dist/styles.css +995 -0
- package/integrations/astro.js +26 -0
- package/integrations/vite.js +81 -0
- package/lib/config-schema.js +219 -0
- package/lib/extract-css.js +117 -0
- package/lib/extract-logos.js +98 -0
- package/lib/extract-tailwind.js +146 -0
- package/lib/generate-helpers.js +320 -0
- package/lib/resolve.js +11 -0
- package/lib/template.js +84 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @stacklist-app/brandkit
|
|
2
|
+
|
|
3
|
+
A config-driven brand guide that bolts onto any website. One `config.json` drives the entire guide — colors, typography, logos, voice, components, spacing, accessibility. Zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
Swap the config and assets to generate a brand guide for any project. An optional `generate` command scrapes an existing codebase (Tailwind config, CSS variables, logo files) to bootstrap the config automatically.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install github:The-Stack-Lab/brandkit
|
|
11
|
+
# or pin to a tag
|
|
12
|
+
npm install github:The-Stack-Lab/brandkit#v1.1.1
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node.js 18+.
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx brandkit init brand # scaffold brand/config.json + starter assets
|
|
21
|
+
npx brandkit generate brand # optional: auto-extract tokens from your codebase
|
|
22
|
+
npx brandkit dev brand # preview at http://localhost:4800 (live reload)
|
|
23
|
+
npx brandkit build brand # bake into static files for production
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The `brand` directory name is your choice — pass any path.
|
|
27
|
+
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
| Command | Description |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `brandkit init [dir]` | Scaffold a new brand guide with a starter `config.json` |
|
|
33
|
+
| `brandkit generate [dir]` | Extract colors, fonts, spacing, and logos from the host codebase and merge into `config.json`. Manual fields (voice, accessibility, components) are preserved. |
|
|
34
|
+
| `brandkit dev [dir]` | Local dev server on `:4800` with SSE live reload |
|
|
35
|
+
| `brandkit build [dir]` | Produce static `index.html` + `styles.css` + `engine.js` with the theme baked in |
|
|
36
|
+
|
|
37
|
+
## Framework integrations
|
|
38
|
+
|
|
39
|
+
Serve the brand guide at `/brand` in your existing dev server and bundle it on build.
|
|
40
|
+
|
|
41
|
+
**Vite**
|
|
42
|
+
```js
|
|
43
|
+
// vite.config.js
|
|
44
|
+
import brandkit from '@stacklist-app/brandkit/integrations/vite'
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
plugins: [brandkit()],
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Astro**
|
|
52
|
+
```js
|
|
53
|
+
// astro.config.mjs
|
|
54
|
+
import brandkit from '@stacklist-app/brandkit/integrations/astro'
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
integrations: [brandkit()],
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Next.js / plain HTML**: run `brandkit build brand` and serve the resulting directory as a static route.
|
|
62
|
+
|
|
63
|
+
## Config
|
|
64
|
+
|
|
65
|
+
`config.json` is the single source of truth. Top-level keys:
|
|
66
|
+
|
|
67
|
+
- `brand` — name, tagline, description, version
|
|
68
|
+
- `fonts` — display + body with Google Fonts import
|
|
69
|
+
- `theme` — CSS variable map (colors, gradients, font vars)
|
|
70
|
+
- `nav` — sidebar structure
|
|
71
|
+
- `colors` — brand / neutrals / semantic palettes
|
|
72
|
+
- `gradients`, `gradientUsage` — gradient definitions + do/don't lists
|
|
73
|
+
- `logos`, `logoSizes` — logo variants + download sizes
|
|
74
|
+
- `typography` — type scale and specimens
|
|
75
|
+
- `hierarchy` — text hierarchy demo
|
|
76
|
+
- `voice` — tone description + do/don't examples
|
|
77
|
+
- `components` — buttons, cards, stats
|
|
78
|
+
- `spacing` — spacing scale tokens
|
|
79
|
+
- `accessibility` — contrast ratio grid
|
|
80
|
+
- `cssVariables` — variable reference
|
|
81
|
+
|
|
82
|
+
See `example/config.json` for a complete reference implementation (Freeway PHX).
|
|
83
|
+
|
|
84
|
+
## How theming works
|
|
85
|
+
|
|
86
|
+
`dist/styles.css` uses CSS custom properties (`var(--purple)`, `var(--font-display)`, etc.) with no `:root` block.
|
|
87
|
+
|
|
88
|
+
- **Dev**: the engine reads `config.theme` and injects a `<style data-brandkit-theme>` tag at runtime.
|
|
89
|
+
- **Build**: `brandkit build` prepends the generated `:root` block to `styles.css` and injects the Google Fonts `<link>` and page title into `index.html`.
|
|
90
|
+
|
|
91
|
+
Swap the config, and every color, gradient, and font token updates everywhere.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT © The Stack Lab
|
package/bin/brandkit.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
var args = process.argv.slice(2);
|
|
4
|
+
var command = args[0] || 'help';
|
|
5
|
+
|
|
6
|
+
switch (command) {
|
|
7
|
+
case 'init':
|
|
8
|
+
require('../cli/init')(args.slice(1));
|
|
9
|
+
break;
|
|
10
|
+
case 'dev':
|
|
11
|
+
require('../cli/dev')(args.slice(1));
|
|
12
|
+
break;
|
|
13
|
+
case 'build':
|
|
14
|
+
require('../cli/build')(args.slice(1));
|
|
15
|
+
break;
|
|
16
|
+
case 'generate':
|
|
17
|
+
require('../cli/generate')(args.slice(1));
|
|
18
|
+
break;
|
|
19
|
+
case 'help':
|
|
20
|
+
case '--help':
|
|
21
|
+
case '-h':
|
|
22
|
+
printHelp();
|
|
23
|
+
break;
|
|
24
|
+
case '--version':
|
|
25
|
+
case '-v':
|
|
26
|
+
var pkg = require('../package.json');
|
|
27
|
+
console.log(pkg.version);
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
console.error('Unknown command: ' + command);
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printHelp() {
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(' brandkit — config-driven brand guide');
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(' Usage:');
|
|
40
|
+
console.log(' brandkit init [dir] Scaffold a new brand guide');
|
|
41
|
+
console.log(' brandkit generate [dir] Auto-generate config from codebase');
|
|
42
|
+
console.log(' brandkit dev [dir] Start dev server with live reload');
|
|
43
|
+
console.log(' brandkit build [dir] Build static files for production');
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(' Options:');
|
|
46
|
+
console.log(' --help, -h Show this help message');
|
|
47
|
+
console.log(' --version, -v Show version number');
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
package/cli/build.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var resolve = require('../lib/resolve');
|
|
4
|
+
var template = require('../lib/template');
|
|
5
|
+
|
|
6
|
+
module.exports = function build(args) {
|
|
7
|
+
var targetDir = path.resolve(args[0] || '.');
|
|
8
|
+
var distDir = resolve.getDistPath();
|
|
9
|
+
|
|
10
|
+
var configPath = path.join(targetDir, 'config.json');
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
console.error('');
|
|
13
|
+
console.error(' No config.json found in ' + targetDir);
|
|
14
|
+
console.error(' Run: brandkit init ' + path.relative(process.cwd(), targetDir));
|
|
15
|
+
console.error('');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(' brandkit build');
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(' Reading ' + path.relative(process.cwd(), configPath) + '...');
|
|
23
|
+
|
|
24
|
+
var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
|
|
26
|
+
template.build(distDir, targetDir, config);
|
|
27
|
+
|
|
28
|
+
var brandName = (config.brand && config.brand.displayName) || 'Brand';
|
|
29
|
+
console.log(' built index.html (' + brandName + ' title + fonts baked in)');
|
|
30
|
+
console.log(' built styles.css (:root variables generated)');
|
|
31
|
+
console.log(' copied engine.js');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(' Ready to deploy. Serve ' + path.relative(process.cwd(), targetDir) + '/ as static files.');
|
|
34
|
+
console.log('');
|
|
35
|
+
};
|
package/cli/dev.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
var http = require('http');
|
|
2
|
+
var fs = require('fs');
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var url = require('url');
|
|
5
|
+
|
|
6
|
+
var MIME_TYPES = {
|
|
7
|
+
'.html': 'text/html',
|
|
8
|
+
'.css': 'text/css',
|
|
9
|
+
'.js': 'application/javascript',
|
|
10
|
+
'.json': 'application/json',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.ico': 'image/x-icon',
|
|
16
|
+
'.woff': 'font/woff',
|
|
17
|
+
'.woff2': 'font/woff2'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = function dev(args) {
|
|
21
|
+
var dir = path.resolve(args[0] || '.');
|
|
22
|
+
var port = 4800;
|
|
23
|
+
|
|
24
|
+
// Parse --port flag
|
|
25
|
+
for (var i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
27
|
+
port = parseInt(args[i + 1], 10) || 4800;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate directory has a config.json
|
|
32
|
+
var configPath = path.join(dir, 'config.json');
|
|
33
|
+
if (!fs.existsSync(configPath)) {
|
|
34
|
+
console.error('');
|
|
35
|
+
console.error(' No config.json found in ' + dir);
|
|
36
|
+
console.error(' Run: brandkit init ' + path.relative(process.cwd(), dir));
|
|
37
|
+
console.error('');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// SSE clients for live reload
|
|
42
|
+
var sseClients = [];
|
|
43
|
+
|
|
44
|
+
var server = http.createServer(function (req, res) {
|
|
45
|
+
var parsed = url.parse(req.url);
|
|
46
|
+
var pathname = parsed.pathname;
|
|
47
|
+
|
|
48
|
+
// SSE endpoint for live reload
|
|
49
|
+
if (pathname === '/__brandkit_reload') {
|
|
50
|
+
res.writeHead(200, {
|
|
51
|
+
'Content-Type': 'text/event-stream',
|
|
52
|
+
'Cache-Control': 'no-cache',
|
|
53
|
+
'Connection': 'keep-alive',
|
|
54
|
+
'Access-Control-Allow-Origin': '*'
|
|
55
|
+
});
|
|
56
|
+
res.write('data: connected\n\n');
|
|
57
|
+
sseClients.push(res);
|
|
58
|
+
req.on('close', function () {
|
|
59
|
+
var idx = sseClients.indexOf(res);
|
|
60
|
+
if (idx !== -1) sseClients.splice(idx, 1);
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Serve static files
|
|
66
|
+
if (pathname === '/') pathname = '/index.html';
|
|
67
|
+
var filePath = path.join(dir, pathname);
|
|
68
|
+
|
|
69
|
+
// Security: prevent path traversal
|
|
70
|
+
if (!filePath.startsWith(dir)) {
|
|
71
|
+
res.writeHead(403);
|
|
72
|
+
res.end('Forbidden');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fs.readFile(filePath, function (err, data) {
|
|
77
|
+
if (err) {
|
|
78
|
+
res.writeHead(404);
|
|
79
|
+
res.end('Not found: ' + pathname);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
var ext = path.extname(filePath).toLowerCase();
|
|
83
|
+
var mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
84
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
85
|
+
|
|
86
|
+
// Inject live reload script into HTML
|
|
87
|
+
if (ext === '.html') {
|
|
88
|
+
var html = data.toString();
|
|
89
|
+
var reloadScript =
|
|
90
|
+
'<script>' +
|
|
91
|
+
'(function(){' +
|
|
92
|
+
'var es=new EventSource("/__brandkit_reload");' +
|
|
93
|
+
'es.onmessage=function(e){if(e.data==="reload")location.reload();};' +
|
|
94
|
+
'es.onerror=function(){es.close();setTimeout(function(){location.reload();},2000);};' +
|
|
95
|
+
'})();' +
|
|
96
|
+
'</script>';
|
|
97
|
+
html = html.replace('</body>', reloadScript + '</body>');
|
|
98
|
+
res.end(html);
|
|
99
|
+
} else {
|
|
100
|
+
res.end(data);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
server.listen(port, function () {
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(' brandkit dev');
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(' Serving brand guide at http://localhost:' + port + '/');
|
|
110
|
+
console.log(' Watching ' + path.relative(process.cwd(), configPath) + ' for changes...');
|
|
111
|
+
console.log('');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Watch config.json for changes
|
|
115
|
+
var debounce = null;
|
|
116
|
+
fs.watch(dir, { recursive: true }, function (eventType, filename) {
|
|
117
|
+
if (!filename) return;
|
|
118
|
+
// Only reload on relevant file changes
|
|
119
|
+
var ext = path.extname(filename).toLowerCase();
|
|
120
|
+
if (['.json', '.html', '.css', '.js'].indexOf(ext) === -1) return;
|
|
121
|
+
|
|
122
|
+
clearTimeout(debounce);
|
|
123
|
+
debounce = setTimeout(function () {
|
|
124
|
+
sseClients.forEach(function (client) {
|
|
125
|
+
client.write('data: reload\n\n');
|
|
126
|
+
});
|
|
127
|
+
}, 100);
|
|
128
|
+
});
|
|
129
|
+
};
|
package/cli/generate.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var extractTailwind = require('../lib/extract-tailwind');
|
|
4
|
+
var extractCSS = require('../lib/extract-css');
|
|
5
|
+
var extractLogos = require('../lib/extract-logos');
|
|
6
|
+
var helpers = require('../lib/generate-helpers');
|
|
7
|
+
var schema = require('../lib/config-schema');
|
|
8
|
+
|
|
9
|
+
module.exports = function generate(args) {
|
|
10
|
+
var brandDir = path.resolve(args[0] || 'brand');
|
|
11
|
+
var projectDir = process.cwd();
|
|
12
|
+
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(' brandkit generate');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(' Scanning project...');
|
|
17
|
+
|
|
18
|
+
var extracted = {};
|
|
19
|
+
var summary = [];
|
|
20
|
+
|
|
21
|
+
// Extract from Tailwind config
|
|
22
|
+
var tw = extractTailwind.extract(projectDir);
|
|
23
|
+
if (tw.colors) {
|
|
24
|
+
summary.push(' Found Tailwind config \u2014 extracting colors, fonts, spacing');
|
|
25
|
+
extracted.tailwindColors = tw.colors;
|
|
26
|
+
}
|
|
27
|
+
if (tw.fonts) extracted.tailwindFonts = tw.fonts;
|
|
28
|
+
if (tw.spacing) extracted.tailwindSpacing = tw.spacing;
|
|
29
|
+
|
|
30
|
+
// Extract CSS custom properties
|
|
31
|
+
var cssVars = extractCSS.extract(projectDir);
|
|
32
|
+
if (cssVars) {
|
|
33
|
+
summary.push(' Found CSS custom properties \u2014 extracting theme variables');
|
|
34
|
+
extracted.cssVars = cssVars;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Extract logos
|
|
38
|
+
var logos = extractLogos.extract(projectDir);
|
|
39
|
+
if (logos && logos.length) {
|
|
40
|
+
var logoNames = logos.map(function (l) { return path.basename(Object.values(l.variants)[0]); });
|
|
41
|
+
summary.push(' Found ' + logoNames.join(', ') + ' \u2014 adding to logos');
|
|
42
|
+
extracted.logos = logos;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!summary.length) {
|
|
46
|
+
summary.push(' No Tailwind config, CSS variables, or logo assets detected');
|
|
47
|
+
summary.push(' Creating starter config with example entries');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load existing config or create starter
|
|
51
|
+
var configPath = path.join(brandDir, 'config.json');
|
|
52
|
+
var existingConfig = null;
|
|
53
|
+
if (fs.existsSync(configPath)) {
|
|
54
|
+
try {
|
|
55
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
56
|
+
console.log(' Found existing config.json \u2014 preserving manual fields');
|
|
57
|
+
} catch (_) {
|
|
58
|
+
console.log(' Existing config.json is invalid \u2014 creating fresh');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var baseConfig = existingConfig || schema.starterConfig();
|
|
63
|
+
|
|
64
|
+
// Build extracted config fields
|
|
65
|
+
var newFields = {};
|
|
66
|
+
|
|
67
|
+
// Colors from Tailwind (with auto-computed oklch and light flag)
|
|
68
|
+
if (extracted.tailwindColors) {
|
|
69
|
+
newFields.colors = buildColors(extracted.tailwindColors);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Theme from CSS variables
|
|
73
|
+
if (extracted.cssVars) {
|
|
74
|
+
newFields.theme = extractCSS.mapToTheme(extracted.cssVars);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fonts from Tailwind
|
|
78
|
+
if (extracted.tailwindFonts) {
|
|
79
|
+
var fonts = {};
|
|
80
|
+
if (extracted.tailwindFonts.display) {
|
|
81
|
+
fonts.display = {
|
|
82
|
+
family: extracted.tailwindFonts.display.family,
|
|
83
|
+
googleImport: extracted.tailwindFonts.display.family + ':wght@300;400;500;600;700',
|
|
84
|
+
description: '__TODO: Describe the display font.'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (extracted.tailwindFonts.body) {
|
|
88
|
+
fonts.body = {
|
|
89
|
+
family: extracted.tailwindFonts.body.family,
|
|
90
|
+
googleImport: extracted.tailwindFonts.body.family + ':wght@300;400;500;600;700',
|
|
91
|
+
description: '__TODO: Describe the body font.'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(fonts).length) newFields.fonts = fonts;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Spacing from Tailwind
|
|
98
|
+
if (extracted.tailwindSpacing) {
|
|
99
|
+
newFields.spacing = extracted.tailwindSpacing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Logos (make paths relative to brand dir)
|
|
103
|
+
if (extracted.logos) {
|
|
104
|
+
newFields.logos = extracted.logos.map(function (logo) {
|
|
105
|
+
var variants = {};
|
|
106
|
+
var keys = Object.keys(logo.variants);
|
|
107
|
+
for (var i = 0; i < keys.length; i++) {
|
|
108
|
+
variants[keys[i]] = path.relative(brandDir, path.join(projectDir, logo.variants[keys[i]]));
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
name: logo.name,
|
|
112
|
+
description: logo.description,
|
|
113
|
+
variants: variants,
|
|
114
|
+
background: logo.background
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Auto-compute derived fields ---
|
|
120
|
+
// Only overwrite if existing field is empty or all-scaffold (__TODO)
|
|
121
|
+
|
|
122
|
+
// Determine the theme to derive from (extracted or existing)
|
|
123
|
+
var theme = newFields.theme || baseConfig.theme;
|
|
124
|
+
|
|
125
|
+
// Auto-generate gradients from theme
|
|
126
|
+
if (theme && isEmptyOrScaffold(baseConfig.gradients)) {
|
|
127
|
+
var gradients = helpers.buildGradientsFromTheme(theme);
|
|
128
|
+
if (gradients.length) {
|
|
129
|
+
newFields.gradients = gradients;
|
|
130
|
+
summary.push(' Gradients: ' + gradients.length + ' auto-generated from theme');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Auto-generate hierarchy from theme
|
|
135
|
+
if (theme && isEmptyOrScaffold(baseConfig.hierarchy)) {
|
|
136
|
+
newFields.hierarchy = helpers.buildHierarchyFromTheme(theme);
|
|
137
|
+
summary.push(' Hierarchy: 4 levels auto-generated from theme colors');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Auto-generate accessibility pairs from all extracted colors
|
|
141
|
+
if (isEmptyOrScaffold(baseConfig.accessibility)) {
|
|
142
|
+
var allColors = [];
|
|
143
|
+
var colorSource = newFields.colors || baseConfig.colors;
|
|
144
|
+
if (colorSource) {
|
|
145
|
+
['brand', 'neutrals', 'semantic'].forEach(function (key) {
|
|
146
|
+
var group = colorSource[key];
|
|
147
|
+
if (group) {
|
|
148
|
+
var items = group.items || group;
|
|
149
|
+
if (Array.isArray(items)) {
|
|
150
|
+
allColors = allColors.concat(items);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (allColors.length) {
|
|
156
|
+
var a11y = helpers.generateA11yPairs(allColors);
|
|
157
|
+
if (a11y.length) {
|
|
158
|
+
newFields.accessibility = a11y;
|
|
159
|
+
summary.push(' Accessibility: ' + a11y.length + ' contrast pairs auto-computed');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Auto-generate cssVariables from theme
|
|
165
|
+
if (theme && isEmptyOrScaffold(baseConfig.cssVariables)) {
|
|
166
|
+
var cssVarSections = helpers.buildCssVariablesFromTheme(theme);
|
|
167
|
+
if (cssVarSections.length) {
|
|
168
|
+
newFields.cssVariables = cssVarSections;
|
|
169
|
+
summary.push(' CSS Variables: ' + cssVarSections.length + ' sections auto-generated');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Auto-scaffold typography if empty or missing
|
|
174
|
+
if (isEmptyOrScaffold(baseConfig.typography)) {
|
|
175
|
+
newFields.typography = helpers.scaffoldTypography();
|
|
176
|
+
summary.push(' Typography: standard type scale scaffolded');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Print all summary messages (including derived fields computed above)
|
|
180
|
+
summary.forEach(function (line) { console.log(line); });
|
|
181
|
+
|
|
182
|
+
// Merge
|
|
183
|
+
var finalConfig = schema.mergeConfigs(baseConfig, newFields);
|
|
184
|
+
|
|
185
|
+
// Ensure brand dir exists
|
|
186
|
+
if (!fs.existsSync(brandDir)) {
|
|
187
|
+
fs.mkdirSync(brandDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2) + '\n');
|
|
191
|
+
|
|
192
|
+
// Count what needs TODO attention
|
|
193
|
+
var todoCount = countTodos(finalConfig);
|
|
194
|
+
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(' Generated ' + path.relative(process.cwd(), configPath));
|
|
197
|
+
if (extracted.tailwindColors) console.log(' Colors: ' + extracted.tailwindColors.length + ' extracted (with oklch)');
|
|
198
|
+
if (extracted.tailwindFonts) console.log(' Fonts: ' + Object.keys(extracted.tailwindFonts).length + ' detected');
|
|
199
|
+
if (extracted.tailwindSpacing) console.log(' Spacing: ' + extracted.tailwindSpacing.length + ' tokens');
|
|
200
|
+
if (extracted.logos) console.log(' Logos: ' + extracted.logos.length + ' files found');
|
|
201
|
+
if (todoCount > 0) console.log(' TODO: ' + todoCount + ' fields need manual or AI attention');
|
|
202
|
+
console.log('');
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function buildColors(colorList) {
|
|
206
|
+
var brand = [];
|
|
207
|
+
var neutrals = [];
|
|
208
|
+
var semantic = [];
|
|
209
|
+
|
|
210
|
+
var semanticNames = ['success', 'warning', 'error', 'danger', 'info'];
|
|
211
|
+
var neutralNames = ['gray', 'grey', 'slate', 'zinc', 'neutral', 'stone', 'black', 'white'];
|
|
212
|
+
|
|
213
|
+
for (var i = 0; i < colorList.length; i++) {
|
|
214
|
+
var c = colorList[i];
|
|
215
|
+
// Skip colors without valid hex (Tailwind function-based colors, etc.)
|
|
216
|
+
if (!c.hex || typeof c.hex !== 'string' || !/^#[0-9a-fA-F]{3,8}$/.test(c.hex)) continue;
|
|
217
|
+
if (!c.name) continue;
|
|
218
|
+
var lowerName = c.name.toLowerCase();
|
|
219
|
+
|
|
220
|
+
var isSemantic = false;
|
|
221
|
+
var isNeutral = false;
|
|
222
|
+
for (var j = 0; j < semanticNames.length; j++) {
|
|
223
|
+
if (lowerName.indexOf(semanticNames[j]) !== -1) { isSemantic = true; break; }
|
|
224
|
+
}
|
|
225
|
+
if (!isSemantic) {
|
|
226
|
+
for (var k = 0; k < neutralNames.length; k++) {
|
|
227
|
+
if (lowerName.indexOf(neutralNames[k]) !== -1) { isNeutral = true; break; }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Auto-compute oklch and light flag
|
|
232
|
+
var entry = {
|
|
233
|
+
name: c.name,
|
|
234
|
+
hex: c.hex,
|
|
235
|
+
oklch: helpers.hexToOklch(c.hex),
|
|
236
|
+
cssVar: '--color-' + c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
237
|
+
role: c.role || '',
|
|
238
|
+
light: helpers.isLightColor(c.hex)
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (isSemantic) semantic.push(entry);
|
|
242
|
+
else if (isNeutral) neutrals.push(entry);
|
|
243
|
+
else brand.push(entry);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
brand: { label: 'Brand', items: brand },
|
|
248
|
+
neutrals: { label: 'Neutrals', items: neutrals },
|
|
249
|
+
semantic: { label: 'Semantic', items: semantic }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function countTodos(obj) {
|
|
254
|
+
var count = 0;
|
|
255
|
+
var str = JSON.stringify(obj);
|
|
256
|
+
var regex = /__TODO/g;
|
|
257
|
+
while (regex.exec(str) !== null) count++;
|
|
258
|
+
return count;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if a field is empty or only contains scaffold/placeholder data.
|
|
263
|
+
* Returns true if the field should be overwritten by auto-generation.
|
|
264
|
+
*/
|
|
265
|
+
function isEmptyOrScaffold(value) {
|
|
266
|
+
if (!value) return true;
|
|
267
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
268
|
+
// Check if all entries are scaffold (contain __TODO)
|
|
269
|
+
var str = JSON.stringify(value);
|
|
270
|
+
if (str.indexOf('__TODO') !== -1) return true;
|
|
271
|
+
return false;
|
|
272
|
+
}
|
package/cli/init.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var resolve = require('../lib/resolve');
|
|
4
|
+
var schema = require('../lib/config-schema');
|
|
5
|
+
|
|
6
|
+
module.exports = function init(args) {
|
|
7
|
+
var targetDir = path.resolve(args[0] || 'brand');
|
|
8
|
+
var distDir = resolve.getDistPath();
|
|
9
|
+
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(' brandkit');
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(' Scaffolding into ' + path.relative(process.cwd(), targetDir) + '/...');
|
|
14
|
+
|
|
15
|
+
// Create target directory
|
|
16
|
+
if (!fs.existsSync(targetDir)) {
|
|
17
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Create logos directory
|
|
21
|
+
var logosDir = path.join(targetDir, 'logos');
|
|
22
|
+
if (!fs.existsSync(logosDir)) {
|
|
23
|
+
fs.mkdirSync(logosDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for --update flag: only update engine files, preserve config
|
|
27
|
+
var isUpdate = args.indexOf('--update') !== -1;
|
|
28
|
+
|
|
29
|
+
// Copy engine files from dist/
|
|
30
|
+
var files = ['engine.js', 'styles.css', 'index.html'];
|
|
31
|
+
for (var i = 0; i < files.length; i++) {
|
|
32
|
+
var src = path.join(distDir, files[i]);
|
|
33
|
+
var dest = path.join(targetDir, files[i]);
|
|
34
|
+
if (fs.existsSync(src)) {
|
|
35
|
+
fs.copyFileSync(src, dest);
|
|
36
|
+
console.log(' copied ' + files[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create starter config.json (skip if exists and not forcing)
|
|
41
|
+
var configPath = path.join(targetDir, 'config.json');
|
|
42
|
+
if (!fs.existsSync(configPath) && !isUpdate) {
|
|
43
|
+
var starter = schema.starterConfig();
|
|
44
|
+
fs.writeFileSync(configPath, JSON.stringify(starter, null, 2) + '\n');
|
|
45
|
+
console.log(' created config.json');
|
|
46
|
+
} else if (isUpdate) {
|
|
47
|
+
console.log(' kept config.json (--update)');
|
|
48
|
+
} else {
|
|
49
|
+
console.log(' kept config.json (already exists)');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
if (isUpdate) {
|
|
54
|
+
console.log(' Updated engine files. Config and logos preserved.');
|
|
55
|
+
} else {
|
|
56
|
+
console.log(' Next steps:');
|
|
57
|
+
console.log(' 1. Edit ' + path.relative(process.cwd(), configPath) + ' with your brand data');
|
|
58
|
+
console.log(' 2. Add logo files to ' + path.relative(process.cwd(), logosDir) + '/');
|
|
59
|
+
console.log(' 3. Run: npx brandkit dev ' + path.relative(process.cwd(), targetDir));
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
};
|