@stacklist-app/brandkit 1.2.3 → 1.3.0

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
@@ -32,6 +32,7 @@ npx brandkit build brand # bake into static files for production
32
32
  | `brandkit dev [dir]` | Local dev server on `:4800` with SSE live reload |
33
33
  | `brandkit build [dir]` | Produce static `index.html` + `styles.css` + `engine.js` (theme baked in) **plus** the agent-native exports below |
34
34
  | `brandkit export [dir]` | Emit just the agent-native brand data (`--format json\|dtcg\|md\|all`, `--out <dir>`) |
35
+ | `brandkit changelog "<msg>" [dir]` | Record a revision: prepend a changelog entry and bump `brand.version` (`--lock`, `--major`, `--version X.Y`, `--date`, `--dir`) |
35
36
 
36
37
  ## Agent-readable brand data
37
38
 
@@ -47,6 +48,22 @@ And for a person who wants to hand the guide to their own agent, the sidebar sho
47
48
 
48
49
  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.
49
50
 
51
+ ## Changelog
52
+
53
+ A brand guide evolves, so brandkit keeps a history of its revisions. Record one with:
54
+
55
+ ```bash
56
+ npx brandkit changelog "Swapped the accent to teal, refreshed the logo"
57
+ ```
58
+
59
+ This prepends a `{ version, date, changes }` entry to `config.changelog` and bumps `brand.version`. A fresh guide starts at **0.1** and climbs with each change (`0.1 → 0.2 → …`); finalize the brand at **1.0** with `--lock`:
60
+
61
+ ```bash
62
+ npx brandkit changelog --lock "Brand finalized"
63
+ ```
64
+
65
+ The history renders on a standalone **`changelog.html`** page (linked from the bottom of the sidebar) and is included in `brand.json` / `brand.md` so agents see it too. `init` also scaffolds an **`AGENTS.md`** into the guide directory — a maintenance contract telling an AI agent the source of truth, to read from the exports, and to record changes with `brandkit changelog`.
66
+
50
67
  ## Framework integrations
51
68
 
52
69
  Serve the brand guide at `/brand` in your existing dev server and bundle it on build.
@@ -73,10 +90,26 @@ export default {
73
90
 
74
91
  **Next.js / plain HTML**: run `brandkit build brand` and serve the resulting directory as a static route.
75
92
 
93
+ ### Serving from a base path
94
+
95
+ brandkit emits page-relative paths, which resolve only when the guide is served from the root or a **trailing-slash** directory URL (`/brand/`). If you serve it at `/brand` with **no** trailing slash — e.g. a Next.js rewrite that maps `/brand` to the built `/brand/index.html` — every relative path resolves against `/` instead, so `styles.css`, `engine.js`, and `config.json` all 404 and the guide renders an empty shell.
96
+
97
+ Set `basePath` in `config.json` to fix this:
98
+
99
+ ```json
100
+ {
101
+ "basePath": "/brand",
102
+ "brand": { "name": "Acme" }
103
+ }
104
+ ```
105
+
106
+ On `brandkit build`, the generated `index.html` then points its stylesheet, engine, and `brand.json` link at `${basePath}/…`, and the engine fetches `${basePath}/config.json` — so the guide loads fully whether or not the serving URL has a trailing slash. The value is normalized (leading slash added, trailing slash stripped). Omitting `basePath` keeps the original page-relative behavior, so existing setups are unaffected.
107
+
76
108
  ## Config
77
109
 
78
110
  `config.json` is the single source of truth. Top-level keys:
79
111
 
112
+ - `basePath` — absolute path the guide is served from (e.g. `/brand`). Omit (or leave empty) when serving at the root or from a trailing-slash directory URL — output stays page-relative and unchanged. See [Serving from a base path](#serving-from-a-base-path).
80
113
  - `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)
81
114
  - `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)
82
115
  - `theme` — CSS variable map (colors, gradients, font vars)
package/bin/brandkit.js CHANGED
@@ -19,6 +19,9 @@ switch (command) {
19
19
  case 'export':
20
20
  require('../cli/export')(args.slice(1));
21
21
  break;
22
+ case 'changelog':
23
+ require('../cli/changelog')(args.slice(1));
24
+ break;
22
25
  case 'help':
23
26
  case '--help':
24
27
  case '-h':
@@ -45,6 +48,7 @@ function printHelp() {
45
48
  console.log(' brandkit dev [dir] Start dev server with live reload');
46
49
  console.log(' brandkit build [dir] Build static files for production');
47
50
  console.log(' brandkit export [dir] Emit agent-native brand data (brand.json, tokens.json, brand.md)');
51
+ console.log(' brandkit changelog "<msg>" Record a revision (bumps version, adds a changelog entry)');
48
52
  console.log('');
49
53
  console.log(' Options:');
50
54
  console.log(' --help, -h Show this help message');
package/cli/build.js CHANGED
@@ -28,7 +28,8 @@ module.exports = function build(args) {
28
28
  var brandName = (config.brand && config.brand.displayName) || 'Brand';
29
29
  console.log(' built index.html (' + brandName + ' title + fonts + embedded brand data)');
30
30
  console.log(' built styles.css (:root variables generated)');
31
- console.log(' copied engine.js');
31
+ console.log(' built changelog.html (' + brandName + ' title + fonts)');
32
+ console.log(' copied engine.js, changelog.js');
32
33
  if (exported && exported.written) {
33
34
  exported.written.forEach(function (f) {
34
35
  console.log(' wrote ' + f + ' (agent-native export)');
@@ -0,0 +1,172 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+
4
+ /**
5
+ * brandkit changelog — record a brand-guide revision.
6
+ *
7
+ * Prepends a `{ version, date, changes }` entry to config.changelog (newest
8
+ * first) and bumps brand.version. A fresh guide starts at 0.1; each call
9
+ * minor-bumps (0.1 → 0.2 → …); --lock jumps to 1.0 when the brand is finalized.
10
+ *
11
+ * brandkit changelog "Added gradients" "Tuned contrast"
12
+ * brandkit changelog --lock "Brand locked"
13
+ * brandkit changelog --major "v2 redesign"
14
+ * brandkit changelog --version 0.5 "Set explicitly"
15
+ * brandkit changelog --dir example "Update the demo"
16
+ */
17
+ module.exports = function changelog(args) {
18
+ var opts = parseArgs(args);
19
+
20
+ if (opts.help) {
21
+ printUsage();
22
+ return;
23
+ }
24
+
25
+ if (opts.error) {
26
+ console.error('');
27
+ console.error(' ' + opts.error);
28
+ console.error('');
29
+ printUsage();
30
+ process.exit(1);
31
+ }
32
+
33
+ var targetDir = path.resolve(opts.dir || '.');
34
+ var configPath = path.join(targetDir, 'config.json');
35
+ if (!fs.existsSync(configPath)) {
36
+ console.error('');
37
+ console.error(' No config.json found in ' + targetDir);
38
+ console.error(' Run: brandkit init ' + path.relative(process.cwd(), targetDir));
39
+ console.error('');
40
+ process.exit(1);
41
+ }
42
+
43
+ // Drop blank/whitespace-only messages so a stray empty arg can't create a
44
+ // blank bullet (and silently bump the version).
45
+ opts.messages = opts.messages.filter(function (m) { return String(m).trim() !== ''; });
46
+ if (!opts.messages.length) {
47
+ console.error('');
48
+ console.error(' Nothing to log — pass at least one change message.');
49
+ console.error(' Example: brandkit changelog "Added the gradient system"');
50
+ console.error('');
51
+ process.exit(1);
52
+ }
53
+
54
+ // Reject a malformed explicit --version so junk never lands in brand.version
55
+ // (which build/template.js consume downstream). The changelog scheme is
56
+ // strictly MAJOR.MINOR, so a patch (1.2.3) is rejected too — keeping the
57
+ // regex, the message, and parseVersion's bump logic consistent.
58
+ if (opts.version && !/^\d+\.\d+$/.test(opts.version)) {
59
+ console.error('');
60
+ console.error(' Invalid --version "' + opts.version + '" — expected MAJOR.MINOR (e.g. 0.3 or 1.0).');
61
+ console.error('');
62
+ process.exit(1);
63
+ }
64
+
65
+ var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
66
+ config.brand = config.brand || {};
67
+ if (!Array.isArray(config.changelog)) config.changelog = [];
68
+
69
+ var prevVersion = config.brand.version || '0.1';
70
+
71
+ // The scheme is MAJOR.MINOR. If the stored version isn't clean (hand-edited,
72
+ // or a 3-part value like the demo's package version), warn rather than
73
+ // silently normalizing — the bump uses only its leading major.minor.
74
+ if (!opts.version && !/^\d+\.\d+$/.test(prevVersion)) {
75
+ console.error(' Note: brand.version "' + prevVersion +
76
+ '" is not MAJOR.MINOR — bumping from its leading major.minor.');
77
+ }
78
+
79
+ // --lock finalizes a pre-1.0 brand at 1.0; refuse to move a >=1.0 brand
80
+ // backward (which would also break the newest-first version ordering).
81
+ if (opts.lock && parseVersion(prevVersion).major >= 1) {
82
+ console.error('');
83
+ console.error(' Already at v' + prevVersion + ' — the brand is locked (>= 1.0).');
84
+ console.error(' Use --major or --version X.Y to bump further.');
85
+ console.error('');
86
+ process.exit(1);
87
+ }
88
+
89
+ var nextVersion = nextVersionFor(prevVersion, opts);
90
+ var date = opts.date || new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
91
+
92
+ var entry = { version: nextVersion, date: date, changes: opts.messages.slice() };
93
+ config.changelog.unshift(entry);
94
+ config.brand.version = nextVersion;
95
+
96
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
97
+
98
+ console.log('');
99
+ console.log(' brandkit changelog');
100
+ console.log('');
101
+ console.log(' v' + prevVersion + ' → v' + nextVersion + ' · ' + opts.messages.length +
102
+ ' change' + (opts.messages.length === 1 ? '' : 's') + ' (' + date + ')');
103
+ opts.messages.forEach(function (m) {
104
+ console.log(' • ' + m);
105
+ });
106
+ console.log('');
107
+ console.log(' Updated ' + path.relative(process.cwd(), configPath));
108
+ console.log(' Run `brandkit build` (or `export`) to refresh the deployed page and agent exports.');
109
+ console.log('');
110
+ };
111
+
112
+ /**
113
+ * Decide the next version string from the previous one + flags.
114
+ * --version X.Y → explicit
115
+ * --lock → 1.0
116
+ * --major → next whole major, minor reset to 0
117
+ * default → minor + 1
118
+ */
119
+ function nextVersionFor(prev, opts) {
120
+ if (opts.version) return opts.version;
121
+ if (opts.lock) return '1.0';
122
+
123
+ var parsed = parseVersion(prev);
124
+ if (opts.major) return (parsed.major + 1) + '.0';
125
+ return parsed.major + '.' + (parsed.minor + 1);
126
+ }
127
+
128
+ // "0.3" → { major: 0, minor: 3 }; tolerant of junk → { 0, 1 }.
129
+ function parseVersion(v) {
130
+ var m = String(v == null ? '' : v).match(/^(\d+)\.(\d+)/);
131
+ if (!m) return { major: 0, minor: 1 };
132
+ return { major: parseInt(m[1], 10), minor: parseInt(m[2], 10) };
133
+ }
134
+
135
+ function parseArgs(args) {
136
+ var opts = { messages: [], dir: '.', lock: false, major: false, version: null, date: null, help: false, error: null };
137
+ var valueFlags = { '--version': 'version', '--date': 'date', '--dir': 'dir' };
138
+ for (var i = 0; i < args.length; i++) {
139
+ var a = args[i];
140
+ if (a === '--help' || a === '-h') opts.help = true;
141
+ else if (a === '--lock') opts.lock = true;
142
+ else if (a === '--major') opts.major = true;
143
+ else if (valueFlags[a]) {
144
+ // A value flag at the end of argv would otherwise read undefined and be
145
+ // silently ignored — error instead.
146
+ if (i + 1 >= args.length) { opts.error = a + ' requires a value.'; break; }
147
+ opts[valueFlags[a]] = args[++i];
148
+ }
149
+ // Reject an unknown --flag (e.g. a "--majro" typo) rather than recording it
150
+ // as a changelog message. Single-dash args pass through so a message can
151
+ // still start with "-" (e.g. "-20% load time").
152
+ else if (a.indexOf('--') === 0) { opts.error = 'Unknown option: ' + a; break; }
153
+ else opts.messages.push(a);
154
+ }
155
+ return opts;
156
+ }
157
+
158
+ function printUsage() {
159
+ console.log('');
160
+ console.log(' brandkit changelog — record a brand-guide revision');
161
+ console.log('');
162
+ console.log(' Usage:');
163
+ console.log(' brandkit changelog "<message>" ["<message>" ...]');
164
+ console.log('');
165
+ console.log(' Options:');
166
+ console.log(' --lock Mark the brand as finalized (version → 1.0)');
167
+ console.log(' --major Bump the major version (e.g. 1.4 → 2.0)');
168
+ console.log(' --version X.Y Set the version explicitly');
169
+ console.log(' --date "<text>" Override the entry date (default: this month)');
170
+ console.log(' --dir <path> Target guide directory (default: .)');
171
+ console.log('');
172
+ }
package/cli/init.js CHANGED
@@ -2,6 +2,7 @@ var fs = require('fs');
2
2
  var path = require('path');
3
3
  var resolve = require('../lib/resolve');
4
4
  var schema = require('../lib/config-schema');
5
+ var agentsDoc = require('../lib/agents-doc').agentsDoc;
5
6
 
6
7
  module.exports = function init(args) {
7
8
  var targetDir = path.resolve(args[0] || 'brand');
@@ -27,7 +28,7 @@ module.exports = function init(args) {
27
28
  var isUpdate = args.indexOf('--update') !== -1;
28
29
 
29
30
  // Copy engine files from dist/
30
- var files = ['engine.js', 'styles.css', 'index.html'];
31
+ var files = ['engine.js', 'styles.css', 'index.html', 'changelog.html', 'changelog.js'];
31
32
  for (var i = 0; i < files.length; i++) {
32
33
  var src = path.join(distDir, files[i]);
33
34
  var dest = path.join(targetDir, files[i]);
@@ -65,6 +66,16 @@ module.exports = function init(args) {
65
66
  console.log(' kept config.json (already exists)');
66
67
  }
67
68
 
69
+ // Maintenance contract for agents/humans working in this dir — written when
70
+ // absent (so older guides pick it up on --update) and never clobbered.
71
+ var agentsPath = path.join(targetDir, 'AGENTS.md');
72
+ if (!fs.existsSync(agentsPath)) {
73
+ fs.writeFileSync(agentsPath, agentsDoc());
74
+ console.log(' created AGENTS.md');
75
+ } else {
76
+ console.log(' kept AGENTS.md (already exists)');
77
+ }
78
+
68
79
  console.log('');
69
80
  if (isUpdate) {
70
81
  console.log(' Updated engine files. Config and logos preserved.');
@@ -37,6 +37,7 @@
37
37
  }
38
38
  },
39
39
  "properties": {
40
+ "basePath": { "type": "string", "description": "Absolute base path the guide is served from (e.g. \"/brand\"). Empty = served at root / relative. Set this when serving from a non-trailing-slash URL (e.g. a framework rewrite)." },
40
41
  "brand": {
41
42
  "type": "object",
42
43
  "required": ["name"],
@@ -250,6 +251,18 @@
250
251
  "cards": { "type": "array" },
251
252
  "stats": { "type": "array" }
252
253
  }
254
+ },
255
+ "changelog": {
256
+ "type": "array",
257
+ "description": "Revision history for the brand guide, newest entry first. Rendered on the standalone changelog.html page and surfaced in the agent exports. Maintain it with `brandkit changelog`. The latest entry's version should match brand.version.",
258
+ "items": {
259
+ "type": "object",
260
+ "properties": {
261
+ "version": { "type": "string", "description": "Revision label, e.g. '0.2'. Starts at 0.1 on a fresh guide; 1.0 = locked/finalized." },
262
+ "date": { "type": "string", "description": "Human-readable date, e.g. 'June 2026'." },
263
+ "changes": { "type": "array", "items": { "type": "string" }, "description": "Bullet list of what changed in this revision." }
264
+ }
265
+ }
253
266
  }
254
267
  }
255
268
  }
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Changelog</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+
11
+ <div class="changelog-page">
12
+
13
+ <!-- Header -->
14
+ <div class="changelog-header">
15
+ <a class="changelog-back" href="index.html">&larr; Back to guide</a>
16
+ <div class="changelog-brand" id="changelog-brand"></div>
17
+ <h1 class="changelog-title">Changelog</h1>
18
+ <p class="changelog-lead" id="changelog-lead"></p>
19
+ </div>
20
+
21
+ <!-- Entries -->
22
+ <div class="changelog-list" id="changelog-list"></div>
23
+
24
+ <!-- Footer -->
25
+ <div class="changelog-footer" id="changelog-footer"></div>
26
+
27
+ </div>
28
+
29
+ <!-- Render script -->
30
+ <script src="changelog.js"></script>
31
+
32
+ </body>
33
+ </html>
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Brandkit Changelog Page
3
+ * Loads config.json → bootstraps theme → renders the version history.
4
+ *
5
+ * A small standalone companion to engine.js: it reuses the same config, the
6
+ * same theme/font bootstrap, and the same escaping, but renders only the
7
+ * changelog (cfg.changelog) so the guide can ship a dedicated history page.
8
+ */
9
+ (function () {
10
+ // Base path the page is served from. The build injects
11
+ // window.__BRANDKIT_BASE__ when config.basePath is set (e.g. "/brand"), so a
12
+ // page served from a non-trailing-slash URL still resolves config.json.
13
+ var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
+ ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
+
16
+ fetch(BASE + '/config.json')
17
+ .then(function (res) { return res.json(); })
18
+ .then(function (config) { init(config); })
19
+ .catch(function () { renderError(); });
20
+
21
+ /* ================================================================
22
+ Helpers (kept in sync with dist/engine.js — no module system here)
23
+ ================================================================ */
24
+ function esc(s) {
25
+ return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
27
+ }
28
+
29
+ function fontStack(f) {
30
+ if (!f || !f.family) return 'sans-serif';
31
+ function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
32
+ var stack = "'" + q(f.family) + "'";
33
+ if (f.fallback) stack += ", '" + q(f.fallback) + "'";
34
+ return stack + ', sans-serif';
35
+ }
36
+
37
+ /* ================================================================
38
+ Bootstrap — fonts + theme variables, mirroring engine.js
39
+ ================================================================ */
40
+ function bootstrap(cfg) {
41
+ if (cfg.fonts) {
42
+ var families = [];
43
+ if (cfg.fonts.display && cfg.fonts.display.googleImport) families.push(cfg.fonts.display.googleImport);
44
+ if (cfg.fonts.body && cfg.fonts.body.googleImport) families.push(cfg.fonts.body.googleImport);
45
+ if (families.length) {
46
+ var link = document.createElement('link');
47
+ link.rel = 'stylesheet';
48
+ link.href = 'https://fonts.googleapis.com/css2?family=' + families.join('&family=') + '&display=swap';
49
+ document.head.appendChild(link);
50
+ }
51
+ }
52
+
53
+ if (cfg.theme) {
54
+ var vars = [];
55
+ var keys = Object.keys(cfg.theme);
56
+ for (var i = 0; i < keys.length; i++) {
57
+ vars.push(' ' + keys[i] + ': ' + cfg.theme[keys[i]] + ';');
58
+ }
59
+ if (cfg.fonts) {
60
+ if (cfg.fonts.display) vars.push(' --font-display: ' + fontStack(cfg.fonts.display) + ';');
61
+ if (cfg.fonts.body) vars.push(' --font-body: ' + fontStack(cfg.fonts.body) + ';');
62
+ }
63
+ var style = document.createElement('style');
64
+ style.setAttribute('data-brandkit-theme', '');
65
+ style.textContent = ':root {\n' + vars.join('\n') + '\n}';
66
+ document.head.appendChild(style);
67
+ }
68
+
69
+ var name = cfg.brand && (cfg.brand.displayName || cfg.brand.name);
70
+ document.title = (name ? name + ' — ' : '') + 'Changelog';
71
+ }
72
+
73
+ /* ================================================================
74
+ Render
75
+ ================================================================ */
76
+ function init(cfg) {
77
+ bootstrap(cfg);
78
+ var brand = cfg.brand || {};
79
+
80
+ var brandEl = document.getElementById('changelog-brand');
81
+ if (brandEl) brandEl.textContent = brand.displayName || brand.name || 'Brand';
82
+
83
+ var leadEl = document.getElementById('changelog-lead');
84
+ if (leadEl) {
85
+ leadEl.textContent = 'A history of every revision to this brand guide — newest first.';
86
+ }
87
+
88
+ var list = document.getElementById('changelog-list');
89
+ if (!list) return;
90
+
91
+ var entries = cfg.changelog || [];
92
+ if (!entries.length) {
93
+ list.innerHTML = '<div class="changelog-empty">No changelog entries yet. ' +
94
+ 'Run <code>brandkit changelog "your update"</code> to add one.</div>';
95
+ return;
96
+ }
97
+
98
+ list.innerHTML = entries.map(function (entry) {
99
+ var changes = (entry.changes || []).map(function (c) {
100
+ return '<li>' + esc(c) + '</li>';
101
+ }).join('');
102
+ return (
103
+ '<div class="changelog-entry">' +
104
+ '<div class="changelog-entry-head">' +
105
+ '<span class="changelog-version">v' + esc(entry.version) + '</span>' +
106
+ (entry.date ? '<span class="changelog-date">' + esc(entry.date) + '</span>' : '') +
107
+ '</div>' +
108
+ (changes ? '<ul class="changelog-changes">' + changes + '</ul>' : '') +
109
+ '</div>'
110
+ );
111
+ }).join('');
112
+
113
+ var footer = document.getElementById('changelog-footer');
114
+ if (footer) {
115
+ var label = brand.guideLabel || 'Web Style Guide';
116
+ footer.innerHTML = esc((brand.url || '') + (brand.url && brand.byline ? ' · ' : '') + (brand.byline || '')) +
117
+ '<br>' + esc(label) + (brand.version ? ' v' + esc(brand.version) : '');
118
+ }
119
+ }
120
+
121
+ function renderError() {
122
+ var list = document.getElementById('changelog-list');
123
+ if (list) {
124
+ list.innerHTML = '<div class="changelog-empty">Could not load config.json. ' +
125
+ 'The changelog page needs to be served over HTTP (not opened from a file).</div>';
126
+ }
127
+ }
128
+ })();
package/dist/engine.js CHANGED
@@ -6,7 +6,13 @@
6
6
  /* ================================================================
7
7
  0. Load config
8
8
  ================================================================ */
9
- var res = await fetch('./config.json');
9
+ // Base path the guide is served from. The build injects
10
+ // window.__BRANDKIT_BASE__ when config.basePath is set (e.g. "/brand"), so a
11
+ // guide served from a non-trailing-slash URL still resolves its assets.
12
+ // Default '.' keeps the original page-relative behavior (dev + root serving).
13
+ var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
+ ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
+ var res = await fetch(BASE + '/config.json');
10
16
  var config = await res.json();
11
17
  init(config);
12
18
 
@@ -139,7 +145,10 @@
139
145
  // Clear any userinfo so basic-auth creds in the page URL don't leak into
140
146
  // the copied prompt (a relative path already drops the page query/hash).
141
147
  function exportUrl(file) {
142
- var u = new URL(file, location.href);
148
+ // When served from a base path, resolve against it so the prompt
149
+ // carries the real export URLs (page URL may have no trailing slash).
150
+ var ref = (BASE && BASE !== '.') ? BASE + '/' + file : file;
151
+ var u = new URL(ref, location.href);
143
152
  u.username = ''; u.password = '';
144
153
  return u.href;
145
154
  }
@@ -174,6 +183,29 @@
174
183
  box.hidden = false;
175
184
  }
176
185
 
186
+ /* ==============================================================
187
+ 2c. Render changelog link — pinned to the bottom of the sidebar when
188
+ a changelog exists, pointing at the standalone changelog.html page.
189
+ ============================================================== */
190
+ function renderChangelogLink() {
191
+ var link = document.getElementById('sidebar-changelog');
192
+ if (!link) return;
193
+ if (!cfg.changelog || !cfg.changelog.length) return;
194
+
195
+ // Resolve against BASE so the link works when the guide is served from a
196
+ // non-trailing-slash base path (e.g. /brand → /brand/index.html).
197
+ var href = (BASE && BASE !== '.') ? BASE + '/changelog.html' : 'changelog.html';
198
+ link.setAttribute('href', href);
199
+ link.innerHTML =
200
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
201
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 2"/>' +
202
+ '<circle cx="12" cy="12" r="9"/>' +
203
+ '</svg>' +
204
+ '<span class="sidebar-changelog-label">Changelog</span>' +
205
+ '<span class="sidebar-changelog-version">v' + esc(cfg.brand && cfg.brand.version) + '</span>';
206
+ link.hidden = false;
207
+ }
208
+
177
209
  /* ==============================================================
178
210
  3. Render colors
179
211
  ============================================================== */
@@ -945,6 +977,7 @@
945
977
  renderShell();
946
978
  renderNav();
947
979
  renderAgentCallout();
980
+ renderChangelogLink();
948
981
  renderSectionIntros();
949
982
  renderColors();
950
983
  renderGradients();
package/dist/index.html CHANGED
@@ -15,6 +15,7 @@
15
15
  <div class="sidebar-brand">brandkit</div>
16
16
  <ul class="sidebar-nav" id="nav"></ul>
17
17
  <div class="sidebar-agent" id="agent-callout" hidden></div>
18
+ <a class="sidebar-changelog" id="sidebar-changelog" href="changelog.html" hidden></a>
18
19
  </nav>
19
20
 
20
21
  <!-- Main Content -->
package/dist/styles.css CHANGED
@@ -94,6 +94,9 @@ body {
94
94
  background: var(--white);
95
95
  border-right: 1px solid var(--mist);
96
96
  z-index: 100;
97
+ /* Column layout so the changelog link can pin to the bottom (margin-top:auto) */
98
+ display: flex;
99
+ flex-direction: column;
97
100
  }
98
101
 
99
102
  .sidebar-brand {
@@ -167,6 +170,34 @@ body {
167
170
  .sidebar-agent-btn:active { transform: translateY(1px); }
168
171
  .sidebar-agent-btn:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
169
172
 
173
+ /* Changelog link — pinned to the bottom of the sidebar column */
174
+ /* The author `display:flex` below would override the UA `[hidden]{display:none}`
175
+ (author beats UA), leaving an empty bordered box when JS hasn't un-hidden it
176
+ (no changelog, or before the engine runs) — so restore hidden explicitly. */
177
+ .sidebar-changelog[hidden] { display: none; }
178
+ .sidebar-changelog {
179
+ margin: auto 16px 4px; /* auto top pushes it to the bottom */
180
+ padding-top: 16px;
181
+ border-top: 1px solid var(--mist);
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 8px;
185
+ font-size: 12px;
186
+ font-weight: 500;
187
+ color: var(--slate);
188
+ text-decoration: none;
189
+ transition: color 150ms ease;
190
+ }
191
+ .sidebar-changelog:hover { color: var(--accent-text); }
192
+ .sidebar-changelog svg { width: 14px; height: 14px; flex: none; }
193
+ .sidebar-changelog-label { flex: 1; }
194
+ .sidebar-changelog-version {
195
+ font-size: 11px;
196
+ color: var(--haze);
197
+ font-variant-numeric: tabular-nums;
198
+ }
199
+ .sidebar-changelog:hover .sidebar-changelog-version { color: var(--slate); }
200
+
170
201
  /* ── Sidebar Grouped Navigation ── */
171
202
  .nav-group {
172
203
  padding: 20px 24px 6px;
@@ -1097,6 +1128,150 @@ body {
1097
1128
  justify-content: space-between;
1098
1129
  }
1099
1130
 
1131
+ /* ── Changelog page (standalone changelog.html) ── */
1132
+ .changelog-page {
1133
+ max-width: 760px;
1134
+ margin: 0 auto;
1135
+ padding: 72px 32px 96px;
1136
+ }
1137
+
1138
+ .changelog-header { margin-bottom: 56px; }
1139
+
1140
+ .changelog-back {
1141
+ display: inline-block;
1142
+ font-size: 13px;
1143
+ font-weight: 500;
1144
+ color: var(--slate);
1145
+ text-decoration: none;
1146
+ margin-bottom: 32px;
1147
+ transition: color 150ms ease;
1148
+ }
1149
+
1150
+ .changelog-back:hover { color: var(--accent-text); }
1151
+
1152
+ .changelog-brand {
1153
+ font-family: var(--font-body), sans-serif;
1154
+ font-size: 11px;
1155
+ font-weight: 700;
1156
+ letter-spacing: 0.12em;
1157
+ text-transform: uppercase;
1158
+ color: var(--slate);
1159
+ margin-bottom: 12px;
1160
+ }
1161
+
1162
+ .changelog-title {
1163
+ font-family: var(--font-display), sans-serif;
1164
+ font-size: 44px;
1165
+ font-weight: 700;
1166
+ letter-spacing: -0.02em;
1167
+ line-height: 1.1;
1168
+ color: var(--ink);
1169
+ margin: 0 0 12px;
1170
+ }
1171
+
1172
+ .changelog-lead {
1173
+ font-size: 15px;
1174
+ line-height: 1.7;
1175
+ color: var(--graphite);
1176
+ margin: 0;
1177
+ }
1178
+
1179
+ /* Each release — a vertical timeline with the version "ticked" on the rail */
1180
+ .changelog-list {
1181
+ border-left: 2px solid var(--mist);
1182
+ padding-left: 32px;
1183
+ }
1184
+
1185
+ .changelog-entry {
1186
+ position: relative;
1187
+ padding-bottom: 40px;
1188
+ }
1189
+
1190
+ .changelog-entry:last-child { padding-bottom: 0; }
1191
+
1192
+ .changelog-entry::before {
1193
+ content: "";
1194
+ position: absolute;
1195
+ left: -41px;
1196
+ top: 6px;
1197
+ width: 10px;
1198
+ height: 10px;
1199
+ border-radius: 50%;
1200
+ background: var(--accent);
1201
+ box-shadow: 0 0 0 4px var(--cloud);
1202
+ }
1203
+
1204
+ .changelog-entry-head {
1205
+ display: flex;
1206
+ align-items: baseline;
1207
+ gap: 12px;
1208
+ margin-bottom: 14px;
1209
+ }
1210
+
1211
+ .changelog-version {
1212
+ font-family: var(--font-display), sans-serif;
1213
+ font-size: 20px;
1214
+ font-weight: 700;
1215
+ letter-spacing: -0.01em;
1216
+ color: var(--accent-text);
1217
+ }
1218
+
1219
+ .changelog-date {
1220
+ font-size: 13px;
1221
+ font-weight: 500;
1222
+ color: var(--slate);
1223
+ }
1224
+
1225
+ .changelog-changes {
1226
+ margin: 0;
1227
+ padding: 0;
1228
+ list-style: none;
1229
+ }
1230
+
1231
+ .changelog-changes li {
1232
+ position: relative;
1233
+ padding-left: 20px;
1234
+ margin-bottom: 8px;
1235
+ font-size: 15px;
1236
+ line-height: 1.6;
1237
+ color: var(--graphite);
1238
+ }
1239
+
1240
+ .changelog-changes li::before {
1241
+ content: "";
1242
+ position: absolute;
1243
+ left: 2px;
1244
+ top: 9px;
1245
+ width: 5px;
1246
+ height: 5px;
1247
+ border-radius: 50%;
1248
+ background: var(--haze);
1249
+ }
1250
+
1251
+ .changelog-empty {
1252
+ font-size: 15px;
1253
+ line-height: 1.7;
1254
+ color: var(--slate);
1255
+ }
1256
+
1257
+ .changelog-empty code {
1258
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1259
+ font-size: 13px;
1260
+ background: var(--mist);
1261
+ padding: 2px 6px;
1262
+ border-radius: 6px;
1263
+ color: var(--ink);
1264
+ }
1265
+
1266
+ .changelog-footer {
1267
+ margin-top: 64px;
1268
+ padding-top: 28px;
1269
+ border-top: 1px solid var(--mist);
1270
+ font-size: 12px;
1271
+ line-height: 1.6;
1272
+ color: var(--slate);
1273
+ }
1274
+
1100
1275
  /* ── Print ── */
1101
1276
  @media print {
1102
1277
  .sidebar, .copy-format-bar, .toast-container { display: none !important; }
@@ -1113,4 +1288,6 @@ body {
1113
1288
  .card-demo-grid, .dark-card-demo-grid, .a11y-grid { grid-template-columns: 1fr; }
1114
1289
  .voice-grid, .gradient-usage, .wordmark-display, .logo-grid { grid-template-columns: 1fr; }
1115
1290
  .type-row { grid-template-columns: 60px 1fr 140px; }
1291
+ .changelog-page { padding: 48px 22px 64px; }
1292
+ .changelog-title { font-size: 34px; }
1116
1293
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The AGENTS.md scaffolded into a brand-guide directory by `brandkit init`.
3
+ *
4
+ * It is a maintenance contract for AI agents (and humans) working in the host
5
+ * project's brand directory: where the source of truth is, how to read the
6
+ * brand, and — crucially — to record changes in the changelog. It is brand-
7
+ * agnostic (it points at config.json for specifics), so it never goes stale.
8
+ */
9
+ function agentsDoc() {
10
+ return [
11
+ '# Working on this brand guide (for AI agents)',
12
+ '',
13
+ 'This directory is a **[brandkit](https://github.com/The-Stack-Lab/brandkit) brand guide**.',
14
+ '`config.json` is the single source of truth — it drives the rendered guide (`index.html`) and',
15
+ 'the machine-readable exports below.',
16
+ '',
17
+ '## Read the brand from the exports, not the page',
18
+ '',
19
+ '- `brand.json` — full structured brand: colors (roles + contrast), typography, voice, logos, spacing',
20
+ '- `tokens.json` — W3C design tokens (DTCG `$type`/`$value`) for code and design tooling',
21
+ '- `brand.md` — a brief on how to stay on-brand',
22
+ '',
23
+ 'These are regenerated by `brandkit build` / `brandkit export`. If they look stale, rebuild.',
24
+ '',
25
+ '## When you change the brand, record it in the changelog',
26
+ '',
27
+ 'Whenever you edit the brand — colors, type, logos, voice, spacing, or anything in `config.json` —',
28
+ 'log what changed so the guide keeps an accurate history:',
29
+ '',
30
+ '```bash',
31
+ 'npx brandkit changelog "what you changed" # e.g. "Swapped accent to teal, refreshed the logo"',
32
+ '```',
33
+ '',
34
+ "This prepends an entry to `config.json`'s `changelog` and bumps `brand.version`.",
35
+ '',
36
+ '**Versioning:** a new guide starts at **0.1** and climbs with each change (0.1 → 0.2 → …). When',
37
+ 'the brand is finalized, lock it at **1.0**:',
38
+ '',
39
+ '```bash',
40
+ 'npx brandkit changelog --lock "Brand finalized"',
41
+ '```',
42
+ '',
43
+ 'The changelog renders on its own page (`changelog.html`) and is included in `brand.json` / `brand.md`.',
44
+ '',
45
+ '## Commands',
46
+ '',
47
+ '```bash',
48
+ 'npx brandkit dev . # preview at http://localhost:4800 (live reload)',
49
+ 'npx brandkit build . # build static files + exports for deploy',
50
+ 'npx brandkit export . # regenerate brand.json / tokens.json / brand.md',
51
+ 'npx brandkit changelog "…" # record a brand change (see above)',
52
+ '```',
53
+ '',
54
+ '## Rules',
55
+ '',
56
+ '- Edit `config.json` — do **not** hand-edit `index.html`, `styles.css`, `engine.js`, or',
57
+ ' `changelog.html`. They are universal and overwritten on every rebuild.',
58
+ '- After changing `config.json`: run `brandkit changelog "…"`, then `brandkit build` (or `export`).',
59
+ ''
60
+ ].join('\n');
61
+ }
62
+
63
+ module.exports = { agentsDoc: agentsDoc };
@@ -19,7 +19,10 @@ function starterConfig() {
19
19
  description: 'Brandkit turns a single config.json into a complete, living brand guide — colors, typography, logos, voice, and components, all driven by design tokens. This is the default guide; swap the config and assets to make it your own.',
20
20
  url: 'github.com/The-Stack-Lab/brandkit',
21
21
  byline: 'Open source · MIT',
22
- version: '1.0',
22
+ // A fresh guide starts at 0.1 and climbs as it's built out; lock it at 1.0
23
+ // when the brand is finalized (`brandkit changelog --lock`). The latest
24
+ // changelog entry's version mirrors this.
25
+ version: '0.1',
23
26
  date: new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),
24
27
  // Renames the "Web Style Guide" label in the header + footer.
25
28
  guideLabel: 'Web Style Guide',
@@ -230,7 +233,17 @@ function starterConfig() {
230
233
  { value: '0', label: 'Runtime dependencies' },
231
234
  { value: '12', label: 'Sections, rendered from tokens' }
232
235
  ]
233
- }
236
+ },
237
+ // Revision history, newest first — shown on changelog.html and exported for
238
+ // agents. A fresh guide starts with this single 0.1 entry; add more with
239
+ // `brandkit changelog "what changed"` as the brand evolves.
240
+ changelog: [
241
+ {
242
+ version: '0.1',
243
+ date: new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),
244
+ changes: ['Initial brand guide scaffolded with brandkit.']
245
+ }
246
+ ]
234
247
  };
235
248
  }
236
249
 
package/lib/export.js CHANGED
@@ -154,6 +154,13 @@ function buildBrandJson(config) {
154
154
  pair: (a.fgName || a.fg || '') + ' on ' + (a.bgName || a.bg || ''),
155
155
  fg: a.fg, bg: a.bg, ratio: a.ratio, rating: a.rating
156
156
  };
157
+ }),
158
+ changelog: (config.changelog || []).map(function (e) {
159
+ return prune({
160
+ version: e.version || null,
161
+ date: e.date || null,
162
+ changes: e.changes || []
163
+ });
157
164
  })
158
165
  };
159
166
 
@@ -328,6 +335,30 @@ function buildBrandMarkdown(config) {
328
335
  }
329
336
  }
330
337
 
338
+ // Changelog — emitted unconditionally so an agent reading this brief always
339
+ // learns the maintenance rule, then the history (if any). brand.md is the
340
+ // file agents are pointed at to "stay on-brand", so the instruction lives here.
341
+ L.push(''); L.push('## Changelog');
342
+ L.push('When you change this brand — colors, type, logos, voice, spacing, or anything in ' +
343
+ '`config.json` — record it so the guide keeps an accurate history: run ' +
344
+ '`brandkit changelog "what changed"`. It prepends an entry and bumps the version (a fresh ' +
345
+ 'guide starts at 0.1 and climbs; add `--lock` to finalize the brand at 1.0).');
346
+ if (config.changelog && config.changelog.length) {
347
+ L.push(''); L.push('Revision history (newest first):'); L.push('');
348
+ config.changelog.forEach(function (e) {
349
+ var head = '- **v' + (e.version || '?') + '**' + (e.date ? ' (' + e.date + ')' : '');
350
+ var changes = (e.changes || []).filter(Boolean);
351
+ if (changes.length <= 1) {
352
+ // Single change reads cleanly inline.
353
+ L.push(head + (changes.length ? ': ' + changes[0] : ''));
354
+ } else {
355
+ // Multiple changes → nested bullets, so each keeps its own punctuation.
356
+ L.push(head + ':');
357
+ changes.forEach(function (c) { L.push(' - ' + c); });
358
+ }
359
+ });
360
+ }
361
+
331
362
  L.push('');
332
363
  return L.join('\n');
333
364
  }
package/lib/template.js CHANGED
@@ -42,6 +42,32 @@ function generateRootCSS(config) {
42
42
  return lines.join('\n');
43
43
  }
44
44
 
45
+ /**
46
+ * Escape a string for safe interpolation inside a double-quoted HTML attribute.
47
+ * A legitimate URL path never contains these chars, so real basePaths pass
48
+ * through unchanged; a hostile value can't break out of the attribute.
49
+ */
50
+ function escapeAttr(s) {
51
+ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;')
52
+ .replace(/</g, '&lt;').replace(/>/g, '&gt;');
53
+ }
54
+
55
+ /**
56
+ * Normalize config.basePath into an absolute, no-trailing-slash prefix.
57
+ * "" / undefined / "/" → "" (served at root, page-relative — unchanged).
58
+ * "brand" or "/brand/" → "/brand".
59
+ */
60
+ function normalizeBasePath(bp) {
61
+ if (!bp || typeof bp !== 'string') return '';
62
+ var s = bp.trim();
63
+ if (!s) return '';
64
+ // Exactly one leading slash: collapse "//brand" (a protocol-relative URL —
65
+ // browsers read it as a cross-origin host) and "brand" alike to "/brand".
66
+ s = '/' + s.replace(/^\/+/, '');
67
+ s = s.replace(/\/+$/, '');
68
+ return s;
69
+ }
70
+
45
71
  /**
46
72
  * Generate a Google Fonts <link> tag from config.fonts.
47
73
  */
@@ -64,6 +90,11 @@ function generateFontsLink(config) {
64
90
  * Writes index.html and styles.css into the target directory.
65
91
  */
66
92
  function build(distDir, targetDir, config) {
93
+ // Absolute base path the guide is served from (e.g. "/brand"). Empty = root /
94
+ // page-relative, in which case nothing below is rewritten (byte-identical to
95
+ // the pre-basePath output). The "/" + file form (no trailing slash on basePath)
96
+ // is what lets a framework rewrite like /brand → /brand/index.html resolve.
97
+ var basePath = normalizeBasePath(config.basePath);
67
98
  // Build styles.css: append the config :root AFTER the stylesheet's default
68
99
  // tokens so brand overrides win the cascade (styles.css ships a baseline
69
100
  // :root; an earlier block would be overridden by it).
@@ -81,15 +112,41 @@ function build(distDir, targetDir, config) {
81
112
  var fontsLink = generateFontsLink(config);
82
113
  var brandName = (config.brand && config.brand.displayName) || 'Brand';
83
114
 
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>');
115
+ // Inject fonts link + a discovery <link> for the structured brand data.
116
+ // When a basePath is set, prefix the brandkit-generated refs (stylesheet,
117
+ // engine, brand.json) so they resolve from a non-trailing-slash URL; also
118
+ // expose the base to the runtime engine via window.__BRANDKIT_BASE__.
119
+ // The Google Fonts link and author-supplied logo paths are left untouched.
120
+ // basePath is author-controlled config, so it's hardened before it lands in
121
+ // markup, each context with its own escaping:
122
+ // - inline <script>: JSON.stringify + "<" → <, so a "</script>" in the
123
+ // value can't break out of the script element (JS-string context).
124
+ // - href/src attributes: HTML-attribute-escaped (basePathAttr), so a stray
125
+ // quote can't break out of the attribute and inject a handler.
126
+ // - all .replace() calls below use function replacers, so a "$" in the path
127
+ // isn't interpreted as a $-replacement pattern.
128
+ var basePathLiteral = JSON.stringify(basePath).replace(/</g, '\\u003C');
129
+ var basePathAttr = escapeAttr(basePath);
130
+ var brandJsonHref = basePath ? basePathAttr + '/brand.json' : 'brand.json';
131
+ var headInject = (basePath ? '<script>window.__BRANDKIT_BASE__ = ' + basePathLiteral + ';</script>\n' : '') +
132
+ (fontsLink ? fontsLink + '\n' : '') +
133
+ '<link rel="alternate" type="application/json" href="' + brandJsonHref + '" title="Brand data">\n';
134
+ htmlTemplate = htmlTemplate.replace('</head>', function () { return headInject + '</head>'; });
88
135
 
89
- // Set title
136
+ if (basePath) {
137
+ // Tolerant of quote style / whitespace around "=" so a future reformat of
138
+ // the template doesn't silently no-op and ship broken (root-relative) refs.
139
+ htmlTemplate = htmlTemplate
140
+ .replace(/href\s*=\s*["']styles\.css["']/, function () { return 'href="' + basePathAttr + '/styles.css"'; })
141
+ .replace(/src\s*=\s*["']engine\.js["']/, function () { return 'src="' + basePathAttr + '/engine.js"'; });
142
+ }
143
+
144
+ // Set title \u2014 escape the brand name so a hostile displayName can't break out
145
+ // of the <title> element (e.g. "</title><script>\u2026").
146
+ var brandNameHtml = escapeAttr(brandName);
90
147
  htmlTemplate = htmlTemplate.replace(
91
148
  '<title>Brand Guide</title>',
92
- '<title>' + brandName + ' \u2014 Brand Guide</title>'
149
+ '<title>' + brandNameHtml + ' \u2014 Brand Guide</title>'
93
150
  );
94
151
 
95
152
  // Embed brand.json inline so an agent that fetches the page gets structured
@@ -104,11 +161,34 @@ function build(distDir, targetDir, config) {
104
161
 
105
162
  fs.writeFileSync(path.join(targetDir, 'index.html'), htmlTemplate);
106
163
 
107
- // Copy engine.js as-is
164
+ // Build the standalone changelog page the same way: inject the base script +
165
+ // fonts link + title, and (when a basePath is set) prefix its own asset refs
166
+ // so it resolves them from a non-trailing-slash URL. It renders from
167
+ // config.json at runtime (like index.html), so no data is embedded here.
168
+ var changelogTemplate = fs.readFileSync(path.join(distDir, 'changelog.html'), 'utf8');
169
+ var changelogHead = (basePath ? '<script>window.__BRANDKIT_BASE__ = ' + basePathLiteral + ';</script>\n' : '') +
170
+ (fontsLink ? fontsLink + '\n' : '');
171
+ changelogTemplate = changelogTemplate.replace('</head>', function () { return changelogHead + '</head>'; });
172
+ if (basePath) {
173
+ changelogTemplate = changelogTemplate
174
+ .replace(/href\s*=\s*["']styles\.css["']/, function () { return 'href="' + basePathAttr + '/styles.css"'; })
175
+ .replace(/src\s*=\s*["']changelog\.js["']/, function () { return 'src="' + basePathAttr + '/changelog.js"'; });
176
+ }
177
+ changelogTemplate = changelogTemplate.replace(
178
+ '<title>Changelog</title>',
179
+ '<title>' + brandNameHtml + ' — Changelog</title>'
180
+ );
181
+ fs.writeFileSync(path.join(targetDir, 'changelog.html'), changelogTemplate);
182
+
183
+ // Copy engine.js + changelog.js as-is
108
184
  fs.copyFileSync(
109
185
  path.join(distDir, 'engine.js'),
110
186
  path.join(targetDir, 'engine.js')
111
187
  );
188
+ fs.copyFileSync(
189
+ path.join(distDir, 'changelog.js'),
190
+ path.join(targetDir, 'changelog.js')
191
+ );
112
192
 
113
193
  return exported;
114
194
  }
@@ -116,5 +196,6 @@ function build(distDir, targetDir, config) {
116
196
  module.exports = {
117
197
  generateRootCSS: generateRootCSS,
118
198
  generateFontsLink: generateFontsLink,
199
+ normalizeBasePath: normalizeBasePath,
119
200
  build: build
120
201
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {