@stacklist-app/brandkit 1.3.1 → 1.3.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.
@@ -4,6 +4,11 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Changelog</title>
7
+ <!-- FOUC gate (mirrors index.html): hide the page until changelog.js finishes
8
+ its first render so the default theme never flashes. Inline so it applies
9
+ before first paint; changelog.js adds html.bk-ready (with a failsafe against
10
+ a failed config fetch). Reduced-motion users get an instant reveal. -->
11
+ <style>.changelog-page{opacity:0;pointer-events:none}html.bk-ready .changelog-page{opacity:1;pointer-events:auto;transition:opacity 120ms ease-out}@media (prefers-reduced-motion:reduce){html.bk-ready .changelog-page{transition:none}}</style>
7
12
  <link rel="stylesheet" href="styles.css">
8
13
  </head>
9
14
  <body>
package/dist/changelog.js CHANGED
@@ -13,12 +13,24 @@
13
13
  var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
14
  ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
15
 
16
+ // Reveal the FOUC-gated page. changelog.html hides .changelog-page (opacity:0)
17
+ // until this runs, so the default theme never flashes before the real brand
18
+ // paints. Idempotent. See the matching gate in engine.js.
19
+ function reveal() { document.documentElement.classList.add('bk-ready'); }
20
+ // Failsafe registered BEFORE the fetch: never leave the page permanently blank
21
+ // if config.json is slow or fails — reveal after 1.5s regardless.
22
+ var revealFailsafe = setTimeout(reveal, 1500);
23
+ function done() { clearTimeout(revealFailsafe); reveal(); }
24
+
16
25
  fetch(BASE + '/config.json')
17
26
  // Guard before parsing: a non-200 response (e.g. a CDN/error page served
18
27
  // with a JSON body) is routed to the error state instead of init().
19
28
  .then(function (res) { if (!res.ok) throw new Error(res.status); return res.json(); })
20
29
  .then(function (config) { init(config); })
21
- .catch(function () { renderError(); });
30
+ .catch(function () { renderError(); })
31
+ // Reveal after render (success) or after the error state is drawn (failure),
32
+ // so the page is always shown rather than stuck blank.
33
+ .then(done, done);
22
34
 
23
35
  /* ================================================================
24
36
  Helpers (kept in sync with dist/engine.js — no module system here)
package/dist/engine.js CHANGED
@@ -12,13 +12,43 @@
12
12
  // Default '.' keeps the original page-relative behavior (dev + root serving).
13
13
  var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
14
  ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
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);
20
- var config = await res.json();
21
- init(config);
15
+
16
+ // Resolve a config-relative asset path (logo variants, brand.headerLogo/
17
+ // sidebarLogo) against BASE. Without this, a guide served from a base path
18
+ // with no trailing slash (e.g. /brand, not /brand/) resolves "logos/x.svg"
19
+ // relative to /brand -> /logos/x.svg -> 404. Absolute and data: URIs pass
20
+ // through untouched. Used for every <img> src and download href built from a
21
+ // config path so previews and downloads behave the same under a basePath.
22
+ function assetUrl(file) {
23
+ if (!file) return file;
24
+ if (/^([a-z]+:)?\/\//i.test(file) || file.indexOf('data:') === 0) return file;
25
+ return (BASE && BASE !== '.') ? BASE + '/' + file.replace(/^\/+/, '') : file;
26
+ }
27
+
28
+ // Reveal the FOUC-gated shell. index.html hides .layout (opacity:0) until this
29
+ // runs, so the default theme never flashes before the real brand paints. The
30
+ // class is added once the first render pass below completes. Idempotent.
31
+ function reveal() { document.documentElement.classList.add('bk-ready'); }
32
+ // Failsafe registered BEFORE the fetch: if config.json is slow or fails, never
33
+ // leave the page permanently blank — reveal after 1.5s no matter what. The
34
+ // finally below clears this on the normal path so it doesn't fire twice.
35
+ var revealFailsafe = setTimeout(reveal, 1500);
36
+
37
+ try {
38
+ var res = await fetch(BASE + '/config.json');
39
+ // Guard before parsing: a non-200 response (e.g. a CDN/error page served
40
+ // with a JSON body) would otherwise flow straight into init() and render
41
+ // confusing output instead of failing cleanly.
42
+ if (!res.ok) throw new Error('Failed to load config.json: ' + res.status);
43
+ var config = await res.json();
44
+ init(config);
45
+ } finally {
46
+ // Reveal after init()'s synchronous render pass (or on error, so a failed
47
+ // load shows the page rather than a blank frame). The throw still surfaces
48
+ // as an unhandled rejection for debugging — finally doesn't swallow it.
49
+ clearTimeout(revealFailsafe);
50
+ reveal();
51
+ }
22
52
 
23
53
  function init(cfg) {
24
54
  var copyFormat = localStorage.getItem('brandkit-copy-format') || 'hex';
@@ -377,7 +407,7 @@
377
407
 
378
408
  return (
379
409
  '<div class="logo-card ' + bgClass + '" style="' + bgStyle + '" data-logo-idx="' + idx + '">' +
380
- '<img src="' + previewSrc + '" alt="' + logo.name + '">' +
410
+ '<img src="' + esc(assetUrl(previewSrc)) + '" alt="' + logo.name + '">' +
381
411
  '<div class="logo-name">' + logo.name + '</div>' +
382
412
  '<div class="logo-description">' + (logo.description || '') + '</div>' +
383
413
  '<div class="logo-controls">' +
@@ -418,6 +448,9 @@
418
448
 
419
449
  var filePath = logoData.variants[format];
420
450
  if (!filePath) return;
451
+ // Resolve against BASE so downloads work under a non-trailing-slash
452
+ // basePath, exactly as the previews above do.
453
+ filePath = assetUrl(filePath);
421
454
 
422
455
  var slug = cfg.brand.name || 'brand';
423
456
  var namePart = logoData.name.toLowerCase()
@@ -881,7 +914,7 @@
881
914
  var wordmark = document.getElementById('header-wordmark');
882
915
  if (wordmark) {
883
916
  if (cfg.brand.headerLogo) {
884
- wordmark.innerHTML = '<img class="header-logo" src="' + esc(cfg.brand.headerLogo) +
917
+ wordmark.innerHTML = '<img class="header-logo" src="' + esc(assetUrl(cfg.brand.headerLogo)) +
885
918
  '" alt="' + esc(cfg.brand.name || 'Logo') + '">';
886
919
  } else {
887
920
  wordmark.textContent = cfg.brand.name;
@@ -987,7 +1020,7 @@
987
1020
  var sidebarBrand = document.querySelector('.sidebar-brand');
988
1021
  if (sidebarBrand) {
989
1022
  if (cfg.brand.sidebarLogo) {
990
- sidebarBrand.innerHTML = '<img class="sidebar-logo" src="' + esc(cfg.brand.sidebarLogo) +
1023
+ sidebarBrand.innerHTML = '<img class="sidebar-logo" src="' + esc(assetUrl(cfg.brand.sidebarLogo)) +
991
1024
  '" alt="' + esc(cfg.brand.name || 'Logo') + '">';
992
1025
  } else {
993
1026
  sidebarBrand.textContent = cfg.brand.name || 'Brand';
package/dist/index.html CHANGED
@@ -4,6 +4,13 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Brand Guide</title>
7
+ <!-- FOUC gate: hide the shell until engine.js finishes its first render, so the
8
+ default theme never flashes before the real brand paints. Inline so it
9
+ applies before first paint (an external sheet would itself flash). engine.js
10
+ adds html.bk-ready once rendered (with a failsafe so a failed config fetch
11
+ can't leave the page permanently blank). Reduced-motion users get an instant
12
+ reveal (gate kept, fade dropped). -->
13
+ <style>.layout{opacity:0;pointer-events:none}html.bk-ready .layout{opacity:1;pointer-events:auto;transition:opacity 120ms ease-out}@media (prefers-reduced-motion:reduce){html.bk-ready .layout{transition:none}}</style>
7
14
  <link rel="stylesheet" href="styles.css">
8
15
  </head>
9
16
  <body>
@@ -12,7 +19,9 @@
12
19
 
13
20
  <!-- Sidebar -->
14
21
  <nav class="sidebar">
15
- <div class="sidebar-brand">brandkit</div>
22
+ <!-- Filled from config.brand (name/sidebarLogo) by renderShell(); left empty
23
+ so the pre-render frame carries no default brand identity. -->
24
+ <div class="sidebar-brand"></div>
16
25
  <ul class="sidebar-nav" id="nav"></ul>
17
26
  <div class="sidebar-agent" id="agent-callout" hidden></div>
18
27
  <a class="sidebar-changelog" id="sidebar-changelog" href="changelog.html" hidden></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {