@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 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 github:The-Stack-Lab/brandkit
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` with the theme baked in |
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 a complete reference implementation (Freeway PHX).
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
- ## How theming works
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
- `dist/styles.css` uses CSS custom properties (`var(--purple)`, `var(--font-display)`, etc.) with no `:root` block.
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>` 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`.
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 baked in)');
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,8}$/.test(c.hex)) continue;
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 || !cfg.gradients || !cfg.gradients.length) return;
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) wordmark.textContent = cfg.brand.name;
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 = 'Web Style Guide v' + cfg.brand.version + '<br>' + cfg.brand.date;
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>Web Style Guide v' + cfg.brand.version + ' \u00B7 ' + cfg.brand.date + '</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
- testerFont.innerHTML =
775
- '<option value="' + cfg.fonts.display.family + '">' + cfg.fonts.display.family + '</option>' +
776
- '<option value="' + cfg.fonts.body.family + '">' + cfg.fonts.body.family + '</option>';
777
- if (testerInput) {
778
- testerInput.style.fontFamily = "'" + cfg.fonts.display.family + "', sans-serif";
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) sidebarBrand.textContent = 'brandkit';
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>