@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 +41 -120
- package/lib/build/runtimes.js +9 -5
- package/lib/search/search-app.jsx +3 -12
- package/lib/search/search.js +15 -53
- package/package.json +1 -1
- package/ui/dist/index.mjs +111 -181
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +121 -3023
- package/ui/dist/server.mjs.map +4 -4
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
|
|
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
|
-
|
|
95
|
-
try { resolved = require.resolve("@canopy-iiif/app/ui/server"); } catch (_) {
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
}
|
package/lib/build/runtimes.js
CHANGED
|
@@ -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();
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
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
|
-
|
|
283
|
+
// Total mount removed
|
|
293
284
|
try {
|
|
294
285
|
window.addEventListener('canopy:search:setQuery', (ev) => {
|
|
295
286
|
try {
|
package/lib/search/search.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|