@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.
@@ -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
- meta &&
457
- meta.search &&
458
- meta.search.tabs &&
459
- Array.isArray(meta.search.tabs.order)
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.map((s) => String(s)).filter(Boolean);
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
- meta &&
465
- meta.search &&
466
- meta.search.assets &&
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(tabsOrder) && tabsOrder.length
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 layout = (props && props.layout) || "grid";
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
  }
@@ -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
- withBase,
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 tabs order from canopy.yml
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.map((s) => String(s)).filter(Boolean);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "0.10.19",
3
+ "version": "0.10.20",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -106,7 +106,7 @@ function Card({
106
106
  );
107
107
  }
108
108
 
109
- // ui/src/layout/TextCard.jsx
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 TextCard({
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
- return /* @__PURE__ */ React3.createElement("a", { href }, /* @__PURE__ */ React3.createElement("article", { className: "canopy-annotation-card" }, /* @__PURE__ */ React3.createElement("h3", null, title), snippet ? /* @__PURE__ */ React3.createElement("p", { className: "mt-2 text-sm leading-relaxed text-slate-700" }, highlighted) : null, metaList.length ? /* @__PURE__ */ React3.createElement("ul", { className: "mt-3 flex flex-wrap gap-2 text-xs text-slate-500" }, metaList.slice(0, 4).map((item, idx) => /* @__PURE__ */ React3.createElement(
165
- "li",
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 renderTextCard(r, r.id || i);
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 shouldRenderAsTextCard = (record) => !isWorkRecord(record) || normalizedType !== "work";
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("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
1260
- if (shouldRenderAsTextCard(r)) {
1261
- return /* @__PURE__ */ React24.createElement("li", { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
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
- "li",
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
- Card,
1310
+ FigureTemplate,
1274
1311
  {
1275
- href: r.href,
1276
- title: r.title || r.href,
1277
- src: r.type === "work" ? r.thumbnail : void 0,
1278
- imgWidth: r.thumbnailWidth,
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 (shouldRenderAsTextCard(r)) {
1288
- return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
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
- Card,
1335
+ FigureTemplate,
1301
1336
  {
1302
- href: r.href,
1303
- title: r.title || r.href,
1304
- src: r.type === "work" ? r.thumbnail : void 0,
1305
- imgWidth: r.thumbnailWidth,
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