@stacklist-app/brandkit 1.2.1 → 1.2.3
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 +3 -1
- package/config.schema.json +4 -2
- package/dist/engine.js +97 -15
- package/dist/index.html +1 -0
- package/dist/styles.css +39 -1
- package/lib/template.js +19 -2
- 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
|
|
@@ -76,7 +78,7 @@ export default {
|
|
|
76
78
|
`config.json` is the single source of truth. Top-level keys:
|
|
77
79
|
|
|
78
80
|
- `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
|
-
- `fonts` — display + body with Google Fonts import
|
|
81
|
+
- `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
82
|
- `theme` — CSS variable map (colors, gradients, font vars)
|
|
81
83
|
- `nav` — sidebar structure
|
|
82
84
|
- `colors` — brand / neutrals / semantic palettes
|
package/config.schema.json
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"type": "object",
|
|
12
12
|
"required": ["family"],
|
|
13
13
|
"properties": {
|
|
14
|
-
"family": { "type": "string", "description": "Font family name (matches the Google Fonts family)." },
|
|
14
|
+
"family": { "type": "string", "minLength": 1, "description": "Font family name (matches the Google Fonts family)." },
|
|
15
|
+
"fallback": { "type": "string", "minLength": 1, "description": "Optional web-available stand-in for brands whose official typeface isn't on the web. When set, the rendered CSS font stack becomes 'family', 'fallback', sans-serif everywhere the font renders; labels still show 'family' alone." },
|
|
15
16
|
"googleImport": { "type": "string", "description": "Google Fonts css2 query segment, e.g. 'Inter:wght@300;400;700'." },
|
|
16
17
|
"description": { "type": "string" }
|
|
17
18
|
}
|
|
@@ -50,7 +51,8 @@
|
|
|
50
51
|
"date": { "type": "string" },
|
|
51
52
|
"guideLabel": { "type": "string", "description": "Renames the 'Web Style Guide' label in the header and footer." },
|
|
52
53
|
"headerLogo": { "type": "string", "description": "Path to a logo image shown in the header instead of the text wordmark (use a light/reversed logo)." },
|
|
53
|
-
"sidebarLogo": { "type": "string", "description": "Path to a logo image shown at the top of the left menu instead of the brand name (use a dark logo)." }
|
|
54
|
+
"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
|
+
"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." }
|
|
54
56
|
}
|
|
55
57
|
},
|
|
56
58
|
"fonts": {
|
package/dist/engine.js
CHANGED
|
@@ -21,6 +21,23 @@
|
|
|
21
21
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/* ==============================================================
|
|
25
|
+
Font stack helper — weaves in an optional `fallback` web stand-in
|
|
26
|
+
for brands whose official typeface isn't web-available:
|
|
27
|
+
'Family', 'Fallback', sans-serif
|
|
28
|
+
No fallback → 'Family', sans-serif (backward-compatible).
|
|
29
|
+
============================================================== */
|
|
30
|
+
function fontStack(f) {
|
|
31
|
+
if (!f || !f.family) return 'sans-serif';
|
|
32
|
+
// Strip control chars (a raw newline ends a CSS string) then escape
|
|
33
|
+
// backslash + single-quote, so a config value can't break out of the
|
|
34
|
+
// quoted CSS string and inject rules into the injected :root block.
|
|
35
|
+
function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
|
|
36
|
+
var stack = "'" + q(f.family) + "'";
|
|
37
|
+
if (f.fallback) stack += ", '" + q(f.fallback) + "'";
|
|
38
|
+
return stack + ', sans-serif';
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
/* ==============================================================
|
|
25
42
|
BOOTSTRAP — inject fonts + CSS variables before any rendering
|
|
26
43
|
============================================================== */
|
|
@@ -47,8 +64,8 @@
|
|
|
47
64
|
}
|
|
48
65
|
// Add font variables from config
|
|
49
66
|
if (cfg.fonts) {
|
|
50
|
-
if (cfg.fonts.display) vars.push(
|
|
51
|
-
if (cfg.fonts.body) vars.push(
|
|
67
|
+
if (cfg.fonts.display) vars.push(' --font-display: ' + fontStack(cfg.fonts.display) + ';');
|
|
68
|
+
if (cfg.fonts.body) vars.push(' --font-body: ' + fontStack(cfg.fonts.body) + ';');
|
|
52
69
|
}
|
|
53
70
|
var style = document.createElement('style');
|
|
54
71
|
style.setAttribute('data-brandkit-theme', '');
|
|
@@ -106,6 +123,57 @@
|
|
|
106
123
|
}).join('');
|
|
107
124
|
}
|
|
108
125
|
|
|
126
|
+
/* ==============================================================
|
|
127
|
+
2b. Render agent callout — a paste-ready prompt that points an AI
|
|
128
|
+
agent at the machine-readable exports (brand.json / tokens.json /
|
|
129
|
+
brand.md) so it reads structured brand data instead of scraping the
|
|
130
|
+
rendered page. Opt out with brand.agentCallout === false.
|
|
131
|
+
============================================================== */
|
|
132
|
+
function renderAgentCallout() {
|
|
133
|
+
var box = document.getElementById('agent-callout');
|
|
134
|
+
if (!box) return;
|
|
135
|
+
if (cfg.brand && cfg.brand.agentCallout === false) return;
|
|
136
|
+
|
|
137
|
+
// Resolve export URLs relative to this page exactly as a link would, so
|
|
138
|
+
// the pasted prompt carries absolute URLs the agent can fetch directly.
|
|
139
|
+
// Clear any userinfo so basic-auth creds in the page URL don't leak into
|
|
140
|
+
// the copied prompt (a relative path already drops the page query/hash).
|
|
141
|
+
function exportUrl(file) {
|
|
142
|
+
var u = new URL(file, location.href);
|
|
143
|
+
u.username = ''; u.password = '';
|
|
144
|
+
return u.href;
|
|
145
|
+
}
|
|
146
|
+
var brandUrl, tokensUrl, mdUrl;
|
|
147
|
+
try {
|
|
148
|
+
brandUrl = exportUrl('brand.json');
|
|
149
|
+
tokensUrl = exportUrl('tokens.json');
|
|
150
|
+
mdUrl = exportUrl('brand.md');
|
|
151
|
+
} catch (_) {
|
|
152
|
+
brandUrl = 'brand.json'; tokensUrl = 'tokens.json'; mdUrl = 'brand.md';
|
|
153
|
+
}
|
|
154
|
+
// Strip control chars / newlines and collapse whitespace so a hostile
|
|
155
|
+
// brand name can't smuggle instructions into the paste-ready agent prompt.
|
|
156
|
+
var name = ((cfg.brand && (cfg.brand.displayName || cfg.brand.name)) || 'this brand')
|
|
157
|
+
.replace(/[\u0000-\u001F\u007F\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF]/g, ' ').replace(/\s+/g, ' ').trim() || 'this brand';
|
|
158
|
+
var prompt =
|
|
159
|
+
'This page is a brandkit brand guide for ' + name + '. It publishes ' +
|
|
160
|
+
'machine-readable brand data — read these instead of scraping the page:\n\n' +
|
|
161
|
+
'- ' + brandUrl + ' — full structured brand: colors (roles + contrast), typography, voice, logos, spacing\n' +
|
|
162
|
+
'- ' + tokensUrl + ' — W3C design tokens (DTCG $type/$value), for code and design tooling\n' +
|
|
163
|
+
'- ' + mdUrl + ' — brand brief: how to stay on-brand\n\n' +
|
|
164
|
+
'Fetch them, then apply ' + name + "'s colors, type, and voice to what you're building.";
|
|
165
|
+
|
|
166
|
+
box.innerHTML =
|
|
167
|
+
'<div class="sidebar-agent-title">Using an AI agent?</div>' +
|
|
168
|
+
'<p class="sidebar-agent-desc">Paste this so it reads the brand data, not the page.</p>' +
|
|
169
|
+
'<button type="button" class="sidebar-agent-btn" id="agent-callout-btn">Copy agent prompt</button>';
|
|
170
|
+
// Set the payload via property (not an HTML attribute) so no font/brand
|
|
171
|
+
// value needs escaping and newlines survive intact.
|
|
172
|
+
var btn = box.querySelector('#agent-callout-btn');
|
|
173
|
+
if (btn) btn.dataset.copy = prompt;
|
|
174
|
+
box.hidden = false;
|
|
175
|
+
}
|
|
176
|
+
|
|
109
177
|
/* ==============================================================
|
|
110
178
|
3. Render colors
|
|
111
179
|
============================================================== */
|
|
@@ -669,6 +737,13 @@
|
|
|
669
737
|
copyText(codeLine.dataset.copy);
|
|
670
738
|
return;
|
|
671
739
|
}
|
|
740
|
+
|
|
741
|
+
// Agent callout prompt
|
|
742
|
+
var agentBtn = e.target.closest('#agent-callout-btn');
|
|
743
|
+
if (agentBtn) {
|
|
744
|
+
copyText(agentBtn.dataset.copy);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
672
747
|
});
|
|
673
748
|
}
|
|
674
749
|
|
|
@@ -734,7 +809,8 @@
|
|
|
734
809
|
if (!testerFont || !testerInput) return;
|
|
735
810
|
|
|
736
811
|
testerFont.addEventListener('change', function () {
|
|
737
|
-
|
|
812
|
+
// The option value is already a complete font stack (incl. fallback).
|
|
813
|
+
testerInput.style.fontFamily = testerFont.value;
|
|
738
814
|
});
|
|
739
815
|
}
|
|
740
816
|
|
|
@@ -790,21 +866,26 @@
|
|
|
790
866
|
var testerFont = document.getElementById('type-tester-font');
|
|
791
867
|
var testerInput = document.getElementById('type-tester-input');
|
|
792
868
|
if (testerFont && cfg.fonts) {
|
|
793
|
-
// De-duplicate
|
|
869
|
+
// De-duplicate by family so a brand using one typeface for both
|
|
794
870
|
// display and body shows a single option, not the same one twice.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
var
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
871
|
+
// The option value carries the full font stack (incl. any fallback)
|
|
872
|
+
// so the tester renders in the web stand-in when one is configured;
|
|
873
|
+
// the label stays the clean family name.
|
|
874
|
+
var fontList = [];
|
|
875
|
+
if (cfg.fonts.display && cfg.fonts.display.family) fontList.push(cfg.fonts.display);
|
|
876
|
+
if (cfg.fonts.body && cfg.fonts.body.family) fontList.push(cfg.fonts.body);
|
|
877
|
+
// null-proto map so a family literally named "__proto__"/"toString"
|
|
878
|
+
// isn't mistaken for already-seen via an inherited Object property.
|
|
879
|
+
var seenFam = Object.create(null);
|
|
880
|
+
var uniqueFonts = [];
|
|
881
|
+
fontList.forEach(function (f) {
|
|
882
|
+
if (!seenFam[f.family]) { seenFam[f.family] = true; uniqueFonts.push(f); }
|
|
802
883
|
});
|
|
803
|
-
testerFont.innerHTML =
|
|
804
|
-
return '<option value="' + esc(f) + '">' + esc(f) + '</option>';
|
|
884
|
+
testerFont.innerHTML = uniqueFonts.map(function (f) {
|
|
885
|
+
return '<option value="' + esc(fontStack(f)) + '">' + esc(f.family) + '</option>';
|
|
805
886
|
}).join('');
|
|
806
|
-
if (testerInput &&
|
|
807
|
-
testerInput.style.fontFamily =
|
|
887
|
+
if (testerInput && uniqueFonts.length) {
|
|
888
|
+
testerInput.style.fontFamily = fontStack(uniqueFonts[0]);
|
|
808
889
|
}
|
|
809
890
|
}
|
|
810
891
|
|
|
@@ -863,6 +944,7 @@
|
|
|
863
944
|
bootstrap();
|
|
864
945
|
renderShell();
|
|
865
946
|
renderNav();
|
|
947
|
+
renderAgentCallout();
|
|
866
948
|
renderSectionIntros();
|
|
867
949
|
renderColors();
|
|
868
950
|
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
|
@@ -2,6 +2,23 @@ var fs = require('fs');
|
|
|
2
2
|
var path = require('path');
|
|
3
3
|
var exporter = require('./export');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Build a CSS font stack from a font def. When the brand's official typeface
|
|
7
|
+
* isn't web-available, an optional `fallback` web stand-in is woven in:
|
|
8
|
+
* 'Family', 'Fallback', sans-serif
|
|
9
|
+
* No fallback → the original 'Family', sans-serif (backward-compatible).
|
|
10
|
+
*/
|
|
11
|
+
function fontStack(font) {
|
|
12
|
+
if (!font || !font.family) return 'sans-serif';
|
|
13
|
+
// Strip control chars (a raw newline ends a CSS string) then escape
|
|
14
|
+
// backslash + single-quote, so a config value can't break out of the quoted
|
|
15
|
+
// CSS string and inject rules into the generated :root block.
|
|
16
|
+
function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
|
|
17
|
+
var stack = "'" + q(font.family) + "'";
|
|
18
|
+
if (font.fallback) stack += ", '" + q(font.fallback) + "'";
|
|
19
|
+
return stack + ', sans-serif';
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
/**
|
|
6
23
|
* Generate a :root CSS block from config.theme + config.fonts.
|
|
7
24
|
*/
|
|
@@ -15,10 +32,10 @@ function generateRootCSS(config) {
|
|
|
15
32
|
}
|
|
16
33
|
if (config.fonts) {
|
|
17
34
|
if (config.fonts.display) {
|
|
18
|
-
lines.push(
|
|
35
|
+
lines.push(' --font-display: ' + fontStack(config.fonts.display) + ';');
|
|
19
36
|
}
|
|
20
37
|
if (config.fonts.body) {
|
|
21
|
-
lines.push(
|
|
38
|
+
lines.push(' --font-body: ' + fontStack(config.fonts.body) + ';');
|
|
22
39
|
}
|
|
23
40
|
}
|
|
24
41
|
lines.push('}');
|