@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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Color conversion and contrast computation helpers for brandkit generate.
3
+ * Pure math — no dependencies.
4
+ */
5
+
6
+ /**
7
+ * Parse hex color to {r, g, b} (0–255).
8
+ */
9
+ function hexToRgb(hex) {
10
+ hex = hex.replace(/^#/, '');
11
+ if (hex.length === 3) {
12
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
13
+ }
14
+ var n = parseInt(hex, 16);
15
+ return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
16
+ }
17
+
18
+ /**
19
+ * Convert hex to "r, g, b" string for CSS rgba(var(--x-rgb), alpha) usage.
20
+ */
21
+ function hexToRgbString(hex) {
22
+ var c = hexToRgb(hex);
23
+ return c.r + ', ' + c.g + ', ' + c.b;
24
+ }
25
+
26
+ /**
27
+ * sRGB channel (0–255) → linear light (0–1).
28
+ */
29
+ function srgbToLinear(c) {
30
+ c = c / 255;
31
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
32
+ }
33
+
34
+ /**
35
+ * Linear light (0–1) → sRGB (0–255).
36
+ */
37
+ function linearToSrgb(c) {
38
+ c = Math.max(0, Math.min(1, c));
39
+ return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
40
+ }
41
+
42
+ /**
43
+ * Convert hex to approximate oklch string.
44
+ * hex → sRGB → linear RGB → OKLab → OKLCH
45
+ */
46
+ function hexToOklch(hex) {
47
+ var rgb = hexToRgb(hex);
48
+ var lr = srgbToLinear(rgb.r);
49
+ var lg = srgbToLinear(rgb.g);
50
+ var lb = srgbToLinear(rgb.b);
51
+
52
+ // Linear RGB → LMS (using OKLab matrix)
53
+ var l_ = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
54
+ var m_ = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
55
+ var s_ = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
56
+
57
+ // Cube root
58
+ var l = Math.cbrt(l_);
59
+ var m = Math.cbrt(m_);
60
+ var s = Math.cbrt(s_);
61
+
62
+ // LMS → OKLab
63
+ var L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
64
+ var a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
65
+ var b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
66
+
67
+ // OKLab → OKLCH
68
+ var C = Math.sqrt(a * a + b * b);
69
+ var H = Math.atan2(b, a) * 180 / Math.PI;
70
+ if (H < 0) H += 360;
71
+
72
+ return 'oklch(' + L.toFixed(2) + ' ' + C.toFixed(2) + ' ' + Math.round(H) + ')';
73
+ }
74
+
75
+ /**
76
+ * Check if a hex color is "light" (for border display on swatches).
77
+ */
78
+ function isLightColor(hex) {
79
+ return relativeLuminance(hex) > 0.35;
80
+ }
81
+
82
+ /**
83
+ * Compute relative luminance of a hex color (WCAG 2.1).
84
+ */
85
+ function relativeLuminance(hex) {
86
+ var rgb = hexToRgb(hex);
87
+ var lr = srgbToLinear(rgb.r);
88
+ var lg = srgbToLinear(rgb.g);
89
+ var lb = srgbToLinear(rgb.b);
90
+ return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
91
+ }
92
+
93
+ /**
94
+ * Compute WCAG contrast ratio between two hex colors.
95
+ * Returns a string like "7.2:1".
96
+ */
97
+ function contrastRatio(hex1, hex2) {
98
+ var l1 = relativeLuminance(hex1);
99
+ var l2 = relativeLuminance(hex2);
100
+ var lighter = Math.max(l1, l2);
101
+ var darker = Math.min(l1, l2);
102
+ var ratio = (lighter + 0.05) / (darker + 0.05);
103
+ return ratio.toFixed(1) + ':1';
104
+ }
105
+
106
+ /**
107
+ * Rate a contrast ratio string per WCAG.
108
+ * Returns 'AAA', 'AA', 'AA Large', or 'Fail'.
109
+ */
110
+ function rateContrast(ratioStr) {
111
+ var ratio = parseFloat(ratioStr);
112
+ if (ratio >= 7) return 'AAA';
113
+ if (ratio >= 4.5) return 'AA';
114
+ if (ratio >= 3) return 'AA Large';
115
+ return 'Fail';
116
+ }
117
+
118
+ /**
119
+ * Generate accessibility pairs from a set of colors.
120
+ * Tests key color combinations against white, black, and each other.
121
+ */
122
+ function generateA11yPairs(colors) {
123
+ var pairs = [];
124
+ var white = '#FFFFFF';
125
+ var testColors = [];
126
+ var seenHexes = {};
127
+
128
+ // Collect unique colors
129
+ for (var i = 0; i < colors.length; i++) {
130
+ var c = colors[i];
131
+ if (c.hex && !seenHexes[c.hex]) {
132
+ seenHexes[c.hex] = true;
133
+ testColors.push({ hex: c.hex, name: c.name || c.hex });
134
+ }
135
+ }
136
+
137
+ // Test each color against white
138
+ for (var j = 0; j < testColors.length; j++) {
139
+ var tc = testColors[j];
140
+ if (tc.hex === white) continue;
141
+ var ratio = contrastRatio(tc.hex, white);
142
+ var rating = rateContrast(ratio);
143
+ if (rating !== 'Fail') {
144
+ pairs.push({
145
+ fg: tc.hex,
146
+ bg: white,
147
+ fgName: tc.name,
148
+ bgName: 'White',
149
+ ratio: ratio,
150
+ rating: rating,
151
+ border: true,
152
+ largeText: rating === 'AA Large'
153
+ });
154
+ }
155
+ // Reverse: white on color
156
+ var revRatio = contrastRatio(white, tc.hex);
157
+ var revRating = rateContrast(revRatio);
158
+ if (revRating !== 'Fail' && !isLightColor(tc.hex)) {
159
+ pairs.push({
160
+ fg: white,
161
+ bg: tc.hex,
162
+ fgName: 'White',
163
+ bgName: tc.name,
164
+ ratio: revRatio,
165
+ rating: revRating,
166
+ border: false,
167
+ largeText: false
168
+ });
169
+ }
170
+ }
171
+
172
+ return pairs;
173
+ }
174
+
175
+ /**
176
+ * Build cssVariables sections from a theme object.
177
+ */
178
+ function buildCssVariablesFromTheme(theme) {
179
+ if (!theme) return [];
180
+ var colorVars = [];
181
+ var gradientVars = [];
182
+ var typographyVars = [];
183
+ var otherVars = [];
184
+
185
+ var keys = Object.keys(theme);
186
+ for (var i = 0; i < keys.length; i++) {
187
+ var prop = keys[i];
188
+ var value = theme[prop];
189
+ var entry = { prop: prop, value: value, comment: '' };
190
+
191
+ if (prop.indexOf('gradient') !== -1) {
192
+ gradientVars.push(entry);
193
+ } else if (prop.indexOf('font') !== -1) {
194
+ typographyVars.push(entry);
195
+ } else if (prop.indexOf('rgb') !== -1) {
196
+ // skip rgb variants — they're implementation details
197
+ continue;
198
+ } else {
199
+ colorVars.push(entry);
200
+ }
201
+ }
202
+
203
+ var sections = [];
204
+ if (colorVars.length) sections.push({ section: 'Colors', vars: colorVars });
205
+ if (gradientVars.length) sections.push({ section: 'Gradients', vars: gradientVars });
206
+ if (typographyVars.length) sections.push({ section: 'Typography', vars: typographyVars });
207
+ if (otherVars.length) sections.push({ section: 'Other', vars: otherVars });
208
+
209
+ return sections;
210
+ }
211
+
212
+ /**
213
+ * Build hierarchy entries from theme colors.
214
+ */
215
+ function buildHierarchyFromTheme(theme) {
216
+ if (!theme) return [];
217
+ // Resolve each level: try preferred key, then fallback, track which key was actually used
218
+ function resolve(preferred, fallbacks, defaultVal) {
219
+ if (theme[preferred]) return { key: preferred, value: theme[preferred] };
220
+ for (var i = 0; i < fallbacks.length; i++) {
221
+ if (theme[fallbacks[i]]) return { key: fallbacks[i], value: theme[fallbacks[i]] };
222
+ }
223
+ return { key: preferred, value: defaultVal };
224
+ }
225
+
226
+ var fg = resolve('--ink', ['--foreground'], '#111827');
227
+ var secondary = resolve('--graphite', ['--slate', '--muted-foreground'], '#374151');
228
+ var tertiary = resolve('--slate', ['--haze', '--muted-foreground'], '#6B7280');
229
+ var accent = resolve('--purple', ['--coral', '--primary'], '#6366F1');
230
+
231
+ function keyToName(key) {
232
+ return key.replace(/^--/, '').replace(/-/g, ' ').replace(/\b\w/g, function (c) { return c.toUpperCase(); });
233
+ }
234
+
235
+ return [
236
+ { class: 'h-primary', label: 'Primary', colorVar: fg.key, colorName: keyToName(fg.key), hex: fg.value, description: 'Primary text — body copy, headings, and content where readability is critical.' },
237
+ { class: 'h-secondary', label: 'Secondary', colorVar: secondary.key, colorName: keyToName(secondary.key), hex: secondary.value, description: 'Secondary text — descriptions, supporting information, and labels.' },
238
+ { class: 'h-tertiary', label: 'Tertiary', colorVar: tertiary.key, colorName: keyToName(tertiary.key), hex: tertiary.value, description: 'Tertiary text — captions, timestamps, footnotes, and metadata.' },
239
+ { class: 'h-accent', label: 'Accent', colorVar: accent.key, colorName: keyToName(accent.key), hex: accent.value, description: 'Accent text — links, interactive labels, and calls to action.' }
240
+ ];
241
+ }
242
+
243
+ /**
244
+ * Parse a CSS gradient string into stops array.
245
+ * E.g. "linear-gradient(135deg, #4A1D75 0%, #6B2FA0 30%)" → [{color, position, name}]
246
+ */
247
+ function parseGradientStops(css) {
248
+ if (!css) return [];
249
+ var stops = [];
250
+ var regex = /(#[0-9a-fA-F]{3,8})\s+(\d+%)/g;
251
+ var match;
252
+ while ((match = regex.exec(css)) !== null) {
253
+ stops.push({ color: match[1], position: match[2], name: match[1] });
254
+ }
255
+ return stops;
256
+ }
257
+
258
+ /**
259
+ * Build gradient entries from theme.
260
+ */
261
+ function buildGradientsFromTheme(theme) {
262
+ if (!theme) return [];
263
+ var gradients = [];
264
+
265
+ var brand = theme['--gradient-brand'];
266
+ if (brand) {
267
+ gradients.push({
268
+ name: 'Brand',
269
+ css: brand,
270
+ description: 'Primary gradient — hero backgrounds, feature sections',
271
+ stops: parseGradientStops(brand)
272
+ });
273
+ }
274
+
275
+ var subtle = theme['--gradient-brand-subtle'];
276
+ if (subtle) {
277
+ gradients.push({
278
+ name: 'Subtle',
279
+ css: subtle,
280
+ description: 'Subtle tint — card backgrounds, hover states'
281
+ });
282
+ }
283
+
284
+ return gradients;
285
+ }
286
+
287
+ /**
288
+ * Scaffold a default type scale.
289
+ */
290
+ function scaffoldTypography() {
291
+ return [
292
+ { name: 'Display XL', font: 'display', size: '72px', weight: 700, tracking: '-0.03em', leading: '1.05', sample: '__TODO: Hero headline' },
293
+ { name: 'Display', font: 'display', size: '56px', weight: 700, tracking: '-0.025em', leading: '1.1', sample: '__TODO: Section headline' },
294
+ { name: 'H1', font: 'display', size: '44px', weight: 700, tracking: '-0.02em', leading: '1.15', sample: '__TODO: Page heading' },
295
+ { name: 'H2', font: 'display', size: '36px', weight: 700, tracking: '-0.015em', leading: '1.2', sample: '__TODO: Section heading' },
296
+ { name: 'H3', font: 'display', size: '28px', weight: 600, tracking: '-0.01em', leading: '1.3', sample: '__TODO: Subsection heading' },
297
+ { name: 'H4', font: 'display', size: '22px', weight: 600, tracking: '-0.005em', leading: '1.35', sample: '__TODO: Card heading' },
298
+ { name: 'Body LG', font: 'body', size: '18px', weight: 400, tracking: '0', leading: '1.7', sample: '__TODO: Intro paragraph text' },
299
+ { name: 'Body', font: 'body', size: '16px', weight: 400, tracking: '0', leading: '1.7', sample: '__TODO: Body copy text' },
300
+ { name: 'Body SM', font: 'body', size: '14px', weight: 400, tracking: '0', leading: '1.6', sample: '__TODO: Small body text' },
301
+ { name: 'Caption', font: 'body', size: '12px', weight: 500, tracking: '0.02em', leading: '1.5', sample: '__TODO: Caption text' },
302
+ { name: 'Overline', font: 'body', size: '11px', weight: 700, tracking: '0.1em', leading: '1.4', sample: '__TODO: Overline text', uppercase: true }
303
+ ];
304
+ }
305
+
306
+ module.exports = {
307
+ hexToRgb: hexToRgb,
308
+ hexToRgbString: hexToRgbString,
309
+ hexToOklch: hexToOklch,
310
+ isLightColor: isLightColor,
311
+ contrastRatio: contrastRatio,
312
+ rateContrast: rateContrast,
313
+ relativeLuminance: relativeLuminance,
314
+ generateA11yPairs: generateA11yPairs,
315
+ buildCssVariablesFromTheme: buildCssVariablesFromTheme,
316
+ buildHierarchyFromTheme: buildHierarchyFromTheme,
317
+ parseGradientStops: parseGradientStops,
318
+ buildGradientsFromTheme: buildGradientsFromTheme,
319
+ scaffoldTypography: scaffoldTypography
320
+ };
package/lib/resolve.js ADDED
@@ -0,0 +1,11 @@
1
+ var path = require('path');
2
+
3
+ /**
4
+ * Returns the absolute path to the dist/ directory containing
5
+ * the universal static assets (engine.js, styles.css, index.html).
6
+ */
7
+ function getDistPath() {
8
+ return path.resolve(__dirname, '..', 'dist');
9
+ }
10
+
11
+ module.exports = { getDistPath: getDistPath };
@@ -0,0 +1,84 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+
4
+ /**
5
+ * Generate a :root CSS block from config.theme + config.fonts.
6
+ */
7
+ function generateRootCSS(config) {
8
+ var lines = [':root {'];
9
+ if (config.theme) {
10
+ var keys = Object.keys(config.theme);
11
+ for (var i = 0; i < keys.length; i++) {
12
+ lines.push(' ' + keys[i] + ': ' + config.theme[keys[i]] + ';');
13
+ }
14
+ }
15
+ if (config.fonts) {
16
+ if (config.fonts.display) {
17
+ lines.push(" --font-display: '" + config.fonts.display.family + "', sans-serif;");
18
+ }
19
+ if (config.fonts.body) {
20
+ lines.push(" --font-body: '" + config.fonts.body.family + "', sans-serif;");
21
+ }
22
+ }
23
+ lines.push('}');
24
+ return lines.join('\n');
25
+ }
26
+
27
+ /**
28
+ * Generate a Google Fonts <link> tag from config.fonts.
29
+ */
30
+ function generateFontsLink(config) {
31
+ if (!config.fonts) return '';
32
+ var families = [];
33
+ if (config.fonts.display && config.fonts.display.googleImport) {
34
+ families.push(config.fonts.display.googleImport);
35
+ }
36
+ if (config.fonts.body && config.fonts.body.googleImport) {
37
+ families.push(config.fonts.body.googleImport);
38
+ }
39
+ if (!families.length) return '';
40
+ return '<link href="https://fonts.googleapis.com/css2?family=' +
41
+ families.join('&family=') + '&display=swap" rel="stylesheet">';
42
+ }
43
+
44
+ /**
45
+ * Build production-ready static files from dist/ templates + config.
46
+ * Writes index.html and styles.css into the target directory.
47
+ */
48
+ function build(distDir, targetDir, config) {
49
+ // Build styles.css: prepend :root block
50
+ var cssTemplate = fs.readFileSync(path.join(distDir, 'styles.css'), 'utf8');
51
+ var rootCSS = generateRootCSS(config);
52
+ var builtCSS = '/* Generated by brandkit build */\n' + rootCSS + '\n\n' + cssTemplate;
53
+ fs.writeFileSync(path.join(targetDir, 'styles.css'), builtCSS);
54
+
55
+ // Build index.html: inject fonts link + title
56
+ var htmlTemplate = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8');
57
+ var fontsLink = generateFontsLink(config);
58
+ var brandName = (config.brand && config.brand.displayName) || 'Brand';
59
+
60
+ // Inject fonts link before </head>
61
+ if (fontsLink) {
62
+ htmlTemplate = htmlTemplate.replace('</head>', fontsLink + '\n</head>');
63
+ }
64
+
65
+ // Set title
66
+ htmlTemplate = htmlTemplate.replace(
67
+ '<title>Brand Guide</title>',
68
+ '<title>' + brandName + ' \u2014 Brand Guide</title>'
69
+ );
70
+
71
+ fs.writeFileSync(path.join(targetDir, 'index.html'), htmlTemplate);
72
+
73
+ // Copy engine.js as-is
74
+ fs.copyFileSync(
75
+ path.join(distDir, 'engine.js'),
76
+ path.join(targetDir, 'engine.js')
77
+ );
78
+ }
79
+
80
+ module.exports = {
81
+ generateRootCSS: generateRootCSS,
82
+ generateFontsLink: generateFontsLink,
83
+ build: build
84
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@stacklist-app/brandkit",
3
+ "version": "1.1.1",
4
+ "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
+ "main": "lib/resolve.js",
6
+ "bin": {
7
+ "brandkit": "bin/brandkit.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "cli/",
12
+ "lib/",
13
+ "dist/",
14
+ "integrations/"
15
+ ],
16
+ "scripts": {
17
+ "dev": "node bin/brandkit.js dev example",
18
+ "build": "node bin/brandkit.js build example",
19
+ "example": "node bin/brandkit.js dev example"
20
+ },
21
+ "keywords": [
22
+ "brand",
23
+ "style-guide",
24
+ "design-system",
25
+ "brandkit",
26
+ "design-tokens"
27
+ ],
28
+ "author": "The Stack Lab",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/The-Stack-Lab/brandkit.git"
36
+ }
37
+ }