@canopy-iiif/app 0.7.10 → 0.7.12

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/mdx.js CHANGED
@@ -51,48 +51,18 @@ let UI_COMPONENTS_MTIME = 0;
51
51
  const DEBUG = process.env.CANOPY_DEBUG === '1' || process.env.CANOPY_DEBUG === 'true';
52
52
  async function loadUiComponents() {
53
53
  // Do not rely on a cached mapping; re-import each time to avoid transient races.
54
- const fallbackCommand = function CommandPalette(props = {}) {
55
- const {
56
- placeholder = 'Search…',
57
- hotkey = 'mod+k',
58
- maxResults = 8,
59
- groupOrder = ['work', 'page'],
60
- button = true,
61
- buttonLabel = 'Search',
62
- label,
63
- searchPath = '/search',
64
- } = props || {};
65
- const text = (typeof label === 'string' && label.trim()) ? label.trim() : buttonLabel;
66
- let json = "{}";
67
- try { json = JSON.stringify({ placeholder, hotkey, maxResults, groupOrder, label: text, searchPath }); } catch (_) { json = "{}"; }
68
- return React.createElement(
69
- 'div',
70
- { 'data-canopy-command': true, className: 'flex-1 min-w-0' },
71
- // Teaser form fallback
72
- React.createElement('div', { className: 'relative w-full' },
73
- React.createElement('style', { dangerouslySetInnerHTML: { __html: ".relative[data-canopy-panel-auto='1']:focus-within [data-canopy-command-panel]{display:block}" } }),
74
- React.createElement('form', { action: searchPath, method: 'get', role: 'search', className: 'group flex items-center gap-2 px-2 py-1.5 rounded-lg border border-slate-300 bg-white/95 backdrop-blur text-slate-700 shadow-sm hover:shadow transition w-full focus-within:ring-2 focus-within:ring-brand-500' },
75
- // Left icon
76
- React.createElement('span', { 'aria-hidden': true, className: 'text-slate-500' }, '🔎'),
77
- // Input teaser
78
- React.createElement('input', { type: 'search', name: 'q', 'data-canopy-command-input': true, placeholder, 'aria-label': 'Search', className: 'flex-1 bg-transparent outline-none placeholder:text-slate-400 py-0.5 min-w-0' }),
79
- // Right submit (navigates)
80
- React.createElement('button', { type: 'submit', 'data-canopy-command-link': true, className: 'inline-flex items-center gap-1 px-2 py-1 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 text-slate-700' }, text)
81
- ),
82
- // SSR placeholder panel; runtime will reuse and control visibility
83
- React.createElement('div', { 'data-canopy-command-panel': true, style: { display: 'none', position: 'absolute', left: 0, right: 0, top: 'calc(100% + 4px)', background: '#fff', border: '1px solid #e5e7eb', borderRadius: '8px', boxShadow: '0 10px 25px rgba(0,0,0,0.12)', zIndex: 1000, overflow: 'auto', maxHeight: '60vh' } },
84
- React.createElement('div', { id: 'cplist' })
85
- )
86
- ),
87
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: json } })
88
- );
89
- };
90
54
  try {
91
- // Prefer resolving the actual dist file and busting cache via mtime to pick up new exports during dev
55
+ // Prefer the workspace dist path during dev to avoid export-map resolution issues
92
56
  let resolved = null;
57
+ try {
58
+ const wsDist = path.join(process.cwd(), 'packages', 'app', 'ui', 'dist', 'server.mjs');
59
+ if (fs.existsSync(wsDist)) resolved = wsDist;
60
+ } catch (_) {}
93
61
  // Prefer explicit dist path to avoid export-map issues
94
- try { resolved = require.resolve("@canopy-iiif/app/ui/dist/server.mjs"); } catch (_) {
95
- try { resolved = require.resolve("@canopy-iiif/app/ui/server"); } catch (_) { resolved = null; }
62
+ if (!resolved) {
63
+ try { resolved = require.resolve("@canopy-iiif/app/ui/dist/server.mjs"); } catch (_) {
64
+ try { resolved = require.resolve("@canopy-iiif/app/ui/server"); } catch (_) { resolved = null; }
65
+ }
96
66
  }
97
67
  // Determine current mtime for change detection
98
68
  let currentPath = resolved || '';
@@ -108,50 +78,45 @@ async function loadUiComponents() {
108
78
  return UI_COMPONENTS;
109
79
  }
110
80
  let mod = null;
81
+ let importErr = null;
111
82
  if (resolved) {
112
83
  const { pathToFileURL } = require("url");
113
- let bust = currentMtime ? `?v=${currentMtime}` : `?v=${Date.now()}`;
114
- mod = await import(pathToFileURL(resolved).href + bust).catch((e) => {
115
- if (DEBUG) { try { console.warn('[canopy][mdx] ESM import failed for', resolved, '\n', e && (e.stack || e.message || String(e))); } catch(_){} }
116
- return null;
117
- });
84
+ const fileUrl = pathToFileURL(resolved).href;
85
+ const attempts = 5;
86
+ for (let i = 0; i < attempts && !mod; i++) {
87
+ const bustVal = currentMtime ? `${currentMtime}-${i}` : `${Date.now()}-${i}`;
88
+ try {
89
+ mod = await import(fileUrl + `?v=${bustVal}`);
90
+ } catch (e) {
91
+ importErr = e;
92
+ if (DEBUG) {
93
+ try { console.warn('[canopy][mdx] ESM import failed for', resolved, '(attempt', i + 1, 'of', attempts + ')\n', e && (e.stack || e.message || String(e))); } catch(_){}
94
+ }
95
+ // Small delay to avoid watch-write race
96
+ await new Promise((r) => setTimeout(r, 60));
97
+ }
98
+ }
118
99
  if (DEBUG) {
119
100
  try { console.log('[canopy][mdx] UI components resolved', { path: resolved, mtime: currentMtime, loaded: !!mod }); } catch(_){}
120
101
  }
121
102
  }
122
103
  if (!mod) {
123
- mod = await import("@canopy-iiif/app/ui/server").catch((e) => {
124
- if (DEBUG) { try { console.warn('[canopy][mdx] Export-map import failed for @canopy-iiif/app/ui/server', '\n', e && (e.stack || e.message || String(e))); } catch(_){} }
125
- return null;
126
- });
127
- if (DEBUG) {
128
- try { console.log('[canopy][mdx] UI components fallback import via export map', { ok: !!mod }); } catch(_){}
104
+ // Try package subpath as a secondary resolution path (not a UI component fallback)
105
+ try {
106
+ mod = await import('@canopy-iiif/app/ui/server');
107
+ } catch (e2) {
108
+ const msgA = importErr && (importErr.stack || importErr.message);
109
+ const msgB = e2 && (e2.stack || e2.message);
110
+ throw new Error('Failed to load @canopy-iiif/app/ui/server. Ensure the UI package is built.\nPath import error: ' + (msgA||'') + '\nExport-map import error: ' + (msgB||''));
129
111
  }
130
112
  }
131
113
  let comp = (mod && typeof mod === 'object') ? mod : {};
132
- // Fallback: import workspace source directly if key exports missing
133
- const needFallback = !(comp && (comp.CommandPalette || comp.FeaturedHero || comp.Viewer));
134
- if (needFallback) {
135
- try {
136
- const ws = path.join(process.cwd(), 'packages', 'app', 'ui', 'server.js');
137
- if (fs.existsSync(ws)) {
138
- const { pathToFileURL } = require('url');
139
- // Use workspace server.js mtime for busting
140
- let bustWs = `?v=${Date.now()}`;
141
- try { const st = fs.statSync(ws); currentPath = ws; currentMtime = Math.floor(st.mtimeMs || 0); bustWs = `?v=${currentMtime}`; } catch (_) {}
142
- const wsMod = await import(pathToFileURL(ws).href + bustWs).catch(() => null);
143
- if (wsMod && typeof wsMod === 'object') {
144
- comp = { ...wsMod, ...comp };
145
- }
146
- if (DEBUG) {
147
- try { console.log('[canopy][mdx] UI components augmented from workspace source', { ws, ok: !!wsMod }); } catch(_){}
148
- }
149
- }
150
- } catch (_) {}
114
+ // Hard-require core exports; do not inject fallbacks
115
+ const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems'];
116
+ const missing = required.filter((k) => !comp || !comp[k]);
117
+ if (missing.length) {
118
+ throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
151
119
  }
152
- // Ensure core placeholders exist to avoid MDX compile failures
153
- if (!comp.CommandPalette) comp.CommandPalette = fallbackCommand;
154
- if (!comp.SearchPanel) comp.SearchPanel = comp.CommandPalette || fallbackCommand;
155
120
  if (DEBUG) {
156
121
  try { console.log('[canopy][mdx] UI component sources', {
157
122
  path: currentPath,
@@ -163,49 +128,7 @@ async function loadUiComponents() {
163
128
  Slider: !!comp.Slider,
164
129
  }); } catch(_){}
165
130
  }
166
- const mkJson = (props) => {
167
- try { return JSON.stringify(props || {}); } catch (_) { return '{}'; }
168
- };
169
- if (!comp.Viewer) comp.Viewer = function Viewer(props){
170
- return React.createElement('div', { 'data-canopy-viewer': '1', className: 'not-prose' },
171
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
172
- );
173
- };
174
- if (!comp.Slider) comp.Slider = function Slider(props){
175
- return React.createElement('div', { 'data-canopy-slider': '1', className: 'not-prose' },
176
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
177
- );
178
- };
179
- if (!comp.RelatedItems) comp.RelatedItems = function RelatedItems(props){
180
- return React.createElement('div', { 'data-canopy-related-items': '1', className: 'not-prose' },
181
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
182
- );
183
- };
184
- if (!comp.SearchForm) comp.SearchForm = function SearchForm(props){
185
- return React.createElement('div', { 'data-canopy-search-form': '1' },
186
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
187
- );
188
- };
189
- if (!comp.SearchResults) comp.SearchResults = function SearchResults(props){
190
- return React.createElement('div', { 'data-canopy-search-results': '1' },
191
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
192
- );
193
- };
194
- if (!comp.SearchSummary) comp.SearchSummary = function SearchSummary(props){
195
- return React.createElement('div', { 'data-canopy-search-summary': '1' },
196
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
197
- );
198
- };
199
- if (!comp.SearchTotal) comp.SearchTotal = function SearchTotal(props){
200
- return React.createElement('div', { 'data-canopy-search-total': '1' },
201
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
202
- );
203
- };
204
- if (!comp.SearchTabs) comp.SearchTabs = function SearchTabs(props){
205
- return React.createElement('div', { 'data-canopy-search-tabs': '1' },
206
- React.createElement('script', { type: 'application/json', dangerouslySetInnerHTML: { __html: mkJson(props) } })
207
- );
208
- };
131
+ // No stub injection beyond this point; UI package must supply these.
209
132
  // Ensure a minimal SSR Hero exists
210
133
  if (!comp.Hero) {
211
134
  comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
@@ -243,11 +166,9 @@ async function loadUiComponents() {
243
166
  UI_COMPONENTS = comp;
244
167
  UI_COMPONENTS_PATH = currentPath;
245
168
  UI_COMPONENTS_MTIME = currentMtime;
246
- } catch (_) {
247
- // As a last resort, supply minimal stubs so pages still compile
248
- UI_COMPONENTS = { CommandPalette: fallbackCommand };
249
- UI_COMPONENTS_PATH = '';
250
- UI_COMPONENTS_MTIME = 0;
169
+ } catch (e) {
170
+ const msg = e && (e.stack || e.message || String(e)) || 'unknown error';
171
+ throw new Error('[canopy][mdx] Failed to load UI components (no fallbacks): ' + msg);
251
172
  }
252
173
  return UI_COMPONENTS;
253
174
  }
@@ -31,17 +31,22 @@ async function ensureCommandFallback() {
31
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
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
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
- function createUI(host){ var rel=(function(){ try{ var w=host.querySelector('.relative'); return w||host; }catch(_){ return host; } })(); try{ var cs = window.getComputedStyle(rel); if(!cs || cs.position==='static') rel.style.position='relative'; }catch(_){ } var existing = rel.querySelector('[data-canopy-command-panel]'); if(existing) return existing; var panel=document.createElement('div'); panel.setAttribute('data-canopy-command-panel',''); panel.style.cssText='position:absolute;left:0;right:0;top:calc(100% + 4px);display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 10px 25px rgba(0,0,0,0.12);z-index:1000;overflow:auto;max-height:60vh;'; panel.innerHTML='<div id="cplist"></div>'; rel.appendChild(panel); return panel; }
35
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 []; } }
36
- 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(); var panel=createUI(host); try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){} if(onSearchPage){ panel.style.display='none'; } var list=panel.querySelector('#cplist'); var teaser = host.querySelector('[data-canopy-command-input]'); var input = teaser; if(!input){ input = document.createElement('input'); input.type='search'; input.placeholder = cfg.placeholder || 'Search…'; input.setAttribute('aria-label','Search'); input.style.cssText='display:block;width:100%;padding:8px 10px;border-bottom:1px solid #e5e7eb;outline:none;'; panel.insertBefore(input, list); }
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
37
43
  // Populate from ?q= URL param if present
38
44
  try {
39
45
  var sp = new URLSearchParams(location.search || '');
40
46
  var qp = sp.get('q');
41
47
  if (qp) input.value = qp;
42
48
  } catch(_) {}
43
- // Only inject a legacy trigger button if neither a trigger nor a teaser input exists
44
- try{ if(!host.querySelector('[data-canopy-command-trigger]') && !host.querySelector('[data-canopy-command-input]')){ var btn=document.createElement('button'); btn.type='button'; btn.setAttribute('data-canopy-command-trigger',''); btn.setAttribute('aria-label','Open search'); btn.className='inline-flex items-center gap-2 px-3 py-1.5 rounded border border-slate-300 text-slate-700 hover:bg-slate-50'; var lbl=((cfg&&cfg.label)||(cfg&&cfg.buttonLabel)||'Search'); var sLbl=document.createElement('span'); sLbl.textContent=String(lbl); var sK=document.createElement('span'); sK.setAttribute('aria-hidden','true'); sK.className='text-slate-500'; sK.textContent='⌘K'; btn.appendChild(sLbl); btn.appendChild(sK); host.appendChild(btn); } }catch(_){ }
49
+ // Do not inject legacy trigger buttons or inputs
45
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); }); }); }
46
51
  function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
47
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(_){} }
@@ -85,7 +90,6 @@ async function ensureCommandFallback() {
85
90
  });
86
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 || ''); }
87
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(); }});
88
- var btn = document.querySelector('[data-canopy-command-trigger]'); if(btn){ btn.addEventListener('click', function(){ openPanel(); }); }
89
93
  try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
90
94
  });
91
95
  })();
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useMemo, useSyncExternalStore, useState } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { SearchFormUI, SearchResultsUI, SearchTabsUI } from '@canopy-iiif/app/ui';
3
+ import { SearchResultsUI, SearchTabsUI } from '@canopy-iiif/app/ui';
4
4
 
5
5
  // Lightweight IndexedDB utilities (no deps) with graceful fallback
6
6
  function hasIDB() {
@@ -233,10 +233,6 @@ function useStore() {
233
233
  return { ...snap, setQuery: store.setQuery, setType: store.setType };
234
234
  }
235
235
 
236
- function FormMount() {
237
- const { query, setQuery, type, setType, types, counts } = useStore();
238
- return <SearchFormUI query={query} onQueryChange={setQuery} type={type} onTypeChange={setType} types={types} counts={counts} />;
239
- }
240
236
  function ResultsMount(props = {}) {
241
237
  const { results, type, loading } = useStore();
242
238
  if (loading) return <div className="text-slate-600">Loading…</div>;
@@ -255,10 +251,6 @@ function SummaryMount() {
255
251
  }, [query, type, shown, total]);
256
252
  return <div className="text-sm text-slate-600">{text}</div>;
257
253
  }
258
- function TotalMount() {
259
- const { shown } = useStore();
260
- return <span>{shown}</span>;
261
- }
262
254
 
263
255
  function parseProps(el) {
264
256
  try {
@@ -284,12 +276,11 @@ function mountAt(selector, Comp) {
284
276
 
285
277
  if (typeof document !== 'undefined') {
286
278
  const run = () => {
287
- // Mount tabs (preferred) or full form if present, plus summary/total/results
279
+ // Mount tabs and other search UI pieces
288
280
  mountAt('[data-canopy-search-tabs]', TabsMount);
289
- mountAt('[data-canopy-search-form]', FormMount);
290
281
  mountAt('[data-canopy-search-results]', ResultsMount);
291
282
  mountAt('[data-canopy-search-summary]', SummaryMount);
292
- mountAt('[data-canopy-search-total]', TotalMount);
283
+ // Total mount removed
293
284
  try {
294
285
  window.addEventListener('canopy:search:setQuery', (ev) => {
295
286
  try {
@@ -6,7 +6,7 @@ const { ensureDirSync, OUT_DIR, htmlShell, fsp } = require('../common');
6
6
 
7
7
  const FALLBACK_SEARCH_APP = `import React, { useEffect, useMemo, useSyncExternalStore, useState } from 'react';
8
8
  import { createRoot } from 'react-dom/client';
9
- import { SearchFormUI, SearchResultsUI } from '@canopy-iiif/app/ui';
9
+ import { SearchResultsUI } from '@canopy-iiif/app/ui';
10
10
 
11
11
  function hasIDB(){ try { return typeof indexedDB !== 'undefined'; } catch (_) { return false; } }
12
12
  function idbOpen(){ return new Promise((resolve)=>{ if(!hasIDB()) return resolve(null); try{ const req = indexedDB.open('canopy-search',1); req.onupgradeneeded=()=>{ const db=req.result; if(!db.objectStoreNames.contains('indexes')) db.createObjectStore('indexes',{keyPath:'version'}); }; req.onsuccess=()=>resolve(req.result); req.onerror=()=>resolve(null);}catch(_){ resolve(null);} }); }
@@ -153,10 +153,6 @@ function useStore() {
153
153
  return { ...snap, setQuery: store.setQuery, setType: store.setType };
154
154
  }
155
155
 
156
- function FormMount() {
157
- const { query, setQuery, type, setType, types, counts } = useStore();
158
- return <SearchFormUI query={query} onQueryChange={setQuery} type={type} onTypeChange={setType} types={types} counts={counts} />;
159
- }
160
156
  function ResultsMount() {
161
157
  const { results, type, loading } = useStore();
162
158
  if (loading) return <div className=\"text-slate-600\">Loading…</div>;
@@ -170,10 +166,7 @@ function SummaryMount() {
170
166
  }, [query, type, shown, total]);
171
167
  return <div className=\"text-sm text-slate-600\">{text}</div>;
172
168
  }
173
- function TotalMount() {
174
- const { shown } = useStore();
175
- return <span>{shown}</span>;
176
- }
169
+
177
170
 
178
171
  function mountAt(selector, Comp) {
179
172
  const nodes = document.querySelectorAll(selector);
@@ -189,10 +182,9 @@ function mountAt(selector, Comp) {
189
182
 
190
183
  if (typeof document !== 'undefined') {
191
184
  const run = () => {
192
- mountAt('[data-canopy-search-form]', FormMount);
193
185
  mountAt('[data-canopy-search-results]', ResultsMount);
194
186
  mountAt('[data-canopy-search-summary]', SummaryMount);
195
- mountAt('[data-canopy-search-total]', TotalMount);
187
+
196
188
  };
197
189
  if (document.readyState !== 'loading') run();
198
190
  else document.addEventListener('DOMContentLoaded', run, { once: true });
@@ -314,14 +306,8 @@ async function ensureSearchRuntime() {
314
306
  plugins: [shimReactPlugin],
315
307
  external: ['@samvera/clover-iiif/*'],
316
308
  };
317
- if (entryExists) {
318
- await esbuild.build({ entryPoints: [entry], ...commonBuild });
319
- } else {
320
- await esbuild.build({
321
- stdin: { contents: FALLBACK_SEARCH_APP, resolveDir: process.cwd(), loader: 'jsx', sourcefile: 'fallback-search-app.jsx' },
322
- ...commonBuild,
323
- });
324
- }
309
+ if (!entryExists) throw new Error('Search runtime entry missing: ' + entry);
310
+ await esbuild.build({ entryPoints: [entry], ...commonBuild });
325
311
  } catch (e) {
326
312
  console.error('Search: bundle error:', e && e.message ? e.message : e);
327
313
  return;
@@ -339,43 +325,18 @@ async function buildSearchPage() {
339
325
  try {
340
326
  const outPath = path.join(OUT_DIR, 'search.html');
341
327
  ensureDirSync(path.dirname(outPath));
342
- // If the author provided content/search/_layout.mdx, render it via MDX; otherwise fall back.
328
+ // Require author-provided content/search/_layout.mdx; do not fall back to a generated page.
343
329
  const searchLayoutPath = path.join(path.resolve('content'), 'search', '_layout.mdx');
344
330
  let body = '';
345
331
  let head = '';
346
- if (require('../common').fs.existsSync(searchLayoutPath)) {
347
- try {
348
- const mdx = require('../build/mdx');
349
- const rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, {});
350
- body = rendered && rendered.body ? rendered.body : '';
351
- head = rendered && rendered.head ? rendered.head : '';
352
- } catch (e) {
353
- console.warn('Search: Failed to render content/search/_layout.mdx, falling back:', e && e.message ? e.message : e);
354
- }
355
- }
356
- if (!body) {
357
- // Minimal mount container; React SearchApp mounts into #search-root
358
- let content = React.createElement(
359
- 'div',
360
- null,
361
- React.createElement('h1', null, 'Search'),
362
- React.createElement('div', { id: 'search-root' })
363
- );
364
- const { loadAppWrapper, getMdxProvider, loadUiComponents } = require('../build/mdx');
365
- const app = await loadAppWrapper();
366
- const wrappedApp = app && app.App ? React.createElement(app.App, null, content) : content;
367
- // Ensure MDX components like <SearchPanel /> resolve when rendering App wrapper
368
- let page = wrappedApp;
369
- try {
370
- const MDXProvider = await getMdxProvider();
371
- const components = await loadUiComponents();
372
- if (MDXProvider && components) {
373
- page = React.createElement(MDXProvider, { components }, wrappedApp);
374
- }
375
- } catch (_) { /* render without provider on failure */ }
376
- body = ReactDOMServer.renderToStaticMarkup(page);
377
- head = app && app.Head ? ReactDOMServer.renderToStaticMarkup(React.createElement(app.Head)) : '';
332
+ if (!require('../common').fs.existsSync(searchLayoutPath)) {
333
+ throw new Error('Missing required file: content/search/_layout.mdx');
378
334
  }
335
+ const mdx = require('../build/mdx');
336
+ const rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, {});
337
+ body = rendered && rendered.body ? rendered.body : '';
338
+ head = rendered && rendered.head ? rendered.head : '';
339
+ if (!body) throw new Error('Search: content/search/_layout.mdx produced empty output');
379
340
  const importMap = '';
380
341
  const cssRel = path.relative(path.dirname(outPath), path.join(OUT_DIR, 'styles', 'styles.css')).split(path.sep).join('/');
381
342
  const jsAbs = path.join(OUT_DIR, 'scripts', 'search.js');
@@ -398,7 +359,8 @@ async function buildSearchPage() {
398
359
  await fsp.writeFile(outPath, html, 'utf8');
399
360
  console.log('Search: Built', path.relative(process.cwd(), outPath));
400
361
  } catch (e) {
401
- console.warn('Search: Failed to build page', e.message);
362
+ console.warn('Search: Failed to build page', e && (e.message || e));
363
+ throw e;
402
364
  }
403
365
  }
404
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",