@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var brandkitVite = require('./vite');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Astro integration for @stacklist-app/brandkit.
|
|
5
|
+
* Wraps the Vite plugin to serve brand guide at /brand.
|
|
6
|
+
*
|
|
7
|
+
* Usage in astro.config.mjs:
|
|
8
|
+
* import brandkit from '@stacklist-app/brandkit/integrations/astro'
|
|
9
|
+
* export default defineConfig({ integrations: [brandkit()] })
|
|
10
|
+
*/
|
|
11
|
+
module.exports = function brandkitAstro(options) {
|
|
12
|
+
var vitePlugin = brandkitVite(options);
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: '@stacklist-app/brandkit',
|
|
16
|
+
hooks: {
|
|
17
|
+
'astro:config:setup': function (params) {
|
|
18
|
+
params.updateConfig({
|
|
19
|
+
vite: {
|
|
20
|
+
plugins: [vitePlugin]
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Vite plugin for @stacklist-app/brandkit.
|
|
6
|
+
* Serves brand guide files at /brand during dev.
|
|
7
|
+
* Copies built files to output on build.
|
|
8
|
+
*
|
|
9
|
+
* Usage in vite.config.js:
|
|
10
|
+
* import brandkit from '@stacklist-app/brandkit/integrations/vite'
|
|
11
|
+
* export default defineConfig({ plugins: [brandkit()] })
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function brandkitVite(options) {
|
|
14
|
+
var brandDir = (options && options.dir) || 'brand';
|
|
15
|
+
var route = (options && options.route) || '/brand';
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
name: '@stacklist-app/brandkit',
|
|
19
|
+
|
|
20
|
+
configureServer: function (server) {
|
|
21
|
+
var dir = path.resolve(brandDir);
|
|
22
|
+
if (!fs.existsSync(dir)) return;
|
|
23
|
+
|
|
24
|
+
server.middlewares.use(route, function (req, res, next) {
|
|
25
|
+
var filePath = path.join(dir, req.url === '/' ? '/index.html' : req.url);
|
|
26
|
+
|
|
27
|
+
if (!filePath.startsWith(dir)) {
|
|
28
|
+
res.statusCode = 403;
|
|
29
|
+
res.end('Forbidden');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
next();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var ext = path.extname(filePath).toLowerCase();
|
|
39
|
+
var mimeTypes = {
|
|
40
|
+
'.html': 'text/html',
|
|
41
|
+
'.css': 'text/css',
|
|
42
|
+
'.js': 'application/javascript',
|
|
43
|
+
'.json': 'application/json',
|
|
44
|
+
'.svg': 'image/svg+xml',
|
|
45
|
+
'.png': 'image/png',
|
|
46
|
+
'.jpg': 'image/jpeg'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
|
|
50
|
+
fs.createReadStream(filePath).pipe(res);
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
closeBundle: function () {
|
|
55
|
+
// Copy brand directory to build output
|
|
56
|
+
var srcDir = path.resolve(brandDir);
|
|
57
|
+
if (!fs.existsSync(srcDir)) return;
|
|
58
|
+
|
|
59
|
+
var outDir = path.resolve('dist', route.replace(/^\//, ''));
|
|
60
|
+
if (!fs.existsSync(outDir)) {
|
|
61
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
copyDir(srcDir, outDir);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function copyDir(src, dest) {
|
|
70
|
+
var entries = fs.readdirSync(src, { withFileTypes: true });
|
|
71
|
+
for (var i = 0; i < entries.length; i++) {
|
|
72
|
+
var srcPath = path.join(src, entries[i].name);
|
|
73
|
+
var destPath = path.join(dest, entries[i].name);
|
|
74
|
+
if (entries[i].isDirectory()) {
|
|
75
|
+
if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
|
|
76
|
+
copyDir(srcPath, destPath);
|
|
77
|
+
} else {
|
|
78
|
+
fs.copyFileSync(srcPath, destPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter config template with __TODO markers for AI agents.
|
|
3
|
+
* Every section includes example entries with the correct field names
|
|
4
|
+
* so AI agents can see the exact schema contract.
|
|
5
|
+
* Extractable fields (colors, fonts, spacing, logos) are populated by generate.
|
|
6
|
+
*/
|
|
7
|
+
function starterConfig() {
|
|
8
|
+
return {
|
|
9
|
+
brand: {
|
|
10
|
+
name: '__TODO',
|
|
11
|
+
displayName: '__TODO',
|
|
12
|
+
tagline: '__TODO',
|
|
13
|
+
description: '__TODO: A brief description of the brand and its visual language.',
|
|
14
|
+
url: '__TODO',
|
|
15
|
+
byline: '__TODO',
|
|
16
|
+
version: '1.0',
|
|
17
|
+
date: new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
|
|
18
|
+
},
|
|
19
|
+
fonts: {
|
|
20
|
+
display: {
|
|
21
|
+
family: 'Inter',
|
|
22
|
+
googleImport: 'Inter:wght@300;400;500;600;700',
|
|
23
|
+
description: '__TODO: Describe the display font character and usage.'
|
|
24
|
+
},
|
|
25
|
+
body: {
|
|
26
|
+
family: 'Inter',
|
|
27
|
+
googleImport: 'Inter:wght@300;400;500;600;700',
|
|
28
|
+
description: '__TODO: Describe the body font character and usage.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
theme: {
|
|
32
|
+
'--purple': '#6366F1',
|
|
33
|
+
'--violet': '#4338CA',
|
|
34
|
+
'--lavender': '#A5B4FC',
|
|
35
|
+
'--coral': '#F87171',
|
|
36
|
+
'--coral-dark': '#DC2626',
|
|
37
|
+
'--coral-light': '#FCA5A5',
|
|
38
|
+
'--magenta': '#EC4899',
|
|
39
|
+
'--ink': '#111827',
|
|
40
|
+
'--graphite': '#374151',
|
|
41
|
+
'--slate': '#6B7280',
|
|
42
|
+
'--haze': '#D1D5DB',
|
|
43
|
+
'--mist': '#F3F4F6',
|
|
44
|
+
'--cloud': '#F9FAFB',
|
|
45
|
+
'--white': '#FFFFFF',
|
|
46
|
+
'--success': '#10B981',
|
|
47
|
+
'--warning': '#F59E0B',
|
|
48
|
+
'--error': '#EF4444',
|
|
49
|
+
'--info': '#3B82F6',
|
|
50
|
+
'--gradient-brand': 'linear-gradient(135deg, #4338CA 0%, #6366F1 50%, #EC4899 100%)',
|
|
51
|
+
'--gradient-brand-subtle': 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(236, 72, 153, 0.06) 100%)',
|
|
52
|
+
'--purple-rgb': '99, 102, 241',
|
|
53
|
+
'--ink-rgb': '17, 24, 39',
|
|
54
|
+
'--error-rgb': '239, 68, 68',
|
|
55
|
+
'--success-rgb': '16, 185, 129'
|
|
56
|
+
},
|
|
57
|
+
nav: [
|
|
58
|
+
{ group: 'Colors', items: [
|
|
59
|
+
{ label: 'Palette', id: 'palette' },
|
|
60
|
+
{ label: 'Gradients', id: 'gradients' }
|
|
61
|
+
]},
|
|
62
|
+
{ group: 'Logos', items: [
|
|
63
|
+
{ label: 'Downloads', id: 'logos' }
|
|
64
|
+
]},
|
|
65
|
+
{ group: 'Content', items: [
|
|
66
|
+
{ label: 'Typography', id: 'typography' },
|
|
67
|
+
{ label: 'Text Hierarchy', id: 'hierarchy' },
|
|
68
|
+
{ label: 'Voice & Tone', id: 'voice' },
|
|
69
|
+
{ label: 'Spacing', id: 'spacing' }
|
|
70
|
+
]},
|
|
71
|
+
{ group: 'Components', items: [
|
|
72
|
+
{ label: 'Buttons & Cards', id: 'components' },
|
|
73
|
+
{ label: 'Accessibility', id: 'accessibility' },
|
|
74
|
+
{ label: 'CSS Variables', id: 'variables' }
|
|
75
|
+
]}
|
|
76
|
+
],
|
|
77
|
+
colors: {
|
|
78
|
+
brand: { label: 'Brand', items: [
|
|
79
|
+
{ name: '__TODO: Primary', hex: '#6366F1', oklch: 'oklch(0.55 0.22 264)', cssVar: '--color-primary', role: '__TODO: Primary accent', light: false },
|
|
80
|
+
{ name: '__TODO: Secondary', hex: '#EC4899', oklch: 'oklch(0.59 0.22 346)', cssVar: '--color-secondary', role: '__TODO: Secondary accent', light: false }
|
|
81
|
+
]},
|
|
82
|
+
neutrals: { label: 'Neutrals', items: [
|
|
83
|
+
{ name: 'Ink', hex: '#111827', oklch: 'oklch(0.17 0.02 265)', cssVar: '--foreground', role: 'Primary text, headings', light: false },
|
|
84
|
+
{ name: 'Slate', hex: '#6B7280', oklch: 'oklch(0.55 0.02 265)', cssVar: '--muted-foreground', role: 'Tertiary text, captions', light: false },
|
|
85
|
+
{ name: 'Mist', hex: '#F3F4F6', oklch: 'oklch(0.96 0.01 265)', cssVar: '--secondary', role: 'Section backgrounds', light: true },
|
|
86
|
+
{ name: 'Cloud', hex: '#F9FAFB', oklch: 'oklch(0.98 0.00 265)', cssVar: '--background', role: 'Page background', light: true }
|
|
87
|
+
]},
|
|
88
|
+
semantic: { label: 'Semantic', items: [
|
|
89
|
+
{ name: 'Success', hex: '#10B981', oklch: 'oklch(0.70 0.17 163)', cssVar: '--success', role: 'Confirmations, positive states', light: false },
|
|
90
|
+
{ name: 'Warning', hex: '#F59E0B', oklch: 'oklch(0.78 0.16 80)', cssVar: '--warning', role: 'Attention, caution states', light: false },
|
|
91
|
+
{ name: 'Error', hex: '#EF4444', oklch: 'oklch(0.59 0.22 27)', cssVar: '--destructive', role: 'Errors, destructive actions', light: false }
|
|
92
|
+
]}
|
|
93
|
+
},
|
|
94
|
+
gradients: [
|
|
95
|
+
{
|
|
96
|
+
name: 'Brand',
|
|
97
|
+
css: 'linear-gradient(135deg, #4338CA 0%, #6366F1 50%, #EC4899 100%)',
|
|
98
|
+
description: '__TODO: Primary gradient usage',
|
|
99
|
+
stops: [
|
|
100
|
+
{ color: '#4338CA', position: '0%', name: 'Violet' },
|
|
101
|
+
{ color: '#6366F1', position: '50%', name: 'Indigo' },
|
|
102
|
+
{ color: '#EC4899', position: '100%', name: 'Pink' }
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Subtle',
|
|
107
|
+
css: 'linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(236, 72, 153, 0.06) 100%)',
|
|
108
|
+
description: 'Card tints, section backgrounds, hover states'
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
gradientUsage: {
|
|
112
|
+
do: ['__TODO: When to use the gradient'],
|
|
113
|
+
dont: ['__TODO: When not to use the gradient']
|
|
114
|
+
},
|
|
115
|
+
sections: {
|
|
116
|
+
gradients: '__TODO: Describe the gradient system.',
|
|
117
|
+
gradientTextDemo: '__TODO: A headline to demo gradient text.',
|
|
118
|
+
logos: 'Download logo files for use across web, social, and print materials. Select format and size, then download.',
|
|
119
|
+
components: '__TODO: Describe the component patterns.',
|
|
120
|
+
spacing: '__TODO: Describe the spacing scale.',
|
|
121
|
+
variables: '__TODO: Describe the CSS variable system.'
|
|
122
|
+
},
|
|
123
|
+
hierarchy: [
|
|
124
|
+
{ class: 'h-primary', label: 'Primary', colorVar: '--ink', colorName: 'Ink', hex: '#111827', description: 'Primary text — body copy, headings, and content where readability is critical.' },
|
|
125
|
+
{ class: 'h-secondary', label: 'Secondary', colorVar: '--graphite', colorName: 'Graphite', hex: '#374151', description: 'Secondary text — descriptions, supporting information, and labels.' },
|
|
126
|
+
{ class: 'h-tertiary', label: 'Tertiary', colorVar: '--slate', colorName: 'Slate', hex: '#6B7280', description: 'Tertiary text — captions, timestamps, footnotes, and metadata.' },
|
|
127
|
+
{ class: 'h-accent', label: 'Accent', colorVar: '--purple', colorName: 'Primary', hex: '#6366F1', description: 'Accent text — links, interactive labels, and calls to action.' }
|
|
128
|
+
],
|
|
129
|
+
logos: [],
|
|
130
|
+
logoSizes: [
|
|
131
|
+
{ label: 'Original', width: null },
|
|
132
|
+
{ label: '800px', width: 800 },
|
|
133
|
+
{ label: '400px', width: 400 },
|
|
134
|
+
{ label: '200px', width: 200 }
|
|
135
|
+
],
|
|
136
|
+
typography: [
|
|
137
|
+
{ name: 'Display XL', font: 'display', size: '72px', weight: 700, tracking: '-0.03em', leading: '1.05', sample: '__TODO: Hero headline' },
|
|
138
|
+
{ name: 'Display', font: 'display', size: '56px', weight: 700, tracking: '-0.025em', leading: '1.1', sample: '__TODO: Section headline' },
|
|
139
|
+
{ name: 'H1', font: 'display', size: '44px', weight: 700, tracking: '-0.02em', leading: '1.15', sample: '__TODO: Page heading' },
|
|
140
|
+
{ name: 'H2', font: 'display', size: '36px', weight: 700, tracking: '-0.015em', leading: '1.2', sample: '__TODO: Section heading' },
|
|
141
|
+
{ name: 'H3', font: 'display', size: '28px', weight: 600, tracking: '-0.01em', leading: '1.3', sample: '__TODO: Subsection heading' },
|
|
142
|
+
{ name: 'H4', font: 'display', size: '22px', weight: 600, tracking: '-0.005em', leading: '1.35', sample: '__TODO: Card heading' },
|
|
143
|
+
{ name: 'Body LG', font: 'body', size: '18px', weight: 400, tracking: '0', leading: '1.7', sample: '__TODO: Intro paragraph' },
|
|
144
|
+
{ name: 'Body', font: 'body', size: '16px', weight: 400, tracking: '0', leading: '1.7', sample: '__TODO: Body copy' },
|
|
145
|
+
{ name: 'Body SM', font: 'body', size: '14px', weight: 400, tracking: '0', leading: '1.6', sample: '__TODO: Small text' },
|
|
146
|
+
{ name: 'Caption', font: 'body', size: '12px', weight: 500, tracking: '0.02em', leading: '1.5', sample: '__TODO: Caption text' },
|
|
147
|
+
{ name: 'Overline', font: 'body', size: '11px', weight: 700, tracking: '0.1em', leading: '1.4', sample: '__TODO: Overline text', uppercase: true }
|
|
148
|
+
],
|
|
149
|
+
voice: {
|
|
150
|
+
description: '__TODO: Describe the brand voice.',
|
|
151
|
+
do: ['__TODO: Example of good copy'],
|
|
152
|
+
dont: ['__TODO: Example of bad copy']
|
|
153
|
+
},
|
|
154
|
+
accessibility: [
|
|
155
|
+
{ fg: '#6366F1', bg: '#FFFFFF', fgName: 'Primary', bgName: 'White', ratio: '__TODO', rating: '__TODO', border: true, largeText: false },
|
|
156
|
+
{ fg: '#FFFFFF', bg: '#111827', fgName: 'White', bgName: 'Ink', ratio: '__TODO', rating: '__TODO', border: false, largeText: false }
|
|
157
|
+
],
|
|
158
|
+
cssVariables: [
|
|
159
|
+
{ section: 'Colors', vars: [
|
|
160
|
+
{ prop: '--color-primary', value: '#6366F1', comment: 'Primary brand color' },
|
|
161
|
+
{ prop: '--foreground', value: '#111827', comment: 'Primary text' }
|
|
162
|
+
]},
|
|
163
|
+
{ section: 'Typography', vars: [
|
|
164
|
+
{ prop: '--font-display', value: "'Inter', sans-serif", comment: 'Display font' },
|
|
165
|
+
{ prop: '--font-body', value: "'Inter', sans-serif", comment: 'Body font' }
|
|
166
|
+
]}
|
|
167
|
+
],
|
|
168
|
+
spacing: [
|
|
169
|
+
{ px: 4, token: 'space-1' }, { px: 8, token: 'space-2' },
|
|
170
|
+
{ px: 12, token: 'space-3' }, { px: 16, token: 'space-4' },
|
|
171
|
+
{ px: 24, token: 'space-6' }, { px: 32, token: 'space-8' },
|
|
172
|
+
{ px: 48, token: 'space-12' }, { px: 64, token: 'space-16' },
|
|
173
|
+
{ px: 96, token: 'space-24' }
|
|
174
|
+
],
|
|
175
|
+
components: {
|
|
176
|
+
buttons: [
|
|
177
|
+
{ variant: 'Primary', items: [
|
|
178
|
+
{ class: 'btn-primary', sizes: ['sm', 'md', 'lg'], labels: ['Small', '__TODO: Medium CTA', '__TODO: Large CTA'] }
|
|
179
|
+
]}
|
|
180
|
+
],
|
|
181
|
+
cards: [
|
|
182
|
+
{ title: '__TODO: Card Title', description: '__TODO: Card description text', tag: '__TODO' }
|
|
183
|
+
],
|
|
184
|
+
stats: [
|
|
185
|
+
{ value: '__TODO', label: '__TODO: Stat description' }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Deep merge: target values win over source for manually-curated fields.
|
|
193
|
+
* Source (extracted) overwrites target for extractable fields.
|
|
194
|
+
*/
|
|
195
|
+
function mergeConfigs(existing, extracted) {
|
|
196
|
+
var result = JSON.parse(JSON.stringify(existing));
|
|
197
|
+
|
|
198
|
+
// Extractable fields: overwrite from extracted data
|
|
199
|
+
// Only fields that come from external sources (Tailwind, CSS, file system)
|
|
200
|
+
// Derived fields (gradients, hierarchy, accessibility, cssVariables, typography)
|
|
201
|
+
// are NOT in this list — they only overwrite if existing is empty/scaffold
|
|
202
|
+
var extractable = ['colors', 'fonts', 'theme', 'spacing', 'logos', 'logoSizes'];
|
|
203
|
+
for (var i = 0; i < extractable.length; i++) {
|
|
204
|
+
var key = extractable[i];
|
|
205
|
+
if (extracted[key] !== undefined && extracted[key] !== null) {
|
|
206
|
+
// For arrays, only overwrite if extracted has actual content
|
|
207
|
+
if (Array.isArray(extracted[key]) && extracted[key].length === 0) continue;
|
|
208
|
+
result[key] = extracted[key];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Everything else: keep existing (manual/curated fields)
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
starterConfig: starterConfig,
|
|
218
|
+
mergeConfigs: mergeConfigs
|
|
219
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract CSS custom properties from CSS files in a project.
|
|
6
|
+
* Looks for :root blocks and @theme blocks.
|
|
7
|
+
*/
|
|
8
|
+
function extract(projectDir) {
|
|
9
|
+
var cssFiles = findCSSFiles(projectDir);
|
|
10
|
+
var allVars = {};
|
|
11
|
+
|
|
12
|
+
for (var i = 0; i < cssFiles.length; i++) {
|
|
13
|
+
var content = fs.readFileSync(cssFiles[i], 'utf8');
|
|
14
|
+
var vars = extractFromContent(content);
|
|
15
|
+
var keys = Object.keys(vars);
|
|
16
|
+
for (var j = 0; j < keys.length; j++) {
|
|
17
|
+
allVars[keys[j]] = vars[keys[j]];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Object.keys(allVars).length ? allVars : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findCSSFiles(dir) {
|
|
25
|
+
var results = [];
|
|
26
|
+
var searchDirs = [
|
|
27
|
+
path.join(dir, 'src'),
|
|
28
|
+
path.join(dir, 'app'),
|
|
29
|
+
path.join(dir, 'styles'),
|
|
30
|
+
dir
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
for (var i = 0; i < searchDirs.length; i++) {
|
|
34
|
+
if (!fs.existsSync(searchDirs[i])) continue;
|
|
35
|
+
scanDir(searchDirs[i], results, 0);
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function scanDir(dir, results, depth) {
|
|
41
|
+
if (depth > 3) return; // Don't go too deep
|
|
42
|
+
var entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
} catch (_) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (var i = 0; i < entries.length; i++) {
|
|
50
|
+
var entry = entries[i];
|
|
51
|
+
var fullPath = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
53
|
+
scanDir(fullPath, results, depth + 1);
|
|
54
|
+
} else if (entry.isFile() && entry.name.endsWith('.css')) {
|
|
55
|
+
results.push(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractFromContent(content) {
|
|
61
|
+
var vars = {};
|
|
62
|
+
|
|
63
|
+
// Match :root { ... } blocks
|
|
64
|
+
var rootRegex = /:root\s*\{([^}]+)\}/g;
|
|
65
|
+
var match;
|
|
66
|
+
while ((match = rootRegex.exec(content)) !== null) {
|
|
67
|
+
parseVarBlock(match[1], vars);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Match @theme { ... } blocks (Tailwind v4)
|
|
71
|
+
var themeRegex = /@theme\s*\{([^}]+)\}/g;
|
|
72
|
+
while ((match = themeRegex.exec(content)) !== null) {
|
|
73
|
+
parseVarBlock(match[1], vars);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return vars;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseVarBlock(block, vars) {
|
|
80
|
+
var lineRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
81
|
+
var match;
|
|
82
|
+
while ((match = lineRegex.exec(block)) !== null) {
|
|
83
|
+
vars[match[1].trim()] = match[2].trim();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map extracted CSS variables to brandkit theme format.
|
|
89
|
+
* Recognizes common shadcn/ui and Tailwind variable names.
|
|
90
|
+
*/
|
|
91
|
+
function mapToTheme(cssVars) {
|
|
92
|
+
if (!cssVars) return null;
|
|
93
|
+
|
|
94
|
+
var mapping = {
|
|
95
|
+
'--background': '--cloud',
|
|
96
|
+
'--foreground': '--ink',
|
|
97
|
+
'--primary': '--purple',
|
|
98
|
+
'--secondary': '--mist',
|
|
99
|
+
'--muted': '--mist',
|
|
100
|
+
'--muted-foreground': '--slate',
|
|
101
|
+
'--accent': '--coral',
|
|
102
|
+
'--destructive': '--error',
|
|
103
|
+
'--border': '--mist'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
var theme = {};
|
|
107
|
+
var keys = Object.keys(cssVars);
|
|
108
|
+
for (var i = 0; i < keys.length; i++) {
|
|
109
|
+
var key = keys[i];
|
|
110
|
+
var mappedKey = mapping[key] || key;
|
|
111
|
+
theme[mappedKey] = cssVars[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return theme;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { extract: extract, mapToTheme: mapToTheme };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find logo/brand asset files in a project.
|
|
6
|
+
* Looks in common asset directories for files matching brand-related patterns.
|
|
7
|
+
*/
|
|
8
|
+
function extract(projectDir) {
|
|
9
|
+
var searchDirs = [
|
|
10
|
+
path.join(projectDir, 'public'),
|
|
11
|
+
path.join(projectDir, 'static'),
|
|
12
|
+
path.join(projectDir, 'src', 'assets'),
|
|
13
|
+
path.join(projectDir, 'assets'),
|
|
14
|
+
path.join(projectDir, 'images'),
|
|
15
|
+
path.join(projectDir, 'img')
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
var patterns = [/logo/i, /brand/i, /mark/i, /icon/i, /wordmark/i, /lockup/i];
|
|
19
|
+
var extensions = ['.svg', '.png', '.jpg', '.jpeg', '.webp'];
|
|
20
|
+
|
|
21
|
+
var found = [];
|
|
22
|
+
|
|
23
|
+
for (var i = 0; i < searchDirs.length; i++) {
|
|
24
|
+
if (!fs.existsSync(searchDirs[i])) continue;
|
|
25
|
+
scanForLogos(searchDirs[i], patterns, extensions, found, projectDir, 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!found.length) return null;
|
|
29
|
+
|
|
30
|
+
// Convert to brandkit logo format
|
|
31
|
+
return found.map(function (file) {
|
|
32
|
+
var ext = path.extname(file.path).toLowerCase();
|
|
33
|
+
var basename = path.basename(file.path, ext);
|
|
34
|
+
var relativePath = file.relativePath;
|
|
35
|
+
|
|
36
|
+
// Determine background hint from filename
|
|
37
|
+
var background = 'light';
|
|
38
|
+
if (/white|light|reversed/i.test(basename)) background = 'dark';
|
|
39
|
+
else if (/dark|black/i.test(basename)) background = 'light';
|
|
40
|
+
|
|
41
|
+
var variants = {};
|
|
42
|
+
var format = ext === '.svg' ? 'svg' : (ext === '.png' ? 'png' : 'jpg');
|
|
43
|
+
variants[format] = relativePath;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
name: prettifyName(basename),
|
|
47
|
+
description: '',
|
|
48
|
+
variants: variants,
|
|
49
|
+
background: background
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scanForLogos(dir, patterns, extensions, results, projectDir, depth) {
|
|
55
|
+
if (depth > 3) return;
|
|
56
|
+
var entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
} catch (_) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (var i = 0; i < entries.length; i++) {
|
|
64
|
+
var entry = entries[i];
|
|
65
|
+
var fullPath = path.join(dir, entry.name);
|
|
66
|
+
|
|
67
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
68
|
+
scanForLogos(fullPath, patterns, extensions, results, projectDir, depth + 1);
|
|
69
|
+
} else if (entry.isFile()) {
|
|
70
|
+
var ext = path.extname(entry.name).toLowerCase();
|
|
71
|
+
if (extensions.indexOf(ext) === -1) continue;
|
|
72
|
+
|
|
73
|
+
var matchesPattern = false;
|
|
74
|
+
for (var j = 0; j < patterns.length; j++) {
|
|
75
|
+
if (patterns[j].test(entry.name)) {
|
|
76
|
+
matchesPattern = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (matchesPattern) {
|
|
82
|
+
results.push({
|
|
83
|
+
path: fullPath,
|
|
84
|
+
relativePath: path.relative(projectDir, fullPath)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function prettifyName(basename) {
|
|
92
|
+
return basename
|
|
93
|
+
.replace(/[-_]+/g, ' ')
|
|
94
|
+
.replace(/\b\w/g, function (c) { return c.toUpperCase(); })
|
|
95
|
+
.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { extract: extract };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract colors, fonts, and spacing from a Tailwind config file.
|
|
6
|
+
* Supports tailwind.config.js and tailwind.config.ts (via regex parsing).
|
|
7
|
+
*/
|
|
8
|
+
function extract(projectDir) {
|
|
9
|
+
var result = { colors: null, fonts: null, spacing: null };
|
|
10
|
+
|
|
11
|
+
// Find tailwind config
|
|
12
|
+
var jsPath = path.join(projectDir, 'tailwind.config.js');
|
|
13
|
+
var tsPath = path.join(projectDir, 'tailwind.config.ts');
|
|
14
|
+
var mjsPath = path.join(projectDir, 'tailwind.config.mjs');
|
|
15
|
+
|
|
16
|
+
var configPath = null;
|
|
17
|
+
if (fs.existsSync(jsPath)) configPath = jsPath;
|
|
18
|
+
else if (fs.existsSync(mjsPath)) configPath = mjsPath;
|
|
19
|
+
else if (fs.existsSync(tsPath)) configPath = tsPath;
|
|
20
|
+
|
|
21
|
+
if (!configPath) return result;
|
|
22
|
+
|
|
23
|
+
var content = fs.readFileSync(configPath, 'utf8');
|
|
24
|
+
|
|
25
|
+
// Try to require JS config directly
|
|
26
|
+
if (configPath.endsWith('.js')) {
|
|
27
|
+
try {
|
|
28
|
+
var config = require(configPath);
|
|
29
|
+
if (config.theme) {
|
|
30
|
+
result.colors = extractColorsFromTheme(config.theme);
|
|
31
|
+
result.fonts = extractFontsFromTheme(config.theme);
|
|
32
|
+
result.spacing = extractSpacingFromTheme(config.theme);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
} catch (_) {
|
|
36
|
+
// Fall through to regex parsing
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Regex parsing for TS or failed JS require
|
|
41
|
+
result.colors = extractColorsFromText(content);
|
|
42
|
+
result.fonts = extractFontsFromText(content);
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractColorsFromTheme(theme) {
|
|
48
|
+
var colors = theme.extend && theme.extend.colors ? theme.extend.colors : theme.colors;
|
|
49
|
+
if (!colors || typeof colors !== 'object') return null;
|
|
50
|
+
|
|
51
|
+
var result = [];
|
|
52
|
+
flattenColors(colors, '', result);
|
|
53
|
+
return result.length ? result : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function flattenColors(obj, prefix, result) {
|
|
57
|
+
var keys = Object.keys(obj);
|
|
58
|
+
for (var i = 0; i < keys.length; i++) {
|
|
59
|
+
var key = keys[i];
|
|
60
|
+
var value = obj[key];
|
|
61
|
+
var name = prefix ? prefix + '-' + key : key;
|
|
62
|
+
|
|
63
|
+
if (typeof value === 'string' && value.match(/^#|^rgb|^hsl|^oklch/)) {
|
|
64
|
+
result.push({ name: name, hex: value, role: '' });
|
|
65
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
66
|
+
if (value.DEFAULT) {
|
|
67
|
+
result.push({ name: name, hex: value.DEFAULT, role: '' });
|
|
68
|
+
}
|
|
69
|
+
flattenColors(value, name, result);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractFontsFromTheme(theme) {
|
|
75
|
+
var fontFamily = theme.extend && theme.extend.fontFamily ? theme.extend.fontFamily : theme.fontFamily;
|
|
76
|
+
if (!fontFamily || typeof fontFamily !== 'object') return null;
|
|
77
|
+
|
|
78
|
+
var result = {};
|
|
79
|
+
var keys = Object.keys(fontFamily);
|
|
80
|
+
for (var i = 0; i < keys.length; i++) {
|
|
81
|
+
var key = keys[i];
|
|
82
|
+
var value = fontFamily[key];
|
|
83
|
+
var family = Array.isArray(value) ? value[0] : value;
|
|
84
|
+
if (typeof family === 'string') {
|
|
85
|
+
// Map common keys to display/body
|
|
86
|
+
if (key === 'sans' || key === 'body' || key === 'text') {
|
|
87
|
+
result.body = { family: family.replace(/['"]/g, '') };
|
|
88
|
+
} else if (key === 'display' || key === 'heading' || key === 'serif') {
|
|
89
|
+
result.display = { family: family.replace(/['"]/g, '') };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Object.keys(result).length ? result : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractSpacingFromTheme(theme) {
|
|
97
|
+
var spacing = theme.extend && theme.extend.spacing ? theme.extend.spacing : theme.spacing;
|
|
98
|
+
if (!spacing || typeof spacing !== 'object') return null;
|
|
99
|
+
|
|
100
|
+
var result = [];
|
|
101
|
+
var keys = Object.keys(spacing);
|
|
102
|
+
for (var i = 0; i < keys.length; i++) {
|
|
103
|
+
var key = keys[i];
|
|
104
|
+
var value = spacing[key];
|
|
105
|
+
if (typeof value === 'string' && value.match(/^\d/)) {
|
|
106
|
+
var px = parseFloat(value);
|
|
107
|
+
if (value.endsWith('rem')) px = px * 16;
|
|
108
|
+
if (!isNaN(px)) {
|
|
109
|
+
result.push({ px: Math.round(px), token: 'space-' + key });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result.length ? result.sort(function (a, b) { return a.px - b.px; }) : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractColorsFromText(content) {
|
|
117
|
+
var colorMatches = [];
|
|
118
|
+
var regex = /['"]?([\w-]+)['"]?\s*:\s*['"]?(#[0-9a-fA-F]{3,8})['"]?/g;
|
|
119
|
+
var match;
|
|
120
|
+
while ((match = regex.exec(content)) !== null) {
|
|
121
|
+
colorMatches.push({ name: match[1], hex: match[2], role: '' });
|
|
122
|
+
}
|
|
123
|
+
return colorMatches.length ? colorMatches : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractFontsFromText(content) {
|
|
127
|
+
var fontRegex = /fontFamily\s*:\s*\{([^}]+)\}/;
|
|
128
|
+
var fontMatch = content.match(fontRegex);
|
|
129
|
+
if (!fontMatch) return null;
|
|
130
|
+
|
|
131
|
+
var result = {};
|
|
132
|
+
var entryRegex = /['"]?(sans|body|display|heading|serif)['"]?\s*:\s*\[?\s*['"]([^'"]+)['"]/g;
|
|
133
|
+
var match;
|
|
134
|
+
while ((match = entryRegex.exec(fontMatch[1])) !== null) {
|
|
135
|
+
var key = match[1];
|
|
136
|
+
var family = match[2];
|
|
137
|
+
if (key === 'sans' || key === 'body') {
|
|
138
|
+
result.body = { family: family };
|
|
139
|
+
} else {
|
|
140
|
+
result.display = { family: family };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Object.keys(result).length ? result : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { extract: extract };
|