@canopy-iiif/app 0.10.19 → 0.10.21

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/ui/dist/index.mjs CHANGED
@@ -106,69 +106,186 @@ 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, "\\$&");
113
113
  }
114
- function buildSnippet({ text = "", query = "", maxLength = 240 }) {
114
+ function buildSnippet({ text = "", query = "", maxLength = 360 }) {
115
115
  const clean = String(text || "").replace(/\s+/g, " ").trim();
116
116
  if (!clean) return "";
117
+ const safeMax = Math.max(60, Number(maxLength) || 360);
117
118
  const term = String(query || "").trim();
118
119
  if (!term)
119
- return clean.length > maxLength ? clean.slice(0, maxLength) + "\u2026" : clean;
120
+ return clean.length > safeMax ? clean.slice(0, safeMax).trimEnd() + "\u2026" : clean;
120
121
  const lower = clean.toLowerCase();
121
122
  const termLower = term.toLowerCase();
122
123
  const idx = lower.indexOf(termLower);
123
- if (idx === -1) {
124
- return clean.length > maxLength ? clean.slice(0, maxLength) + "\u2026" : clean;
124
+ if (idx === -1)
125
+ return clean.length > safeMax ? clean.slice(0, safeMax).trimEnd() + "\u2026" : clean;
126
+ const padding = Math.max(0, Math.floor((safeMax - term.length) / 2));
127
+ let start = Math.max(0, idx - padding);
128
+ let end = start + safeMax;
129
+ if (end > clean.length) {
130
+ end = clean.length;
131
+ start = Math.max(0, end - safeMax);
125
132
  }
126
- const context = Math.max(0, maxLength / 2);
127
- const start = Math.max(0, idx - context);
128
- const end = Math.min(clean.length, idx + term.length + context);
129
- let snippet = clean.slice(start, end);
133
+ let snippet = clean.slice(start, end).trim();
130
134
  if (start > 0) snippet = "\u2026" + snippet;
131
135
  if (end < clean.length) snippet = snippet + "\u2026";
132
136
  return snippet;
133
137
  }
134
- function highlightSnippet(snippet, query) {
135
- if (!query) return snippet;
138
+ function highlightTextNode(text, query, keyPrefix = "") {
139
+ if (!query) return text;
136
140
  const term = String(query).trim();
137
- if (!term) return snippet;
138
- const parts = String(snippet).split(
139
- new RegExp(`(${escapeRegExp(term)})`, "gi")
140
- );
141
+ if (!term) return text;
142
+ const regex = new RegExp(`(${escapeRegExp(term)})`, "gi");
143
+ const parts = String(text).split(regex);
141
144
  const termLower = term.toLowerCase();
142
- return parts.map(
143
- (part, idx) => part.toLowerCase() === termLower ? /* @__PURE__ */ React3.createElement("mark", { key: idx }, part) : /* @__PURE__ */ React3.createElement(React3.Fragment, { key: idx }, part)
144
- );
145
+ return parts.map((part, idx) => {
146
+ if (!part) return null;
147
+ if (part.toLowerCase() === termLower) {
148
+ return /* @__PURE__ */ React3.createElement("mark", { key: `${keyPrefix}-${idx}` }, part);
149
+ }
150
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, { key: `${keyPrefix}-${idx}` }, part);
151
+ });
152
+ }
153
+ function tokenizeInlineMarkdown(input = "") {
154
+ const tokens = [];
155
+ let text = input;
156
+ while (text.length) {
157
+ if (text.startsWith("\n")) {
158
+ tokens.push({ type: "break" });
159
+ text = text.slice(1);
160
+ continue;
161
+ }
162
+ if (text.startsWith("**")) {
163
+ const closing = text.indexOf("**", 2);
164
+ if (closing !== -1) {
165
+ const inner = text.slice(2, closing);
166
+ tokens.push({ type: "strong", children: tokenizeInlineMarkdown(inner) });
167
+ text = text.slice(closing + 2);
168
+ continue;
169
+ }
170
+ }
171
+ if (text.startsWith("__")) {
172
+ const closing = text.indexOf("__", 2);
173
+ if (closing !== -1) {
174
+ const inner = text.slice(2, closing);
175
+ tokens.push({ type: "strong", children: tokenizeInlineMarkdown(inner) });
176
+ text = text.slice(closing + 2);
177
+ continue;
178
+ }
179
+ }
180
+ if (text.startsWith("*")) {
181
+ if (!text.startsWith("**")) {
182
+ const closing = text.indexOf("*", 1);
183
+ if (closing !== -1) {
184
+ const inner = text.slice(1, closing);
185
+ tokens.push({ type: "em", children: tokenizeInlineMarkdown(inner) });
186
+ text = text.slice(closing + 1);
187
+ continue;
188
+ }
189
+ }
190
+ }
191
+ if (text.startsWith("_")) {
192
+ if (!text.startsWith("__")) {
193
+ const closing = text.indexOf("_", 1);
194
+ if (closing !== -1) {
195
+ const inner = text.slice(1, closing);
196
+ tokens.push({ type: "em", children: tokenizeInlineMarkdown(inner) });
197
+ text = text.slice(closing + 1);
198
+ continue;
199
+ }
200
+ }
201
+ }
202
+ if (text.startsWith("`")) {
203
+ const closing = text.indexOf("`", 1);
204
+ if (closing !== -1) {
205
+ const inner = text.slice(1, closing);
206
+ tokens.push({ type: "code", value: inner });
207
+ text = text.slice(closing + 1);
208
+ continue;
209
+ }
210
+ }
211
+ if (text.startsWith("[")) {
212
+ const endLabel = text.indexOf("]");
213
+ const startHref = endLabel !== -1 ? text.indexOf("(", endLabel) : -1;
214
+ const endHref = startHref !== -1 ? text.indexOf(")", startHref) : -1;
215
+ if (endLabel !== -1 && startHref === endLabel + 1 && endHref !== -1) {
216
+ const label = text.slice(1, endLabel);
217
+ const href = text.slice(startHref + 1, endHref);
218
+ tokens.push({
219
+ type: "link",
220
+ href,
221
+ children: tokenizeInlineMarkdown(label)
222
+ });
223
+ text = text.slice(endHref + 1);
224
+ continue;
225
+ }
226
+ }
227
+ const specials = ["**", "__", "*", "_", "`", "[", "\n"];
228
+ const nextIndex = specials.map((needle) => needle === "\n" ? text.indexOf("\n") : text.indexOf(needle)).filter((idx) => idx > 0).reduce((min, idx) => min === -1 || idx < min ? idx : min, -1);
229
+ if (nextIndex === -1) {
230
+ tokens.push({ type: "text", value: text });
231
+ break;
232
+ }
233
+ tokens.push({ type: "text", value: text.slice(0, nextIndex) });
234
+ text = text.slice(nextIndex);
235
+ }
236
+ return tokens;
237
+ }
238
+ function renderMarkdownTokens(tokens, query, keyPrefix = "token") {
239
+ return tokens.map((token, idx) => {
240
+ const key = `${keyPrefix}-${idx}`;
241
+ switch (token.type) {
242
+ case "strong":
243
+ return /* @__PURE__ */ React3.createElement("strong", { key }, renderMarkdownTokens(token.children || [], query, key));
244
+ case "em":
245
+ return /* @__PURE__ */ React3.createElement("em", { key }, renderMarkdownTokens(token.children || [], query, key));
246
+ case "code":
247
+ return /* @__PURE__ */ React3.createElement("code", { key }, token.value);
248
+ case "link":
249
+ return /* @__PURE__ */ React3.createElement("a", { key, href: token.href, target: "_blank", rel: "noreferrer" }, renderMarkdownTokens(token.children || [], query, key));
250
+ case "break":
251
+ return /* @__PURE__ */ React3.createElement("br", { key });
252
+ case "text":
253
+ default:
254
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, { key }, highlightTextNode(token.value || "", query, key));
255
+ }
256
+ });
145
257
  }
146
- function TextCard({
258
+ function formatDisplayUrl(href = "") {
259
+ try {
260
+ const url = new URL(href, href.startsWith("http") ? void 0 : "http://example.com");
261
+ if (!href.startsWith("http")) return href;
262
+ const displayPath = url.pathname.replace(/\/$/, "");
263
+ return `${url.host}${displayPath}${url.search}`.replace(/\/$/, "");
264
+ } catch (_) {
265
+ return href;
266
+ }
267
+ }
268
+ function ArticleCard({
147
269
  href = "#",
148
270
  title = "Untitled",
149
271
  annotation = "",
150
272
  summary = "",
273
+ summaryMarkdown = "",
151
274
  metadata = [],
152
275
  query = ""
153
276
  }) {
154
- const snippetSource = annotation || summary;
277
+ const snippetSource = summaryMarkdown || annotation || summary;
155
278
  const snippet = useMemo(
156
279
  () => buildSnippet({ text: snippetSource, query }),
157
280
  [snippetSource, query]
158
281
  );
159
- const highlighted = useMemo(
160
- () => highlightSnippet(snippet, query),
161
- [snippet, query]
282
+ const snippetTokens = useMemo(
283
+ () => tokenizeInlineMarkdown(snippet),
284
+ [snippet]
162
285
  );
163
286
  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));
287
+ const displayUrl = useMemo(() => formatDisplayUrl(href), [href]);
288
+ 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" }, renderMarkdownTokens(snippetTokens, query)) : 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
289
  }
173
290
 
174
291
  // ui/src/layout/Grid.jsx
@@ -1221,71 +1338,103 @@ function MdxSearchTabs(props) {
1221
1338
 
1222
1339
  // ui/src/search/SearchResults.jsx
1223
1340
  import React24 from "react";
1341
+ function DefaultArticleTemplate({ record, query }) {
1342
+ if (!record) return null;
1343
+ const metadata = Array.isArray(record.metadata) ? record.metadata : [];
1344
+ return /* @__PURE__ */ React24.createElement(
1345
+ ArticleCard,
1346
+ {
1347
+ href: record.href,
1348
+ title: record.title || record.href || "Untitled",
1349
+ annotation: record.annotation,
1350
+ summary: record.summary || record.summaryValue || "",
1351
+ summaryMarkdown: record.summaryMarkdown || record.summary || record.summaryValue || "",
1352
+ metadata,
1353
+ query
1354
+ }
1355
+ );
1356
+ }
1357
+ function DefaultFigureTemplate({ record, thumbnailAspectRatio }) {
1358
+ if (!record) return null;
1359
+ const hasDims = Number.isFinite(Number(record.thumbnailWidth)) && Number(record.thumbnailWidth) > 0 && Number.isFinite(Number(record.thumbnailHeight)) && Number(record.thumbnailHeight) > 0;
1360
+ const aspect = Number.isFinite(Number(thumbnailAspectRatio)) && Number(thumbnailAspectRatio) > 0 ? Number(thumbnailAspectRatio) : hasDims ? Number(record.thumbnailWidth) / Number(record.thumbnailHeight) : void 0;
1361
+ return /* @__PURE__ */ React24.createElement(
1362
+ Card,
1363
+ {
1364
+ href: record.href,
1365
+ title: record.title || record.href,
1366
+ src: record.type === "work" ? record.thumbnail : void 0,
1367
+ imgWidth: record.thumbnailWidth,
1368
+ imgHeight: record.thumbnailHeight,
1369
+ aspectRatio: aspect
1370
+ }
1371
+ );
1372
+ }
1224
1373
  function SearchResults({
1225
1374
  results = [],
1226
1375
  type = "all",
1227
1376
  layout = "grid",
1228
- query = ""
1377
+ query = "",
1378
+ templates = {},
1379
+ variant = "auto"
1229
1380
  }) {
1230
1381
  if (!results.length) {
1231
1382
  return /* @__PURE__ */ React24.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React24.createElement("em", null, "No results"));
1232
1383
  }
1233
1384
  const normalizedType = String(type || "all").toLowerCase();
1385
+ const normalizedVariant = variant === "figure" || variant === "article" ? variant : "auto";
1234
1386
  const isAnnotationView = normalizedType === "annotation";
1387
+ const FigureTemplate = templates && templates.figure ? templates.figure : DefaultFigureTemplate;
1388
+ const ArticleTemplate = templates && templates.article ? templates.article : DefaultArticleTemplate;
1235
1389
  if (isAnnotationView) {
1236
1390
  return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
1237
1391
  if (!r) return null;
1238
- return renderTextCard(r, r.id || i);
1392
+ return /* @__PURE__ */ React24.createElement(
1393
+ ArticleTemplate,
1394
+ {
1395
+ key: r.id || i,
1396
+ record: r,
1397
+ query,
1398
+ layout
1399
+ }
1400
+ );
1239
1401
  }));
1240
1402
  }
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
1403
  const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
1257
- const shouldRenderAsTextCard = (record) => !isWorkRecord(record) || normalizedType !== "work";
1404
+ const shouldRenderAsArticle = (record) => {
1405
+ if (normalizedVariant === "article") return true;
1406
+ if (normalizedVariant === "figure") return false;
1407
+ return !isWorkRecord(record) || normalizedType !== "work";
1408
+ };
1258
1409
  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));
1410
+ return /* @__PURE__ */ React24.createElement("div", { id: "search-results", className: "space-y-6" }, results.map((r, i) => {
1411
+ if (shouldRenderAsArticle(r)) {
1412
+ return /* @__PURE__ */ React24.createElement("div", { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
1262
1413
  }
1263
1414
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
1264
1415
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
1265
1416
  return /* @__PURE__ */ React24.createElement(
1266
- "li",
1417
+ "div",
1267
1418
  {
1268
1419
  key: i,
1269
1420
  className: `search-result ${r.type}`,
1270
1421
  "data-thumbnail-aspect-ratio": aspect
1271
1422
  },
1272
1423
  /* @__PURE__ */ React24.createElement(
1273
- Card,
1424
+ FigureTemplate,
1274
1425
  {
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
1426
+ record: r,
1427
+ query,
1428
+ layout,
1429
+ thumbnailAspectRatio: aspect
1281
1430
  }
1282
1431
  )
1283
1432
  );
1284
1433
  }));
1285
1434
  }
1286
1435
  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));
1436
+ if (shouldRenderAsArticle(r)) {
1437
+ return /* @__PURE__ */ React24.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, /* @__PURE__ */ React24.createElement(ArticleTemplate, { record: r, query, layout }));
1289
1438
  }
1290
1439
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
1291
1440
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
@@ -1297,14 +1446,12 @@ function SearchResults({
1297
1446
  "data-thumbnail-aspect-ratio": aspect
1298
1447
  },
1299
1448
  /* @__PURE__ */ React24.createElement(
1300
- Card,
1449
+ FigureTemplate,
1301
1450
  {
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
1451
+ record: r,
1452
+ query,
1453
+ layout,
1454
+ thumbnailAspectRatio: aspect
1308
1455
  }
1309
1456
  )
1310
1457
  );
@@ -1574,6 +1721,7 @@ function MarkdownTable({ className = "", ...rest }) {
1574
1721
  return /* @__PURE__ */ React28.createElement("div", { className: "markdown-table__frame" }, /* @__PURE__ */ React28.createElement("table", { className: merged, ...rest }));
1575
1722
  }
1576
1723
  export {
1724
+ ArticleCard,
1577
1725
  Button,
1578
1726
  ButtonWrapper,
1579
1727
  CanopyBrand,
@@ -1600,7 +1748,6 @@ export {
1600
1748
  MdxSearchTabs as SearchTabs,
1601
1749
  SearchTabs as SearchTabsUI,
1602
1750
  Slider,
1603
- TextCard,
1604
1751
  Viewer
1605
1752
  };
1606
1753
  //# sourceMappingURL=index.mjs.map