@stacklist-app/brandkit 1.2.1 → 1.2.2

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
@@ -76,7 +76,7 @@ export default {
76
76
  `config.json` is the single source of truth. Top-level keys:
77
77
 
78
78
  - `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
79
+ - `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
80
  - `theme` — CSS variable map (colors, gradients, font vars)
81
81
  - `nav` — sidebar structure
82
82
  - `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
  }
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(" --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', '');
@@ -734,7 +751,8 @@
734
751
  if (!testerFont || !testerInput) return;
735
752
 
736
753
  testerFont.addEventListener('change', function () {
737
- testerInput.style.fontFamily = "'" + testerFont.value + "', sans-serif";
754
+ // The option value is already a complete font stack (incl. fallback).
755
+ testerInput.style.fontFamily = testerFont.value;
738
756
  });
739
757
  }
740
758
 
@@ -790,21 +808,26 @@
790
808
  var testerFont = document.getElementById('type-tester-font');
791
809
  var testerInput = document.getElementById('type-tester-input');
792
810
  if (testerFont && cfg.fonts) {
793
- // De-duplicate families so a brand using one typeface for both
811
+ // De-duplicate by family so a brand using one typeface for both
794
812
  // 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); }
813
+ // The option value carries the full font stack (incl. any fallback)
814
+ // so the tester renders in the web stand-in when one is configured;
815
+ // the label stays the clean family name.
816
+ var fontList = [];
817
+ if (cfg.fonts.display && cfg.fonts.display.family) fontList.push(cfg.fonts.display);
818
+ if (cfg.fonts.body && cfg.fonts.body.family) fontList.push(cfg.fonts.body);
819
+ // null-proto map so a family literally named "__proto__"/"toString"
820
+ // isn't mistaken for already-seen via an inherited Object property.
821
+ var seenFam = Object.create(null);
822
+ var uniqueFonts = [];
823
+ fontList.forEach(function (f) {
824
+ if (!seenFam[f.family]) { seenFam[f.family] = true; uniqueFonts.push(f); }
802
825
  });
803
- testerFont.innerHTML = uniqueFams.map(function (f) {
804
- return '<option value="' + esc(f) + '">' + esc(f) + '</option>';
826
+ testerFont.innerHTML = uniqueFonts.map(function (f) {
827
+ return '<option value="' + esc(fontStack(f)) + '">' + esc(f.family) + '</option>';
805
828
  }).join('');
806
- if (testerInput && uniqueFams.length) {
807
- testerInput.style.fontFamily = "'" + uniqueFams[0] + "', sans-serif";
829
+ if (testerInput && uniqueFonts.length) {
830
+ testerInput.style.fontFamily = fontStack(uniqueFonts[0]);
808
831
  }
809
832
  }
810
833
 
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.2",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {