@canopy-iiif/app 0.7.13 → 0.7.16

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/lib/build/iiif.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  CONTENT_DIR,
12
12
  ensureDirSync,
13
13
  htmlShell,
14
+ rootRelativeHref,
14
15
  } = require("../common");
15
16
  const mdx = require("./mdx");
16
17
  const { log, logLine, logResponse } = require("./log");
@@ -21,7 +22,7 @@ const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
21
22
  const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
22
23
  // Primary global index location
23
24
  const IIIF_CACHE_INDEX = path.join(IIIF_CACHE_DIR, "index.json");
24
- // Legacy locations kept for backward compatibility (read + optional write)
25
+ // Additional legacy locations kept for backward compatibility (read + optional write)
25
26
  const IIIF_CACHE_INDEX_LEGACY = path.join(
26
27
  IIIF_CACHE_DIR,
27
28
  "manifest-index.json"
@@ -149,7 +150,7 @@ async function loadManifestIndex() {
149
150
  return { byId, collection: idx.collection || null };
150
151
  }
151
152
  }
152
- // Fallback: legacy in .cache/iiif
153
+ // Legacy index location retained for backward compatibility
153
154
  if (fs.existsSync(IIIF_CACHE_INDEX_LEGACY)) {
154
155
  const idx = await readJson(IIIF_CACHE_INDEX_LEGACY);
155
156
  if (idx && typeof idx === "object") {
@@ -166,7 +167,7 @@ async function loadManifestIndex() {
166
167
  return { byId, collection: idx.collection || null };
167
168
  }
168
169
  }
169
- // Fallback: legacy in manifests subdir
170
+ // Legacy manifests index retained for backward compatibility
170
171
  if (fs.existsSync(IIIF_CACHE_INDEX_MANIFESTS)) {
171
172
  const idx = await readJson(IIIF_CACHE_INDEX_MANIFESTS);
172
173
  if (idx && typeof idx === "object") {
@@ -405,8 +406,9 @@ async function ensureFeaturedInCache(cfg) {
405
406
  }
406
407
  } catch (_) {}
407
408
  }
408
- } catch (_) {
409
- // ignore failures; fallback SSR will still render a minimal hero without content
409
+ } catch (err) {
410
+ const message = err && err.message ? err.message : err;
411
+ throw new Error(`[iiif] Failed to populate featured cache: ${message}`);
410
412
  }
411
413
  }
412
414
 
@@ -642,33 +644,20 @@ async function buildIiifCollectionPages(CONFIG) {
642
644
  1200;
643
645
 
644
646
  // Compile the works layout component once per run
647
+ const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
648
+ if (!fs.existsSync(worksLayoutPath)) {
649
+ throw new Error(
650
+ "IIIF build requires content/works/_layout.mdx. Create the layout instead of relying on generated output."
651
+ );
652
+ }
645
653
  let WorksLayoutComp = null;
646
654
  try {
647
- const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
648
655
  WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
649
- } catch (_) {
650
- // Minimal fallback layout if missing or fails to compile
651
- WorksLayoutComp = function FallbackWorksLayout({ manifest }) {
652
- const title = firstLabelString(manifest && manifest.label);
653
- return React.createElement(
654
- "div",
655
- { className: "content" },
656
- React.createElement("h1", null, title || "Untitled"),
657
- // Render viewer placeholder for hydration
658
- React.createElement(
659
- "div",
660
- { "data-canopy-viewer": "1" },
661
- React.createElement("script", {
662
- type: "application/json",
663
- dangerouslySetInnerHTML: {
664
- __html: JSON.stringify({
665
- iiifContent: manifest && (manifest.id || ""),
666
- }),
667
- },
668
- })
669
- )
670
- );
671
- };
656
+ } catch (err) {
657
+ const message = err && err.message ? err.message : err;
658
+ throw new Error(
659
+ `Failed to compile content/works/_layout.mdx: ${message}`
660
+ );
672
661
  }
673
662
 
674
663
  for (let ci = 0; ci < chunks; ci++) {
@@ -807,20 +796,8 @@ async function buildIiifCollectionPages(CONFIG) {
807
796
  href = withBase(href);
808
797
  return React.createElement("a", { href, ...rest }, props.children);
809
798
  };
810
- // Map exported UI components into MDX, with sensible aliases/fallbacks
799
+ // Map exported UI components into MDX and add anchor helper
811
800
  const compMap = { ...components, a: Anchor };
812
- if (!compMap.SearchPanel && compMap.CommandPalette) {
813
- compMap.SearchPanel = compMap.CommandPalette;
814
- }
815
- if (!components.HelloWorld) {
816
- components.HelloWorld = components.Fallback
817
- ? (props) =>
818
- React.createElement(components.Fallback, {
819
- name: "HelloWorld",
820
- ...props,
821
- })
822
- : () => null;
823
- }
824
801
  let MDXProvider = null;
825
802
  try {
826
803
  const mod = await import("@mdx-js/react");
@@ -1009,7 +986,7 @@ async function buildIiifCollectionPages(CONFIG) {
1009
986
  iiifRecords.push({
1010
987
  id: String(manifest.id || id),
1011
988
  title,
1012
- href: href.split(path.sep).join("/"),
989
+ href: rootRelativeHref(href.split(path.sep).join("/")),
1013
990
  type: "work",
1014
991
  thumbnail: thumbUrl || undefined,
1015
992
  thumbnailWidth:
package/lib/build/mdx.js CHANGED
@@ -101,7 +101,7 @@ async function loadUiComponents() {
101
101
  }
102
102
  }
103
103
  if (!mod) {
104
- // Try package subpath as a secondary resolution path (not a UI component fallback)
104
+ // Try package subpath as a secondary resolution path to avoid export-map issues
105
105
  try {
106
106
  mod = await import('@canopy-iiif/app/ui/server');
107
107
  } catch (e2) {
@@ -112,7 +112,7 @@ async function loadUiComponents() {
112
112
  }
113
113
  let comp = (mod && typeof mod === 'object') ? mod : {};
114
114
  // Hard-require core exports; do not inject fallbacks
115
- const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems'];
115
+ const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
116
116
  const missing = required.filter((k) => !comp || !comp[k]);
117
117
  if (missing.length) {
118
118
  throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
@@ -128,41 +128,6 @@ async function loadUiComponents() {
128
128
  Slider: !!comp.Slider,
129
129
  }); } catch(_){}
130
130
  }
131
- // No stub injection beyond this point; UI package must supply these.
132
- // Ensure a minimal SSR Hero exists
133
- if (!comp.Hero) {
134
- comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
135
- const h = typeof height === 'number' ? `${height}px` : String(height || '').trim() || '360px';
136
- const base = { position: 'relative', width: '100%', height: h, overflow: 'hidden', backgroundColor: 'var(--color-gray-muted)', ...style };
137
- const title = (item && item.title) || '';
138
- const href = (item && item.href) || '#';
139
- const thumbnail = (item && item.thumbnail) || '';
140
- return React.createElement('div', { className: ['canopy-hero', className].filter(Boolean).join(' '), style: base, ...rest },
141
- thumbnail ? React.createElement('img', { src: thumbnail, alt: '', 'aria-hidden': 'true', style: { position:'absolute', inset:0, width:'100%', height:'100%', objectFit:'cover', objectPosition:'center', filter:'none' } }) : null,
142
- React.createElement('div', { className:'canopy-hero-overlay', style: { position:'absolute', left:0, right:0, bottom:0, padding:'1rem', background:'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 55%, rgba(0,0,0,0.65) 100%)', color:'white' } },
143
- React.createElement('h3', { style: { margin:0, fontSize:'1.5rem', fontWeight:600, lineHeight:1.2, textShadow:'0 1px 3px rgba(0,0,0,0.6)' } },
144
- React.createElement('a', { href, style:{ color:'inherit', textDecoration:'none' }, className:'canopy-hero-link' }, title)
145
- )
146
- )
147
- );
148
- };
149
- }
150
- // Provide a minimal SSR FeaturedHero fallback if missing
151
- if (!comp.FeaturedHero) {
152
- try {
153
- const helpers = require('../components/featured');
154
- comp.FeaturedHero = function FeaturedHero(props) {
155
- try {
156
- const list = helpers && helpers.readFeaturedFromCacheSync ? helpers.readFeaturedFromCacheSync() : [];
157
- if (!Array.isArray(list) || list.length === 0) return null;
158
- const index = (props && typeof props.index === 'number') ? Math.max(0, Math.min(list.length - 1, Math.floor(props.index))) : null;
159
- const pick = (index != null) ? index : ((props && (props.random === true || props.random === 'true')) ? Math.floor(Math.random() * list.length) : 0);
160
- const item = list[pick] || list[0];
161
- return React.createElement(comp.Hero, { ...props, item });
162
- } catch (_) { return null; }
163
- };
164
- } catch (_) { /* ignore */ }
165
- }
166
131
  UI_COMPONENTS = comp;
167
132
  UI_COMPONENTS_PATH = currentPath;
168
133
  UI_COMPONENTS_MTIME = currentMtime;
@@ -265,26 +230,9 @@ async function loadAppWrapper() {
265
230
  ok = false;
266
231
  }
267
232
  if (!ok) {
268
- // If default export swallowed children, try to recover using __MDXLayout
269
- if (!App && mod.__MDXLayout) {
270
- App = mod.__MDXLayout;
271
- }
272
- // Fallback to pass-through wrapper to avoid blocking builds
273
- if (!App) {
274
- App = function PassThrough(props) {
275
- return React.createElement(React.Fragment, null, props.children);
276
- };
277
- }
278
- try {
279
- require("./log").log(
280
- "! Warning: content/_app.mdx did not clearly render {children}; proceeding with best-effort wrapper\n",
281
- "yellow"
282
- );
283
- } catch (_) {
284
- console.warn(
285
- "Warning: content/_app.mdx did not clearly render {children}; proceeding."
286
- );
287
- }
233
+ throw new Error(
234
+ "content/_app.mdx must render {children}. Update the layout so downstream pages receive their content."
235
+ );
288
236
  }
289
237
  APP_WRAPPER = { App, Head };
290
238
  return APP_WRAPPER;
@@ -390,7 +338,7 @@ async function ensureClientRuntime() {
390
338
  esbuild = require("esbuild");
391
339
  } catch (_) {}
392
340
  }
393
- if (!esbuild) return;
341
+ if (!esbuild) throw new Error('Viewer runtime bundling requires esbuild. Install dependencies before building.');
394
342
  ensureDirSync(OUT_DIR);
395
343
  const scriptsDir = path.join(OUT_DIR, 'scripts');
396
344
  ensureDirSync(scriptsDir);
@@ -509,6 +457,9 @@ async function ensureClientRuntime() {
509
457
  async function ensureFacetsRuntime() {
510
458
  let esbuild = null;
511
459
  try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
460
+ if (!esbuild) {
461
+ throw new Error('RelatedItems runtime bundling requires esbuild. Install dependencies before building.');
462
+ }
512
463
  ensureDirSync(OUT_DIR);
513
464
  const scriptsDir = path.join(OUT_DIR, 'scripts');
514
465
  ensureDirSync(scriptsDir);
@@ -589,18 +540,30 @@ async function ensureFacetsRuntime() {
589
540
  });
590
541
  `;
591
542
  const shim = { name: 'facets-vanilla', setup(){} };
592
- if (esbuild) {
593
- try {
594
- await esbuild.build({ stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' }, outfile: outFile, platform: 'browser', format: 'iife', bundle: true, sourcemap: false, target: ['es2018'], logLevel: 'silent', minify: true, plugins: [shim] });
595
- } catch(e){ try{ console.error('RelatedItems: bundle error:', e && e.message ? e.message : e); }catch(_){ }
596
- // Fallback: write the entry script directly so the file exists
597
- try { fs.writeFileSync(outFile, entry, 'utf8'); } catch(_){}
598
- return; }
599
- try { const { logLine } = require('./log'); let size=0; try{ const st = fs.statSync(outFile); size = st && st.size || 0; }catch(_){} const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : ''; const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/'); logLine(`✓ Wrote ${rel}${kb}`, 'cyan'); } catch(_){}
600
- } else {
601
- // No esbuild: write a non-bundled version (no imports used)
602
- try { fs.writeFileSync(outFile, entry, 'utf8'); } catch(_){}
543
+ try {
544
+ await esbuild.build({
545
+ stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' },
546
+ outfile: outFile,
547
+ platform: 'browser',
548
+ format: 'iife',
549
+ bundle: true,
550
+ sourcemap: false,
551
+ target: ['es2018'],
552
+ logLevel: 'silent',
553
+ minify: true,
554
+ plugins: [shim]
555
+ });
556
+ } catch (e) {
557
+ const message = e && e.message ? e.message : e;
558
+ throw new Error(`RelatedItems runtime build failed: ${message}`);
603
559
  }
560
+ try {
561
+ const { logLine } = require('./log');
562
+ let size = 0; try { const st = fs.statSync(outFile); size = st && st.size || 0; } catch (_) {}
563
+ const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : '';
564
+ const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
565
+ logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
566
+ } catch (_) {}
604
567
  }
605
568
 
606
569
  // Bundle a separate client runtime for the Clover Slider to keep payloads split.
@@ -611,7 +574,7 @@ async function ensureSliderRuntime() {
611
574
  } catch (_) {
612
575
  try { esbuild = require("esbuild"); } catch (_) {}
613
576
  }
614
- if (!esbuild) return;
577
+ if (!esbuild) throw new Error('Slider runtime bundling requires esbuild. Install dependencies before building.');
615
578
  ensureDirSync(OUT_DIR);
616
579
  const scriptsDir = path.join(OUT_DIR, 'scripts');
617
580
  ensureDirSync(scriptsDir);
@@ -735,8 +698,8 @@ async function ensureSliderRuntime() {
735
698
  plugins: [plugin],
736
699
  });
737
700
  } catch (e) {
738
- try { console.error('Slider: bundle error:', e && e.message ? e.message : e); } catch (_) {}
739
- return;
701
+ const message = e && e.message ? e.message : e;
702
+ throw new Error(`Slider runtime build failed: ${message}`);
740
703
  }
741
704
  try {
742
705
  const { logLine } = require('./log');
@@ -757,7 +720,7 @@ async function ensureReactGlobals() {
757
720
  esbuild = require("esbuild");
758
721
  } catch (_) {}
759
722
  }
760
- if (!esbuild) return;
723
+ if (!esbuild) throw new Error('React globals bundling requires esbuild. Install dependencies before building.');
761
724
  const { path } = require("../common");
762
725
  ensureDirSync(OUT_DIR);
763
726
  const scriptsDir = path.join(OUT_DIR, "scripts");
@@ -792,7 +755,7 @@ async function ensureReactGlobals() {
792
755
  async function ensureHeroRuntime() {
793
756
  let esbuild = null;
794
757
  try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
795
- if (!esbuild) return;
758
+ if (!esbuild) throw new Error('Hero runtime bundling requires esbuild. Install dependencies before building.');
796
759
  const { path } = require("../common");
797
760
  ensureDirSync(OUT_DIR);
798
761
  const scriptsDir = path.join(OUT_DIR, 'scripts');
@@ -1,12 +1,10 @@
1
-
2
1
  const { logLine } = require('./log');
3
- const { fs, fsp, path, OUT_DIR, ensureDirSync } = require('../common');
2
+ const { fs, path, OUT_DIR, ensureDirSync } = require('../common');
4
3
 
5
4
  async function prepareAllRuntimes() {
6
5
  const mdx = require('./mdx');
7
6
  try { await mdx.ensureClientRuntime(); } catch (_) {}
8
7
  try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
9
- // Optional: Hero runtime is SSR-only by default; enable explicitly to avoid bundling Node deps in browser
10
8
  try {
11
9
  if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
12
10
  if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
@@ -14,103 +12,60 @@ async function prepareAllRuntimes() {
14
12
  } catch (_) {}
15
13
  try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
16
14
  try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
17
- try { await ensureCommandFallback(); } catch (_) {}
15
+ await prepareCommandRuntime();
18
16
  try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
19
17
  }
20
18
 
21
- module.exports = { prepareAllRuntimes };
19
+ async function resolveEsbuild() {
20
+ try { return require('../../ui/node_modules/esbuild'); } catch (_) {
21
+ try { return require('esbuild'); } catch (_) {
22
+ return null;
23
+ }
24
+ }
25
+ }
22
26
 
23
- async function ensureCommandFallback() {
24
- const cmdOut = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
25
- ensureDirSync(path.dirname(cmdOut));
26
- const fallback = `
27
- (function(){
28
- function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
29
- function parseProps(el){ try{ const s = el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
30
- function norm(s){ try{ return String(s||'').toLowerCase(); }catch(_){ return ''; } }
31
- function withBase(href){ try{ var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; if(!bp) return href; if(/^https?:/i.test(href)) return href; var clean = href.replace(/^\\/+/, ''); return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean; } catch(_){ return href; } }
32
- function rootBase(){ try { var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_) { return ''; } }
33
- function isOnSearchPage(){ try{ var base=rootBase(); var p=String(location.pathname||''); if(base && p.startsWith(base)) p=p.slice(base.length); if(p.endsWith('/')) p=p.slice(0,-1); return p==='/search'; }catch(_){ return false; } }
34
- async function loadRecords(){ try{ var v=''; try{ var m = await fetch(rootBase() + '/api/index.json').then(function(r){return r&&r.ok?r.json():null;}).catch(function(){return null;}); v=(m&&m.version)||''; }catch(_){} var res = await fetch(rootBase() + '/api/search-index.json' + (v?('?v='+encodeURIComponent(v)):'')).catch(function(){return null;}); var j = res && res.ok ? await res.json().catch(function(){return[];}) : []; return Array.isArray(j) ? j : (j && j.records) || []; } catch(_){ return []; } }
35
- ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var onSearchPage = isOnSearchPage();
36
- var panel = (function(){ try{ return host.querySelector('[data-canopy-command-panel]') || null; }catch(_){ return null; } })();
37
- if(!panel) return; // no SSR panel present; do nothing
38
- try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){}
39
- if(onSearchPage){ panel.style.display='none'; }
40
- var list=panel.querySelector('#cplist');
41
- var input = (function(){ try{ return host.querySelector('[data-canopy-command-input]') || null; }catch(_){ return null; } })();
42
- if(!input) return; // require SSR input; no dynamic creation
43
- // Populate from ?q= URL param if present
44
- try {
45
- var sp = new URLSearchParams(location.search || '');
46
- var qp = sp.get('q');
47
- if (qp) input.value = qp;
48
- } catch(_) {}
49
- // Do not inject legacy trigger buttons or inputs
50
- var records = await loadRecords(); function render(items){ list.innerHTML=''; if(!items.length){ panel.style.display='none'; return; } var groups=new Map(); items.forEach(function(r){ var t=String(r.type||'page'); if(!groups.has(t)) groups.set(t, []); groups.get(t).push(r); }); function gl(t){ if(t==='work') return 'Works'; if(t==='page') return 'Pages'; return t.charAt(0).toUpperCase()+t.slice(1);} var ordered=[].concat(groupOrder.filter(function(t){return groups.has(t);})).concat(Array.from(groups.keys()).filter(function(t){return groupOrder.indexOf(t)===-1;})); ordered.forEach(function(t){ var hdr=document.createElement('div'); hdr.textContent=gl(t); hdr.style.cssText='padding:6px 12px;font-weight:600;color:#374151'; list.appendChild(hdr); groups.get(t).forEach(function(r){ var it=document.createElement('div'); it.setAttribute('data-canopy-item',''); it.tabIndex=0; it.style.cssText='display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;outline:none;'; var thumb=(String(r.type||'')==='work' && r.thumbnail)?r.thumbnail:''; if(thumb){ var img=document.createElement('img'); img.src=thumb; img.alt=''; img.style.cssText='width:40px;height:40px;object-fit:cover;border-radius:4px'; it.appendChild(img);} var span=document.createElement('span'); span.textContent=r.title||r.href; it.appendChild(span); it.onmouseenter=function(){ it.style.background='#f8fafc'; }; it.onmouseleave=function(){ it.style.background='transparent'; }; it.onfocus=function(){ it.style.background='#eef2ff'; try{ it.scrollIntoView({ block: 'nearest' }); }catch(_){} }; it.onblur=function(){ it.style.background='transparent'; }; it.onclick=function(){ try{ window.location.href = withBase(String(r.href||'')); }catch(_){} panel.style.display='none'; }; list.appendChild(it); }); }); }
51
- function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
52
- function filterAndShow(q){ try{ var qq=norm(q); if(!qq){ try{ panel.style.display='block'; list.innerHTML=''; }catch(_){} return; } var out=[]; for(var i=0;i<records.length;i++){ var r=records[i]; var title=String((r&&r.title)||''); if(!title) continue; if(norm(title).indexOf(qq)!==-1) out.push(r); if(out.length>=maxResults) break; } render(out); }catch(_){} }
53
- input.addEventListener('input', function(){ if(onSearchPage){ try{ var ev = new CustomEvent('canopy:search:setQuery', { detail: { query: (input.value||'') } }); window.dispatchEvent(ev); }catch(_){ } return; } filterAndShow(input.value||''); });
54
- // Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
55
- input.addEventListener('keydown', function(e){
56
- if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
57
- else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
58
- });
59
- list.addEventListener('keydown', function(e){
60
- var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
61
- if(!cur) return;
62
- if(e.key==='ArrowDown'){
63
- e.preventDefault();
64
- try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
65
- } else if(e.key==='ArrowUp'){
66
- e.preventDefault();
67
- try{ var arr2=getItems(); var i2=arr2.indexOf(cur); if(i2<=0){ input && input.focus && input.focus(); } else { var prv=arr2[i2-1]; (prv||cur).focus(); } }catch(_){}
68
- } else if(e.key==='Enter'){
69
- e.preventDefault(); try{ cur.click(); }catch(_){}
70
- } else if(e.key==='Escape'){
71
- panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
72
- }
73
- });
74
- document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
75
- document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
76
- // Hotkey support (e.g., mod+k)
77
- document.addEventListener('keydown', function(e){
78
- try {
79
- var want = String((cfg && cfg.hotkey) || '').toLowerCase();
80
- if (!want) return;
81
- var isMod = e.metaKey || e.ctrlKey;
82
- if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
83
- e.preventDefault();
84
- if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
85
- panel.style.display='block';
86
- (input && input.focus && input.focus());
87
- filterAndShow(input && input.value || '');
88
- }
89
- } catch(_) { }
90
- });
91
- function openPanel(){ if(onSearchPage){ try{ var ev3 = new CustomEvent('canopy:search:setQuery', { detail: {} }); window.dispatchEvent(ev3); }catch(_){ } return; } panel.style.display='block'; (input && input.focus && input.focus()); filterAndShow(input && input.value || ''); }
92
- host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
93
- try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
94
- });
95
- })();
96
- `;
97
- await fsp.writeFile(cmdOut, fallback, 'utf8');
98
- try { logLine(`✓ Wrote ${path.relative(process.cwd(), cmdOut)} (fallback)`, 'cyan'); } catch (_) {}
27
+ async function prepareCommandRuntime() {
28
+ const esbuild = await resolveEsbuild();
29
+ if (!esbuild) throw new Error('Command runtime bundling requires esbuild. Install dependencies before building.');
30
+ ensureDirSync(OUT_DIR);
31
+ const scriptsDir = path.join(OUT_DIR, 'scripts');
32
+ ensureDirSync(scriptsDir);
33
+ const entry = path.join(__dirname, '..', 'search', 'command-runtime.js');
34
+ const outFile = path.join(scriptsDir, 'canopy-command.js');
35
+ await esbuild.build({
36
+ entryPoints: [entry],
37
+ outfile: outFile,
38
+ platform: 'browser',
39
+ format: 'iife',
40
+ bundle: true,
41
+ sourcemap: false,
42
+ target: ['es2018'],
43
+ logLevel: 'silent',
44
+ minify: true,
45
+ });
46
+ try {
47
+ let size = 0;
48
+ try { const st = fs.statSync(outFile); size = st.size || 0; } catch (_) {}
49
+ const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : '';
50
+ const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
51
+ logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
52
+ } catch (_) {}
99
53
  }
100
54
 
101
55
  async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
102
56
  const search = require('../search/search');
103
- try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
57
+ try { logLine(`• Writing search runtime${label ? ' (' + label + ')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
58
+
104
59
  let timedOut = false;
105
60
  await Promise.race([
106
61
  search.ensureSearchRuntime(),
107
62
  new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
108
63
  ]).catch(() => {
109
- try { console.warn(`Search: Bundling runtime timed out${label ? ' ('+label+')' : ''}, skipping`); } catch (_) {}
64
+ try { console.warn(`Search: Bundling runtime timed out${label ? ' (' + label + ')' : ''}, skipping`); } catch (_) {}
110
65
  });
111
66
  if (timedOut) {
112
- try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
67
+ try { logLine(`! Search runtime not bundled${label ? ' (' + label + ')' : ''}\n`, 'yellow'); } catch (_) {}
113
68
  }
114
69
  }
115
70
 
116
- module.exports = { prepareAllRuntimes, ensureCommandFallback, prepareSearchRuntime };
71
+ module.exports = { prepareAllRuntimes, prepareCommandRuntime, prepareSearchRuntime };
@@ -1,5 +1,6 @@
1
1
 
2
2
  const { logLine } = require('./log');
3
+ const { rootRelativeHref } = require('../common');
3
4
 
4
5
  function pagesToRecords(pageRecords) {
5
6
  const list = Array.isArray(pageRecords) ? pageRecords : [];
@@ -7,7 +8,7 @@ function pagesToRecords(pageRecords) {
7
8
  .filter((p) => p && p.href && p.searchInclude)
8
9
  .map((p) => ({
9
10
  title: p.title || p.href,
10
- href: p.href,
11
+ href: rootRelativeHref(p.href),
11
12
  type: p.searchType || 'page',
12
13
  }));
13
14
  }
@@ -18,11 +19,11 @@ function maybeMockRecords() {
18
19
  const svg = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="400" height="300" fill="#dbeafe"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="24" fill="#1d4ed8">Mock</text></svg>');
19
20
  const thumb = `data:image/svg+xml;charset=utf-8,${svg}`;
20
21
  for (let i = 1; i <= 120; i++) {
21
- mock.push({ title: `Mock Work #${i}`, href: `works/mock-${i}.html`, type: 'work', thumbnail: thumb });
22
+ mock.push({ title: `Mock Work #${i}`, href: rootRelativeHref(`works/mock-${i}.html`), type: 'work', thumbnail: thumb });
22
23
  }
23
- mock.push({ title: 'Mock Doc A', href: 'getting-started/index.html', type: 'docs' });
24
- mock.push({ title: 'Mock Doc B', href: 'getting-started/example.html', type: 'docs' });
25
- mock.push({ title: 'Mock Page', href: 'index.html', type: 'page' });
24
+ mock.push({ title: 'Mock Doc A', href: rootRelativeHref('getting-started/index.html'), type: 'docs' });
25
+ mock.push({ title: 'Mock Doc B', href: rootRelativeHref('getting-started/example.html'), type: 'docs' });
26
+ mock.push({ title: 'Mock Page', href: rootRelativeHref('index.html'), type: 'page' });
26
27
  return mock;
27
28
  }
28
29
 
@@ -23,7 +23,8 @@ async function buildFacetsForWorks(combined, labelWhitelist) {
23
23
  const rec = combined[i];
24
24
  if (!rec || String(rec.type) !== 'work') continue;
25
25
  const href = String(rec.href || '');
26
- const m = href.match(/^works\/(.+)\.html$/i);
26
+ const normalizedHref = href.replace(/^\/+/, '');
27
+ const m = normalizedHref.match(/^works\/(.+)\.html$/i);
27
28
  if (!m) continue;
28
29
  const slug = m[1];
29
30
  const p = path.resolve('.cache/iiif/manifests', slug + '.json');
@@ -169,7 +170,7 @@ module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearch
169
170
 
170
171
 
171
172
  async function collectMdxPageRecords() {
172
- const { fs, fsp, path, CONTENT_DIR } = require('../common');
173
+ const { fs, fsp, path, CONTENT_DIR, rootRelativeHref } = require('../common');
173
174
  const mdx = require('./mdx');
174
175
  const pagesHelpers = require('./pages');
175
176
  const pages = [];
@@ -185,7 +186,7 @@ async function collectMdxPageRecords() {
185
186
  const title = mdx.extractTitle(src);
186
187
  const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
187
188
  if (base !== 'sitemap.mdx') {
188
- const href = rel.split(path.sep).join('/');
189
+ const href = rootRelativeHref(rel.split(path.sep).join('/'));
189
190
  const underSearch = /^search\//i.test(href) || href.toLowerCase() === 'search.html';
190
191
  let include = !underSearch;
191
192
  let resolvedType = null;
@@ -70,21 +70,15 @@ async function ensureStyles() {
70
70
  }
71
71
 
72
72
  function resolveTailwindCli() {
73
- try {
74
- const cliJs = require.resolve("tailwindcss/lib/cli.js");
75
- return { cmd: process.execPath, args: [cliJs] };
76
- } catch (_) {}
77
- try {
78
- const localBin = path.join(
79
- process.cwd(),
80
- "node_modules",
81
- ".bin",
82
- process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
83
- );
84
- if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
85
- } catch (_) {}
86
- return null;
87
- }
73
+ const localBin = path.join(
74
+ process.cwd(),
75
+ "node_modules",
76
+ ".bin",
77
+ process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
78
+ );
79
+ if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
80
+ return { cmd: 'tailwindcss', args: [] };
81
+ }
88
82
  function buildTailwindCli({ input, output, config, minify = true }) {
89
83
  try {
90
84
  const cli = resolveTailwindCli();
package/lib/common.js CHANGED
@@ -60,6 +60,25 @@ function withBase(href) {
60
60
  return href;
61
61
  }
62
62
 
63
+ function rootRelativeHref(href) {
64
+ try {
65
+ let raw = href == null ? '' : String(href);
66
+ raw = raw.trim();
67
+ if (!raw) return '/';
68
+ if (/^[a-z][a-z0-9+.-]*:/i.test(raw)) return raw;
69
+ if (raw.startsWith('//')) return raw;
70
+ if (raw.startsWith('#') || raw.startsWith('?')) return raw;
71
+ let cleaned = raw;
72
+ if (cleaned.startsWith('/')) cleaned = cleaned.replace(/^\/+/, '');
73
+ while (cleaned.startsWith('./')) cleaned = cleaned.slice(2);
74
+ while (cleaned.startsWith('../')) cleaned = cleaned.slice(3);
75
+ if (!cleaned) return '/';
76
+ return '/' + cleaned;
77
+ } catch (_) {
78
+ return href;
79
+ }
80
+ }
81
+
63
82
  // Convert a site-relative path (e.g., "/api/foo.json") to an absolute URL.
64
83
  // Handles either:
65
84
  // - BASE_ORIGIN that may already include a path prefix (e.g., https://host/org/repo)
@@ -121,4 +140,5 @@ module.exports = {
121
140
  BASE_ORIGIN,
122
141
  absoluteUrl,
123
142
  applyBaseToHtml,
143
+ rootRelativeHref,
124
144
  };
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const yaml = require('js-yaml');
4
+ const { rootRelativeHref } = require('../common');
4
5
 
5
6
  function firstLabelString(label) {
6
7
  if (!label) return 'Untitled';
@@ -101,7 +102,7 @@ function readFeaturedFromCacheSync() {
101
102
  if (!m) continue;
102
103
  const rec = {
103
104
  title: firstLabelString(m && m.label),
104
- href: path.join('works', slug + '.html').split(path.sep).join('/'),
105
+ href: rootRelativeHref(path.join('works', slug + '.html').split(path.sep).join('/')),
105
106
  type: 'work',
106
107
  };
107
108
  if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);