@canopy-iiif/app 0.10.19 → 0.10.20
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/search/search-app.jsx +74 -13
- package/lib/search/search.js +155 -15
- package/package.json +1 -1
- package/ui/dist/index.mjs +82 -49
- package/ui/dist/index.mjs.map +3 -3
- package/ui/styles/components/_article-card.scss +51 -0
- package/ui/styles/components/_card.scss +0 -33
- package/ui/styles/components/index.scss +1 -0
- package/ui/styles/index.css +50 -33
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
SearchTabsUI,
|
|
6
6
|
SearchFiltersDialog,
|
|
7
7
|
} from "@canopy-iiif/app/ui";
|
|
8
|
+
import resultTemplates from "__CANOPY_SEARCH_RESULT_TEMPLATES__";
|
|
8
9
|
|
|
9
10
|
function readBasePath() {
|
|
10
11
|
const normalize = (val) => {
|
|
@@ -160,6 +161,7 @@ function createSearchStore() {
|
|
|
160
161
|
filters: {},
|
|
161
162
|
activeFilterCount: 0,
|
|
162
163
|
annotationsEnabled: false,
|
|
164
|
+
resultSettings: {},
|
|
163
165
|
};
|
|
164
166
|
const listeners = new Set();
|
|
165
167
|
function notify() {
|
|
@@ -447,24 +449,65 @@ function createSearchStore() {
|
|
|
447
449
|
let version = "";
|
|
448
450
|
let tabsOrder = [];
|
|
449
451
|
let annotationsAssetPath = "";
|
|
452
|
+
let resultsOrder = [];
|
|
453
|
+
let resultLayoutMap = {};
|
|
450
454
|
try {
|
|
451
455
|
const meta = await fetch("./api/index.json")
|
|
452
456
|
.then((r) => (r && r.ok ? r.json() : null))
|
|
453
457
|
.catch(() => null);
|
|
454
458
|
if (meta && typeof meta.version === "string") version = meta.version;
|
|
459
|
+
const searchMeta = meta && meta.search ? meta.search : {};
|
|
455
460
|
const ord =
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
? meta.search.tabs.order
|
|
461
|
+
searchMeta &&
|
|
462
|
+
searchMeta.tabs &&
|
|
463
|
+
Array.isArray(searchMeta.tabs.order)
|
|
464
|
+
? searchMeta.tabs.order
|
|
461
465
|
: [];
|
|
462
|
-
tabsOrder = ord
|
|
466
|
+
tabsOrder = ord
|
|
467
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
468
|
+
.filter(Boolean);
|
|
469
|
+
const resultsMeta =
|
|
470
|
+
searchMeta && searchMeta.results ? searchMeta.results : null;
|
|
471
|
+
if (resultsMeta) {
|
|
472
|
+
const rawOrder = Array.isArray(resultsMeta.order)
|
|
473
|
+
? resultsMeta.order
|
|
474
|
+
: [];
|
|
475
|
+
const rawSettings =
|
|
476
|
+
resultsMeta.settings && typeof resultsMeta.settings === "object"
|
|
477
|
+
? resultsMeta.settings
|
|
478
|
+
: {};
|
|
479
|
+
const normalizedOrder = [];
|
|
480
|
+
const normalizedMap = {};
|
|
481
|
+
const seenTypes = new Set();
|
|
482
|
+
const resolveEntry = (value) => {
|
|
483
|
+
const type = String(value || "")
|
|
484
|
+
.trim()
|
|
485
|
+
.toLowerCase();
|
|
486
|
+
if (!type || seenTypes.has(type)) return;
|
|
487
|
+
const cfg = rawSettings[type] || rawSettings[value] || {};
|
|
488
|
+
const layout =
|
|
489
|
+
String((cfg && cfg.layout) || "").toLowerCase() === "grid"
|
|
490
|
+
? "grid"
|
|
491
|
+
: "list";
|
|
492
|
+
const result =
|
|
493
|
+
String((cfg && cfg.result) || "").toLowerCase() === "figure"
|
|
494
|
+
? "figure"
|
|
495
|
+
: "article";
|
|
496
|
+
normalizedMap[type] = { layout, result };
|
|
497
|
+
normalizedOrder.push(type);
|
|
498
|
+
seenTypes.add(type);
|
|
499
|
+
};
|
|
500
|
+
rawOrder.forEach(resolveEntry);
|
|
501
|
+
Object.keys(rawSettings || {}).forEach(resolveEntry);
|
|
502
|
+
if (normalizedOrder.length) {
|
|
503
|
+
resultsOrder = normalizedOrder;
|
|
504
|
+
resultLayoutMap = normalizedMap;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
463
507
|
const annotationsAsset =
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
meta.search.assets.annotations;
|
|
508
|
+
searchMeta &&
|
|
509
|
+
searchMeta.assets &&
|
|
510
|
+
searchMeta.assets.annotations;
|
|
468
511
|
if (annotationsAsset && annotationsAsset.path) {
|
|
469
512
|
annotationsAssetPath = String(annotationsAsset.path);
|
|
470
513
|
}
|
|
@@ -661,7 +704,9 @@ function createSearchStore() {
|
|
|
661
704
|
if (annotationsRecords.length && !ts.includes("annotation"))
|
|
662
705
|
ts.push("annotation");
|
|
663
706
|
const order =
|
|
664
|
-
Array.isArray(
|
|
707
|
+
Array.isArray(resultsOrder) && resultsOrder.length
|
|
708
|
+
? resultsOrder
|
|
709
|
+
: Array.isArray(tabsOrder) && tabsOrder.length
|
|
665
710
|
? tabsOrder
|
|
666
711
|
: ["work", "docs", "page"];
|
|
667
712
|
ts.sort((a, b) => {
|
|
@@ -693,6 +738,7 @@ function createSearchStore() {
|
|
|
693
738
|
types: ts,
|
|
694
739
|
loading: false,
|
|
695
740
|
annotationsEnabled: annotationsRecords.length > 0,
|
|
741
|
+
resultSettings: resultLayoutMap,
|
|
696
742
|
});
|
|
697
743
|
await hydrateFacets();
|
|
698
744
|
} catch (_) {
|
|
@@ -754,15 +800,30 @@ function useStore() {
|
|
|
754
800
|
}
|
|
755
801
|
|
|
756
802
|
function ResultsMount(props = {}) {
|
|
757
|
-
const {results, type, loading, query} = useStore();
|
|
803
|
+
const {results, type, loading, query, resultSettings} = useStore();
|
|
758
804
|
if (loading) return <div className="text-slate-600">Loading…</div>;
|
|
759
|
-
const
|
|
805
|
+
const normalizedType = String(type || "").trim().toLowerCase();
|
|
806
|
+
const hasOverrides =
|
|
807
|
+
resultSettings && Object.keys(resultSettings || {}).length > 0;
|
|
808
|
+
const typeSettings =
|
|
809
|
+
(normalizedType && resultSettings && resultSettings[normalizedType]) || null;
|
|
810
|
+
let layout = (props && props.layout) || "grid";
|
|
811
|
+
let variant = "auto";
|
|
812
|
+
if (typeSettings) {
|
|
813
|
+
layout = typeSettings.layout || "list";
|
|
814
|
+
variant = typeSettings.result || "article";
|
|
815
|
+
} else if (hasOverrides) {
|
|
816
|
+
layout = "list";
|
|
817
|
+
variant = "article";
|
|
818
|
+
}
|
|
760
819
|
return (
|
|
761
820
|
<SearchResultsUI
|
|
762
821
|
results={results}
|
|
763
822
|
type={type}
|
|
764
823
|
layout={layout}
|
|
765
824
|
query={query}
|
|
825
|
+
templates={resultTemplates}
|
|
826
|
+
variant={variant}
|
|
766
827
|
/>
|
|
767
828
|
);
|
|
768
829
|
}
|
package/lib/search/search.js
CHANGED
|
@@ -2,18 +2,94 @@ const React = require('react');
|
|
|
2
2
|
const ReactDOMServer = require('react-dom/server');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const {
|
|
5
|
+
fs,
|
|
6
|
+
fsp,
|
|
5
7
|
path,
|
|
6
|
-
|
|
8
|
+
CONTENT_DIR,
|
|
7
9
|
rootRelativeHref,
|
|
8
10
|
ensureDirSync,
|
|
9
11
|
OUT_DIR,
|
|
10
12
|
htmlShell,
|
|
11
|
-
fsp,
|
|
12
13
|
canopyBodyClassForType,
|
|
13
14
|
} = require('../common');
|
|
14
15
|
|
|
16
|
+
const SEARCH_TEMPLATES_ALIAS = '__CANOPY_SEARCH_RESULT_TEMPLATES__';
|
|
17
|
+
const SEARCH_TEMPLATES_CACHE_DIR = path.resolve('.cache/search');
|
|
18
|
+
const SEARCH_TEMPLATE_FILES = [
|
|
19
|
+
{ key: 'figure', filename: '_result-figure.mdx' },
|
|
20
|
+
{ key: 'article', filename: '_result-article.mdx' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function resolveSearchTemplatePath(filename) {
|
|
24
|
+
try {
|
|
25
|
+
const candidate = path.join(CONTENT_DIR, 'search', filename);
|
|
26
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
27
|
+
} catch (_) {}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildSearchTemplatesModule() {
|
|
32
|
+
ensureDirSync(SEARCH_TEMPLATES_CACHE_DIR);
|
|
33
|
+
const outPath = path.join(SEARCH_TEMPLATES_CACHE_DIR, 'result-templates-entry.js');
|
|
34
|
+
const fallback = resolveSearchTemplatePath('_result.mdx');
|
|
35
|
+
const lines = [
|
|
36
|
+
'// Auto-generated search result templates map',
|
|
37
|
+
];
|
|
38
|
+
for (const spec of SEARCH_TEMPLATE_FILES) {
|
|
39
|
+
const templateName = `${spec.key}Template`;
|
|
40
|
+
const specific = resolveSearchTemplatePath(spec.filename);
|
|
41
|
+
const resolved = specific || fallback;
|
|
42
|
+
if (resolved) {
|
|
43
|
+
lines.push(`import ${templateName} from ${JSON.stringify(resolved)};`);
|
|
44
|
+
} else {
|
|
45
|
+
lines.push(`const ${templateName} = null;`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(`export const ${spec.key} = ${templateName};`);
|
|
48
|
+
}
|
|
49
|
+
lines.push(`export default { ${SEARCH_TEMPLATE_FILES.map((spec) => spec.key).join(', ')} };`);
|
|
50
|
+
await fsp.writeFile(outPath, lines.join('\n'), 'utf8');
|
|
51
|
+
return outPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createResultTemplatesAliasPlugin(entryPath) {
|
|
55
|
+
return {
|
|
56
|
+
name: 'canopy-search-result-templates',
|
|
57
|
+
setup(build) {
|
|
58
|
+
build.onResolve({ filter: new RegExp(`^${SEARCH_TEMPLATES_ALIAS.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }, () => ({
|
|
59
|
+
path: entryPath,
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createSearchMdxPlugin() {
|
|
66
|
+
return {
|
|
67
|
+
name: 'canopy-search-mdx',
|
|
68
|
+
setup(build) {
|
|
69
|
+
build.onResolve({ filter: /\.mdx$/ }, (args) => ({
|
|
70
|
+
path: path.resolve(args.resolveDir, args.path),
|
|
71
|
+
namespace: 'canopy-search-mdx',
|
|
72
|
+
}));
|
|
73
|
+
build.onLoad({ filter: /.*/, namespace: 'canopy-search-mdx' }, async (args) => {
|
|
74
|
+
const { compile } = await import('@mdx-js/mdx');
|
|
75
|
+
const source = await fsp.readFile(args.path, 'utf8');
|
|
76
|
+
const compiled = await compile(source, {
|
|
77
|
+
jsx: true,
|
|
78
|
+
development: false,
|
|
79
|
+
providerImportSource: '@mdx-js/react',
|
|
80
|
+
format: 'mdx',
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
contents: String(compiled),
|
|
84
|
+
loader: 'jsx',
|
|
85
|
+
resolveDir: path.dirname(args.path),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
15
92
|
async function ensureSearchRuntime() {
|
|
16
|
-
const { fs, path } = require('../common');
|
|
17
93
|
ensureDirSync(OUT_DIR);
|
|
18
94
|
let esbuild = null;
|
|
19
95
|
try { esbuild = require('../../ui/node_modules/esbuild'); } catch (_) { try { esbuild = require('esbuild'); } catch (_) {} }
|
|
@@ -22,6 +98,9 @@ async function ensureSearchRuntime() {
|
|
|
22
98
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
23
99
|
ensureDirSync(scriptsDir);
|
|
24
100
|
const outFile = path.join(scriptsDir, 'search.js');
|
|
101
|
+
const templatesEntry = await buildSearchTemplatesModule();
|
|
102
|
+
const templatesPlugin = createResultTemplatesAliasPlugin(templatesEntry);
|
|
103
|
+
const mdxPlugin = createSearchMdxPlugin();
|
|
25
104
|
// Ensure a global React shim is available to reduce search.js size
|
|
26
105
|
try {
|
|
27
106
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
@@ -93,6 +172,30 @@ async function ensureSearchRuntime() {
|
|
|
93
172
|
].join(''),
|
|
94
173
|
loader: 'js',
|
|
95
174
|
}));
|
|
175
|
+
build.onResolve({ filter: /^react\/jsx-runtime$/ }, () => ({ path: 'react/jsx-runtime', namespace: 'react-jsx-shim' }));
|
|
176
|
+
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({ path: 'react/jsx-dev-runtime', namespace: 'react-jsx-shim' }));
|
|
177
|
+
build.onLoad({ filter: /.*/, namespace: 'react-jsx-shim' }, () => ({
|
|
178
|
+
contents: [
|
|
179
|
+
"const R = (typeof window!=='undefined' && window.React) || {};\n",
|
|
180
|
+
"const Fragment = R.Fragment || 'div';\n",
|
|
181
|
+
"const createElement = typeof R.createElement === 'function' ? R.createElement.bind(R) : null;\n",
|
|
182
|
+
"function normalizeProps(props, key){\n",
|
|
183
|
+
" if (key !== undefined && key !== null) {\n",
|
|
184
|
+
" props = props && typeof props === 'object' ? { ...props, key } : { key };\n",
|
|
185
|
+
" }\n",
|
|
186
|
+
" return props || {};\n",
|
|
187
|
+
"}\n",
|
|
188
|
+
"function jsx(type, props, key){\n",
|
|
189
|
+
" if (!createElement) return null;\n",
|
|
190
|
+
" return createElement(type, normalizeProps(props, key));\n",
|
|
191
|
+
"}\n",
|
|
192
|
+
"const jsxs = jsx;\n",
|
|
193
|
+
"const jsxDEV = jsx;\n",
|
|
194
|
+
"export { jsx, jsxs, jsxDEV, Fragment };\n",
|
|
195
|
+
"export default { jsx, jsxs, jsxDEV, Fragment };\n",
|
|
196
|
+
].join(''),
|
|
197
|
+
loader: 'js',
|
|
198
|
+
}));
|
|
96
199
|
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: 'react-dom/client', namespace: 'rdc-shim' }));
|
|
97
200
|
build.onLoad({ filter: /.*/, namespace: 'rdc-shim' }, () => ({
|
|
98
201
|
contents: [
|
|
@@ -124,7 +227,7 @@ async function ensureSearchRuntime() {
|
|
|
124
227
|
sourcemap: true,
|
|
125
228
|
target: ['es2018'],
|
|
126
229
|
logLevel: 'silent',
|
|
127
|
-
plugins: [shimReactPlugin],
|
|
230
|
+
plugins: [shimReactPlugin, templatesPlugin, mdxPlugin],
|
|
128
231
|
external: ['@samvera/clover-iiif/*'],
|
|
129
232
|
};
|
|
130
233
|
if (!entryExists) throw new Error('Search runtime entry missing: ' + entry);
|
|
@@ -327,8 +430,9 @@ async function writeSearchIndex(records) {
|
|
|
327
430
|
// Also write a small metadata file with a stable version hash for cache-busting and IDB keying
|
|
328
431
|
try {
|
|
329
432
|
const version = crypto.createHash('sha256').update(indexJson).digest('hex');
|
|
330
|
-
// Read optional search
|
|
433
|
+
// Read optional search configuration from canopy.yml
|
|
331
434
|
let tabsOrder = [];
|
|
435
|
+
let resultsConfigEntries = [];
|
|
332
436
|
try {
|
|
333
437
|
const yaml = require('js-yaml');
|
|
334
438
|
const common = require('../common');
|
|
@@ -339,24 +443,60 @@ async function writeSearchIndex(records) {
|
|
|
339
443
|
const searchCfg = data && data.search ? data.search : {};
|
|
340
444
|
const tabs = searchCfg && searchCfg.tabs ? searchCfg.tabs : {};
|
|
341
445
|
const order = Array.isArray(tabs && tabs.order) ? tabs.order : [];
|
|
342
|
-
tabsOrder = order
|
|
446
|
+
tabsOrder = order
|
|
447
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
448
|
+
.filter(Boolean);
|
|
449
|
+
const resultsCfg =
|
|
450
|
+
searchCfg && searchCfg.results && typeof searchCfg.results === 'object'
|
|
451
|
+
? searchCfg.results
|
|
452
|
+
: null;
|
|
453
|
+
if (resultsCfg) {
|
|
454
|
+
const entries = [];
|
|
455
|
+
Object.keys(resultsCfg).forEach((key) => {
|
|
456
|
+
if (!key) return;
|
|
457
|
+
const type = String(key).trim().toLowerCase();
|
|
458
|
+
if (!type) return;
|
|
459
|
+
if (entries.find((entry) => entry.type === type)) return;
|
|
460
|
+
const cfg = resultsCfg[key] && typeof resultsCfg[key] === 'object' ? resultsCfg[key] : {};
|
|
461
|
+
const layoutRaw = cfg && cfg.layout ? String(cfg.layout).toLowerCase() : '';
|
|
462
|
+
const resultRaw = cfg && cfg.result ? String(cfg.result).toLowerCase() : '';
|
|
463
|
+
const layout = layoutRaw === 'grid' ? 'grid' : 'list';
|
|
464
|
+
const result = resultRaw === 'figure' ? 'figure' : 'article';
|
|
465
|
+
entries.push({ type, layout, result });
|
|
466
|
+
});
|
|
467
|
+
if (entries.length) {
|
|
468
|
+
resultsConfigEntries = entries;
|
|
469
|
+
tabsOrder = entries.map((entry) => entry.type);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
343
472
|
}
|
|
344
473
|
} catch (_) {}
|
|
474
|
+
const searchMeta = {
|
|
475
|
+
tabs: { order: tabsOrder },
|
|
476
|
+
assets: {
|
|
477
|
+
display: { path: 'search-records.json', bytes: displayBytes },
|
|
478
|
+
...(annotationRecords.length
|
|
479
|
+
? { annotations: { path: 'search-index-annotations.json', bytes: annotationsBytes } }
|
|
480
|
+
: {}),
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
if (resultsConfigEntries.length) {
|
|
484
|
+
const resultSettings = resultsConfigEntries.reduce((acc, entry) => {
|
|
485
|
+
acc[entry.type] = { layout: entry.layout, result: entry.result };
|
|
486
|
+
return acc;
|
|
487
|
+
}, {});
|
|
488
|
+
searchMeta.results = {
|
|
489
|
+
order: resultsConfigEntries.map((entry) => entry.type),
|
|
490
|
+
settings: resultSettings,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
345
493
|
const meta = {
|
|
346
494
|
version,
|
|
347
495
|
records: indexRecords.length,
|
|
348
496
|
bytes: approxBytes,
|
|
349
497
|
updatedAt: new Date().toISOString(),
|
|
350
498
|
// Expose optional search config to the client runtime
|
|
351
|
-
search:
|
|
352
|
-
tabs: { order: tabsOrder },
|
|
353
|
-
assets: {
|
|
354
|
-
display: { path: 'search-records.json', bytes: displayBytes },
|
|
355
|
-
...(annotationRecords.length
|
|
356
|
-
? { annotations: { path: 'search-index-annotations.json', bytes: annotationsBytes } }
|
|
357
|
-
: {}),
|
|
358
|
-
},
|
|
359
|
-
},
|
|
499
|
+
search: searchMeta,
|
|
360
500
|
};
|
|
361
501
|
const metaPath = path.join(apiDir, 'index.json');
|
|
362
502
|
await fsp.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf8');
|
package/package.json
CHANGED
package/ui/dist/index.mjs
CHANGED
|
@@ -106,7 +106,7 @@ function Card({
|
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// ui/src/layout/
|
|
109
|
+
// ui/src/layout/ArticleCard.jsx
|
|
110
110
|
import React3, { useMemo } from "react";
|
|
111
111
|
function escapeRegExp(str = "") {
|
|
112
112
|
return String(str).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
@@ -143,7 +143,17 @@ function highlightSnippet(snippet, query) {
|
|
|
143
143
|
(part, idx) => part.toLowerCase() === termLower ? /* @__PURE__ */ React3.createElement("mark", { key: idx }, part) : /* @__PURE__ */ React3.createElement(React3.Fragment, { key: idx }, part)
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
|
-
function
|
|
146
|
+
function formatDisplayUrl(href = "") {
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(href, href.startsWith("http") ? void 0 : "http://example.com");
|
|
149
|
+
if (!href.startsWith("http")) return href;
|
|
150
|
+
const displayPath = url.pathname.replace(/\/$/, "");
|
|
151
|
+
return `${url.host}${displayPath}${url.search}`.replace(/\/$/, "");
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return href;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function ArticleCard({
|
|
147
157
|
href = "#",
|
|
148
158
|
title = "Untitled",
|
|
149
159
|
annotation = "",
|
|
@@ -161,14 +171,8 @@ function TextCard({
|
|
|
161
171
|
[snippet, query]
|
|
162
172
|
);
|
|
163
173
|
const metaList = Array.isArray(metadata) ? metadata.map((m) => String(m || "")).filter(Boolean) : [];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{
|
|
167
|
-
key: `${item}-${idx}`,
|
|
168
|
-
className: "rounded-full border border-slate-200 bg-slate-50 px-2 py-1"
|
|
169
|
-
},
|
|
170
|
-
item
|
|
171
|
-
))) : null));
|
|
174
|
+
const displayUrl = useMemo(() => formatDisplayUrl(href), [href]);
|
|
175
|
+
return /* @__PURE__ */ React3.createElement("a", { href, className: "canopy-article-card" }, /* @__PURE__ */ React3.createElement("article", null, displayUrl ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__url" }, displayUrl) : null, /* @__PURE__ */ React3.createElement("h3", null, title), snippet ? /* @__PURE__ */ React3.createElement("p", { className: "canopy-article-card__snippet" }, highlighted) : null, metaList.length ? /* @__PURE__ */ React3.createElement("ul", { className: "canopy-article-card__meta" }, metaList.slice(0, 3).map((item, idx) => /* @__PURE__ */ React3.createElement("li", { key: `${item}-${idx}` }, item))) : null));
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
// ui/src/layout/Grid.jsx
|
|
@@ -1221,71 +1225,102 @@ function MdxSearchTabs(props) {
|
|
|
1221
1225
|
|
|
1222
1226
|
// ui/src/search/SearchResults.jsx
|
|
1223
1227
|
import React24 from "react";
|
|
1228
|
+
function DefaultArticleTemplate({ record, query }) {
|
|
1229
|
+
if (!record) return null;
|
|
1230
|
+
const metadata = Array.isArray(record.metadata) ? record.metadata : [];
|
|
1231
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1232
|
+
ArticleCard,
|
|
1233
|
+
{
|
|
1234
|
+
href: record.href,
|
|
1235
|
+
title: record.title || record.href || "Untitled",
|
|
1236
|
+
annotation: record.annotation,
|
|
1237
|
+
summary: record.summary || record.summaryValue || "",
|
|
1238
|
+
metadata,
|
|
1239
|
+
query
|
|
1240
|
+
}
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
function DefaultFigureTemplate({ record, thumbnailAspectRatio }) {
|
|
1244
|
+
if (!record) return null;
|
|
1245
|
+
const hasDims = Number.isFinite(Number(record.thumbnailWidth)) && Number(record.thumbnailWidth) > 0 && Number.isFinite(Number(record.thumbnailHeight)) && Number(record.thumbnailHeight) > 0;
|
|
1246
|
+
const aspect = Number.isFinite(Number(thumbnailAspectRatio)) && Number(thumbnailAspectRatio) > 0 ? Number(thumbnailAspectRatio) : hasDims ? Number(record.thumbnailWidth) / Number(record.thumbnailHeight) : void 0;
|
|
1247
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1248
|
+
Card,
|
|
1249
|
+
{
|
|
1250
|
+
href: record.href,
|
|
1251
|
+
title: record.title || record.href,
|
|
1252
|
+
src: record.type === "work" ? record.thumbnail : void 0,
|
|
1253
|
+
imgWidth: record.thumbnailWidth,
|
|
1254
|
+
imgHeight: record.thumbnailHeight,
|
|
1255
|
+
aspectRatio: aspect
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1224
1259
|
function SearchResults({
|
|
1225
1260
|
results = [],
|
|
1226
1261
|
type = "all",
|
|
1227
1262
|
layout = "grid",
|
|
1228
|
-
query = ""
|
|
1263
|
+
query = "",
|
|
1264
|
+
templates = {},
|
|
1265
|
+
variant = "auto"
|
|
1229
1266
|
}) {
|
|
1230
1267
|
if (!results.length) {
|
|
1231
1268
|
return /* @__PURE__ */ React24.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React24.createElement("em", null, "No results"));
|
|
1232
1269
|
}
|
|
1233
1270
|
const normalizedType = String(type || "all").toLowerCase();
|
|
1271
|
+
const normalizedVariant = variant === "figure" || variant === "article" ? variant : "auto";
|
|
1234
1272
|
const isAnnotationView = normalizedType === "annotation";
|
|
1273
|
+
const FigureTemplate = templates && templates.figure ? templates.figure : DefaultFigureTemplate;
|
|
1274
|
+
const ArticleTemplate = templates && templates.article ? templates.article : DefaultArticleTemplate;
|
|
1235
1275
|
if (isAnnotationView) {
|
|
1236
1276
|
return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
|
|
1237
1277
|
if (!r) return null;
|
|
1238
|
-
return
|
|
1278
|
+
return /* @__PURE__ */ React24.createElement(
|
|
1279
|
+
ArticleTemplate,
|
|
1280
|
+
{
|
|
1281
|
+
key: r.id || i,
|
|
1282
|
+
record: r,
|
|
1283
|
+
query,
|
|
1284
|
+
layout
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1239
1287
|
}));
|
|
1240
1288
|
}
|
|
1241
|
-
const renderTextCard = (record, key) => {
|
|
1242
|
-
if (!record) return null;
|
|
1243
|
-
return /* @__PURE__ */ React24.createElement(
|
|
1244
|
-
TextCard,
|
|
1245
|
-
{
|
|
1246
|
-
key,
|
|
1247
|
-
href: record.href,
|
|
1248
|
-
title: record.title || record.href || "Untitled",
|
|
1249
|
-
annotation: record.annotation,
|
|
1250
|
-
summary: record.summary || record.summaryValue || "",
|
|
1251
|
-
metadata: Array.isArray(record.metadata) ? record.metadata : [],
|
|
1252
|
-
query
|
|
1253
|
-
}
|
|
1254
|
-
);
|
|
1255
|
-
};
|
|
1256
1289
|
const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
|
|
1257
|
-
const
|
|
1290
|
+
const shouldRenderAsArticle = (record) => {
|
|
1291
|
+
if (normalizedVariant === "article") return true;
|
|
1292
|
+
if (normalizedVariant === "figure") return false;
|
|
1293
|
+
return !isWorkRecord(record) || normalizedType !== "work";
|
|
1294
|
+
};
|
|
1258
1295
|
if (layout === "list") {
|
|
1259
|
-
return /* @__PURE__ */ React24.createElement("
|
|
1260
|
-
if (
|
|
1261
|
-
return /* @__PURE__ */ React24.createElement("
|
|
1296
|
+
return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-6" }, results.map((r, i) => {
|
|
1297
|
+
if (shouldRenderAsArticle(r)) {
|
|
1298
|
+
return /* @__PURE__ */ React24.createElement("div", { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
|
|
1262
1299
|
}
|
|
1263
1300
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
1264
1301
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
1265
1302
|
return /* @__PURE__ */ React24.createElement(
|
|
1266
|
-
"
|
|
1303
|
+
"div",
|
|
1267
1304
|
{
|
|
1268
1305
|
key: i,
|
|
1269
1306
|
className: `search-result ${r.type}`,
|
|
1270
1307
|
"data-thumbnail-aspect-ratio": aspect
|
|
1271
1308
|
},
|
|
1272
1309
|
/* @__PURE__ */ React24.createElement(
|
|
1273
|
-
|
|
1310
|
+
FigureTemplate,
|
|
1274
1311
|
{
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
imgHeight: r.thumbnailHeight,
|
|
1280
|
-
aspectRatio: aspect
|
|
1312
|
+
record: r,
|
|
1313
|
+
query,
|
|
1314
|
+
layout,
|
|
1315
|
+
thumbnailAspectRatio: aspect
|
|
1281
1316
|
}
|
|
1282
1317
|
)
|
|
1283
1318
|
);
|
|
1284
1319
|
}));
|
|
1285
1320
|
}
|
|
1286
1321
|
return /* @__PURE__ */ React24.createElement("div", { id: "search-results" }, /* @__PURE__ */ React24.createElement(Grid, null, results.map((r, i) => {
|
|
1287
|
-
if (
|
|
1288
|
-
return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` },
|
|
1322
|
+
if (shouldRenderAsArticle(r)) {
|
|
1323
|
+
return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
|
|
1289
1324
|
}
|
|
1290
1325
|
const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
|
|
1291
1326
|
const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
|
|
@@ -1297,14 +1332,12 @@ function SearchResults({
|
|
|
1297
1332
|
"data-thumbnail-aspect-ratio": aspect
|
|
1298
1333
|
},
|
|
1299
1334
|
/* @__PURE__ */ React24.createElement(
|
|
1300
|
-
|
|
1335
|
+
FigureTemplate,
|
|
1301
1336
|
{
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
imgHeight: r.thumbnailHeight,
|
|
1307
|
-
aspectRatio: aspect
|
|
1337
|
+
record: r,
|
|
1338
|
+
query,
|
|
1339
|
+
layout,
|
|
1340
|
+
thumbnailAspectRatio: aspect
|
|
1308
1341
|
}
|
|
1309
1342
|
)
|
|
1310
1343
|
);
|
|
@@ -1574,6 +1607,7 @@ function MarkdownTable({ className = "", ...rest }) {
|
|
|
1574
1607
|
return /* @__PURE__ */ React28.createElement("div", { className: "markdown-table__frame" }, /* @__PURE__ */ React28.createElement("table", { className: merged, ...rest }));
|
|
1575
1608
|
}
|
|
1576
1609
|
export {
|
|
1610
|
+
ArticleCard,
|
|
1577
1611
|
Button,
|
|
1578
1612
|
ButtonWrapper,
|
|
1579
1613
|
CanopyBrand,
|
|
@@ -1600,7 +1634,6 @@ export {
|
|
|
1600
1634
|
MdxSearchTabs as SearchTabs,
|
|
1601
1635
|
SearchTabs as SearchTabsUI,
|
|
1602
1636
|
Slider,
|
|
1603
|
-
TextCard,
|
|
1604
1637
|
Viewer
|
|
1605
1638
|
};
|
|
1606
1639
|
//# sourceMappingURL=index.mjs.map
|