@stacklist-app/brandkit 1.1.1 → 1.2.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 +37 -11
- package/bin/brandkit.js +4 -0
- package/cli/build.js +7 -2
- package/cli/export.js +64 -0
- package/cli/generate.js +35 -1
- package/cli/init.js +16 -0
- package/config.schema.json +253 -0
- package/dist/engine.js +49 -13
- package/dist/logos/brandkit-mark-white.svg +8 -0
- package/dist/logos/brandkit-mark.svg +14 -0
- package/dist/logos/brandkit-wordmark-dark.svg +15 -0
- package/dist/logos/brandkit-wordmark-light.svg +9 -0
- package/dist/styles.css +113 -30
- package/integrations/vite.js +20 -1
- package/lib/config-schema.js +124 -78
- package/lib/export.js +356 -0
- package/lib/extract-css.js +1 -1
- package/lib/generate-helpers.js +1 -1
- package/lib/template.js +26 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,9 +7,7 @@ Swap the config and assets to generate a brand guide for any project. An optiona
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install
|
|
11
|
-
# or pin to a tag
|
|
12
|
-
npm install github:The-Stack-Lab/brandkit#v1.1.1
|
|
10
|
+
npm install @stacklist-app/brandkit
|
|
13
11
|
```
|
|
14
12
|
|
|
15
13
|
Requires Node.js 18+.
|
|
@@ -23,7 +21,7 @@ npx brandkit dev brand # preview at http://localhost:4800 (live reload)
|
|
|
23
21
|
npx brandkit build brand # bake into static files for production
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
The `brand` directory name is your choice — pass any path.
|
|
24
|
+
`init` scaffolds a complete, ready-to-run demo brand (the self-titled "Brandkit" guide), so `dev` shows a polished guide immediately — then you swap in your own colors, fonts, copy, and logos. The `brand` directory name is your choice — pass any path.
|
|
27
25
|
|
|
28
26
|
## CLI
|
|
29
27
|
|
|
@@ -32,7 +30,20 @@ The `brand` directory name is your choice — pass any path.
|
|
|
32
30
|
| `brandkit init [dir]` | Scaffold a new brand guide with a starter `config.json` |
|
|
33
31
|
| `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
32
|
| `brandkit dev [dir]` | Local dev server on `:4800` with SSE live reload |
|
|
35
|
-
| `brandkit build [dir]` | Produce static `index.html` + `styles.css` + `engine.js`
|
|
33
|
+
| `brandkit build [dir]` | Produce static `index.html` + `styles.css` + `engine.js` (theme baked in) **plus** the agent-native exports below |
|
|
34
|
+
| `brandkit export [dir]` | Emit just the agent-native brand data (`--format json\|dtcg\|md\|all`, `--out <dir>`) |
|
|
35
|
+
|
|
36
|
+
## Agent-readable brand data
|
|
37
|
+
|
|
38
|
+
A brandkit guide isn't just a page for people — `build` (and the standalone `export` command) emit machine-native views of the brand so an AI agent or design-token tool can consume it without scraping HTML:
|
|
39
|
+
|
|
40
|
+
- **`brand.json`** — normalized, semantic brand: colors with **roles + contrast pairings**, the accent fill/text split, voice do/don'ts, typography, logos with usage, spacing. The agent-first view.
|
|
41
|
+
- **`tokens.json`** — the same colors, fonts, and spacing in **W3C Design Tokens (DTCG)** format (`{ "$type", "$value" }`), readable by Style Dictionary, Tokens Studio, and Figma.
|
|
42
|
+
- **`brand.md`** — an LLM brief ("how to write/design on-brand") you can drop straight into an agent's context.
|
|
43
|
+
|
|
44
|
+
`build` also makes the data **discoverable from the deployed page**: it injects `<link rel="alternate" type="application/json" href="brand.json">` and embeds the brand inline as `<script type="application/json" id="brandkit-brand">`, so an agent that fetches the URL gets structured brand data with zero extra requests.
|
|
45
|
+
|
|
46
|
+
A JSON Schema for `config.json` ships at [`config.schema.json`](config.schema.json) — point your editor's `$schema` at it for validation and autocomplete.
|
|
36
47
|
|
|
37
48
|
## Framework integrations
|
|
38
49
|
|
|
@@ -64,7 +75,7 @@ export default {
|
|
|
64
75
|
|
|
65
76
|
`config.json` is the single source of truth. Top-level keys:
|
|
66
77
|
|
|
67
|
-
- `brand` — name, tagline, description, version
|
|
78
|
+
- `brand` — name, tagline, description, version, date; optional `guideLabel` (renames the "Web Style Guide" header/footer label), `headerLogo` and `sidebarLogo` (logo image paths that replace the text wordmark in the header and left menu)
|
|
68
79
|
- `fonts` — display + body with Google Fonts import
|
|
69
80
|
- `theme` — CSS variable map (colors, gradients, font vars)
|
|
70
81
|
- `nav` — sidebar structure
|
|
@@ -79,14 +90,29 @@ export default {
|
|
|
79
90
|
- `accessibility` — contrast ratio grid
|
|
80
91
|
- `cssVariables` — variable reference
|
|
81
92
|
|
|
82
|
-
See `example/config.json` for
|
|
93
|
+
See `example/config.json` for the complete default demo brand — the same config `init` scaffolds.
|
|
94
|
+
|
|
95
|
+
## Theming
|
|
96
|
+
|
|
97
|
+
`dist/styles.css` ships a baseline `:root` block with sensible defaults for **every** token it consumes, so a minimal or partial config never renders broken. Your config only needs to *override* what it wants to change.
|
|
98
|
+
|
|
99
|
+
The accent uses a fill/text split (the same convention as Material and shadcn/ui):
|
|
100
|
+
|
|
101
|
+
- `--accent` — the fill color (buttons, active states)
|
|
102
|
+
- `--accent-foreground` — text/icons **on** the fill (must meet contrast against `--accent`)
|
|
103
|
+
- `--accent-text` — the accent used **as** text or links on a light surface
|
|
104
|
+
|
|
105
|
+
This lets a low-contrast fill (say a bright orange) stay accessible: set `--accent-foreground: #000000` and `--accent-text` to a darker shade. Pre-1.1.2 configs that set `--purple` / `--purple-rgb` still work — those feed `--accent`.
|
|
106
|
+
|
|
107
|
+
By default the **primary button** and the **header** carry the accent. Brands that separate a brand role from the accent — e.g. a black primary CTA and a black hero, with orange kept as the accent — override:
|
|
83
108
|
|
|
84
|
-
|
|
109
|
+
- `--primary` / `--primary-foreground` — primary-button fill and the text on it (default to `--accent` / `--accent-foreground`)
|
|
110
|
+
- `--header-bg` — the header/hero background behind the wordmark or logo (defaults to `--gradient-brand`)
|
|
85
111
|
|
|
86
|
-
|
|
112
|
+
The fallbacks mean single-accent brands need set none of these.
|
|
87
113
|
|
|
88
|
-
- **Dev**: the engine reads `config.theme` and injects a `<style data-brandkit-theme>`
|
|
89
|
-
- **Build**: `brandkit build`
|
|
114
|
+
- **Dev**: the engine reads `config.theme` and injects a `<style data-brandkit-theme>` block at runtime, which cascades over the defaults.
|
|
115
|
+
- **Build**: `brandkit build` appends the generated `:root` after the stylesheet's defaults and injects the Google Fonts `<link>` and page title into `index.html`.
|
|
90
116
|
|
|
91
117
|
Swap the config, and every color, gradient, and font token updates everywhere.
|
|
92
118
|
|
package/bin/brandkit.js
CHANGED
|
@@ -16,6 +16,9 @@ switch (command) {
|
|
|
16
16
|
case 'generate':
|
|
17
17
|
require('../cli/generate')(args.slice(1));
|
|
18
18
|
break;
|
|
19
|
+
case 'export':
|
|
20
|
+
require('../cli/export')(args.slice(1));
|
|
21
|
+
break;
|
|
19
22
|
case 'help':
|
|
20
23
|
case '--help':
|
|
21
24
|
case '-h':
|
|
@@ -41,6 +44,7 @@ function printHelp() {
|
|
|
41
44
|
console.log(' brandkit generate [dir] Auto-generate config from codebase');
|
|
42
45
|
console.log(' brandkit dev [dir] Start dev server with live reload');
|
|
43
46
|
console.log(' brandkit build [dir] Build static files for production');
|
|
47
|
+
console.log(' brandkit export [dir] Emit agent-native brand data (brand.json, tokens.json, brand.md)');
|
|
44
48
|
console.log('');
|
|
45
49
|
console.log(' Options:');
|
|
46
50
|
console.log(' --help, -h Show this help message');
|
package/cli/build.js
CHANGED
|
@@ -23,12 +23,17 @@ module.exports = function build(args) {
|
|
|
23
23
|
|
|
24
24
|
var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
25
|
|
|
26
|
-
template.build(distDir, targetDir, config);
|
|
26
|
+
var exported = template.build(distDir, targetDir, config);
|
|
27
27
|
|
|
28
28
|
var brandName = (config.brand && config.brand.displayName) || 'Brand';
|
|
29
|
-
console.log(' built index.html (' + brandName + ' title + fonts
|
|
29
|
+
console.log(' built index.html (' + brandName + ' title + fonts + embedded brand data)');
|
|
30
30
|
console.log(' built styles.css (:root variables generated)');
|
|
31
31
|
console.log(' copied engine.js');
|
|
32
|
+
if (exported && exported.written) {
|
|
33
|
+
exported.written.forEach(function (f) {
|
|
34
|
+
console.log(' wrote ' + f + ' (agent-native export)');
|
|
35
|
+
});
|
|
36
|
+
}
|
|
32
37
|
console.log('');
|
|
33
38
|
console.log(' Ready to deploy. Serve ' + path.relative(process.cwd(), targetDir) + '/ as static files.');
|
|
34
39
|
console.log('');
|
package/cli/export.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var exporter = require('../lib/export');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* brandkit export [dir] [--format json|dtcg|md|all] [--out <dir>]
|
|
7
|
+
*
|
|
8
|
+
* Emit agent-native views of a brand from its config.json:
|
|
9
|
+
* brand.json — normalized, semantic brand (roles, usage, contrast, assets)
|
|
10
|
+
* tokens.json — W3C Design Tokens (DTCG) format
|
|
11
|
+
* brand.md — an LLM brief ("how to be on-brand"), a.k.a. llms.txt
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function exportCmd(args) {
|
|
14
|
+
args = args || [];
|
|
15
|
+
var positional = [];
|
|
16
|
+
var format = 'all';
|
|
17
|
+
var outDir = null;
|
|
18
|
+
|
|
19
|
+
for (var i = 0; i < args.length; i++) {
|
|
20
|
+
var a = args[i];
|
|
21
|
+
if (a === '--format' && args[i + 1]) { format = args[++i]; }
|
|
22
|
+
else if (a.indexOf('--format=') === 0) { format = a.slice('--format='.length); }
|
|
23
|
+
else if (a === '--out' && args[i + 1]) { outDir = args[++i]; }
|
|
24
|
+
else if (a.indexOf('--out=') === 0) { outDir = a.slice('--out='.length); }
|
|
25
|
+
else if (a.charAt(0) !== '-') { positional.push(a); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var targetDir = path.resolve(positional[0] || '.');
|
|
29
|
+
var configPath = path.join(targetDir, 'config.json');
|
|
30
|
+
if (!fs.existsSync(configPath)) {
|
|
31
|
+
console.error('');
|
|
32
|
+
console.error(' No config.json found in ' + targetDir);
|
|
33
|
+
console.error(' Run: brandkit init ' + path.relative(process.cwd(), targetDir));
|
|
34
|
+
console.error('');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var config;
|
|
39
|
+
try {
|
|
40
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error('');
|
|
43
|
+
console.error(' Could not parse ' + path.relative(process.cwd(), configPath) + ': ' + e.message);
|
|
44
|
+
console.error('');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var dest = outDir ? path.resolve(outDir) : targetDir;
|
|
49
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
50
|
+
|
|
51
|
+
var result = exporter.writeExports(config, dest, format.split(','));
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' brandkit export');
|
|
55
|
+
console.log('');
|
|
56
|
+
if (!result.written.length) {
|
|
57
|
+
console.log(' Nothing written — unknown --format "' + format + '" (use json|dtcg|md|all).');
|
|
58
|
+
} else {
|
|
59
|
+
result.written.forEach(function (f) {
|
|
60
|
+
console.log(' wrote ' + path.join(path.relative(process.cwd(), dest) || '.', f));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
};
|
package/cli/generate.js
CHANGED
|
@@ -72,6 +72,7 @@ module.exports = function generate(args) {
|
|
|
72
72
|
// Theme from CSS variables
|
|
73
73
|
if (extracted.cssVars) {
|
|
74
74
|
newFields.theme = extractCSS.mapToTheme(extracted.cssVars);
|
|
75
|
+
ensureThemeDefaults(newFields.theme);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// Fonts from Tailwind
|
|
@@ -202,6 +203,39 @@ module.exports = function generate(args) {
|
|
|
202
203
|
console.log('');
|
|
203
204
|
};
|
|
204
205
|
|
|
206
|
+
// Fill in the companion tokens styles.css consumes but a host stylesheet
|
|
207
|
+
// rarely defines: the "r, g, b" variants used by rgba() tints, and the
|
|
208
|
+
// accent on-color pairing. A light fill (e.g. orange) gets a black
|
|
209
|
+
// foreground so buttons stay legible; --accent-text defaults to the fill.
|
|
210
|
+
function ensureThemeDefaults(theme) {
|
|
211
|
+
if (!theme) return;
|
|
212
|
+
var rgbPairs = {
|
|
213
|
+
'--accent': '--accent-rgb',
|
|
214
|
+
'--ink': '--ink-rgb',
|
|
215
|
+
'--error': '--error-rgb',
|
|
216
|
+
'--success': '--success-rgb'
|
|
217
|
+
};
|
|
218
|
+
Object.keys(rgbPairs).forEach(function (hexKey) {
|
|
219
|
+
var rgbKey = rgbPairs[hexKey];
|
|
220
|
+
var val = theme[hexKey];
|
|
221
|
+
if (val && !theme[rgbKey] && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val)) {
|
|
222
|
+
theme[rgbKey] = helpers.hexToRgbString(val);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
if (theme['--accent'] && !theme['--accent-foreground'] && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(theme['--accent'])) {
|
|
226
|
+
// Pick the on-fill color with the higher WCAG contrast (not a luminance
|
|
227
|
+
// threshold — a mid-tone fill like orange reads better with black text
|
|
228
|
+
// even though it isn't "light").
|
|
229
|
+
var lum = helpers.relativeLuminance(theme['--accent']);
|
|
230
|
+
var contrastWhite = 1.05 / (lum + 0.05);
|
|
231
|
+
var contrastBlack = (lum + 0.05) / 0.05;
|
|
232
|
+
theme['--accent-foreground'] = contrastBlack >= contrastWhite ? '#000000' : '#FFFFFF';
|
|
233
|
+
}
|
|
234
|
+
if (theme['--accent'] && !theme['--accent-text']) {
|
|
235
|
+
theme['--accent-text'] = theme['--accent'];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
205
239
|
function buildColors(colorList) {
|
|
206
240
|
var brand = [];
|
|
207
241
|
var neutrals = [];
|
|
@@ -213,7 +247,7 @@ function buildColors(colorList) {
|
|
|
213
247
|
for (var i = 0; i < colorList.length; i++) {
|
|
214
248
|
var c = colorList[i];
|
|
215
249
|
// Skip colors without valid hex (Tailwind function-based colors, etc.)
|
|
216
|
-
if (!c.hex || typeof c.hex !== 'string' || !/^#[0-9a-fA-F]{3
|
|
250
|
+
if (!c.hex || typeof c.hex !== 'string' || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(c.hex)) continue;
|
|
217
251
|
if (!c.name) continue;
|
|
218
252
|
var lowerName = c.name.toLowerCase();
|
|
219
253
|
|
package/cli/init.js
CHANGED
|
@@ -37,6 +37,22 @@ module.exports = function init(args) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Copy the default showcase logos so the scaffold renders complete out of
|
|
41
|
+
// the box. Skipped on --update (preserve the user's assets) and skipped per
|
|
42
|
+
// file if the user already has one with that name.
|
|
43
|
+
var distLogosDir = path.join(distDir, 'logos');
|
|
44
|
+
if (!isUpdate && fs.existsSync(distLogosDir)) {
|
|
45
|
+
var logoFiles = fs.readdirSync(distLogosDir);
|
|
46
|
+
for (var l = 0; l < logoFiles.length; l++) {
|
|
47
|
+
var logoSrc = path.join(distLogosDir, logoFiles[l]);
|
|
48
|
+
var logoDest = path.join(logosDir, logoFiles[l]);
|
|
49
|
+
if (!fs.existsSync(logoDest)) {
|
|
50
|
+
fs.copyFileSync(logoSrc, logoDest);
|
|
51
|
+
console.log(' copied logos/' + logoFiles[l]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
// Create starter config.json (skip if exists and not forcing)
|
|
41
57
|
var configPath = path.join(targetDir, 'config.json');
|
|
42
58
|
if (!fs.existsSync(configPath) && !isUpdate) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/The-Stack-Lab/brandkit/config.schema.json",
|
|
4
|
+
"title": "brandkit config",
|
|
5
|
+
"description": "Configuration for a brandkit brand guide. The single source of truth — drives the rendered guide and the agent-native exports (brand.json, tokens.json, brand.md).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["brand", "fonts", "theme"],
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"definitions": {
|
|
10
|
+
"font": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["family"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"family": { "type": "string", "description": "Font family name (matches the Google Fonts family)." },
|
|
15
|
+
"googleImport": { "type": "string", "description": "Google Fonts css2 query segment, e.g. 'Inter:wght@300;400;700'." },
|
|
16
|
+
"description": { "type": "string" }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"colorItem": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"name": { "type": "string" },
|
|
23
|
+
"hex": { "type": "string", "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" },
|
|
24
|
+
"oklch": { "type": "string" },
|
|
25
|
+
"cssVar": { "type": "string" },
|
|
26
|
+
"role": { "type": "string" },
|
|
27
|
+
"light": { "type": "boolean", "description": "Hint that the swatch needs a border on light backgrounds." }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"colorGroup": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"label": { "type": "string" },
|
|
34
|
+
"items": { "type": "array", "items": { "$ref": "#/definitions/colorItem" } }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"properties": {
|
|
39
|
+
"brand": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["name"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"name": { "type": "string", "description": "Short brand/slug name; shown as the header wordmark when no headerLogo is set." },
|
|
44
|
+
"displayName": { "type": "string" },
|
|
45
|
+
"tagline": { "type": "string" },
|
|
46
|
+
"description": { "type": "string" },
|
|
47
|
+
"url": { "type": "string" },
|
|
48
|
+
"byline": { "type": "string" },
|
|
49
|
+
"version": { "type": "string", "description": "The brand guide's own revision (shown in the banner/footer) — not the brandkit package version." },
|
|
50
|
+
"date": { "type": "string" },
|
|
51
|
+
"guideLabel": { "type": "string", "description": "Renames the 'Web Style Guide' label in the header and footer." },
|
|
52
|
+
"headerLogo": { "type": "string", "description": "Path to a logo image shown in the header instead of the text wordmark (use a light/reversed logo)." },
|
|
53
|
+
"sidebarLogo": { "type": "string", "description": "Path to a logo image shown at the top of the left menu instead of the brand name (use a dark logo)." }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"fonts": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"display": { "$ref": "#/definitions/font" },
|
|
60
|
+
"body": { "$ref": "#/definitions/font" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"theme": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"description": "CSS custom property map. Keys are token names (e.g. '--accent'). The engine only needs to OVERRIDE the baseline defaults shipped in styles.css.",
|
|
66
|
+
"additionalProperties": { "type": "string" },
|
|
67
|
+
"properties": {
|
|
68
|
+
"--accent": { "type": "string", "description": "Accent fill color (buttons, active states)." },
|
|
69
|
+
"--accent-foreground": { "type": "string", "description": "Text/icon color ON the accent fill (must meet contrast against --accent)." },
|
|
70
|
+
"--accent-text": { "type": "string", "description": "Accent used AS text/links on a light surface (must meet AA)." },
|
|
71
|
+
"--primary": { "type": "string", "description": "Primary action fill (primary buttons / CTAs). Defaults to --accent; override to separate the primary action from the accent (e.g. a black CTA with an orange accent)." },
|
|
72
|
+
"--primary-foreground": { "type": "string", "description": "Text/icon color ON the primary fill (must meet contrast against --primary). Defaults to --accent-foreground." },
|
|
73
|
+
"--header-bg": { "type": "string", "description": "Header/hero background behind the wordmark or logo. Defaults to --gradient-brand; override (e.g. a flat dark color) for a hero that shouldn't carry the accent." }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"nav": {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"group": { "type": "string" },
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": { "label": { "type": "string" }, "id": { "type": "string" } }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"colors": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"brand": { "$ref": "#/definitions/colorGroup" },
|
|
96
|
+
"neutrals": { "$ref": "#/definitions/colorGroup" },
|
|
97
|
+
"semantic": { "$ref": "#/definitions/colorGroup" }
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"gradients": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"name": { "type": "string" },
|
|
106
|
+
"css": { "type": "string" },
|
|
107
|
+
"description": { "type": "string" },
|
|
108
|
+
"stops": {
|
|
109
|
+
"type": "array",
|
|
110
|
+
"items": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"color": { "type": "string" },
|
|
114
|
+
"position": { "type": "string" },
|
|
115
|
+
"name": { "type": "string" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"gradientUsage": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"do": { "type": "array", "items": { "type": "string" } },
|
|
126
|
+
"dont": { "type": "array", "items": { "type": "string" } }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"sections": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": { "type": "string" }
|
|
132
|
+
},
|
|
133
|
+
"hierarchy": {
|
|
134
|
+
"type": "array",
|
|
135
|
+
"items": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"class": { "type": "string" },
|
|
139
|
+
"label": { "type": "string" },
|
|
140
|
+
"colorVar": { "type": "string" },
|
|
141
|
+
"colorName": { "type": "string" },
|
|
142
|
+
"hex": { "type": "string" },
|
|
143
|
+
"description": { "type": "string" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"logos": {
|
|
148
|
+
"type": "array",
|
|
149
|
+
"items": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"required": ["name", "variants"],
|
|
152
|
+
"properties": {
|
|
153
|
+
"name": { "type": "string" },
|
|
154
|
+
"description": { "type": "string" },
|
|
155
|
+
"variants": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"description": "Map of format → file path, e.g. { \"svg\": \"logos/mark.svg\" }.",
|
|
158
|
+
"additionalProperties": { "type": "string" }
|
|
159
|
+
},
|
|
160
|
+
"background": { "type": "string", "enum": ["light", "dark", "gradient"] }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"logoSizes": {
|
|
165
|
+
"type": "array",
|
|
166
|
+
"items": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"properties": {
|
|
169
|
+
"label": { "type": "string" },
|
|
170
|
+
"width": { "type": ["integer", "null"] }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"typography": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"name": { "type": "string" },
|
|
180
|
+
"font": { "type": "string", "enum": ["display", "body"] },
|
|
181
|
+
"size": { "type": "string" },
|
|
182
|
+
"weight": { "type": ["integer", "string"] },
|
|
183
|
+
"tracking": { "type": "string" },
|
|
184
|
+
"leading": { "type": "string" },
|
|
185
|
+
"sample": { "type": "string" },
|
|
186
|
+
"uppercase": { "type": "boolean" }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"voice": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": {
|
|
193
|
+
"description": { "type": "string" },
|
|
194
|
+
"do": { "type": "array", "items": { "type": "string" } },
|
|
195
|
+
"dont": { "type": "array", "items": { "type": "string" } }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"accessibility": {
|
|
199
|
+
"type": "array",
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"fg": { "type": "string" },
|
|
204
|
+
"bg": { "type": "string" },
|
|
205
|
+
"fgName": { "type": "string" },
|
|
206
|
+
"bgName": { "type": "string" },
|
|
207
|
+
"ratio": { "type": "string" },
|
|
208
|
+
"rating": { "type": "string", "enum": ["AAA", "AA", "AA Large", "Fail"] },
|
|
209
|
+
"border": { "type": "boolean" },
|
|
210
|
+
"largeText": { "type": "boolean" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"cssVariables": {
|
|
215
|
+
"type": "array",
|
|
216
|
+
"items": {
|
|
217
|
+
"type": "object",
|
|
218
|
+
"properties": {
|
|
219
|
+
"section": { "type": "string" },
|
|
220
|
+
"vars": {
|
|
221
|
+
"type": "array",
|
|
222
|
+
"items": {
|
|
223
|
+
"type": "object",
|
|
224
|
+
"properties": {
|
|
225
|
+
"prop": { "type": "string" },
|
|
226
|
+
"value": { "type": "string" },
|
|
227
|
+
"comment": { "type": "string" }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"spacing": {
|
|
235
|
+
"type": "array",
|
|
236
|
+
"items": {
|
|
237
|
+
"type": "object",
|
|
238
|
+
"properties": {
|
|
239
|
+
"px": { "type": "number" },
|
|
240
|
+
"token": { "type": "string" }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"components": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"buttons": { "type": "array" },
|
|
248
|
+
"cards": { "type": "array" },
|
|
249
|
+
"stats": { "type": "array" }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
package/dist/engine.js
CHANGED
|
@@ -163,8 +163,18 @@
|
|
|
163
163
|
4. Render gradients
|
|
164
164
|
============================================================== */
|
|
165
165
|
function renderGradients() {
|
|
166
|
+
var gradientsSection = document.getElementById('gradients');
|
|
167
|
+
// No gradient configured → hide the whole section. The swatches are
|
|
168
|
+
// CSS-backed (var(--gradient-brand)), so leaving the scaffolding would
|
|
169
|
+
// render the default gradient under an empty "Gradient System" header.
|
|
170
|
+
if (!cfg.gradients || !cfg.gradients.length) {
|
|
171
|
+
if (gradientsSection) gradientsSection.style.display = 'none';
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (gradientsSection) gradientsSection.style.display = '';
|
|
175
|
+
|
|
166
176
|
var stopsContainer = document.getElementById('gradient-stops');
|
|
167
|
-
if (!stopsContainer
|
|
177
|
+
if (!stopsContainer) return;
|
|
168
178
|
var brand = cfg.gradients[0];
|
|
169
179
|
if (brand && brand.stops && brand.stops.length) {
|
|
170
180
|
stopsContainer.innerHTML = brand.stops.map(function (s) {
|
|
@@ -732,11 +742,20 @@
|
|
|
732
742
|
18. Render shell (header, intro, footer, misc)
|
|
733
743
|
============================================================== */
|
|
734
744
|
function renderShell() {
|
|
735
|
-
// Header
|
|
745
|
+
// Header — show brand.headerLogo image if set, else the brand name as text.
|
|
746
|
+
// brand.guideLabel renames the "Web Style Guide" label (header + footer).
|
|
747
|
+
var guideLabel = cfg.brand.guideLabel || 'Web Style Guide';
|
|
736
748
|
var wordmark = document.getElementById('header-wordmark');
|
|
737
|
-
if (wordmark)
|
|
749
|
+
if (wordmark) {
|
|
750
|
+
if (cfg.brand.headerLogo) {
|
|
751
|
+
wordmark.innerHTML = '<img class="header-logo" src="' + esc(cfg.brand.headerLogo) +
|
|
752
|
+
'" alt="' + esc(cfg.brand.name || 'Logo') + '">';
|
|
753
|
+
} else {
|
|
754
|
+
wordmark.textContent = cfg.brand.name;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
738
757
|
var meta = document.getElementById('header-meta');
|
|
739
|
-
if (meta) meta.innerHTML =
|
|
758
|
+
if (meta) meta.innerHTML = esc(guideLabel) + ' v' + esc(cfg.brand.version) + '<br>' + esc(cfg.brand.date);
|
|
740
759
|
|
|
741
760
|
// Intro
|
|
742
761
|
var intro = document.getElementById('intro');
|
|
@@ -749,8 +768,8 @@
|
|
|
749
768
|
// Footer
|
|
750
769
|
var footer = document.getElementById('footer');
|
|
751
770
|
if (footer) footer.innerHTML =
|
|
752
|
-
'<span>' + cfg.brand.url + ' \u00B7 ' + cfg.brand.byline + '</span>' +
|
|
753
|
-
'<span>
|
|
771
|
+
'<span>' + esc(cfg.brand.url) + ' \u00B7 ' + esc(cfg.brand.byline) + '</span>' +
|
|
772
|
+
'<span>' + esc(guideLabel) + ' v' + esc(cfg.brand.version) + ' \u00B7 ' + esc(cfg.brand.date) + '</span>';
|
|
754
773
|
|
|
755
774
|
// Typography specimens — read descriptions from config
|
|
756
775
|
var typeDisplayName = document.getElementById('type-display-name');
|
|
@@ -771,11 +790,21 @@
|
|
|
771
790
|
var testerFont = document.getElementById('type-tester-font');
|
|
772
791
|
var testerInput = document.getElementById('type-tester-input');
|
|
773
792
|
if (testerFont && cfg.fonts) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
793
|
+
// De-duplicate families so a brand using one typeface for both
|
|
794
|
+
// display and body shows a single option, not the same one twice.
|
|
795
|
+
var famList = [];
|
|
796
|
+
if (cfg.fonts.display && cfg.fonts.display.family) famList.push(cfg.fonts.display.family);
|
|
797
|
+
if (cfg.fonts.body && cfg.fonts.body.family) famList.push(cfg.fonts.body.family);
|
|
798
|
+
var seenFam = {};
|
|
799
|
+
var uniqueFams = [];
|
|
800
|
+
famList.forEach(function (f) {
|
|
801
|
+
if (!seenFam[f]) { seenFam[f] = true; uniqueFams.push(f); }
|
|
802
|
+
});
|
|
803
|
+
testerFont.innerHTML = uniqueFams.map(function (f) {
|
|
804
|
+
return '<option value="' + esc(f) + '">' + esc(f) + '</option>';
|
|
805
|
+
}).join('');
|
|
806
|
+
if (testerInput && uniqueFams.length) {
|
|
807
|
+
testerInput.style.fontFamily = "'" + uniqueFams[0] + "', sans-serif";
|
|
779
808
|
}
|
|
780
809
|
}
|
|
781
810
|
|
|
@@ -816,9 +845,16 @@
|
|
|
816
845
|
'</div>';
|
|
817
846
|
}
|
|
818
847
|
|
|
819
|
-
// Sidebar brand name
|
|
848
|
+
// Sidebar brand — logo image if brand.sidebarLogo is set, else brand name
|
|
820
849
|
var sidebarBrand = document.querySelector('.sidebar-brand');
|
|
821
|
-
if (sidebarBrand)
|
|
850
|
+
if (sidebarBrand) {
|
|
851
|
+
if (cfg.brand.sidebarLogo) {
|
|
852
|
+
sidebarBrand.innerHTML = '<img class="sidebar-logo" src="' + esc(cfg.brand.sidebarLogo) +
|
|
853
|
+
'" alt="' + esc(cfg.brand.name || 'Logo') + '">';
|
|
854
|
+
} else {
|
|
855
|
+
sidebarBrand.textContent = cfg.brand.name || 'Brand';
|
|
856
|
+
}
|
|
857
|
+
}
|
|
822
858
|
}
|
|
823
859
|
|
|
824
860
|
/* ==============================================================
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="Brandkit mark, reversed">
|
|
2
|
+
<rect x="1.5" y="1.5" width="93" height="93" rx="20.5" fill="none" stroke="#FFFFFF" stroke-width="3"/>
|
|
3
|
+
<g fill="#FFFFFF">
|
|
4
|
+
<rect x="26" y="28" width="44" height="11" rx="5.5"/>
|
|
5
|
+
<rect x="26" y="43" width="44" height="11" rx="5.5" opacity="0.82"/>
|
|
6
|
+
<rect x="26" y="58" width="28" height="11" rx="5.5" opacity="0.64"/>
|
|
7
|
+
</g>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="Brandkit mark">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bk-grad" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#4F46E5"/>
|
|
5
|
+
<stop offset="1" stop-color="#7C3AED"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="96" height="96" rx="22" fill="url(#bk-grad)"/>
|
|
9
|
+
<g fill="#FFFFFF">
|
|
10
|
+
<rect x="26" y="28" width="44" height="11" rx="5.5"/>
|
|
11
|
+
<rect x="26" y="43" width="44" height="11" rx="5.5" opacity="0.82"/>
|
|
12
|
+
<rect x="26" y="58" width="28" height="11" rx="5.5" opacity="0.64"/>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|