@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 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
@@ -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, '&gt;').replace(/"/g, '&quot;');
22
22
  }
23
23
 
24
+ /* ==============================================================
25
+ Font stack helper — weaves in an optional `fallback` web stand-in
26
+ for brands whose official typeface isn't web-available:
27
+ 'Family', 'Fallback', sans-serif
28
+ No fallback → 'Family', sans-serif (backward-compatible).
29
+ ============================================================== */
30
+ function fontStack(f) {
31
+ if (!f || !f.family) return 'sans-serif';
32
+ // Strip control chars (a raw newline ends a CSS string) then escape
33
+ // backslash + single-quote, so a config value can't break out of the
34
+ // quoted CSS string and inject rules into the injected :root block.
35
+ function q(s) { return String(s).replace(/[\u0000-\u001F\u007F]/g, '').replace(/[\\']/g, '\\$&'); }
36
+ var stack = "'" + q(f.family) + "'";
37
+ if (f.fallback) stack += ", '" + q(f.fallback) + "'";
38
+ return stack + ', sans-serif';
39
+ }
40
+
24
41
  /* ==============================================================
25
42
  BOOTSTRAP — inject fonts + CSS variables before any rendering
26
43
  ============================================================== */
@@ -47,8 +64,8 @@
47
64
  }
48
65
  // Add font variables from config
49
66
  if (cfg.fonts) {
50
- if (cfg.fonts.display) vars.push(" --font-display: '" + cfg.fonts.display.family + "', sans-serif;");
51
- if (cfg.fonts.body) vars.push(" --font-body: '" + cfg.fonts.body.family + "', sans-serif;");
67
+ if (cfg.fonts.display) vars.push(' --font-display: ' + fontStack(cfg.fonts.display) + ';');
68
+ if (cfg.fonts.body) vars.push(' --font-body: ' + fontStack(cfg.fonts.body) + ';');
52
69
  }
53
70
  var style = document.createElement('style');
54
71
  style.setAttribute('data-brandkit-theme', '');
@@ -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
- testerInput.style.fontFamily = "'" + testerFont.value + "', sans-serif";
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 families so a brand using one typeface for both
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
- var famList = [];
796
- if (cfg.fonts.display && cfg.fonts.display.family) famList.push(cfg.fonts.display.family);
797
- if (cfg.fonts.body && cfg.fonts.body.family) famList.push(cfg.fonts.body.family);
798
- var seenFam = {};
799
- var uniqueFams = [];
800
- famList.forEach(function (f) {
801
- if (!seenFam[f]) { seenFam[f] = true; uniqueFams.push(f); }
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 = uniqueFams.map(function (f) {
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 && uniqueFams.length) {
807
- testerInput.style.fontFamily = "'" + uniqueFams[0] + "', sans-serif";
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
@@ -14,6 +14,7 @@
14
14
  <nav class="sidebar">
15
15
  <div class="sidebar-brand">brandkit</div>
16
16
  <ul class="sidebar-nav" id="nav"></ul>
17
+ <div class="sidebar-agent" id="agent-callout" hidden></div>
17
18
  </nav>
18
19
 
19
20
  <!-- Main Content -->
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: 10px;
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(" --font-display: '" + config.fonts.display.family + "', sans-serif;");
35
+ lines.push(' --font-display: ' + fontStack(config.fonts.display) + ';');
19
36
  }
20
37
  if (config.fonts.body) {
21
- lines.push(" --font-body: '" + config.fonts.body.family + "', sans-serif;");
38
+ lines.push(' --font-body: ' + fontStack(config.fonts.body) + ';');
22
39
  }
23
40
  }
24
41
  lines.push('}');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {