@stacklist-app/brandkit 1.2.2 → 1.2.4
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 +18 -0
- package/config.schema.json +3 -1
- package/dist/engine.js +69 -1
- package/dist/index.html +1 -0
- package/dist/styles.css +39 -1
- package/lib/template.js +60 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ A brandkit guide isn't just a page for people — `build` (and the standalone `e
|
|
|
43
43
|
|
|
44
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
45
|
|
|
46
|
+
And for a person who wants to hand the guide to their own agent, the sidebar shows a **"Using an AI agent?"** callout with a one-click copy-paste prompt — it carries absolute URLs to `brand.json` / `tokens.json` / `brand.md` and tells the agent to read those instead of scraping the page. Hide it with `brand.agentCallout: false`.
|
|
47
|
+
|
|
46
48
|
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.
|
|
47
49
|
|
|
48
50
|
## Framework integrations
|
|
@@ -71,10 +73,26 @@ export default {
|
|
|
71
73
|
|
|
72
74
|
**Next.js / plain HTML**: run `brandkit build brand` and serve the resulting directory as a static route.
|
|
73
75
|
|
|
76
|
+
### Serving from a base path
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
Set `basePath` in `config.json` to fix this:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"basePath": "/brand",
|
|
85
|
+
"brand": { "name": "Acme" }
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
74
91
|
## Config
|
|
75
92
|
|
|
76
93
|
`config.json` is the single source of truth. Top-level keys:
|
|
77
94
|
|
|
95
|
+
- `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).
|
|
78
96
|
- `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)
|
|
79
97
|
- `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)
|
|
80
98
|
- `theme` — CSS variable map (colors, gradients, font vars)
|
package/config.schema.json
CHANGED
|
@@ -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"],
|
|
@@ -51,7 +52,8 @@
|
|
|
51
52
|
"date": { "type": "string" },
|
|
52
53
|
"guideLabel": { "type": "string", "description": "Renames the 'Web Style Guide' label in the header and footer." },
|
|
53
54
|
"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
|
+
"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)." },
|
|
56
|
+
"agentCallout": { "type": "boolean", "description": "Show the 'Using an AI agent?' callout under the sidebar nav — a copy-paste prompt pointing an agent at the brand.json / tokens.json / brand.md exports. Defaults to true; set false to hide it." }
|
|
55
57
|
}
|
|
56
58
|
},
|
|
57
59
|
"fonts": {
|
package/dist/engine.js
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
/* ================================================================
|
|
7
7
|
0. Load config
|
|
8
8
|
================================================================ */
|
|
9
|
-
|
|
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
|
|
|
@@ -123,6 +129,60 @@
|
|
|
123
129
|
}).join('');
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
/* ==============================================================
|
|
133
|
+
2b. Render agent callout — a paste-ready prompt that points an AI
|
|
134
|
+
agent at the machine-readable exports (brand.json / tokens.json /
|
|
135
|
+
brand.md) so it reads structured brand data instead of scraping the
|
|
136
|
+
rendered page. Opt out with brand.agentCallout === false.
|
|
137
|
+
============================================================== */
|
|
138
|
+
function renderAgentCallout() {
|
|
139
|
+
var box = document.getElementById('agent-callout');
|
|
140
|
+
if (!box) return;
|
|
141
|
+
if (cfg.brand && cfg.brand.agentCallout === false) return;
|
|
142
|
+
|
|
143
|
+
// Resolve export URLs relative to this page exactly as a link would, so
|
|
144
|
+
// the pasted prompt carries absolute URLs the agent can fetch directly.
|
|
145
|
+
// Clear any userinfo so basic-auth creds in the page URL don't leak into
|
|
146
|
+
// the copied prompt (a relative path already drops the page query/hash).
|
|
147
|
+
function exportUrl(file) {
|
|
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);
|
|
152
|
+
u.username = ''; u.password = '';
|
|
153
|
+
return u.href;
|
|
154
|
+
}
|
|
155
|
+
var brandUrl, tokensUrl, mdUrl;
|
|
156
|
+
try {
|
|
157
|
+
brandUrl = exportUrl('brand.json');
|
|
158
|
+
tokensUrl = exportUrl('tokens.json');
|
|
159
|
+
mdUrl = exportUrl('brand.md');
|
|
160
|
+
} catch (_) {
|
|
161
|
+
brandUrl = 'brand.json'; tokensUrl = 'tokens.json'; mdUrl = 'brand.md';
|
|
162
|
+
}
|
|
163
|
+
// Strip control chars / newlines and collapse whitespace so a hostile
|
|
164
|
+
// brand name can't smuggle instructions into the paste-ready agent prompt.
|
|
165
|
+
var name = ((cfg.brand && (cfg.brand.displayName || cfg.brand.name)) || 'this brand')
|
|
166
|
+
.replace(/[\u0000-\u001F\u007F\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF]/g, ' ').replace(/\s+/g, ' ').trim() || 'this brand';
|
|
167
|
+
var prompt =
|
|
168
|
+
'This page is a brandkit brand guide for ' + name + '. It publishes ' +
|
|
169
|
+
'machine-readable brand data — read these instead of scraping the page:\n\n' +
|
|
170
|
+
'- ' + brandUrl + ' — full structured brand: colors (roles + contrast), typography, voice, logos, spacing\n' +
|
|
171
|
+
'- ' + tokensUrl + ' — W3C design tokens (DTCG $type/$value), for code and design tooling\n' +
|
|
172
|
+
'- ' + mdUrl + ' — brand brief: how to stay on-brand\n\n' +
|
|
173
|
+
'Fetch them, then apply ' + name + "'s colors, type, and voice to what you're building.";
|
|
174
|
+
|
|
175
|
+
box.innerHTML =
|
|
176
|
+
'<div class="sidebar-agent-title">Using an AI agent?</div>' +
|
|
177
|
+
'<p class="sidebar-agent-desc">Paste this so it reads the brand data, not the page.</p>' +
|
|
178
|
+
'<button type="button" class="sidebar-agent-btn" id="agent-callout-btn">Copy agent prompt</button>';
|
|
179
|
+
// Set the payload via property (not an HTML attribute) so no font/brand
|
|
180
|
+
// value needs escaping and newlines survive intact.
|
|
181
|
+
var btn = box.querySelector('#agent-callout-btn');
|
|
182
|
+
if (btn) btn.dataset.copy = prompt;
|
|
183
|
+
box.hidden = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
126
186
|
/* ==============================================================
|
|
127
187
|
3. Render colors
|
|
128
188
|
============================================================== */
|
|
@@ -686,6 +746,13 @@
|
|
|
686
746
|
copyText(codeLine.dataset.copy);
|
|
687
747
|
return;
|
|
688
748
|
}
|
|
749
|
+
|
|
750
|
+
// Agent callout prompt
|
|
751
|
+
var agentBtn = e.target.closest('#agent-callout-btn');
|
|
752
|
+
if (agentBtn) {
|
|
753
|
+
copyText(agentBtn.dataset.copy);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
689
756
|
});
|
|
690
757
|
}
|
|
691
758
|
|
|
@@ -886,6 +953,7 @@
|
|
|
886
953
|
bootstrap();
|
|
887
954
|
renderShell();
|
|
888
955
|
renderNav();
|
|
956
|
+
renderAgentCallout();
|
|
889
957
|
renderSectionIntros();
|
|
890
958
|
renderColors();
|
|
891
959
|
renderGradients();
|
package/dist/index.html
CHANGED
package/dist/styles.css
CHANGED
|
@@ -129,11 +129,49 @@ body {
|
|
|
129
129
|
.sidebar-nav a:hover { color: var(--ink); background: var(--cloud); }
|
|
130
130
|
.sidebar-nav a.active { color: var(--accent-text); border-left-color: var(--accent-text); background: rgba(var(--accent-rgb), 0.04); font-weight: 500; }
|
|
131
131
|
|
|
132
|
+
/* Agent callout — paste-ready prompt that points an AI agent at the exports */
|
|
133
|
+
.sidebar-agent {
|
|
134
|
+
margin: 28px 16px 8px;
|
|
135
|
+
padding: 14px;
|
|
136
|
+
border: 1px solid var(--mist);
|
|
137
|
+
border-radius: 12px;
|
|
138
|
+
background: rgba(var(--accent-rgb), 0.04);
|
|
139
|
+
}
|
|
140
|
+
.sidebar-agent-title {
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
color: var(--ink);
|
|
144
|
+
margin-bottom: 4px;
|
|
145
|
+
}
|
|
146
|
+
.sidebar-agent-desc {
|
|
147
|
+
font-size: 11px;
|
|
148
|
+
line-height: 1.4;
|
|
149
|
+
color: var(--slate);
|
|
150
|
+
margin: 0 0 10px;
|
|
151
|
+
}
|
|
152
|
+
.sidebar-agent-btn {
|
|
153
|
+
display: block;
|
|
154
|
+
width: 100%;
|
|
155
|
+
padding: 7px 10px;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
font-weight: 500;
|
|
158
|
+
font-family: inherit;
|
|
159
|
+
color: var(--primary-foreground);
|
|
160
|
+
background: var(--primary);
|
|
161
|
+
border: none;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
transition: filter 150ms ease;
|
|
165
|
+
}
|
|
166
|
+
.sidebar-agent-btn:hover { filter: brightness(0.9); }
|
|
167
|
+
.sidebar-agent-btn:active { transform: translateY(1px); }
|
|
168
|
+
.sidebar-agent-btn:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
|
|
169
|
+
|
|
132
170
|
/* ── Sidebar Grouped Navigation ── */
|
|
133
171
|
.nav-group {
|
|
134
172
|
padding: 20px 24px 6px;
|
|
135
173
|
font-family: var(--font-body), sans-serif;
|
|
136
|
-
font-size:
|
|
174
|
+
font-size: 12px;
|
|
137
175
|
font-weight: 700;
|
|
138
176
|
letter-spacing: 0.10em;
|
|
139
177
|
text-transform: uppercase;
|
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, '&').replace(/"/g, '"')
|
|
52
|
+
.replace(/</g, '<').replace(/>/g, '>');
|
|
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,10 +112,34 @@ 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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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>'; });
|
|
135
|
+
|
|
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
|
+
}
|
|
88
143
|
|
|
89
144
|
// Set title
|
|
90
145
|
htmlTemplate = htmlTemplate.replace(
|
|
@@ -116,5 +171,6 @@ function build(distDir, targetDir, config) {
|
|
|
116
171
|
module.exports = {
|
|
117
172
|
generateRootCSS: generateRootCSS,
|
|
118
173
|
generateFontsLink: generateFontsLink,
|
|
174
|
+
normalizeBasePath: normalizeBasePath,
|
|
119
175
|
build: build
|
|
120
176
|
};
|