@canopy-iiif/app 0.7.9 → 0.7.10
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/build.js +20 -1
- package/lib/build/iiif.js +68 -2
- package/lib/build/mdx.js +275 -5
- package/lib/build/pages.js +7 -3
- package/lib/build/runtimes.js +62 -8
- package/lib/build/verify.js +60 -0
- package/lib/components/featured.js +144 -0
- package/lib/search/search-app.jsx +34 -3
- package/lib/search/search.js +11 -2
- package/package.json +1 -1
- package/ui/dist/index.mjs +74 -27
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +3109 -21
- package/ui/dist/server.mjs.map +4 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { fs, path, OUT_DIR } = require('../common');
|
|
2
|
+
const { logLine } = require('./log');
|
|
3
|
+
|
|
4
|
+
function readFileSafe(p) {
|
|
5
|
+
try { return fs.readFileSync(p, 'utf8'); } catch (_) { return ''; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasHtmlFiles(dir) {
|
|
9
|
+
let count = 0;
|
|
10
|
+
if (!fs.existsSync(dir)) return 0;
|
|
11
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
for (const e of entries) {
|
|
13
|
+
const p = path.join(dir, e.name);
|
|
14
|
+
if (e.isDirectory()) count += hasHtmlFiles(p);
|
|
15
|
+
else if (e.isFile() && p.toLowerCase().endsWith('.html')) count++;
|
|
16
|
+
}
|
|
17
|
+
return count;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function verifyHomepageElements(outDir) {
|
|
21
|
+
const idx = path.join(outDir, 'index.html');
|
|
22
|
+
const html = readFileSafe(idx);
|
|
23
|
+
const okHero = /class=\"[^\"]*canopy-hero/.test(html) || /<div[^>]+canopy-hero/.test(html);
|
|
24
|
+
const okCommand = /data-canopy-command=/.test(html);
|
|
25
|
+
const okCommandTrigger = /data-canopy-command-trigger/.test(html);
|
|
26
|
+
const okCommandScriptRef = /<script[^>]+canopy-command\.js/.test(html);
|
|
27
|
+
return { okHero, okCommand, okCommandTrigger, okCommandScriptRef, htmlPath: idx };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function verifyBuildOutput(options = {}) {
|
|
31
|
+
const outDir = path.resolve(options.outDir || OUT_DIR);
|
|
32
|
+
logLine("\nVerify build output", "magenta", { bright: true, underscore: true });
|
|
33
|
+
const total = hasHtmlFiles(outDir);
|
|
34
|
+
const okAny = total > 0;
|
|
35
|
+
const indexPath = path.join(outDir, 'index.html');
|
|
36
|
+
const hasIndex = fs.existsSync(indexPath) && fs.statSync(indexPath).size > 0;
|
|
37
|
+
const { okHero, okCommand, okCommandTrigger, okCommandScriptRef } = verifyHomepageElements(outDir);
|
|
38
|
+
|
|
39
|
+
const ck = (label, ok, extra) => {
|
|
40
|
+
const status = ok ? '✓' : '✗';
|
|
41
|
+
logLine(`${status} ${label}${extra ? ` ${extra}` : ''}`, ok ? 'green' : 'red');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
ck('HTML pages exist', okAny, okAny ? `(${total})` : '');
|
|
45
|
+
ck('homepage exists', hasIndex, hasIndex ? `(${indexPath})` : '');
|
|
46
|
+
ck('homepage: Hero present', okHero);
|
|
47
|
+
ck('homepage: Command present', okCommand);
|
|
48
|
+
ck('homepage: Command trigger present', okCommandTrigger);
|
|
49
|
+
ck('homepage: Command script referenced', okCommandScriptRef);
|
|
50
|
+
|
|
51
|
+
// Do not fail build on missing SSR trigger; the client runtime injects a default.
|
|
52
|
+
const ok = okAny && hasIndex && okHero && okCommand && okCommandScriptRef;
|
|
53
|
+
if (!ok) {
|
|
54
|
+
const err = new Error('Build verification failed');
|
|
55
|
+
err.outDir = outDir;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { verifyBuildOutput };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
function firstLabelString(label) {
|
|
6
|
+
if (!label) return 'Untitled';
|
|
7
|
+
if (typeof label === 'string') return label;
|
|
8
|
+
try {
|
|
9
|
+
const keys = Object.keys(label || {});
|
|
10
|
+
if (!keys.length) return 'Untitled';
|
|
11
|
+
const arr = label[keys[0]];
|
|
12
|
+
if (Array.isArray(arr) && arr.length) return String(arr[0]);
|
|
13
|
+
} catch (_) {}
|
|
14
|
+
return 'Untitled';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeIiifId(raw) {
|
|
18
|
+
try {
|
|
19
|
+
const s = String(raw || '');
|
|
20
|
+
if (!/^https?:\/\//i.test(s)) return s;
|
|
21
|
+
const u = new URL(s);
|
|
22
|
+
const entries = Array.from(u.searchParams.entries()).sort(
|
|
23
|
+
(a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
|
|
24
|
+
);
|
|
25
|
+
u.search = '';
|
|
26
|
+
for (const [k, v] of entries) u.searchParams.append(k, v);
|
|
27
|
+
return u.toString();
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return String(raw || '');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function equalIiifId(a, b) {
|
|
34
|
+
try {
|
|
35
|
+
const an = normalizeIiifId(a);
|
|
36
|
+
const bn = normalizeIiifId(b);
|
|
37
|
+
if (an === bn) return true;
|
|
38
|
+
const ua = new URL(an);
|
|
39
|
+
const ub = new URL(bn);
|
|
40
|
+
return ua.origin === ub.origin && ua.pathname === ub.pathname;
|
|
41
|
+
} catch (_) {
|
|
42
|
+
return String(a || '') === String(b || '');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readYaml(p) {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(p)) return null;
|
|
49
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
50
|
+
return yaml.load(raw) || null;
|
|
51
|
+
} catch (_) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readJson(p) {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(p)) return null;
|
|
59
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
} catch (_) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findSlugByIdFromDiskSync(nid) {
|
|
67
|
+
try {
|
|
68
|
+
const dir = path.resolve('.cache/iiif/manifests');
|
|
69
|
+
if (!fs.existsSync(dir)) return null;
|
|
70
|
+
const names = fs.readdirSync(dir);
|
|
71
|
+
for (const name of names) {
|
|
72
|
+
if (!name || !name.toLowerCase().endsWith('.json')) continue;
|
|
73
|
+
const fp = path.join(dir, name);
|
|
74
|
+
try {
|
|
75
|
+
const obj = readJson(fp);
|
|
76
|
+
const mid = normalizeIiifId(String((obj && (obj.id || obj['@id'])) || ''));
|
|
77
|
+
if (mid && equalIiifId(mid, nid)) return name.replace(/\.json$/i, '');
|
|
78
|
+
} catch (_) {}
|
|
79
|
+
}
|
|
80
|
+
} catch (_) {}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readFeaturedFromCacheSync() {
|
|
85
|
+
try {
|
|
86
|
+
const debug = !!process.env.CANOPY_DEBUG_FEATURED;
|
|
87
|
+
const cfg = readYaml(path.resolve('canopy.yml')) || {};
|
|
88
|
+
const featured = Array.isArray(cfg && cfg.featured) ? cfg.featured : [];
|
|
89
|
+
if (!featured.length) return [];
|
|
90
|
+
const idx = readJson(path.resolve('.cache/iiif/index.json')) || {};
|
|
91
|
+
const byId = Array.isArray(idx && idx.byId) ? idx.byId : [];
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const id of featured) {
|
|
94
|
+
const nid = normalizeIiifId(id);
|
|
95
|
+
if (debug) { try { console.log('[featured] id:', id); } catch (_) {} }
|
|
96
|
+
const entry = byId.find((e) => e && e.type === 'Manifest' && equalIiifId(e.id, nid));
|
|
97
|
+
const slug = entry && entry.slug ? String(entry.slug) : findSlugByIdFromDiskSync(nid);
|
|
98
|
+
if (debug) { try { console.log('[featured] - slug:', slug || '(none)'); } catch (_) {} }
|
|
99
|
+
if (!slug) continue;
|
|
100
|
+
const m = readJson(path.resolve('.cache/iiif/manifests', slug + '.json'));
|
|
101
|
+
if (!m) continue;
|
|
102
|
+
const rec = {
|
|
103
|
+
title: firstLabelString(m && m.label),
|
|
104
|
+
href: path.join('works', slug + '.html').split(path.sep).join('/'),
|
|
105
|
+
type: 'work',
|
|
106
|
+
};
|
|
107
|
+
if (entry && entry.thumbnail) rec.thumbnail = String(entry.thumbnail);
|
|
108
|
+
if (entry && typeof entry.thumbnailWidth === 'number') rec.thumbnailWidth = entry.thumbnailWidth;
|
|
109
|
+
if (entry && typeof entry.thumbnailHeight === 'number') rec.thumbnailHeight = entry.thumbnailHeight;
|
|
110
|
+
if (!rec.thumbnail) {
|
|
111
|
+
try {
|
|
112
|
+
const t = m && m.thumbnail;
|
|
113
|
+
if (Array.isArray(t) && t.length) {
|
|
114
|
+
const first = t[0] || {};
|
|
115
|
+
const tid = first.id || first['@id'] || first.url || '';
|
|
116
|
+
if (tid) rec.thumbnail = String(tid);
|
|
117
|
+
if (typeof first.width === 'number') rec.thumbnailWidth = first.width;
|
|
118
|
+
if (typeof first.height === 'number') rec.thumbnailHeight = first.height;
|
|
119
|
+
} else if (t && typeof t === 'object') {
|
|
120
|
+
const tid = t.id || t['@id'] || t.url || '';
|
|
121
|
+
if (tid) rec.thumbnail = String(tid);
|
|
122
|
+
if (typeof t.width === 'number') rec.thumbnailWidth = t.width;
|
|
123
|
+
if (typeof t.height === 'number') rec.thumbnailHeight = t.height;
|
|
124
|
+
}
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
}
|
|
127
|
+
out.push(rec);
|
|
128
|
+
}
|
|
129
|
+
if (debug) { try { console.log('[featured] total:', out.length); } catch (_) {} }
|
|
130
|
+
return out;
|
|
131
|
+
} catch (_) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
firstLabelString,
|
|
138
|
+
normalizeIiifId,
|
|
139
|
+
equalIiifId,
|
|
140
|
+
readYaml,
|
|
141
|
+
readJson,
|
|
142
|
+
findSlugByIdFromDiskSync,
|
|
143
|
+
readFeaturedFromCacheSync,
|
|
144
|
+
};
|
|
@@ -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 } from '@canopy-iiif/app/ui';
|
|
3
|
+
import { SearchFormUI, SearchResultsUI, SearchTabsUI } from '@canopy-iiif/app/ui';
|
|
4
4
|
|
|
5
5
|
// Lightweight IndexedDB utilities (no deps) with graceful fallback
|
|
6
6
|
function hasIDB() {
|
|
@@ -133,11 +133,14 @@ function createSearchStore() {
|
|
|
133
133
|
// Broadcast new index installs to other tabs
|
|
134
134
|
let bc = null;
|
|
135
135
|
try { if (typeof BroadcastChannel !== 'undefined') bc = new BroadcastChannel('canopy-search'); } catch (_) {}
|
|
136
|
-
// Try to load meta
|
|
136
|
+
// Try to load meta for cache-busting and tab order; fall back to hash of JSON
|
|
137
137
|
let version = '';
|
|
138
|
+
let tabsOrder = [];
|
|
138
139
|
try {
|
|
139
140
|
const meta = await fetch('./api/index.json').then((r) => (r && r.ok ? r.json() : null)).catch(() => null);
|
|
140
141
|
if (meta && typeof meta.version === 'string') version = meta.version;
|
|
142
|
+
const ord = meta && meta.search && meta.search.tabs && Array.isArray(meta.search.tabs.order) ? meta.search.tabs.order : [];
|
|
143
|
+
tabsOrder = ord.map((s) => String(s)).filter(Boolean);
|
|
141
144
|
} catch (_) {}
|
|
142
145
|
const res = await fetch('./api/search-index.json' + (version ? `?v=${encodeURIComponent(version)}` : ''));
|
|
143
146
|
const text = await res.text();
|
|
@@ -198,8 +201,22 @@ function createSearchStore() {
|
|
|
198
201
|
} catch (_) {}
|
|
199
202
|
|
|
200
203
|
const ts = Array.from(new Set(data.map((r) => String((r && r.type) || 'page'))));
|
|
201
|
-
const order = ['work', 'docs', 'page'];
|
|
204
|
+
const order = Array.isArray(tabsOrder) && tabsOrder.length ? tabsOrder : ['work', 'docs', 'page'];
|
|
202
205
|
ts.sort((a, b) => { const ia = order.indexOf(a); const ib = order.indexOf(b); return (ia<0?99:ia)-(ib<0?99:ib) || a.localeCompare(b); });
|
|
206
|
+
// Default to configured first tab if no type param present
|
|
207
|
+
try {
|
|
208
|
+
const p = new URLSearchParams(location.search);
|
|
209
|
+
const hasType = p.has('type');
|
|
210
|
+
if (!hasType) {
|
|
211
|
+
let def = (order && order.length ? order[0] : 'all');
|
|
212
|
+
if (!ts.includes(def)) def = ts[0] || 'all';
|
|
213
|
+
if (def && def !== 'all') {
|
|
214
|
+
p.set('type', def);
|
|
215
|
+
history.replaceState(null, '', `${location.pathname}?${p.toString()}`);
|
|
216
|
+
}
|
|
217
|
+
set({ type: def });
|
|
218
|
+
}
|
|
219
|
+
} catch (_) {}
|
|
203
220
|
set({ index: idx, records: data, types: ts, loading: false });
|
|
204
221
|
} catch (_) { set({ loading: false }); }
|
|
205
222
|
})();
|
|
@@ -226,6 +243,10 @@ function ResultsMount(props = {}) {
|
|
|
226
243
|
const layout = (props && props.layout) || 'grid';
|
|
227
244
|
return <SearchResultsUI results={results} type={type} layout={layout} />;
|
|
228
245
|
}
|
|
246
|
+
function TabsMount() {
|
|
247
|
+
const { type, setType, types, counts } = useStore();
|
|
248
|
+
return <SearchTabsUI type={type} onTypeChange={setType} types={types} counts={counts} />;
|
|
249
|
+
}
|
|
229
250
|
function SummaryMount() {
|
|
230
251
|
const { query, type, shown, total } = useStore();
|
|
231
252
|
const text = useMemo(() => {
|
|
@@ -263,10 +284,20 @@ function mountAt(selector, Comp) {
|
|
|
263
284
|
|
|
264
285
|
if (typeof document !== 'undefined') {
|
|
265
286
|
const run = () => {
|
|
287
|
+
// Mount tabs (preferred) or full form if present, plus summary/total/results
|
|
288
|
+
mountAt('[data-canopy-search-tabs]', TabsMount);
|
|
266
289
|
mountAt('[data-canopy-search-form]', FormMount);
|
|
267
290
|
mountAt('[data-canopy-search-results]', ResultsMount);
|
|
268
291
|
mountAt('[data-canopy-search-summary]', SummaryMount);
|
|
269
292
|
mountAt('[data-canopy-search-total]', TotalMount);
|
|
293
|
+
try {
|
|
294
|
+
window.addEventListener('canopy:search:setQuery', (ev) => {
|
|
295
|
+
try {
|
|
296
|
+
const q = ev && ev.detail && typeof ev.detail.query === 'string' ? ev.detail.query : (document.querySelector('[data-canopy-command-input]')?.value || '');
|
|
297
|
+
if (typeof q === 'string') store.setQuery(q);
|
|
298
|
+
} catch (_) {}
|
|
299
|
+
});
|
|
300
|
+
} catch (_) {}
|
|
270
301
|
};
|
|
271
302
|
if (document.readyState !== 'loading') run();
|
|
272
303
|
else document.addEventListener('DOMContentLoaded', run, { once: true });
|
package/lib/search/search.js
CHANGED
|
@@ -361,10 +361,19 @@ async function buildSearchPage() {
|
|
|
361
361
|
React.createElement('h1', null, 'Search'),
|
|
362
362
|
React.createElement('div', { id: 'search-root' })
|
|
363
363
|
);
|
|
364
|
-
const { loadAppWrapper } = require('../build/mdx');
|
|
364
|
+
const { loadAppWrapper, getMdxProvider, loadUiComponents } = require('../build/mdx');
|
|
365
365
|
const app = await loadAppWrapper();
|
|
366
366
|
const wrappedApp = app && app.App ? React.createElement(app.App, null, content) : content;
|
|
367
|
-
|
|
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);
|
|
368
377
|
head = app && app.Head ? ReactDOMServer.renderToStaticMarkup(React.createElement(app.Head)) : '';
|
|
369
378
|
}
|
|
370
379
|
const importMap = '';
|
package/package.json
CHANGED
package/ui/dist/index.mjs
CHANGED
|
@@ -341,12 +341,24 @@ function SearchTotal(props) {
|
|
|
341
341
|
return /* @__PURE__ */ React11.createElement("div", { "data-canopy-search-total": "1" }, /* @__PURE__ */ React11.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
// ui/src/search/
|
|
344
|
+
// ui/src/search/MdxSearchTabs.jsx
|
|
345
345
|
import React12 from "react";
|
|
346
|
+
function MdxSearchTabs(props) {
|
|
347
|
+
let json = "{}";
|
|
348
|
+
try {
|
|
349
|
+
json = JSON.stringify(props || {});
|
|
350
|
+
} catch (_) {
|
|
351
|
+
json = "{}";
|
|
352
|
+
}
|
|
353
|
+
return /* @__PURE__ */ React12.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React12.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ui/src/search/SearchForm.jsx
|
|
357
|
+
import React13 from "react";
|
|
346
358
|
function SearchForm({ query, onQueryChange, type = "all", onTypeChange, types = [], counts = {} }) {
|
|
347
359
|
const orderedTypes = Array.isArray(types) ? types : [];
|
|
348
360
|
const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
|
|
349
|
-
return /* @__PURE__ */
|
|
361
|
+
return /* @__PURE__ */ React13.createElement("form", { onSubmit: (e) => e.preventDefault(), className: "space-y-3" }, /* @__PURE__ */ React13.createElement(
|
|
350
362
|
"input",
|
|
351
363
|
{
|
|
352
364
|
id: "search-input",
|
|
@@ -356,11 +368,11 @@ function SearchForm({ query, onQueryChange, type = "all", onTypeChange, types =
|
|
|
356
368
|
onChange: (e) => onQueryChange && onQueryChange(e.target.value),
|
|
357
369
|
className: "w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-brand-500"
|
|
358
370
|
}
|
|
359
|
-
), /* @__PURE__ */
|
|
371
|
+
), /* @__PURE__ */ React13.createElement("div", { role: "tablist", "aria-label": "Search types", className: "flex items-center gap-2 border-b border-slate-200" }, orderedTypes.map((t) => {
|
|
360
372
|
const active = String(type).toLowerCase() === String(t).toLowerCase();
|
|
361
373
|
const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
|
|
362
374
|
const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
|
|
363
|
-
return /* @__PURE__ */
|
|
375
|
+
return /* @__PURE__ */ React13.createElement(
|
|
364
376
|
"button",
|
|
365
377
|
{
|
|
366
378
|
key: t,
|
|
@@ -379,27 +391,27 @@ function SearchForm({ query, onQueryChange, type = "all", onTypeChange, types =
|
|
|
379
391
|
}
|
|
380
392
|
|
|
381
393
|
// ui/src/search/SearchResults.jsx
|
|
382
|
-
import
|
|
394
|
+
import React14 from "react";
|
|
383
395
|
function SearchResults({
|
|
384
396
|
results = [],
|
|
385
397
|
type = "all",
|
|
386
398
|
layout = "grid"
|
|
387
399
|
}) {
|
|
388
400
|
if (!results.length) {
|
|
389
|
-
return /* @__PURE__ */
|
|
401
|
+
return /* @__PURE__ */ React14.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React14.createElement("em", null, "No results"));
|
|
390
402
|
}
|
|
391
403
|
if (layout === "list") {
|
|
392
|
-
return /* @__PURE__ */
|
|
404
|
+
return /* @__PURE__ */ React14.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
|
|
393
405
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
394
406
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
395
|
-
return /* @__PURE__ */
|
|
407
|
+
return /* @__PURE__ */ React14.createElement(
|
|
396
408
|
"li",
|
|
397
409
|
{
|
|
398
410
|
key: i,
|
|
399
411
|
className: `search-result ${r.type}`,
|
|
400
412
|
"data-thumbnail-aspect-ratio": aspect
|
|
401
413
|
},
|
|
402
|
-
/* @__PURE__ */
|
|
414
|
+
/* @__PURE__ */ React14.createElement(
|
|
403
415
|
Card,
|
|
404
416
|
{
|
|
405
417
|
href: r.href,
|
|
@@ -413,17 +425,17 @@ function SearchResults({
|
|
|
413
425
|
);
|
|
414
426
|
}));
|
|
415
427
|
}
|
|
416
|
-
return /* @__PURE__ */
|
|
428
|
+
return /* @__PURE__ */ React14.createElement("div", { id: "search-results" }, /* @__PURE__ */ React14.createElement(Grid, null, results.map((r, i) => {
|
|
417
429
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
418
430
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
419
|
-
return /* @__PURE__ */
|
|
431
|
+
return /* @__PURE__ */ React14.createElement(
|
|
420
432
|
GridItem,
|
|
421
433
|
{
|
|
422
434
|
key: i,
|
|
423
435
|
className: `search-result ${r.type}`,
|
|
424
436
|
"data-thumbnail-aspect-ratio": aspect
|
|
425
437
|
},
|
|
426
|
-
/* @__PURE__ */
|
|
438
|
+
/* @__PURE__ */ React14.createElement(
|
|
427
439
|
Card,
|
|
428
440
|
{
|
|
429
441
|
href: r.href,
|
|
@@ -438,6 +450,33 @@ function SearchResults({
|
|
|
438
450
|
})));
|
|
439
451
|
}
|
|
440
452
|
|
|
453
|
+
// ui/src/search/SearchTabs.jsx
|
|
454
|
+
import React15 from "react";
|
|
455
|
+
function SearchTabs({ type = "all", onTypeChange, types = [], counts = {} }) {
|
|
456
|
+
const orderedTypes = Array.isArray(types) ? types : [];
|
|
457
|
+
const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
|
|
458
|
+
return /* @__PURE__ */ React15.createElement("div", { role: "tablist", "aria-label": "Search types", className: "flex items-center gap-2 border-b border-slate-200" }, orderedTypes.map((t) => {
|
|
459
|
+
const active = String(type).toLowerCase() === String(t).toLowerCase();
|
|
460
|
+
const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
|
|
461
|
+
const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
|
|
462
|
+
return /* @__PURE__ */ React15.createElement(
|
|
463
|
+
"button",
|
|
464
|
+
{
|
|
465
|
+
key: t,
|
|
466
|
+
role: "tab",
|
|
467
|
+
"aria-selected": active,
|
|
468
|
+
type: "button",
|
|
469
|
+
onClick: () => onTypeChange && onTypeChange(t),
|
|
470
|
+
className: "px-3 py-1.5 text-sm rounded-t-md border-b-2 -mb-px transition-colors " + (active ? "border-brand-600 text-brand-700" : "border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300")
|
|
471
|
+
},
|
|
472
|
+
toLabel(t),
|
|
473
|
+
" (",
|
|
474
|
+
c,
|
|
475
|
+
")"
|
|
476
|
+
);
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
|
|
441
480
|
// ui/src/search/useSearch.js
|
|
442
481
|
import { useEffect as useEffect4, useMemo, useRef as useRef2, useState as useState4 } from "react";
|
|
443
482
|
function useSearch(query, type) {
|
|
@@ -508,7 +547,7 @@ function useSearch(query, type) {
|
|
|
508
547
|
}
|
|
509
548
|
|
|
510
549
|
// ui/src/search/Search.jsx
|
|
511
|
-
import
|
|
550
|
+
import React16 from "react";
|
|
512
551
|
function Search(props) {
|
|
513
552
|
let json = "{}";
|
|
514
553
|
try {
|
|
@@ -516,7 +555,7 @@ function Search(props) {
|
|
|
516
555
|
} catch (_) {
|
|
517
556
|
json = "{}";
|
|
518
557
|
}
|
|
519
|
-
return /* @__PURE__ */
|
|
558
|
+
return /* @__PURE__ */ React16.createElement("div", { "data-canopy-search": "1", className: "not-prose" }, /* @__PURE__ */ React16.createElement(
|
|
520
559
|
"script",
|
|
521
560
|
{
|
|
522
561
|
type: "application/json",
|
|
@@ -526,7 +565,7 @@ function Search(props) {
|
|
|
526
565
|
}
|
|
527
566
|
|
|
528
567
|
// ui/src/command/MdxCommandPalette.jsx
|
|
529
|
-
import
|
|
568
|
+
import React17 from "react";
|
|
530
569
|
function MdxCommandPalette(props = {}) {
|
|
531
570
|
const {
|
|
532
571
|
placeholder = "Search\u2026",
|
|
@@ -534,20 +573,25 @@ function MdxCommandPalette(props = {}) {
|
|
|
534
573
|
maxResults = 8,
|
|
535
574
|
groupOrder = ["work", "page"],
|
|
536
575
|
button = true,
|
|
537
|
-
|
|
576
|
+
// kept for backward compat; ignored by teaser form
|
|
577
|
+
buttonLabel = "Search",
|
|
578
|
+
label,
|
|
579
|
+
searchPath = "/search"
|
|
538
580
|
} = props || {};
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
581
|
+
const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
|
|
582
|
+
const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath };
|
|
583
|
+
return /* @__PURE__ */ React17.createElement("div", { "data-canopy-command": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React17.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React17.createElement("style", null, `.relative[data-canopy-panel-auto='1']:focus-within [data-canopy-command-panel]{display:block}`), /* @__PURE__ */ React17.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" }, /* @__PURE__ */ React17.createElement("svg", { "aria-hidden": true, viewBox: "0 0 20 20", fill: "none", className: "w-4 h-4 text-slate-500" }, /* @__PURE__ */ React17.createElement("path", { stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", d: "m19 19-4-4m-2.5-6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0Z" })), /* @__PURE__ */ React17.createElement(
|
|
584
|
+
"input",
|
|
542
585
|
{
|
|
543
|
-
type: "
|
|
544
|
-
"
|
|
545
|
-
|
|
546
|
-
"
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
586
|
+
type: "search",
|
|
587
|
+
name: "q",
|
|
588
|
+
inputMode: "search",
|
|
589
|
+
"data-canopy-command-input": true,
|
|
590
|
+
placeholder,
|
|
591
|
+
className: "flex-1 bg-transparent outline-none placeholder:text-slate-400 py-0.5 min-w-0",
|
|
592
|
+
"aria-label": "Search"
|
|
593
|
+
}
|
|
594
|
+
), /* @__PURE__ */ React17.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" }, /* @__PURE__ */ React17.createElement("span", null, text))), /* @__PURE__ */ React17.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: 8, boxShadow: "0 10px 25px rgba(0,0,0,0.12)", zIndex: 1e3, overflow: "auto", maxHeight: "60vh" } }, /* @__PURE__ */ React17.createElement("div", { id: "cplist" }))), /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
|
|
551
595
|
}
|
|
552
596
|
export {
|
|
553
597
|
Card,
|
|
@@ -560,9 +604,12 @@ export {
|
|
|
560
604
|
Search,
|
|
561
605
|
MdxSearchForm as SearchForm,
|
|
562
606
|
SearchForm as SearchFormUI,
|
|
607
|
+
MdxCommandPalette as SearchPanel,
|
|
563
608
|
MdxSearchResults as SearchResults,
|
|
564
609
|
SearchResults as SearchResultsUI,
|
|
565
610
|
SearchSummary,
|
|
611
|
+
MdxSearchTabs as SearchTabs,
|
|
612
|
+
SearchTabs as SearchTabsUI,
|
|
566
613
|
SearchTotal,
|
|
567
614
|
Slider,
|
|
568
615
|
Viewer,
|