@stacklist-app/brandkit 1.1.2 → 1.2.2

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.2
10
+ npm install @stacklist-app/brandkit
13
11
  ```
14
12
 
15
13
  Requires Node.js 18+.
@@ -32,7 +30,20 @@ npx brandkit build brand # bake into static files for production
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
 
@@ -65,7 +76,7 @@ export default {
65
76
  `config.json` is the single source of truth. Top-level keys:
66
77
 
67
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
- - `fonts` — display + body with Google Fonts import
79
+ - `fonts` — display + body with Google Fonts import; each font takes an optional `fallback` web stand-in for brands whose official typeface isn't web-available (the rendered font stack becomes `'family', 'fallback', sans-serif`, while labels keep the clean family name)
69
80
  - `theme` — CSS variable map (colors, gradients, font vars)
70
81
  - `nav` — sidebar structure
71
82
  - `colors` — brand / neutrals / semantic palettes
@@ -93,6 +104,13 @@ The accent uses a fill/text split (the same convention as Material and shadcn/ui
93
104
 
94
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`.
95
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:
108
+
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`)
111
+
112
+ The fallbacks mean single-accent brands need set none of these.
113
+
96
114
  - **Dev**: the engine reads `config.theme` and injects a `<style data-brandkit-theme>` block at runtime, which cascades over the defaults.
97
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`.
98
116
 
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
+ };
@@ -0,0 +1,254 @@
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", "minLength": 1, "description": "Font family name (matches the Google Fonts family)." },
15
+ "fallback": { "type": "string", "minLength": 1, "description": "Optional web-available stand-in for brands whose official typeface isn't on the web. When set, the rendered CSS font stack becomes 'family', 'fallback', sans-serif everywhere the font renders; labels still show 'family' alone." },
16
+ "googleImport": { "type": "string", "description": "Google Fonts css2 query segment, e.g. 'Inter:wght@300;400;700'." },
17
+ "description": { "type": "string" }
18
+ }
19
+ },
20
+ "colorItem": {
21
+ "type": "object",
22
+ "properties": {
23
+ "name": { "type": "string" },
24
+ "hex": { "type": "string", "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" },
25
+ "oklch": { "type": "string" },
26
+ "cssVar": { "type": "string" },
27
+ "role": { "type": "string" },
28
+ "light": { "type": "boolean", "description": "Hint that the swatch needs a border on light backgrounds." }
29
+ }
30
+ },
31
+ "colorGroup": {
32
+ "type": "object",
33
+ "properties": {
34
+ "label": { "type": "string" },
35
+ "items": { "type": "array", "items": { "$ref": "#/definitions/colorItem" } }
36
+ }
37
+ }
38
+ },
39
+ "properties": {
40
+ "brand": {
41
+ "type": "object",
42
+ "required": ["name"],
43
+ "properties": {
44
+ "name": { "type": "string", "description": "Short brand/slug name; shown as the header wordmark when no headerLogo is set." },
45
+ "displayName": { "type": "string" },
46
+ "tagline": { "type": "string" },
47
+ "description": { "type": "string" },
48
+ "url": { "type": "string" },
49
+ "byline": { "type": "string" },
50
+ "version": { "type": "string", "description": "The brand guide's own revision (shown in the banner/footer) — not the brandkit package version." },
51
+ "date": { "type": "string" },
52
+ "guideLabel": { "type": "string", "description": "Renames the 'Web Style Guide' label in the header and footer." },
53
+ "headerLogo": { "type": "string", "description": "Path to a logo image shown in the header instead of the text wordmark (use a light/reversed logo)." },
54
+ "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)." }
55
+ }
56
+ },
57
+ "fonts": {
58
+ "type": "object",
59
+ "properties": {
60
+ "display": { "$ref": "#/definitions/font" },
61
+ "body": { "$ref": "#/definitions/font" }
62
+ }
63
+ },
64
+ "theme": {
65
+ "type": "object",
66
+ "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.",
67
+ "additionalProperties": { "type": "string" },
68
+ "properties": {
69
+ "--accent": { "type": "string", "description": "Accent fill color (buttons, active states)." },
70
+ "--accent-foreground": { "type": "string", "description": "Text/icon color ON the accent fill (must meet contrast against --accent)." },
71
+ "--accent-text": { "type": "string", "description": "Accent used AS text/links on a light surface (must meet AA)." },
72
+ "--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)." },
73
+ "--primary-foreground": { "type": "string", "description": "Text/icon color ON the primary fill (must meet contrast against --primary). Defaults to --accent-foreground." },
74
+ "--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." }
75
+ }
76
+ },
77
+ "nav": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "object",
81
+ "properties": {
82
+ "group": { "type": "string" },
83
+ "items": {
84
+ "type": "array",
85
+ "items": {
86
+ "type": "object",
87
+ "properties": { "label": { "type": "string" }, "id": { "type": "string" } }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ },
93
+ "colors": {
94
+ "type": "object",
95
+ "properties": {
96
+ "brand": { "$ref": "#/definitions/colorGroup" },
97
+ "neutrals": { "$ref": "#/definitions/colorGroup" },
98
+ "semantic": { "$ref": "#/definitions/colorGroup" }
99
+ }
100
+ },
101
+ "gradients": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "object",
105
+ "properties": {
106
+ "name": { "type": "string" },
107
+ "css": { "type": "string" },
108
+ "description": { "type": "string" },
109
+ "stops": {
110
+ "type": "array",
111
+ "items": {
112
+ "type": "object",
113
+ "properties": {
114
+ "color": { "type": "string" },
115
+ "position": { "type": "string" },
116
+ "name": { "type": "string" }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ },
123
+ "gradientUsage": {
124
+ "type": "object",
125
+ "properties": {
126
+ "do": { "type": "array", "items": { "type": "string" } },
127
+ "dont": { "type": "array", "items": { "type": "string" } }
128
+ }
129
+ },
130
+ "sections": {
131
+ "type": "object",
132
+ "additionalProperties": { "type": "string" }
133
+ },
134
+ "hierarchy": {
135
+ "type": "array",
136
+ "items": {
137
+ "type": "object",
138
+ "properties": {
139
+ "class": { "type": "string" },
140
+ "label": { "type": "string" },
141
+ "colorVar": { "type": "string" },
142
+ "colorName": { "type": "string" },
143
+ "hex": { "type": "string" },
144
+ "description": { "type": "string" }
145
+ }
146
+ }
147
+ },
148
+ "logos": {
149
+ "type": "array",
150
+ "items": {
151
+ "type": "object",
152
+ "required": ["name", "variants"],
153
+ "properties": {
154
+ "name": { "type": "string" },
155
+ "description": { "type": "string" },
156
+ "variants": {
157
+ "type": "object",
158
+ "description": "Map of format → file path, e.g. { \"svg\": \"logos/mark.svg\" }.",
159
+ "additionalProperties": { "type": "string" }
160
+ },
161
+ "background": { "type": "string", "enum": ["light", "dark", "gradient"] }
162
+ }
163
+ }
164
+ },
165
+ "logoSizes": {
166
+ "type": "array",
167
+ "items": {
168
+ "type": "object",
169
+ "properties": {
170
+ "label": { "type": "string" },
171
+ "width": { "type": ["integer", "null"] }
172
+ }
173
+ }
174
+ },
175
+ "typography": {
176
+ "type": "array",
177
+ "items": {
178
+ "type": "object",
179
+ "properties": {
180
+ "name": { "type": "string" },
181
+ "font": { "type": "string", "enum": ["display", "body"] },
182
+ "size": { "type": "string" },
183
+ "weight": { "type": ["integer", "string"] },
184
+ "tracking": { "type": "string" },
185
+ "leading": { "type": "string" },
186
+ "sample": { "type": "string" },
187
+ "uppercase": { "type": "boolean" }
188
+ }
189
+ }
190
+ },
191
+ "voice": {
192
+ "type": "object",
193
+ "properties": {
194
+ "description": { "type": "string" },
195
+ "do": { "type": "array", "items": { "type": "string" } },
196
+ "dont": { "type": "array", "items": { "type": "string" } }
197
+ }
198
+ },
199
+ "accessibility": {
200
+ "type": "array",
201
+ "items": {
202
+ "type": "object",
203
+ "properties": {
204
+ "fg": { "type": "string" },
205
+ "bg": { "type": "string" },
206
+ "fgName": { "type": "string" },
207
+ "bgName": { "type": "string" },
208
+ "ratio": { "type": "string" },
209
+ "rating": { "type": "string", "enum": ["AAA", "AA", "AA Large", "Fail"] },
210
+ "border": { "type": "boolean" },
211
+ "largeText": { "type": "boolean" }
212
+ }
213
+ }
214
+ },
215
+ "cssVariables": {
216
+ "type": "array",
217
+ "items": {
218
+ "type": "object",
219
+ "properties": {
220
+ "section": { "type": "string" },
221
+ "vars": {
222
+ "type": "array",
223
+ "items": {
224
+ "type": "object",
225
+ "properties": {
226
+ "prop": { "type": "string" },
227
+ "value": { "type": "string" },
228
+ "comment": { "type": "string" }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ },
235
+ "spacing": {
236
+ "type": "array",
237
+ "items": {
238
+ "type": "object",
239
+ "properties": {
240
+ "px": { "type": "number" },
241
+ "token": { "type": "string" }
242
+ }
243
+ }
244
+ },
245
+ "components": {
246
+ "type": "object",
247
+ "properties": {
248
+ "buttons": { "type": "array" },
249
+ "cards": { "type": "array" },
250
+ "stats": { "type": "array" }
251
+ }
252
+ }
253
+ }
254
+ }
package/dist/engine.js CHANGED
@@ -21,6 +21,23 @@
21
21
  .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
22
22
  }
23
23
 
24
+ /* ==============================================================
25
+ Font stack helper — weaves in an optional `fallback` web stand-in
26
+ for brands whose official typeface isn't web-available:
27
+ 'Family', 'Fallback', sans-serif
28
+ No fallback → 'Family', sans-serif (backward-compatible).
29
+ ============================================================== */
30
+ function fontStack(f) {
31
+ if (!f || !f.family) return 'sans-serif';
32
+ // Strip control chars (a raw newline ends a CSS string) then escape
33
+ // backslash + single-quote, so a config value can't break out of the
34
+ // quoted CSS string and inject rules into the injected :root block.
35
+ function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
36
+ var stack = "'" + q(f.family) + "'";
37
+ if (f.fallback) stack += ", '" + q(f.fallback) + "'";
38
+ return stack + ', sans-serif';
39
+ }
40
+
24
41
  /* ==============================================================
25
42
  BOOTSTRAP — inject fonts + CSS variables before any rendering
26
43
  ============================================================== */
@@ -47,8 +64,8 @@
47
64
  }
48
65
  // Add font variables from config
49
66
  if (cfg.fonts) {
50
- if (cfg.fonts.display) vars.push(" --font-display: '" + cfg.fonts.display.family + "', sans-serif;");
51
- if (cfg.fonts.body) vars.push(" --font-body: '" + cfg.fonts.body.family + "', sans-serif;");
67
+ if (cfg.fonts.display) vars.push(' --font-display: ' + fontStack(cfg.fonts.display) + ';');
68
+ if (cfg.fonts.body) vars.push(' --font-body: ' + fontStack(cfg.fonts.body) + ';');
52
69
  }
53
70
  var style = document.createElement('style');
54
71
  style.setAttribute('data-brandkit-theme', '');
@@ -734,7 +751,8 @@
734
751
  if (!testerFont || !testerInput) return;
735
752
 
736
753
  testerFont.addEventListener('change', function () {
737
- testerInput.style.fontFamily = "'" + testerFont.value + "', sans-serif";
754
+ // The option value is already a complete font stack (incl. fallback).
755
+ testerInput.style.fontFamily = testerFont.value;
738
756
  });
739
757
  }
740
758
 
@@ -790,21 +808,26 @@
790
808
  var testerFont = document.getElementById('type-tester-font');
791
809
  var testerInput = document.getElementById('type-tester-input');
792
810
  if (testerFont && cfg.fonts) {
793
- // De-duplicate families so a brand using one typeface for both
811
+ // De-duplicate by family so a brand using one typeface for both
794
812
  // 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); }
813
+ // The option value carries the full font stack (incl. any fallback)
814
+ // so the tester renders in the web stand-in when one is configured;
815
+ // the label stays the clean family name.
816
+ var fontList = [];
817
+ if (cfg.fonts.display && cfg.fonts.display.family) fontList.push(cfg.fonts.display);
818
+ if (cfg.fonts.body && cfg.fonts.body.family) fontList.push(cfg.fonts.body);
819
+ // null-proto map so a family literally named "__proto__"/"toString"
820
+ // isn't mistaken for already-seen via an inherited Object property.
821
+ var seenFam = Object.create(null);
822
+ var uniqueFonts = [];
823
+ fontList.forEach(function (f) {
824
+ if (!seenFam[f.family]) { seenFam[f.family] = true; uniqueFonts.push(f); }
802
825
  });
803
- testerFont.innerHTML = uniqueFams.map(function (f) {
804
- return '<option value="' + esc(f) + '">' + esc(f) + '</option>';
826
+ testerFont.innerHTML = uniqueFonts.map(function (f) {
827
+ return '<option value="' + esc(fontStack(f)) + '">' + esc(f.family) + '</option>';
805
828
  }).join('');
806
- if (testerInput && uniqueFams.length) {
807
- testerInput.style.fontFamily = "'" + uniqueFams[0] + "', sans-serif";
829
+ if (testerInput && uniqueFonts.length) {
830
+ testerInput.style.fontFamily = fontStack(uniqueFonts[0]);
808
831
  }
809
832
  }
810
833
 
package/dist/styles.css CHANGED
@@ -41,10 +41,22 @@
41
41
  --accent-foreground: #FFFFFF;
42
42
  --accent-text: var(--accent);
43
43
 
44
+ /* Primary action — the fill for primary buttons / CTAs.
45
+ Defaults to the accent, so single-accent brands are unchanged. Brands that
46
+ separate the primary action from the accent (e.g. a black CTA with an orange
47
+ accent) override --primary / --primary-foreground. */
48
+ --primary: var(--accent);
49
+ --primary-foreground: var(--accent-foreground);
50
+
44
51
  /* Gradients */
45
52
  --gradient-brand: linear-gradient(135deg, #4338CA 0%, #6366F1 50%, #EC4899 100%);
46
53
  --gradient-brand-subtle: linear-gradient(135deg, rgba(99, 102, 241, 0.06) 0%, rgba(236, 72, 153, 0.06) 100%);
47
54
 
55
+ /* Header background — the hero block behind the wordmark/logo.
56
+ Defaults to the brand gradient; override (e.g. a flat dark color) for a
57
+ hero that shouldn't carry the accent. */
58
+ --header-bg: var(--gradient-brand);
59
+
48
60
  /* Semantic */
49
61
  --success: #10B981;
50
62
  --success-rgb: 16, 185, 129;
@@ -231,7 +243,7 @@ body {
231
243
 
232
244
  /* ── Header ── */
233
245
  .header {
234
- background: var(--gradient-brand);
246
+ background: var(--header-bg);
235
247
  border-radius: 24px;
236
248
  padding: 64px 56px;
237
249
  margin-bottom: 72px;
@@ -863,7 +875,7 @@ body {
863
875
  .btn-sm { height: 32px; padding: 0 16px; font-size: 13px; border-radius: 9999px; }
864
876
  .btn-md { height: 40px; padding: 0 24px; font-size: 14px; border-radius: 9999px; }
865
877
  .btn-lg { height: 48px; padding: 0 28px; font-size: 16px; border-radius: 9999px; }
866
- .btn-primary { background: var(--accent); color: var(--accent-foreground); }
878
+ .btn-primary { background: var(--primary); color: var(--primary-foreground); }
867
879
  .btn-primary:hover { filter: brightness(0.9); }
868
880
  .btn-secondary { background: var(--mist); color: var(--ink); }
869
881
  .btn-secondary:hover { filter: brightness(0.96); }
@@ -52,7 +52,6 @@ module.exports = function brandkitVite(options) {
52
52
  },
53
53
 
54
54
  closeBundle: function () {
55
- // Copy brand directory to build output
56
55
  var srcDir = path.resolve(brandDir);
57
56
  if (!fs.existsSync(srcDir)) return;
58
57
 
@@ -61,7 +60,27 @@ module.exports = function brandkitVite(options) {
61
60
  fs.mkdirSync(outDir, { recursive: true });
62
61
  }
63
62
 
63
+ // Copy assets (config.json, logos, anything in brand/) to the output.
64
64
  copyDir(srcDir, outDir);
65
+
66
+ // Then produce a real production build into the output: theme baked into
67
+ // styles.css, engine.js copied, and the agent-native exports written +
68
+ // embedded in index.html. This makes the deployed /brand carry brand.json,
69
+ // tokens.json, brand.md and the inline <script id="brandkit-brand"> — so an
70
+ // agent pointed at the URL gets structured brand data without a manual
71
+ // `brandkit build` step. Falls back to the raw copy if config is missing.
72
+ var configPath = path.join(srcDir, 'config.json');
73
+ if (fs.existsSync(configPath)) {
74
+ try {
75
+ var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
76
+ var template = require('../lib/template');
77
+ var resolve = require('../lib/resolve');
78
+ template.build(resolve.getDistPath(), outDir, config);
79
+ } catch (e) {
80
+ // Leave the raw copy in place; don't fail the host build, but say why.
81
+ console.warn('[brandkit] agent-export step skipped: ' + e.message);
82
+ }
83
+ }
65
84
  }
66
85
  };
67
86
  };
package/lib/export.js ADDED
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Agent-native exports — project a render-shaped config.json into machine-first
3
+ * views an AI agent or design-token tool can consume without scraping the HTML.
4
+ *
5
+ * buildBrandJson(config) → normalized, semantic brand (roles, usage, contrast, asset paths)
6
+ * buildTokensJson(config) → W3C Design Tokens (DTCG) format for token tooling
7
+ * buildBrandMarkdown(config) → an LLM brief ("how to be on-brand") a.k.a. llms.txt
8
+ *
9
+ * Pure functions, Node builtins only. Contrast is computed via generate-helpers.
10
+ */
11
+ var fs = require('fs');
12
+ var path = require('path');
13
+ var helpers = require('./generate-helpers');
14
+
15
+ var HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
16
+
17
+ // Map format aliases to the canonical set.
18
+ function normalizeFormats(formats) {
19
+ if (!formats || formats === 'all') return ['json', 'dtcg', 'md'];
20
+ var list = Array.isArray(formats) ? formats : [formats];
21
+ var canon = [];
22
+ list.forEach(function (f) {
23
+ f = String(f).toLowerCase();
24
+ if (f === 'all') { canon = ['json', 'dtcg', 'md']; }
25
+ else if (f === 'json' || f === 'brand') { if (canon.indexOf('json') === -1) canon.push('json'); }
26
+ else if (f === 'dtcg' || f === 'tokens') { if (canon.indexOf('dtcg') === -1) canon.push('dtcg'); }
27
+ else if (f === 'md' || f === 'markdown' || f === 'llms') { if (canon.indexOf('md') === -1) canon.push('md'); }
28
+ });
29
+ // Return canon as-is (possibly empty) so an unknown format surfaces as
30
+ // "nothing written" rather than silently writing every file.
31
+ return canon;
32
+ }
33
+
34
+ /**
35
+ * Write the requested export files into targetDir. Returns the written
36
+ * filenames and the brand.json object (so callers can also embed it).
37
+ */
38
+ function writeExports(config, targetDir, formats) {
39
+ var want = normalizeFormats(formats);
40
+ var written = [];
41
+ var brandJson = null;
42
+ if (want.indexOf('json') !== -1) {
43
+ brandJson = buildBrandJson(config);
44
+ fs.writeFileSync(path.join(targetDir, 'brand.json'), JSON.stringify(brandJson, null, 2) + '\n');
45
+ written.push('brand.json');
46
+ }
47
+ if (want.indexOf('dtcg') !== -1) {
48
+ fs.writeFileSync(path.join(targetDir, 'tokens.json'), JSON.stringify(buildTokensJson(config), null, 2) + '\n');
49
+ written.push('tokens.json');
50
+ }
51
+ if (want.indexOf('md') !== -1) {
52
+ fs.writeFileSync(path.join(targetDir, 'brand.md'), buildBrandMarkdown(config));
53
+ written.push('brand.md');
54
+ }
55
+ return { written: written, brandJson: brandJson };
56
+ }
57
+
58
+ function isHex(v) {
59
+ return typeof v === 'string' && HEX_RE.test(v);
60
+ }
61
+
62
+ // "#4338CA on #FFFFFF" → { ratio: "7.9:1", rating: "AAA" }
63
+ function contrast(fg, bg) {
64
+ if (!isHex(fg) || !isHex(bg)) return null;
65
+ var ratio = helpers.contrastRatio(fg, bg);
66
+ return { ratio: ratio, rating: helpers.rateContrast(ratio) };
67
+ }
68
+
69
+ function themeVal(config, key) {
70
+ return config.theme && config.theme[key];
71
+ }
72
+
73
+ /**
74
+ * Normalized, agent-first brand. Encodes roles + usage + contrast, not layout.
75
+ */
76
+ function buildBrandJson(config) {
77
+ config = config || {};
78
+ var brand = config.brand || {};
79
+ var theme = config.theme || {};
80
+ var fonts = config.fonts || {};
81
+ var colors = config.colors || {};
82
+
83
+ var accentFill = theme['--accent'];
84
+ var accentOnFill = theme['--accent-foreground'];
85
+ var accentText = theme['--accent-text'];
86
+
87
+ var out = {
88
+ name: brand.name || null,
89
+ displayName: brand.displayName || brand.name || null,
90
+ tagline: brand.tagline || null,
91
+ description: brand.description || null,
92
+ url: brand.url || null,
93
+ version: brand.version || null,
94
+ generatedBy: 'brandkit',
95
+ voice: config.voice ? {
96
+ summary: config.voice.description || null,
97
+ do: config.voice.do || [],
98
+ dont: config.voice.dont || []
99
+ } : null,
100
+ color: {
101
+ accent: accentFill ? {
102
+ fill: accentFill,
103
+ onFill: accentOnFill || null,
104
+ asText: accentText || null,
105
+ contrast: {
106
+ onFill: accentOnFill ? contrast(accentOnFill, accentFill) : null,
107
+ asTextOnWhite: accentText ? contrast(accentText, theme['--white'] || '#FFFFFF') : null
108
+ }
109
+ } : null,
110
+ // Only surfaced when a brand separates its primary action from the accent.
111
+ primary: theme['--primary'] ? {
112
+ fill: theme['--primary'],
113
+ onFill: theme['--primary-foreground'] || null,
114
+ contrast: {
115
+ onFill: theme['--primary-foreground'] ? contrast(theme['--primary-foreground'], theme['--primary']) : null
116
+ }
117
+ } : null,
118
+ brand: projectColorGroup(colors.brand),
119
+ neutral: projectColorGroup(colors.neutrals),
120
+ semantic: projectColorGroup(colors.semantic)
121
+ },
122
+ gradient: (theme['--gradient-brand'] || config.gradientUsage) ? {
123
+ brand: theme['--gradient-brand'] || null,
124
+ subtle: theme['--gradient-brand-subtle'] || null,
125
+ usage: config.gradientUsage ? {
126
+ do: config.gradientUsage.do || [],
127
+ dont: config.gradientUsage.dont || []
128
+ } : null
129
+ } : null,
130
+ type: {
131
+ display: projectFont(fonts.display),
132
+ body: projectFont(fonts.body),
133
+ scale: (config.typography || []).map(function (t) {
134
+ return { name: t.name, size: t.size, weight: t.weight, font: t.font };
135
+ })
136
+ },
137
+ spacing: (config.spacing || []).map(function (s) {
138
+ return { token: s.token, px: s.px };
139
+ }),
140
+ logos: (config.logos || []).map(function (l) {
141
+ var variants = l.variants || {};
142
+ var formats = Object.keys(variants);
143
+ return {
144
+ name: l.name,
145
+ description: l.description || null,
146
+ use: backgroundToUse(l.background),
147
+ formats: formats,
148
+ src: variants.svg || variants.png || variants.jpg || (formats.length ? variants[formats[0]] : null),
149
+ variants: variants
150
+ };
151
+ }),
152
+ accessibility: (config.accessibility || []).map(function (a) {
153
+ return {
154
+ pair: (a.fgName || a.fg || '') + ' on ' + (a.bgName || a.bg || ''),
155
+ fg: a.fg, bg: a.bg, ratio: a.ratio, rating: a.rating
156
+ };
157
+ })
158
+ };
159
+
160
+ return prune(out);
161
+ }
162
+
163
+ function projectColorGroup(group) {
164
+ if (!group) return [];
165
+ var items = group.items || group;
166
+ if (!Array.isArray(items)) return [];
167
+ return items.map(function (c) {
168
+ return prune({
169
+ name: c.name || c.label || null,
170
+ hex: c.hex || null,
171
+ oklch: c.oklch || null,
172
+ role: c.role || c.usage || null,
173
+ token: c.cssVar || c.token || null
174
+ });
175
+ });
176
+ }
177
+
178
+ function projectFont(f) {
179
+ if (!f) return null;
180
+ return prune({ family: f.family || null, googleImport: f.googleImport || null, description: f.description || null });
181
+ }
182
+
183
+ // "Space Grotesk — Space Grotesk — a geometric sans" → "Space Grotesk — a geometric sans"
184
+ function fontLine(f) {
185
+ var family = f.family || '';
186
+ var desc = f.description || '';
187
+ if (desc) {
188
+ var stripped = desc.replace(new RegExp('^' + family.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*[—:-]\\s*'), '');
189
+ return family + (stripped ? ' — ' + stripped : '');
190
+ }
191
+ return family;
192
+ }
193
+
194
+ function backgroundToUse(bg) {
195
+ if (bg === 'light') return 'light backgrounds';
196
+ if (bg === 'dark') return 'dark backgrounds';
197
+ if (bg === 'gradient') return 'the brand gradient';
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * W3C Design Tokens (DTCG) — color / fontFamily / dimension groups.
203
+ * Strict-ish so Style Dictionary, Tokens Studio, and Figma can read it.
204
+ */
205
+ function buildTokensJson(config) {
206
+ config = config || {};
207
+ var theme = config.theme || {};
208
+ var fonts = config.fonts || {};
209
+ var brand = config.brand || {};
210
+
211
+ var color = {};
212
+ Object.keys(theme).forEach(function (key) {
213
+ var val = theme[key];
214
+ if (isHex(val)) {
215
+ color[key.replace(/^--/, '')] = { $type: 'color', $value: val };
216
+ }
217
+ });
218
+
219
+ var fontFamily = {};
220
+ if (fonts.display && fonts.display.family) fontFamily.display = { $type: 'fontFamily', $value: fonts.display.family };
221
+ if (fonts.body && fonts.body.family) fontFamily.body = { $type: 'fontFamily', $value: fonts.body.family };
222
+
223
+ var dimension = {};
224
+ (config.spacing || []).forEach(function (s) {
225
+ if (s.token && (s.px || s.px === 0)) dimension[s.token] = { $type: 'dimension', $value: s.px + 'px' };
226
+ });
227
+
228
+ var out = {
229
+ $description: 'Design tokens for ' + (brand.displayName || brand.name || 'brand') + ' — W3C DTCG format, generated by brandkit'
230
+ };
231
+ if (Object.keys(color).length) out.color = color;
232
+ if (Object.keys(fontFamily).length) out.fontFamily = fontFamily;
233
+ if (Object.keys(dimension).length) out.dimension = dimension;
234
+ return out;
235
+ }
236
+
237
+ /**
238
+ * LLM brief — drop into an agent's context to write/design on-brand.
239
+ */
240
+ function buildBrandMarkdown(config) {
241
+ config = config || {};
242
+ var brand = config.brand || {};
243
+ var theme = config.theme || {};
244
+ var fonts = config.fonts || {};
245
+ var name = brand.displayName || brand.name || 'Brand';
246
+ var L = [];
247
+
248
+ L.push('# ' + name + ' — Brand Brief');
249
+ L.push('');
250
+ L.push('> Machine-readable brand guidance generated by brandkit from `config.json`.');
251
+ L.push('> Companion files: `brand.json` (full structured brand), `tokens.json` (W3C design tokens).');
252
+ if (brand.description) { L.push(''); L.push(brand.description); }
253
+
254
+ if (config.voice) {
255
+ L.push(''); L.push('## Voice & Tone');
256
+ if (config.voice.description) L.push(config.voice.description);
257
+ if (config.voice.do && config.voice.do.length) {
258
+ L.push(''); L.push('**Do:**');
259
+ config.voice.do.forEach(function (d) { L.push('- ' + d); });
260
+ }
261
+ if (config.voice.dont && config.voice.dont.length) {
262
+ L.push(''); L.push("**Don't:**");
263
+ config.voice.dont.forEach(function (d) { L.push('- ' + d); });
264
+ }
265
+ }
266
+
267
+ L.push(''); L.push('## Color');
268
+ var accent = theme['--accent'];
269
+ if (accent) {
270
+ var onFill = theme['--accent-foreground'];
271
+ var asText = theme['--accent-text'];
272
+ var c1 = onFill ? contrast(onFill, accent) : null;
273
+ var c2 = asText ? contrast(asText, theme['--white'] || '#FFFFFF') : null;
274
+ L.push('- **Accent (fill):** `' + accent + '`' + (onFill ? ' — put `' + onFill + '` text/icons on it' + (c1 ? ' (' + c1.ratio + ' ' + c1.rating + ')' : '') : '') + '.');
275
+ if (asText) L.push('- **Accent as text/links on light:** `' + asText + '`' + (c2 ? ' (' + c2.ratio + ' ' + c2.rating + ' on white)' : '') + '.');
276
+ }
277
+ var primary = theme['--primary'];
278
+ if (primary && primary !== accent) {
279
+ var pOn = theme['--primary-foreground'];
280
+ var pc = pOn ? contrast(pOn, primary) : null;
281
+ L.push('- **Primary action (CTA fill):** `' + primary + '`' + (pOn ? ' with `' + pOn + '` text' + (pc ? ' (' + pc.ratio + ' ' + pc.rating + ')' : '') : '') + '.');
282
+ }
283
+ var sem = [];
284
+ ['--success', '--warning', '--error'].forEach(function (k) {
285
+ if (theme[k]) sem.push(k.replace('--', '') + ' `' + theme[k] + '`');
286
+ });
287
+ if (sem.length) L.push('- **Semantic:** ' + sem.join(', ') + '.');
288
+ var neutralBits = [];
289
+ if (theme['--ink']) neutralBits.push('Ink `' + theme['--ink'] + '` (primary text)');
290
+ if (theme['--slate']) neutralBits.push('Slate `' + theme['--slate'] + '` (muted)');
291
+ if (theme['--cloud']) neutralBits.push('Cloud `' + theme['--cloud'] + '` (page bg)');
292
+ if (neutralBits.length) L.push('- **Neutrals:** ' + neutralBits.join(', ') + '.');
293
+
294
+ // Surface any low-contrast pairs as explicit warnings
295
+ var warns = (config.accessibility || []).filter(function (a) {
296
+ return a.rating && a.rating !== 'AAA' && a.rating !== 'AA';
297
+ });
298
+ if (warns.length) {
299
+ L.push(''); L.push('**Contrast cautions:**');
300
+ warns.forEach(function (a) {
301
+ var note = a.rating === 'AA Large' ? ' — large text only.' : ' — fails AA; do not use this pair for text.';
302
+ L.push('- ' + (a.fgName || a.fg) + ' on ' + (a.bgName || a.bg) + ' is ' + a.ratio + ' (' + a.rating + ')' + note);
303
+ });
304
+ }
305
+
306
+ if (fonts.display || fonts.body) {
307
+ L.push(''); L.push('## Typography');
308
+ if (fonts.display) L.push('- **Display:** ' + fontLine(fonts.display));
309
+ if (fonts.body) L.push('- **Body:** ' + fontLine(fonts.body));
310
+ }
311
+
312
+ if (config.logos && config.logos.length) {
313
+ L.push(''); L.push('## Logos');
314
+ config.logos.forEach(function (l) {
315
+ var use = backgroundToUse(l.background);
316
+ var variants = l.variants || {};
317
+ var src = variants.svg || variants.png || variants.jpg || '';
318
+ L.push('- **' + l.name + '**' + (use ? ' — use on ' + use : '') + (src ? ' — `' + src + '`' : ''));
319
+ });
320
+ }
321
+
322
+ if (theme['--gradient-brand']) {
323
+ L.push(''); L.push('## Gradient');
324
+ L.push('- **Brand gradient:** `' + theme['--gradient-brand'] + '`');
325
+ if (config.gradientUsage) {
326
+ if (config.gradientUsage.do && config.gradientUsage.do.length) L.push('- Use for: ' + config.gradientUsage.do.join('; ') + '.');
327
+ if (config.gradientUsage.dont && config.gradientUsage.dont.length) L.push("- Don't use for: " + config.gradientUsage.dont.join('; ') + '.');
328
+ }
329
+ }
330
+
331
+ L.push('');
332
+ return L.join('\n');
333
+ }
334
+
335
+ // Recursively drop null / empty-array / empty-object keys for a clean export.
336
+ function prune(obj) {
337
+ if (Array.isArray(obj)) return obj.map(prune);
338
+ if (obj === null || typeof obj !== 'object') return obj;
339
+ var out = {};
340
+ Object.keys(obj).forEach(function (k) {
341
+ var v = prune(obj[k]);
342
+ if (v === null || v === undefined) return;
343
+ if (Array.isArray(v) && v.length === 0) return;
344
+ if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) return;
345
+ out[k] = v;
346
+ });
347
+ return out;
348
+ }
349
+
350
+ module.exports = {
351
+ buildBrandJson: buildBrandJson,
352
+ buildTokensJson: buildTokensJson,
353
+ buildBrandMarkdown: buildBrandMarkdown,
354
+ writeExports: writeExports,
355
+ normalizeFormats: normalizeFormats
356
+ };
package/lib/template.js CHANGED
@@ -1,5 +1,23 @@
1
1
  var fs = require('fs');
2
2
  var path = require('path');
3
+ var exporter = require('./export');
4
+
5
+ /**
6
+ * Build a CSS font stack from a font def. When the brand's official typeface
7
+ * isn't web-available, an optional `fallback` web stand-in is woven in:
8
+ * 'Family', 'Fallback', sans-serif
9
+ * No fallback → the original 'Family', sans-serif (backward-compatible).
10
+ */
11
+ function fontStack(font) {
12
+ if (!font || !font.family) return 'sans-serif';
13
+ // Strip control chars (a raw newline ends a CSS string) then escape
14
+ // backslash + single-quote, so a config value can't break out of the quoted
15
+ // CSS string and inject rules into the generated :root block.
16
+ function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
17
+ var stack = "'" + q(font.family) + "'";
18
+ if (font.fallback) stack += ", '" + q(font.fallback) + "'";
19
+ return stack + ', sans-serif';
20
+ }
3
21
 
4
22
  /**
5
23
  * Generate a :root CSS block from config.theme + config.fonts.
@@ -14,10 +32,10 @@ function generateRootCSS(config) {
14
32
  }
15
33
  if (config.fonts) {
16
34
  if (config.fonts.display) {
17
- lines.push(" --font-display: '" + config.fonts.display.family + "', sans-serif;");
35
+ lines.push(' --font-display: ' + fontStack(config.fonts.display) + ';');
18
36
  }
19
37
  if (config.fonts.body) {
20
- lines.push(" --font-body: '" + config.fonts.body.family + "', sans-serif;");
38
+ lines.push(' --font-body: ' + fontStack(config.fonts.body) + ';');
21
39
  }
22
40
  }
23
41
  lines.push('}');
@@ -54,15 +72,19 @@ function build(distDir, targetDir, config) {
54
72
  var builtCSS = cssTemplate + '\n\n/* Generated by brandkit build — brand overrides */\n' + rootCSS + '\n';
55
73
  fs.writeFileSync(path.join(targetDir, 'styles.css'), builtCSS);
56
74
 
57
- // Build index.html: inject fonts link + title
75
+ // Agent-native exports: brand.json (semantic), tokens.json (W3C DTCG),
76
+ // brand.md (LLM brief). brandJson is reused to embed the data in the page.
77
+ var exported = exporter.writeExports(config, targetDir, 'all');
78
+
79
+ // Build index.html: inject fonts link + title + agent-discoverable brand data
58
80
  var htmlTemplate = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8');
59
81
  var fontsLink = generateFontsLink(config);
60
82
  var brandName = (config.brand && config.brand.displayName) || 'Brand';
61
83
 
62
- // Inject fonts link before </head>
63
- if (fontsLink) {
64
- htmlTemplate = htmlTemplate.replace('</head>', fontsLink + '\n</head>');
65
- }
84
+ // Inject fonts link + a discovery <link> for the structured brand data
85
+ var headInject = (fontsLink ? fontsLink + '\n' : '') +
86
+ '<link rel="alternate" type="application/json" href="brand.json" title="Brand data">\n';
87
+ htmlTemplate = htmlTemplate.replace('</head>', headInject + '</head>');
66
88
 
67
89
  // Set title
68
90
  htmlTemplate = htmlTemplate.replace(
@@ -70,6 +92,16 @@ function build(distDir, targetDir, config) {
70
92
  '<title>' + brandName + ' \u2014 Brand Guide</title>'
71
93
  );
72
94
 
95
+ // Embed brand.json inline so an agent that fetches the page gets structured
96
+ // brand data with zero extra requests or scraping. Every "<" is escaped to
97
+ // its JSON unicode form (<, which JSON.parse restores to "<"), so no
98
+ // config value can produce a "</script" sequence and break out of the block.
99
+ if (exported.brandJson) {
100
+ var embedded = JSON.stringify(exported.brandJson).replace(/</g, '\\u003C');
101
+ var embed = '<script type="application/json" id="brandkit-brand">' + embedded + '</script>\n';
102
+ htmlTemplate = htmlTemplate.replace('</body>', embed + '</body>');
103
+ }
104
+
73
105
  fs.writeFileSync(path.join(targetDir, 'index.html'), htmlTemplate);
74
106
 
75
107
  // Copy engine.js as-is
@@ -77,6 +109,8 @@ function build(distDir, targetDir, config) {
77
109
  path.join(distDir, 'engine.js'),
78
110
  path.join(targetDir, 'engine.js')
79
111
  );
112
+
113
+ return exported;
80
114
  }
81
115
 
82
116
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.1.2",
3
+ "version": "1.2.2",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "cli/",
12
12
  "lib/",
13
13
  "dist/",
14
- "integrations/"
14
+ "integrations/",
15
+ "config.schema.json"
15
16
  ],
16
17
  "scripts": {
17
18
  "dev": "node bin/brandkit.js dev example",