@canopy-iiif/app 0.8.4 → 0.8.5

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
@@ -104,11 +104,76 @@ function Card({
104
104
  );
105
105
  }
106
106
 
107
+ // ui/src/layout/AnnotationCard.jsx
108
+ import React3, { useMemo } from "react";
109
+ function escapeRegExp(str = "") {
110
+ return String(str).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
111
+ }
112
+ function buildSnippet({ text = "", query = "", maxLength = 240 }) {
113
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
114
+ if (!clean) return "";
115
+ const term = String(query || "").trim();
116
+ if (!term)
117
+ return clean.length > maxLength ? clean.slice(0, maxLength) + "\u2026" : clean;
118
+ const lower = clean.toLowerCase();
119
+ const termLower = term.toLowerCase();
120
+ const idx = lower.indexOf(termLower);
121
+ if (idx === -1) {
122
+ return clean.length > maxLength ? clean.slice(0, maxLength) + "\u2026" : clean;
123
+ }
124
+ const context = Math.max(0, maxLength / 2);
125
+ const start = Math.max(0, idx - context);
126
+ const end = Math.min(clean.length, idx + term.length + context);
127
+ let snippet = clean.slice(start, end);
128
+ if (start > 0) snippet = "\u2026" + snippet;
129
+ if (end < clean.length) snippet = snippet + "\u2026";
130
+ return snippet;
131
+ }
132
+ function highlightSnippet(snippet, query) {
133
+ if (!query) return snippet;
134
+ const term = String(query).trim();
135
+ if (!term) return snippet;
136
+ const parts = String(snippet).split(
137
+ new RegExp(`(${escapeRegExp(term)})`, "gi")
138
+ );
139
+ const termLower = term.toLowerCase();
140
+ return parts.map(
141
+ (part, idx) => part.toLowerCase() === termLower ? /* @__PURE__ */ React3.createElement("mark", { key: idx }, part) : /* @__PURE__ */ React3.createElement(React3.Fragment, { key: idx }, part)
142
+ );
143
+ }
144
+ function AnnotationCard({
145
+ href = "#",
146
+ title = "Untitled",
147
+ annotation = "",
148
+ summary = "",
149
+ metadata = [],
150
+ query = ""
151
+ }) {
152
+ const snippetSource = annotation || summary;
153
+ const snippet = useMemo(
154
+ () => buildSnippet({ text: snippetSource, query }),
155
+ [snippetSource, query]
156
+ );
157
+ const highlighted = useMemo(
158
+ () => highlightSnippet(snippet, query),
159
+ [snippet, query]
160
+ );
161
+ const metaList = Array.isArray(metadata) ? metadata.map((m) => String(m || "")).filter(Boolean) : [];
162
+ 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(
163
+ "li",
164
+ {
165
+ key: `${item}-${idx}`,
166
+ className: "rounded-full border border-slate-200 bg-slate-50 px-2 py-1"
167
+ },
168
+ item
169
+ ))) : null));
170
+ }
171
+
107
172
  // ui/src/layout/Grid.jsx
108
173
  import Masonry from "react-masonry-css";
109
- import React3 from "react";
174
+ import React4 from "react";
110
175
  function GridItem({ children, className = "", style = {}, ...rest }) {
111
- return /* @__PURE__ */ React3.createElement(
176
+ return /* @__PURE__ */ React4.createElement(
112
177
  "div",
113
178
  {
114
179
  className: `canopy-grid-item ${className}`.trim(),
@@ -136,7 +201,7 @@ function Grid({
136
201
  640: 2
137
202
  };
138
203
  const vars = { "--grid-gap": gap, "--grid-padding-y": paddingY };
139
- return /* @__PURE__ */ React3.createElement("div", { className: "canopy-grid-wrap" }, /* @__PURE__ */ React3.createElement(
204
+ return /* @__PURE__ */ React4.createElement("div", { className: "canopy-grid-wrap" }, /* @__PURE__ */ React4.createElement(
140
205
  "style",
141
206
  {
142
207
  dangerouslySetInnerHTML: {
@@ -148,7 +213,7 @@ function Grid({
148
213
  `
149
214
  }
150
215
  }
151
- ), /* @__PURE__ */ React3.createElement(
216
+ ), /* @__PURE__ */ React4.createElement(
152
217
  Masonry,
153
218
  {
154
219
  breakpointCols: cols,
@@ -162,7 +227,7 @@ function Grid({
162
227
  }
163
228
 
164
229
  // ui/src/iiif/Viewer.jsx
165
- import React4, { useEffect as useEffect2, useState as useState2 } from "react";
230
+ import React5, { useEffect as useEffect2, useState as useState2 } from "react";
166
231
  var DEFAULT_VIEWER_OPTIONS = {
167
232
  showDownload: false,
168
233
  showIIIFBadge: false,
@@ -218,7 +283,7 @@ var Viewer = (props) => {
218
283
  } catch (_) {
219
284
  json = "{}";
220
285
  }
221
- return /* @__PURE__ */ React4.createElement("div", { "data-canopy-viewer": "1", className: "not-prose" }, /* @__PURE__ */ React4.createElement(
286
+ return /* @__PURE__ */ React5.createElement("div", { "data-canopy-viewer": "1", className: "not-prose" }, /* @__PURE__ */ React5.createElement(
222
287
  "script",
223
288
  {
224
289
  type: "application/json",
@@ -226,11 +291,11 @@ var Viewer = (props) => {
226
291
  }
227
292
  ));
228
293
  }
229
- return /* @__PURE__ */ React4.createElement(CloverViewer, { ...props, options: mergedOptions });
294
+ return /* @__PURE__ */ React5.createElement(CloverViewer, { ...props, options: mergedOptions });
230
295
  };
231
296
 
232
297
  // ui/src/iiif/Slider.jsx
233
- import React5, { useEffect as useEffect3, useState as useState3 } from "react";
298
+ import React6, { useEffect as useEffect3, useState as useState3 } from "react";
234
299
  var Slider = (props) => {
235
300
  const [CloverSlider, setCloverSlider] = useState3(null);
236
301
  useEffect3(() => {
@@ -256,7 +321,7 @@ var Slider = (props) => {
256
321
  } catch (_) {
257
322
  json = "{}";
258
323
  }
259
- return /* @__PURE__ */ React5.createElement("div", { "data-canopy-slider": "1", className: "not-prose" }, /* @__PURE__ */ React5.createElement(
324
+ return /* @__PURE__ */ React6.createElement("div", { "data-canopy-slider": "1", className: "not-prose" }, /* @__PURE__ */ React6.createElement(
260
325
  "script",
261
326
  {
262
327
  type: "application/json",
@@ -264,11 +329,11 @@ var Slider = (props) => {
264
329
  }
265
330
  ));
266
331
  }
267
- return /* @__PURE__ */ React5.createElement(CloverSlider, { ...props });
332
+ return /* @__PURE__ */ React6.createElement(CloverSlider, { ...props });
268
333
  };
269
334
 
270
335
  // ui/src/iiif/MdxRelatedItems.jsx
271
- import React6 from "react";
336
+ import React7 from "react";
272
337
  function MdxRelatedItems(props) {
273
338
  let json = "{}";
274
339
  try {
@@ -276,11 +341,11 @@ function MdxRelatedItems(props) {
276
341
  } catch (_) {
277
342
  json = "{}";
278
343
  }
279
- return /* @__PURE__ */ React6.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React6.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
344
+ return /* @__PURE__ */ React7.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React7.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
280
345
  }
281
346
 
282
347
  // ui/src/search/MdxSearchResults.jsx
283
- import React7 from "react";
348
+ import React8 from "react";
284
349
  function MdxSearchResults(props) {
285
350
  let json = "{}";
286
351
  try {
@@ -288,11 +353,11 @@ function MdxSearchResults(props) {
288
353
  } catch (_) {
289
354
  json = "{}";
290
355
  }
291
- return /* @__PURE__ */ React7.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React7.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
356
+ return /* @__PURE__ */ React8.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
292
357
  }
293
358
 
294
359
  // ui/src/search/SearchSummary.jsx
295
- import React8 from "react";
360
+ import React9 from "react";
296
361
  function SearchSummary(props) {
297
362
  let json = "{}";
298
363
  try {
@@ -300,11 +365,11 @@ function SearchSummary(props) {
300
365
  } catch (_) {
301
366
  json = "{}";
302
367
  }
303
- return /* @__PURE__ */ React8.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
368
+ return /* @__PURE__ */ React9.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React9.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
304
369
  }
305
370
 
306
371
  // ui/src/search/MdxSearchTabs.jsx
307
- import React9 from "react";
372
+ import React10 from "react";
308
373
  function MdxSearchTabs(props) {
309
374
  let json = "{}";
310
375
  try {
@@ -312,31 +377,50 @@ function MdxSearchTabs(props) {
312
377
  } catch (_) {
313
378
  json = "{}";
314
379
  }
315
- return /* @__PURE__ */ React9.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React9.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
380
+ return /* @__PURE__ */ React10.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React10.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
316
381
  }
317
382
 
318
383
  // ui/src/search/SearchResults.jsx
319
- import React10 from "react";
384
+ import React11 from "react";
320
385
  function SearchResults({
321
386
  results = [],
322
387
  type = "all",
323
- layout = "grid"
388
+ layout = "grid",
389
+ query = ""
324
390
  }) {
325
391
  if (!results.length) {
326
- return /* @__PURE__ */ React10.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React10.createElement("em", null, "No results"));
392
+ return /* @__PURE__ */ React11.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React11.createElement("em", null, "No results"));
393
+ }
394
+ const isAnnotationView = String(type).toLowerCase() === "annotation";
395
+ if (isAnnotationView) {
396
+ return /* @__PURE__ */ React11.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
397
+ if (!r || !r.annotation) return null;
398
+ return /* @__PURE__ */ React11.createElement(
399
+ AnnotationCard,
400
+ {
401
+ key: r.id || i,
402
+ href: r.href,
403
+ title: r.title || r.href || "Untitled",
404
+ annotation: r.annotation,
405
+ summary: r.summary,
406
+ metadata: Array.isArray(r.metadata) ? r.metadata : [],
407
+ query
408
+ }
409
+ );
410
+ }));
327
411
  }
328
412
  if (layout === "list") {
329
- return /* @__PURE__ */ React10.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
413
+ return /* @__PURE__ */ React11.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
330
414
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
331
415
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
332
- return /* @__PURE__ */ React10.createElement(
416
+ return /* @__PURE__ */ React11.createElement(
333
417
  "li",
334
418
  {
335
419
  key: i,
336
420
  className: `search-result ${r.type}`,
337
421
  "data-thumbnail-aspect-ratio": aspect
338
422
  },
339
- /* @__PURE__ */ React10.createElement(
423
+ /* @__PURE__ */ React11.createElement(
340
424
  Card,
341
425
  {
342
426
  href: r.href,
@@ -350,17 +434,17 @@ function SearchResults({
350
434
  );
351
435
  }));
352
436
  }
353
- return /* @__PURE__ */ React10.createElement("div", { id: "search-results" }, /* @__PURE__ */ React10.createElement(Grid, null, results.map((r, i) => {
437
+ return /* @__PURE__ */ React11.createElement("div", { id: "search-results" }, /* @__PURE__ */ React11.createElement(Grid, null, results.map((r, i) => {
354
438
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
355
439
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
356
- return /* @__PURE__ */ React10.createElement(
440
+ return /* @__PURE__ */ React11.createElement(
357
441
  GridItem,
358
442
  {
359
443
  key: i,
360
444
  className: `search-result ${r.type}`,
361
445
  "data-thumbnail-aspect-ratio": aspect
362
446
  },
363
- /* @__PURE__ */ React10.createElement(
447
+ /* @__PURE__ */ React11.createElement(
364
448
  Card,
365
449
  {
366
450
  href: r.href,
@@ -376,7 +460,7 @@ function SearchResults({
376
460
  }
377
461
 
378
462
  // ui/src/search/SearchTabs.jsx
379
- import React11 from "react";
463
+ import React12 from "react";
380
464
  function SearchTabs({
381
465
  type = "all",
382
466
  onTypeChange,
@@ -391,7 +475,7 @@ function SearchTabs({
391
475
  const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
392
476
  const hasFilters = typeof onOpenFilters === "function";
393
477
  const filterBadge = activeFilterCount > 0 ? ` (${activeFilterCount})` : "";
394
- return /* @__PURE__ */ React11.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React11.createElement(
478
+ return /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React12.createElement(
395
479
  "div",
396
480
  {
397
481
  role: "tablist",
@@ -402,7 +486,7 @@ function SearchTabs({
402
486
  const active = String(type).toLowerCase() === String(t).toLowerCase();
403
487
  const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
404
488
  const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
405
- return /* @__PURE__ */ React11.createElement(
489
+ return /* @__PURE__ */ React12.createElement(
406
490
  "button",
407
491
  {
408
492
  key: t,
@@ -417,7 +501,7 @@ function SearchTabs({
417
501
  ")"
418
502
  );
419
503
  })
420
- ), hasFilters ? /* @__PURE__ */ React11.createElement(
504
+ ), hasFilters ? /* @__PURE__ */ React12.createElement(
421
505
  "button",
422
506
  {
423
507
  type: "button",
@@ -425,12 +509,12 @@ function SearchTabs({
425
509
  "aria-expanded": filtersOpen ? "true" : "false",
426
510
  className: "inline-flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 shadow-sm transition hover:border-brand-200 hover:bg-brand-50 hover:text-brand-700"
427
511
  },
428
- /* @__PURE__ */ React11.createElement("span", null, filtersLabel, filterBadge)
512
+ /* @__PURE__ */ React12.createElement("span", null, filtersLabel, filterBadge)
429
513
  ) : null);
430
514
  }
431
515
 
432
516
  // ui/src/search/SearchFiltersDialog.jsx
433
- import React12 from "react";
517
+ import React13 from "react";
434
518
  function toArray(input) {
435
519
  if (!input) return [];
436
520
  if (Array.isArray(input)) return input;
@@ -469,20 +553,20 @@ function FacetSection({ facet, selected, onToggle }) {
469
553
  const selectedValues = selected.get(String(slug)) || /* @__PURE__ */ new Set();
470
554
  const checkboxId = (valueSlug) => `filter-${slug}-${valueSlug}`;
471
555
  const hasSelection = selectedValues.size > 0;
472
- const [quickQuery, setQuickQuery] = React12.useState("");
556
+ const [quickQuery, setQuickQuery] = React13.useState("");
473
557
  const hasQuery = quickQuery.trim().length > 0;
474
- const filteredValues = React12.useMemo(
558
+ const filteredValues = React13.useMemo(
475
559
  () => facetMatches(values, quickQuery),
476
560
  [values, quickQuery]
477
561
  );
478
- return /* @__PURE__ */ React12.createElement(
562
+ return /* @__PURE__ */ React13.createElement(
479
563
  "details",
480
564
  {
481
565
  className: "canopy-search-filters__facet",
482
566
  open: hasSelection
483
567
  },
484
- /* @__PURE__ */ React12.createElement("summary", { className: "canopy-search-filters__facet-summary" }, /* @__PURE__ */ React12.createElement("span", null, label), /* @__PURE__ */ React12.createElement("span", { className: "canopy-search-filters__facet-count" }, values.length)),
485
- /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters__facet-content" }, /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters__quick" }, /* @__PURE__ */ React12.createElement(
568
+ /* @__PURE__ */ React13.createElement("summary", { className: "canopy-search-filters__facet-summary" }, /* @__PURE__ */ React13.createElement("span", null, label), /* @__PURE__ */ React13.createElement("span", { className: "canopy-search-filters__facet-count" }, values.length)),
569
+ /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters__facet-content" }, /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters__quick" }, /* @__PURE__ */ React13.createElement(
486
570
  "input",
487
571
  {
488
572
  type: "search",
@@ -492,7 +576,7 @@ function FacetSection({ facet, selected, onToggle }) {
492
576
  className: "canopy-search-filters__quick-input",
493
577
  "aria-label": `Filter ${label} values`
494
578
  }
495
- ), quickQuery ? /* @__PURE__ */ React12.createElement(
579
+ ), quickQuery ? /* @__PURE__ */ React13.createElement(
496
580
  "button",
497
581
  {
498
582
  type: "button",
@@ -500,11 +584,11 @@ function FacetSection({ facet, selected, onToggle }) {
500
584
  className: "canopy-search-filters__quick-clear"
501
585
  },
502
586
  "Clear"
503
- ) : null), hasQuery && !filteredValues.length ? /* @__PURE__ */ React12.createElement("p", { className: "canopy-search-filters__facet-notice" }, "No matches found.") : null, /* @__PURE__ */ React12.createElement("ul", { className: "canopy-search-filters__facet-list" }, filteredValues.map((entry) => {
587
+ ) : null), hasQuery && !filteredValues.length ? /* @__PURE__ */ React13.createElement("p", { className: "canopy-search-filters__facet-notice" }, "No matches found.") : null, /* @__PURE__ */ React13.createElement("ul", { className: "canopy-search-filters__facet-list" }, filteredValues.map((entry) => {
504
588
  const valueSlug = String(entry.slug || entry.value || "");
505
589
  const isChecked = selectedValues.has(valueSlug);
506
590
  const inputId = checkboxId(valueSlug);
507
- return /* @__PURE__ */ React12.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React12.createElement(
591
+ return /* @__PURE__ */ React13.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React13.createElement(
508
592
  "input",
509
593
  {
510
594
  id: inputId,
@@ -516,15 +600,15 @@ function FacetSection({ facet, selected, onToggle }) {
516
600
  if (onToggle) onToggle(slug, valueSlug, nextChecked);
517
601
  }
518
602
  }
519
- ), /* @__PURE__ */ React12.createElement(
603
+ ), /* @__PURE__ */ React13.createElement(
520
604
  "label",
521
605
  {
522
606
  htmlFor: inputId,
523
607
  className: "canopy-search-filters__facet-label"
524
608
  },
525
- /* @__PURE__ */ React12.createElement("span", null, entry.value, " ", Number.isFinite(entry.doc_count) ? /* @__PURE__ */ React12.createElement("span", { className: "canopy-search-filters__facet-count" }, "(", entry.doc_count, ")") : null)
609
+ /* @__PURE__ */ React13.createElement("span", null, entry.value, " ", Number.isFinite(entry.doc_count) ? /* @__PURE__ */ React13.createElement("span", { className: "canopy-search-filters__facet-count" }, "(", entry.doc_count, ")") : null)
526
610
  ));
527
- }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React12.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
611
+ }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React13.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
528
612
  );
529
613
  }
530
614
  function SearchFiltersDialog(props = {}) {
@@ -544,7 +628,7 @@ function SearchFiltersDialog(props = {}) {
544
628
  0
545
629
  );
546
630
  if (!open) return null;
547
- return /* @__PURE__ */ React12.createElement(
631
+ return /* @__PURE__ */ React13.createElement(
548
632
  "div",
549
633
  {
550
634
  role: "dialog",
@@ -555,7 +639,7 @@ function SearchFiltersDialog(props = {}) {
555
639
  onOpenChange(false);
556
640
  }
557
641
  },
558
- /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters" }, /* @__PURE__ */ React12.createElement("header", { className: "canopy-search-filters__header" }, /* @__PURE__ */ React12.createElement("div", null, /* @__PURE__ */ React12.createElement("h2", { className: "canopy-search-filters__title" }, title), /* @__PURE__ */ React12.createElement("p", { className: "canopy-search-filters__subtitle" }, subtitle)), /* @__PURE__ */ React12.createElement(
642
+ /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters" }, /* @__PURE__ */ React13.createElement("header", { className: "canopy-search-filters__header" }, /* @__PURE__ */ React13.createElement("div", null, /* @__PURE__ */ React13.createElement("h2", { className: "canopy-search-filters__title" }, title), /* @__PURE__ */ React13.createElement("p", { className: "canopy-search-filters__subtitle" }, subtitle)), /* @__PURE__ */ React13.createElement(
559
643
  "button",
560
644
  {
561
645
  type: "button",
@@ -563,7 +647,7 @@ function SearchFiltersDialog(props = {}) {
563
647
  className: "canopy-search-filters__close"
564
648
  },
565
649
  "Close"
566
- )), /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters__body" }, Array.isArray(facets) && facets.length ? /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters__facets" }, facets.map((facet) => /* @__PURE__ */ React12.createElement(
650
+ )), /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters__body" }, Array.isArray(facets) && facets.length ? /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters__facets" }, facets.map((facet) => /* @__PURE__ */ React13.createElement(
567
651
  FacetSection,
568
652
  {
569
653
  key: facet.slug || facet.label,
@@ -571,7 +655,7 @@ function SearchFiltersDialog(props = {}) {
571
655
  selected: selectedMap,
572
656
  onToggle
573
657
  }
574
- ))) : /* @__PURE__ */ React12.createElement("p", { className: "canopy-search-filters__empty" }, "No filters are available for this collection.")), /* @__PURE__ */ React12.createElement("footer", { className: "canopy-search-filters__footer" }, /* @__PURE__ */ React12.createElement("div", null, activeCount ? `${activeCount} filter${activeCount === 1 ? "" : "s"} applied` : "No filters applied"), /* @__PURE__ */ React12.createElement("div", { className: "canopy-search-filters__footer-actions" }, /* @__PURE__ */ React12.createElement(
658
+ ))) : /* @__PURE__ */ React13.createElement("p", { className: "canopy-search-filters__empty" }, "No filters are available for this collection.")), /* @__PURE__ */ React13.createElement("footer", { className: "canopy-search-filters__footer" }, /* @__PURE__ */ React13.createElement("div", null, activeCount ? `${activeCount} filter${activeCount === 1 ? "" : "s"} applied` : "No filters applied"), /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-filters__footer-actions" }, /* @__PURE__ */ React13.createElement(
575
659
  "button",
576
660
  {
577
661
  type: "button",
@@ -582,7 +666,7 @@ function SearchFiltersDialog(props = {}) {
582
666
  className: "canopy-search-filters__button canopy-search-filters__button--secondary"
583
667
  },
584
668
  "Clear all"
585
- ), /* @__PURE__ */ React12.createElement(
669
+ ), /* @__PURE__ */ React13.createElement(
586
670
  "button",
587
671
  {
588
672
  type: "button",
@@ -595,14 +679,14 @@ function SearchFiltersDialog(props = {}) {
595
679
  }
596
680
 
597
681
  // ui/src/search-form/MdxSearchFormModal.jsx
598
- import React16 from "react";
682
+ import React17 from "react";
599
683
 
600
684
  // ui/src/Icons.jsx
601
- import React13 from "react";
602
- var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React13.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React13.createElement("path", { d: "M456.69 421.39L362.6 327.3a173.81 173.81 0 0034.84-104.58C397.44 126.38 319.06 48 222.72 48S48 126.38 48 222.72s78.38 174.72 174.72 174.72A173.81 173.81 0 00327.3 362.6l94.09 94.09a25 25 0 0035.3-35.3zM97.92 222.72a124.8 124.8 0 11124.8 124.8 124.95 124.95 0 01-124.8-124.8z" }));
685
+ import React14 from "react";
686
+ var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React14.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React14.createElement("path", { d: "M456.69 421.39L362.6 327.3a173.81 173.81 0 0034.84-104.58C397.44 126.38 319.06 48 222.72 48S48 126.38 48 222.72s78.38 174.72 174.72 174.72A173.81 173.81 0 00327.3 362.6l94.09 94.09a25 25 0 0035.3-35.3zM97.92 222.72a124.8 124.8 0 11124.8 124.8 124.95 124.95 0 01-124.8-124.8z" }));
603
687
 
604
688
  // ui/src/search/SearchPanelForm.jsx
605
- import React14 from "react";
689
+ import React15 from "react";
606
690
  function readBasePath() {
607
691
  const normalize = (val) => {
608
692
  const raw = typeof val === "string" ? val.trim() : "";
@@ -661,21 +745,22 @@ function SearchPanelForm(props = {}) {
661
745
  buttonLabel = "Search",
662
746
  label,
663
747
  searchPath = "/search",
664
- inputId: inputIdProp
748
+ inputId: inputIdProp,
749
+ clearLabel = "Clear search"
665
750
  } = props || {};
666
751
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
667
- const action = React14.useMemo(
752
+ const action = React15.useMemo(
668
753
  () => resolveSearchPath(searchPath),
669
754
  [searchPath]
670
755
  );
671
- const autoId = typeof React14.useId === "function" ? React14.useId() : void 0;
672
- const [fallbackId] = React14.useState(
756
+ const autoId = typeof React15.useId === "function" ? React15.useId() : void 0;
757
+ const [fallbackId] = React15.useState(
673
758
  () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
674
759
  );
675
760
  const inputId = inputIdProp || autoId || fallbackId;
676
- const inputRef = React14.useRef(null);
677
- const [hasValue, setHasValue] = React14.useState(false);
678
- const focusInput = React14.useCallback(() => {
761
+ const inputRef = React15.useRef(null);
762
+ const [hasValue, setHasValue] = React15.useState(false);
763
+ const focusInput = React15.useCallback(() => {
679
764
  const el = inputRef.current;
680
765
  if (!el) return;
681
766
  if (document.activeElement === el) return;
@@ -688,30 +773,44 @@ function SearchPanelForm(props = {}) {
688
773
  }
689
774
  }
690
775
  }, []);
691
- const handlePointerDown = React14.useCallback(
776
+ const handlePointerDown = React15.useCallback(
692
777
  (event) => {
693
778
  const target = event.target;
694
779
  if (target && typeof target.closest === "function") {
695
780
  if (target.closest("[data-canopy-search-form-trigger]")) return;
781
+ if (target.closest("[data-canopy-search-form-clear]")) return;
696
782
  }
697
783
  event.preventDefault();
698
784
  focusInput();
699
785
  },
700
786
  [focusInput]
701
787
  );
702
- React14.useEffect(() => {
788
+ React15.useEffect(() => {
703
789
  const el = inputRef.current;
704
790
  if (!el) return;
705
791
  if (el.value && el.value.trim()) {
706
792
  setHasValue(true);
707
793
  }
708
794
  }, []);
709
- const handleInputChange = React14.useCallback((event) => {
795
+ const handleInputChange = React15.useCallback((event) => {
710
796
  var _a;
711
- const nextHasValue = Boolean(((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim());
797
+ const nextHasValue = Boolean(
798
+ ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
799
+ );
712
800
  setHasValue(nextHasValue);
713
801
  }, []);
714
- return /* @__PURE__ */ React14.createElement(
802
+ const handleClear = React15.useCallback((event) => {
803
+ }, []);
804
+ const handleClearKey = React15.useCallback(
805
+ (event) => {
806
+ if (event.key === "Enter" || event.key === " ") {
807
+ event.preventDefault();
808
+ handleClear(event);
809
+ }
810
+ },
811
+ [handleClear]
812
+ );
813
+ return /* @__PURE__ */ React15.createElement(
715
814
  "form",
716
815
  {
717
816
  action,
@@ -721,59 +820,63 @@ function SearchPanelForm(props = {}) {
721
820
  spellCheck: "false",
722
821
  className: "canopy-search-form canopy-search-form-shell",
723
822
  onPointerDown: handlePointerDown,
724
- "data-placeholder": placeholder || "",
725
823
  "data-has-value": hasValue ? "1" : "0"
726
824
  },
727
- /* @__PURE__ */ React14.createElement(
728
- "label",
825
+ /* @__PURE__ */ React15.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React15.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React15.createElement(
826
+ "input",
729
827
  {
730
- htmlFor: inputId,
731
- className: "canopy-search-form__label"
828
+ id: inputId,
829
+ type: "search",
830
+ name: "q",
831
+ inputMode: "search",
832
+ "data-canopy-search-form-input": true,
833
+ placeholder,
834
+ className: "canopy-search-form__input",
835
+ "aria-label": "Search",
836
+ ref: inputRef,
837
+ onChange: handleInputChange,
838
+ onInput: handleInputChange
839
+ }
840
+ )),
841
+ hasValue ? /* @__PURE__ */ React15.createElement(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ className: "canopy-search-form__clear",
846
+ onClick: handleClear,
847
+ onPointerDown: (event) => event.stopPropagation(),
848
+ onKeyDown: handleClearKey,
849
+ "aria-label": clearLabel,
850
+ "data-canopy-search-form-clear": true
732
851
  },
733
- /* @__PURE__ */ React14.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }),
734
- /* @__PURE__ */ React14.createElement(
735
- "input",
736
- {
737
- id: inputId,
738
- type: "search",
739
- name: "q",
740
- inputMode: "search",
741
- "data-canopy-search-form-input": true,
742
- placeholder,
743
- className: "canopy-search-form__input",
744
- "aria-label": "Search",
745
- ref: inputRef,
746
- onChange: handleInputChange,
747
- onInput: handleInputChange
748
- }
749
- )
750
- ),
751
- /* @__PURE__ */ React14.createElement(
852
+ "\xD7"
853
+ ) : null,
854
+ /* @__PURE__ */ React15.createElement(
752
855
  "button",
753
856
  {
754
857
  type: "submit",
755
858
  "data-canopy-search-form-trigger": "submit",
756
859
  className: "canopy-search-form__submit"
757
860
  },
758
- /* @__PURE__ */ React14.createElement("span", null, text),
759
- /* @__PURE__ */ React14.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React14.createElement("span", null, "\u2318"), /* @__PURE__ */ React14.createElement("span", null, "K"))
861
+ /* @__PURE__ */ React15.createElement("span", null, text),
862
+ /* @__PURE__ */ React15.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React15.createElement("span", null, "\u2318"), /* @__PURE__ */ React15.createElement("span", null, "K"))
760
863
  )
761
864
  );
762
865
  }
763
866
 
764
867
  // ui/src/search/SearchPanelTeaserResults.jsx
765
- import React15 from "react";
868
+ import React16 from "react";
766
869
  function SearchPanelTeaserResults(props = {}) {
767
870
  const { style, className } = props || {};
768
871
  const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
769
- return /* @__PURE__ */ React15.createElement(
872
+ return /* @__PURE__ */ React16.createElement(
770
873
  "div",
771
874
  {
772
875
  "data-canopy-search-form-panel": true,
773
876
  className: classes || void 0,
774
877
  style
775
878
  },
776
- /* @__PURE__ */ React15.createElement("div", { id: "cplist" })
879
+ /* @__PURE__ */ React16.createElement("div", { id: "cplist" })
777
880
  );
778
881
  }
779
882
 
@@ -793,11 +896,11 @@ function MdxSearchFormModal(props = {}) {
793
896
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
794
897
  const resolvedSearchPath = resolveSearchPath(searchPath);
795
898
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
796
- return /* @__PURE__ */ React16.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React16.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React16.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React16.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React16.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
899
+ return /* @__PURE__ */ React17.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React17.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React17.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React17.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
797
900
  }
798
901
 
799
902
  // ui/src/search/SearchPanel.jsx
800
- import React17 from "react";
903
+ import React18 from "react";
801
904
  function SearchPanel(props = {}) {
802
905
  const {
803
906
  placeholder = "Search\u2026",
@@ -814,9 +917,10 @@ function SearchPanel(props = {}) {
814
917
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
815
918
  const resolvedSearchPath = resolveSearchPath(searchPath);
816
919
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
817
- return /* @__PURE__ */ React17.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React17.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React17.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React17.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
920
+ return /* @__PURE__ */ React18.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React18.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React18.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React18.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React18.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
818
921
  }
819
922
  export {
923
+ AnnotationCard,
820
924
  Card,
821
925
  Grid,
822
926
  GridItem,