@stacklist-app/brandkit 1.3.0 → 1.3.1

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/dist/changelog.js CHANGED
@@ -14,7 +14,9 @@
14
14
  ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
15
 
16
16
  fetch(BASE + '/config.json')
17
- .then(function (res) { return res.json(); })
17
+ // Guard before parsing: a non-200 response (e.g. a CDN/error page served
18
+ // with a JSON body) is routed to the error state instead of init().
19
+ .then(function (res) { if (!res.ok) throw new Error(res.status); return res.json(); })
18
20
  .then(function (config) { init(config); })
19
21
  .catch(function () { renderError(); });
20
22
 
@@ -34,14 +36,28 @@
34
36
  return stack + ', sans-serif';
35
37
  }
36
38
 
39
+ // Strip the characters a config value would need to break out of the
40
+ // injected <style> block ( < > ), the :root {…} rule ( { } ), or its own
41
+ // declaration to smuggle in another ( ; ). Mirrors cssVal() in
42
+ // dist/engine.js. A real CSS value never contains them.
43
+ function cssVal(v) { return String(v == null ? '' : v).replace(/[<>{};]/g, ''); }
44
+
37
45
  /* ================================================================
38
46
  Bootstrap — fonts + theme variables, mirroring engine.js
39
47
  ================================================================ */
40
48
  function bootstrap(cfg) {
41
49
  if (cfg.fonts) {
50
+ // Dedupe families: when display and body share a font, request it once.
51
+ var seen = {};
42
52
  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);
53
+ function addFamily(f) {
54
+ if (f && f.googleImport && !seen[f.googleImport]) {
55
+ seen[f.googleImport] = 1;
56
+ families.push(f.googleImport);
57
+ }
58
+ }
59
+ addFamily(cfg.fonts.display);
60
+ addFamily(cfg.fonts.body);
45
61
  if (families.length) {
46
62
  var link = document.createElement('link');
47
63
  link.rel = 'stylesheet';
@@ -54,7 +70,7 @@
54
70
  var vars = [];
55
71
  var keys = Object.keys(cfg.theme);
56
72
  for (var i = 0; i < keys.length; i++) {
57
- vars.push(' ' + keys[i] + ': ' + cfg.theme[keys[i]] + ';');
73
+ vars.push(' ' + cssVal(keys[i]) + ': ' + cssVal(cfg.theme[keys[i]]) + ';');
58
74
  }
59
75
  if (cfg.fonts) {
60
76
  if (cfg.fonts.display) vars.push(' --font-display: ' + fontStack(cfg.fonts.display) + ';');
package/dist/engine.js CHANGED
@@ -13,6 +13,10 @@
13
13
  var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
14
  ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
15
  var res = await fetch(BASE + '/config.json');
16
+ // Guard before parsing: a non-200 response (e.g. a CDN/error page served
17
+ // with a JSON body) would otherwise flow straight into init() and render
18
+ // confusing output instead of failing cleanly.
19
+ if (!res.ok) throw new Error('Failed to load config.json: ' + res.status);
16
20
  var config = await res.json();
17
21
  init(config);
18
22
 
@@ -44,15 +48,36 @@
44
48
  return stack + ', sans-serif';
45
49
  }
46
50
 
51
+ /* ==============================================================
52
+ CSS-value sanitizer — strips the characters a config value would
53
+ need to break out of the injected <style> block ( < > ), out of the
54
+ :root {…} rule ( { } ), or out of its own declaration to smuggle in
55
+ another one ( ; ). A legitimate custom-property token/value never
56
+ contains them, so real configs pass through unchanged; an
57
+ adversarial value like "}</style><script>..." or
58
+ "red; background-image:url(x)" is defanged.
59
+ ============================================================== */
60
+ function cssVal(v) { return String(v == null ? '' : v).replace(/[<>{};]/g, ''); }
61
+
47
62
  /* ==============================================================
48
63
  BOOTSTRAP — inject fonts + CSS variables before any rendering
49
64
  ============================================================== */
50
65
  function bootstrap() {
51
66
  // Inject Google Fonts
52
67
  if (cfg.fonts) {
68
+ // Dedupe families: when display and body share a font (e.g. both
69
+ // Inter), requesting it once avoids a redundant URL + duplicate
70
+ // font CSS work.
71
+ var seen = {};
53
72
  var families = [];
54
- if (cfg.fonts.display && cfg.fonts.display.googleImport) families.push(cfg.fonts.display.googleImport);
55
- if (cfg.fonts.body && cfg.fonts.body.googleImport) families.push(cfg.fonts.body.googleImport);
73
+ function addFamily(f) {
74
+ if (f && f.googleImport && !seen[f.googleImport]) {
75
+ seen[f.googleImport] = 1;
76
+ families.push(f.googleImport);
77
+ }
78
+ }
79
+ addFamily(cfg.fonts.display);
80
+ addFamily(cfg.fonts.body);
56
81
  if (families.length) {
57
82
  var link = document.createElement('link');
58
83
  link.rel = 'stylesheet';
@@ -66,7 +91,7 @@
66
91
  var vars = [];
67
92
  var keys = Object.keys(cfg.theme);
68
93
  for (var i = 0; i < keys.length; i++) {
69
- vars.push(' ' + keys[i] + ': ' + cfg.theme[keys[i]] + ';');
94
+ vars.push(' ' + cssVal(keys[i]) + ': ' + cssVal(cfg.theme[keys[i]]) + ';');
70
95
  }
71
96
  // Add font variables from config
72
97
  if (cfg.fonts) {
@@ -202,7 +227,7 @@
202
227
  '<circle cx="12" cy="12" r="9"/>' +
203
228
  '</svg>' +
204
229
  '<span class="sidebar-changelog-label">Changelog</span>' +
205
- '<span class="sidebar-changelog-version">v' + esc(cfg.brand && cfg.brand.version) + '</span>';
230
+ '<span class="sidebar-changelog-version">v' + esc((cfg.brand && cfg.brand.version) || '') + '</span>';
206
231
  link.hidden = false;
207
232
  }
208
233
 
package/lib/template.js CHANGED
@@ -19,6 +19,20 @@ function fontStack(font) {
19
19
  return stack + ', sans-serif';
20
20
  }
21
21
 
22
+ /**
23
+ * Strip the characters a config value would need to break out of the
24
+ * generated :root block ( < > ), out of the :root {…} rule ( { } ), or out
25
+ * of its own declaration to smuggle in another one ( ; ). A legitimate CSS
26
+ * token/value never contains them, so real configs pass through unchanged;
27
+ * an adversarial value like "}</style><script>..." or
28
+ * "red; background-image:url(x)" is defanged. Mirrors cssVal() in
29
+ * dist/engine.js + dist/changelog.js so dev (runtime-injected) and
30
+ * production (build-baked) :root blocks sanitize identically.
31
+ */
32
+ function cssValue(v) {
33
+ return String(v == null ? '' : v).replace(/[<>{};]/g, '');
34
+ }
35
+
22
36
  /**
23
37
  * Generate a :root CSS block from config.theme + config.fonts.
24
38
  */
@@ -27,7 +41,7 @@ function generateRootCSS(config) {
27
41
  if (config.theme) {
28
42
  var keys = Object.keys(config.theme);
29
43
  for (var i = 0; i < keys.length; i++) {
30
- lines.push(' ' + keys[i] + ': ' + config.theme[keys[i]] + ';');
44
+ lines.push(' ' + cssValue(keys[i]) + ': ' + cssValue(config.theme[keys[i]]) + ';');
31
45
  }
32
46
  }
33
47
  if (config.fonts) {
@@ -73,13 +87,19 @@ function normalizeBasePath(bp) {
73
87
  */
74
88
  function generateFontsLink(config) {
75
89
  if (!config.fonts) return '';
90
+ // Dedupe families: when display and body share a font (e.g. both Inter),
91
+ // baking it once avoids a redundant ...&family=Inter:...&family=Inter:...
92
+ // URL and the duplicate font CSS work it triggers.
93
+ var seen = {};
76
94
  var families = [];
77
- if (config.fonts.display && config.fonts.display.googleImport) {
78
- families.push(config.fonts.display.googleImport);
79
- }
80
- if (config.fonts.body && config.fonts.body.googleImport) {
81
- families.push(config.fonts.body.googleImport);
95
+ function addFamily(f) {
96
+ if (f && f.googleImport && !seen[f.googleImport]) {
97
+ seen[f.googleImport] = 1;
98
+ families.push(f.googleImport);
99
+ }
82
100
  }
101
+ addFamily(config.fonts.display);
102
+ addFamily(config.fonts.body);
83
103
  if (!families.length) return '';
84
104
  return '<link href="https://fonts.googleapis.com/css2?family=' +
85
105
  families.join('&family=') + '&display=swap" rel="stylesheet">';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {