@stacklist-app/brandkit 1.2.4 → 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 +17 -0
- package/bin/brandkit.js +4 -0
- package/cli/build.js +2 -1
- package/cli/changelog.js +172 -0
- package/cli/init.js +12 -1
- package/config.schema.json +12 -0
- package/dist/changelog.html +33 -0
- package/dist/changelog.js +128 -0
- package/dist/engine.js +24 -0
- package/dist/index.html +1 -0
- package/dist/styles.css +177 -0
- package/lib/agents-doc.js +63 -0
- package/lib/config-schema.js +15 -2
- package/lib/export.js +31 -0
- package/lib/template.js +28 -3
- package/package.json +1 -1
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.
|
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('
|
|
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)');
|
package/cli/changelog.js
ADDED
|
@@ -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.');
|
package/config.schema.json
CHANGED
|
@@ -251,6 +251,18 @@
|
|
|
251
251
|
"cards": { "type": "array" },
|
|
252
252
|
"stats": { "type": "array" }
|
|
253
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
|
+
}
|
|
254
266
|
}
|
|
255
267
|
}
|
|
256
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">← 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, '&').replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
@@ -183,6 +183,29 @@
|
|
|
183
183
|
box.hidden = false;
|
|
184
184
|
}
|
|
185
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
|
+
|
|
186
209
|
/* ==============================================================
|
|
187
210
|
3. Render colors
|
|
188
211
|
============================================================== */
|
|
@@ -954,6 +977,7 @@
|
|
|
954
977
|
renderShell();
|
|
955
978
|
renderNav();
|
|
956
979
|
renderAgentCallout();
|
|
980
|
+
renderChangelogLink();
|
|
957
981
|
renderSectionIntros();
|
|
958
982
|
renderColors();
|
|
959
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 };
|
package/lib/config-schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -141,10 +141,12 @@ function build(distDir, targetDir, config) {
|
|
|
141
141
|
.replace(/src\s*=\s*["']engine\.js["']/, function () { return 'src="' + basePathAttr + '/engine.js"'; });
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// Set title
|
|
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);
|
|
145
147
|
htmlTemplate = htmlTemplate.replace(
|
|
146
148
|
'<title>Brand Guide</title>',
|
|
147
|
-
'<title>' +
|
|
149
|
+
'<title>' + brandNameHtml + ' \u2014 Brand Guide</title>'
|
|
148
150
|
);
|
|
149
151
|
|
|
150
152
|
// Embed brand.json inline so an agent that fetches the page gets structured
|
|
@@ -159,11 +161,34 @@ function build(distDir, targetDir, config) {
|
|
|
159
161
|
|
|
160
162
|
fs.writeFileSync(path.join(targetDir, 'index.html'), htmlTemplate);
|
|
161
163
|
|
|
162
|
-
//
|
|
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
|
|
163
184
|
fs.copyFileSync(
|
|
164
185
|
path.join(distDir, 'engine.js'),
|
|
165
186
|
path.join(targetDir, 'engine.js')
|
|
166
187
|
);
|
|
188
|
+
fs.copyFileSync(
|
|
189
|
+
path.join(distDir, 'changelog.js'),
|
|
190
|
+
path.join(targetDir, 'changelog.js')
|
|
191
|
+
);
|
|
167
192
|
|
|
168
193
|
return exported;
|
|
169
194
|
}
|