@canopy-iiif/app 0.7.14 → 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
@@ -22,7 +22,7 @@ const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
22
22
  const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
23
23
  // Primary global index location
24
24
  const IIIF_CACHE_INDEX = path.join(IIIF_CACHE_DIR, "index.json");
25
- // Legacy locations kept for backward compatibility (read + optional write)
25
+ // Additional legacy locations kept for backward compatibility (read + optional write)
26
26
  const IIIF_CACHE_INDEX_LEGACY = path.join(
27
27
  IIIF_CACHE_DIR,
28
28
  "manifest-index.json"
@@ -150,7 +150,7 @@ async function loadManifestIndex() {
150
150
  return { byId, collection: idx.collection || null };
151
151
  }
152
152
  }
153
- // Fallback: legacy in .cache/iiif
153
+ // Legacy index location retained for backward compatibility
154
154
  if (fs.existsSync(IIIF_CACHE_INDEX_LEGACY)) {
155
155
  const idx = await readJson(IIIF_CACHE_INDEX_LEGACY);
156
156
  if (idx && typeof idx === "object") {
@@ -167,7 +167,7 @@ async function loadManifestIndex() {
167
167
  return { byId, collection: idx.collection || null };
168
168
  }
169
169
  }
170
- // Fallback: legacy in manifests subdir
170
+ // Legacy manifests index retained for backward compatibility
171
171
  if (fs.existsSync(IIIF_CACHE_INDEX_MANIFESTS)) {
172
172
  const idx = await readJson(IIIF_CACHE_INDEX_MANIFESTS);
173
173
  if (idx && typeof idx === "object") {
@@ -406,8 +406,9 @@ async function ensureFeaturedInCache(cfg) {
406
406
  }
407
407
  } catch (_) {}
408
408
  }
409
- } catch (_) {
410
- // 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}`);
411
412
  }
412
413
  }
413
414
 
@@ -643,33 +644,20 @@ async function buildIiifCollectionPages(CONFIG) {
643
644
  1200;
644
645
 
645
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
+ }
646
653
  let WorksLayoutComp = null;
647
654
  try {
648
- const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
649
655
  WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
650
- } catch (_) {
651
- // Minimal fallback layout if missing or fails to compile
652
- WorksLayoutComp = function FallbackWorksLayout({ manifest }) {
653
- const title = firstLabelString(manifest && manifest.label);
654
- return React.createElement(
655
- "div",
656
- { className: "content" },
657
- React.createElement("h1", null, title || "Untitled"),
658
- // Render viewer placeholder for hydration
659
- React.createElement(
660
- "div",
661
- { "data-canopy-viewer": "1" },
662
- React.createElement("script", {
663
- type: "application/json",
664
- dangerouslySetInnerHTML: {
665
- __html: JSON.stringify({
666
- iiifContent: manifest && (manifest.id || ""),
667
- }),
668
- },
669
- })
670
- )
671
- );
672
- };
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
+ );
673
661
  }
674
662
 
675
663
  for (let ci = 0; ci < chunks; ci++) {
@@ -808,20 +796,8 @@ async function buildIiifCollectionPages(CONFIG) {
808
796
  href = withBase(href);
809
797
  return React.createElement("a", { href, ...rest }, props.children);
810
798
  };
811
- // Map exported UI components into MDX, with sensible aliases/fallbacks
799
+ // Map exported UI components into MDX and add anchor helper
812
800
  const compMap = { ...components, a: Anchor };
813
- if (!compMap.SearchPanel && compMap.CommandPalette) {
814
- compMap.SearchPanel = compMap.CommandPalette;
815
- }
816
- if (!components.HelloWorld) {
817
- components.HelloWorld = components.Fallback
818
- ? (props) =>
819
- React.createElement(components.Fallback, {
820
- name: "HelloWorld",
821
- ...props,
822
- })
823
- : () => null;
824
- }
825
801
  let MDXProvider = null;
826
802
  try {
827
803
  const mod = await import("@mdx-js/react");
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,121 +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){
32
- try{
33
- var raw = href == null ? '' : String(href);
34
- raw = raw.trim();
35
- if(!raw) return raw;
36
- var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : '';
37
- if(!bp){
38
- if(/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.indexOf('//') === 0 || raw.charAt(0)==='#' || raw.charAt(0)==='?') return raw;
39
- var cleaned = raw.replace(/^\\/+/, '');
40
- while(cleaned.indexOf('./') === 0) cleaned = cleaned.slice(2);
41
- while(cleaned.indexOf('../') === 0) cleaned = cleaned.slice(3);
42
- if(!cleaned) return '/';
43
- return '/' + cleaned;
44
- }
45
- if(/^https?:/i.test(raw)) return raw;
46
- var clean = raw.replace(/^\\/+/, '');
47
- return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean;
48
- } catch(_){ return href; }
49
- }
50
- 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 ''; } }
51
- 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; } }
52
- 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 []; } }
53
- 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();
54
- var panel = (function(){ try{ return host.querySelector('[data-canopy-command-panel]') || null; }catch(_){ return null; } })();
55
- if(!panel) return; // no SSR panel present; do nothing
56
- try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){}
57
- if(onSearchPage){ panel.style.display='none'; }
58
- var list=panel.querySelector('#cplist');
59
- var input = (function(){ try{ return host.querySelector('[data-canopy-command-input]') || null; }catch(_){ return null; } })();
60
- if(!input) return; // require SSR input; no dynamic creation
61
- // Populate from ?q= URL param if present
62
- try {
63
- var sp = new URLSearchParams(location.search || '');
64
- var qp = sp.get('q');
65
- if (qp) input.value = qp;
66
- } catch(_) {}
67
- // Do not inject legacy trigger buttons or inputs
68
- 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); }); }); }
69
- function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
70
- 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(_){} }
71
- 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||''); });
72
- // Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
73
- input.addEventListener('keydown', function(e){
74
- if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
75
- else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
76
- });
77
- list.addEventListener('keydown', function(e){
78
- var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
79
- if(!cur) return;
80
- if(e.key==='ArrowDown'){
81
- e.preventDefault();
82
- try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
83
- } else if(e.key==='ArrowUp'){
84
- e.preventDefault();
85
- 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(_){}
86
- } else if(e.key==='Enter'){
87
- e.preventDefault(); try{ cur.click(); }catch(_){}
88
- } else if(e.key==='Escape'){
89
- panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
90
- }
91
- });
92
- document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
93
- document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
94
- // Hotkey support (e.g., mod+k)
95
- document.addEventListener('keydown', function(e){
96
- try {
97
- var want = String((cfg && cfg.hotkey) || '').toLowerCase();
98
- if (!want) return;
99
- var isMod = e.metaKey || e.ctrlKey;
100
- if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
101
- e.preventDefault();
102
- if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
103
- panel.style.display='block';
104
- (input && input.focus && input.focus());
105
- filterAndShow(input && input.value || '');
106
- }
107
- } catch(_) { }
108
- });
109
- 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 || ''); }
110
- host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
111
- try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
112
- });
113
- })();
114
- `;
115
- await fsp.writeFile(cmdOut, fallback, 'utf8');
116
- 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 (_) {}
117
53
  }
118
54
 
119
55
  async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
120
56
  const search = require('../search/search');
121
- try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
57
+ try { logLine(`• Writing search runtime${label ? ' (' + label + ')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
58
+
122
59
  let timedOut = false;
123
60
  await Promise.race([
124
61
  search.ensureSearchRuntime(),
125
62
  new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
126
63
  ]).catch(() => {
127
- 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 (_) {}
128
65
  });
129
66
  if (timedOut) {
130
- try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
67
+ try { logLine(`! Search runtime not bundled${label ? ' (' + label + ')' : ''}\n`, 'yellow'); } catch (_) {}
131
68
  }
132
69
  }
133
70
 
134
- module.exports = { prepareAllRuntimes, ensureCommandFallback, prepareSearchRuntime };
71
+ module.exports = { prepareAllRuntimes, prepareCommandRuntime, prepareSearchRuntime };
@@ -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();