@canopy-iiif/app 0.8.4 → 0.8.6

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,48 @@ var Slider = (props) => {
264
329
  }
265
330
  ));
266
331
  }
267
- return /* @__PURE__ */ React5.createElement(CloverSlider, { ...props });
332
+ return /* @__PURE__ */ React6.createElement(CloverSlider, { ...props });
333
+ };
334
+
335
+ // ui/src/iiif/Scroll.jsx
336
+ import React7, { useEffect as useEffect4, useState as useState4 } from "react";
337
+ var Scroll = (props) => {
338
+ const [CloverScroll, setCloverScroll] = useState4(null);
339
+ useEffect4(() => {
340
+ let mounted = true;
341
+ const canUseDom = typeof window !== "undefined" && typeof document !== "undefined";
342
+ if (canUseDom) {
343
+ import("@samvera/clover-iiif/scroll").then((mod) => {
344
+ if (!mounted) return;
345
+ const Comp = mod && (mod.default || mod.Scroll || mod);
346
+ setCloverScroll(() => Comp);
347
+ }).catch(() => {
348
+ });
349
+ }
350
+ return () => {
351
+ mounted = false;
352
+ };
353
+ }, []);
354
+ if (!CloverScroll) {
355
+ let json = "{}";
356
+ try {
357
+ json = JSON.stringify(props || {});
358
+ } catch (_) {
359
+ json = "{}";
360
+ }
361
+ return /* @__PURE__ */ React7.createElement("div", { "data-canopy-scroll": "1", className: "not-prose" }, /* @__PURE__ */ React7.createElement(
362
+ "script",
363
+ {
364
+ type: "application/json",
365
+ dangerouslySetInnerHTML: { __html: json }
366
+ }
367
+ ));
368
+ }
369
+ return /* @__PURE__ */ React7.createElement(CloverScroll, { ...props });
268
370
  };
269
371
 
270
372
  // ui/src/iiif/MdxRelatedItems.jsx
271
- import React6 from "react";
373
+ import React8 from "react";
272
374
  function MdxRelatedItems(props) {
273
375
  let json = "{}";
274
376
  try {
@@ -276,11 +378,11 @@ function MdxRelatedItems(props) {
276
378
  } catch (_) {
277
379
  json = "{}";
278
380
  }
279
- return /* @__PURE__ */ React6.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React6.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
381
+ return /* @__PURE__ */ React8.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
280
382
  }
281
383
 
282
384
  // ui/src/search/MdxSearchResults.jsx
283
- import React7 from "react";
385
+ import React9 from "react";
284
386
  function MdxSearchResults(props) {
285
387
  let json = "{}";
286
388
  try {
@@ -288,11 +390,11 @@ function MdxSearchResults(props) {
288
390
  } catch (_) {
289
391
  json = "{}";
290
392
  }
291
- return /* @__PURE__ */ React7.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React7.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
393
+ return /* @__PURE__ */ React9.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React9.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
292
394
  }
293
395
 
294
396
  // ui/src/search/SearchSummary.jsx
295
- import React8 from "react";
397
+ import React10 from "react";
296
398
  function SearchSummary(props) {
297
399
  let json = "{}";
298
400
  try {
@@ -300,11 +402,11 @@ function SearchSummary(props) {
300
402
  } catch (_) {
301
403
  json = "{}";
302
404
  }
303
- return /* @__PURE__ */ React8.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
405
+ return /* @__PURE__ */ React10.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React10.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
304
406
  }
305
407
 
306
408
  // ui/src/search/MdxSearchTabs.jsx
307
- import React9 from "react";
409
+ import React11 from "react";
308
410
  function MdxSearchTabs(props) {
309
411
  let json = "{}";
310
412
  try {
@@ -312,31 +414,50 @@ function MdxSearchTabs(props) {
312
414
  } catch (_) {
313
415
  json = "{}";
314
416
  }
315
- return /* @__PURE__ */ React9.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React9.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
417
+ return /* @__PURE__ */ React11.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React11.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
316
418
  }
317
419
 
318
420
  // ui/src/search/SearchResults.jsx
319
- import React10 from "react";
421
+ import React12 from "react";
320
422
  function SearchResults({
321
423
  results = [],
322
424
  type = "all",
323
- layout = "grid"
425
+ layout = "grid",
426
+ query = ""
324
427
  }) {
325
428
  if (!results.length) {
326
- return /* @__PURE__ */ React10.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React10.createElement("em", null, "No results"));
429
+ return /* @__PURE__ */ React12.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React12.createElement("em", null, "No results"));
430
+ }
431
+ const isAnnotationView = String(type).toLowerCase() === "annotation";
432
+ if (isAnnotationView) {
433
+ return /* @__PURE__ */ React12.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
434
+ if (!r || !r.annotation) return null;
435
+ return /* @__PURE__ */ React12.createElement(
436
+ AnnotationCard,
437
+ {
438
+ key: r.id || i,
439
+ href: r.href,
440
+ title: r.title || r.href || "Untitled",
441
+ annotation: r.annotation,
442
+ summary: r.summary,
443
+ metadata: Array.isArray(r.metadata) ? r.metadata : [],
444
+ query
445
+ }
446
+ );
447
+ }));
327
448
  }
328
449
  if (layout === "list") {
329
- return /* @__PURE__ */ React10.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
450
+ return /* @__PURE__ */ React12.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
330
451
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
331
452
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
332
- return /* @__PURE__ */ React10.createElement(
453
+ return /* @__PURE__ */ React12.createElement(
333
454
  "li",
334
455
  {
335
456
  key: i,
336
457
  className: `search-result ${r.type}`,
337
458
  "data-thumbnail-aspect-ratio": aspect
338
459
  },
339
- /* @__PURE__ */ React10.createElement(
460
+ /* @__PURE__ */ React12.createElement(
340
461
  Card,
341
462
  {
342
463
  href: r.href,
@@ -350,17 +471,17 @@ function SearchResults({
350
471
  );
351
472
  }));
352
473
  }
353
- return /* @__PURE__ */ React10.createElement("div", { id: "search-results" }, /* @__PURE__ */ React10.createElement(Grid, null, results.map((r, i) => {
474
+ return /* @__PURE__ */ React12.createElement("div", { id: "search-results" }, /* @__PURE__ */ React12.createElement(Grid, null, results.map((r, i) => {
354
475
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
355
476
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
356
- return /* @__PURE__ */ React10.createElement(
477
+ return /* @__PURE__ */ React12.createElement(
357
478
  GridItem,
358
479
  {
359
480
  key: i,
360
481
  className: `search-result ${r.type}`,
361
482
  "data-thumbnail-aspect-ratio": aspect
362
483
  },
363
- /* @__PURE__ */ React10.createElement(
484
+ /* @__PURE__ */ React12.createElement(
364
485
  Card,
365
486
  {
366
487
  href: r.href,
@@ -376,7 +497,7 @@ function SearchResults({
376
497
  }
377
498
 
378
499
  // ui/src/search/SearchTabs.jsx
379
- import React11 from "react";
500
+ import React13 from "react";
380
501
  function SearchTabs({
381
502
  type = "all",
382
503
  onTypeChange,
@@ -391,7 +512,7 @@ function SearchTabs({
391
512
  const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
392
513
  const hasFilters = typeof onOpenFilters === "function";
393
514
  const filterBadge = activeFilterCount > 0 ? ` (${activeFilterCount})` : "";
394
- return /* @__PURE__ */ React11.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React11.createElement(
515
+ return /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React13.createElement(
395
516
  "div",
396
517
  {
397
518
  role: "tablist",
@@ -402,7 +523,7 @@ function SearchTabs({
402
523
  const active = String(type).toLowerCase() === String(t).toLowerCase();
403
524
  const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
404
525
  const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
405
- return /* @__PURE__ */ React11.createElement(
526
+ return /* @__PURE__ */ React13.createElement(
406
527
  "button",
407
528
  {
408
529
  key: t,
@@ -417,7 +538,7 @@ function SearchTabs({
417
538
  ")"
418
539
  );
419
540
  })
420
- ), hasFilters ? /* @__PURE__ */ React11.createElement(
541
+ ), hasFilters ? /* @__PURE__ */ React13.createElement(
421
542
  "button",
422
543
  {
423
544
  type: "button",
@@ -425,12 +546,12 @@ function SearchTabs({
425
546
  "aria-expanded": filtersOpen ? "true" : "false",
426
547
  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
548
  },
428
- /* @__PURE__ */ React11.createElement("span", null, filtersLabel, filterBadge)
549
+ /* @__PURE__ */ React13.createElement("span", null, filtersLabel, filterBadge)
429
550
  ) : null);
430
551
  }
431
552
 
432
553
  // ui/src/search/SearchFiltersDialog.jsx
433
- import React12 from "react";
554
+ import React14 from "react";
434
555
  function toArray(input) {
435
556
  if (!input) return [];
436
557
  if (Array.isArray(input)) return input;
@@ -469,20 +590,20 @@ function FacetSection({ facet, selected, onToggle }) {
469
590
  const selectedValues = selected.get(String(slug)) || /* @__PURE__ */ new Set();
470
591
  const checkboxId = (valueSlug) => `filter-${slug}-${valueSlug}`;
471
592
  const hasSelection = selectedValues.size > 0;
472
- const [quickQuery, setQuickQuery] = React12.useState("");
593
+ const [quickQuery, setQuickQuery] = React14.useState("");
473
594
  const hasQuery = quickQuery.trim().length > 0;
474
- const filteredValues = React12.useMemo(
595
+ const filteredValues = React14.useMemo(
475
596
  () => facetMatches(values, quickQuery),
476
597
  [values, quickQuery]
477
598
  );
478
- return /* @__PURE__ */ React12.createElement(
599
+ return /* @__PURE__ */ React14.createElement(
479
600
  "details",
480
601
  {
481
602
  className: "canopy-search-filters__facet",
482
603
  open: hasSelection
483
604
  },
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(
605
+ /* @__PURE__ */ React14.createElement("summary", { className: "canopy-search-filters__facet-summary" }, /* @__PURE__ */ React14.createElement("span", null, label), /* @__PURE__ */ React14.createElement("span", { className: "canopy-search-filters__facet-count" }, values.length)),
606
+ /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters__facet-content" }, /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters__quick" }, /* @__PURE__ */ React14.createElement(
486
607
  "input",
487
608
  {
488
609
  type: "search",
@@ -492,7 +613,7 @@ function FacetSection({ facet, selected, onToggle }) {
492
613
  className: "canopy-search-filters__quick-input",
493
614
  "aria-label": `Filter ${label} values`
494
615
  }
495
- ), quickQuery ? /* @__PURE__ */ React12.createElement(
616
+ ), quickQuery ? /* @__PURE__ */ React14.createElement(
496
617
  "button",
497
618
  {
498
619
  type: "button",
@@ -500,11 +621,11 @@ function FacetSection({ facet, selected, onToggle }) {
500
621
  className: "canopy-search-filters__quick-clear"
501
622
  },
502
623
  "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) => {
624
+ ) : null), hasQuery && !filteredValues.length ? /* @__PURE__ */ React14.createElement("p", { className: "canopy-search-filters__facet-notice" }, "No matches found.") : null, /* @__PURE__ */ React14.createElement("ul", { className: "canopy-search-filters__facet-list" }, filteredValues.map((entry) => {
504
625
  const valueSlug = String(entry.slug || entry.value || "");
505
626
  const isChecked = selectedValues.has(valueSlug);
506
627
  const inputId = checkboxId(valueSlug);
507
- return /* @__PURE__ */ React12.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React12.createElement(
628
+ return /* @__PURE__ */ React14.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React14.createElement(
508
629
  "input",
509
630
  {
510
631
  id: inputId,
@@ -516,15 +637,15 @@ function FacetSection({ facet, selected, onToggle }) {
516
637
  if (onToggle) onToggle(slug, valueSlug, nextChecked);
517
638
  }
518
639
  }
519
- ), /* @__PURE__ */ React12.createElement(
640
+ ), /* @__PURE__ */ React14.createElement(
520
641
  "label",
521
642
  {
522
643
  htmlFor: inputId,
523
644
  className: "canopy-search-filters__facet-label"
524
645
  },
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)
646
+ /* @__PURE__ */ React14.createElement("span", null, entry.value, " ", Number.isFinite(entry.doc_count) ? /* @__PURE__ */ React14.createElement("span", { className: "canopy-search-filters__facet-count" }, "(", entry.doc_count, ")") : null)
526
647
  ));
527
- }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React12.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
648
+ }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React14.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
528
649
  );
529
650
  }
530
651
  function SearchFiltersDialog(props = {}) {
@@ -544,7 +665,7 @@ function SearchFiltersDialog(props = {}) {
544
665
  0
545
666
  );
546
667
  if (!open) return null;
547
- return /* @__PURE__ */ React12.createElement(
668
+ return /* @__PURE__ */ React14.createElement(
548
669
  "div",
549
670
  {
550
671
  role: "dialog",
@@ -555,7 +676,7 @@ function SearchFiltersDialog(props = {}) {
555
676
  onOpenChange(false);
556
677
  }
557
678
  },
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(
679
+ /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters" }, /* @__PURE__ */ React14.createElement("header", { className: "canopy-search-filters__header" }, /* @__PURE__ */ React14.createElement("div", null, /* @__PURE__ */ React14.createElement("h2", { className: "canopy-search-filters__title" }, title), /* @__PURE__ */ React14.createElement("p", { className: "canopy-search-filters__subtitle" }, subtitle)), /* @__PURE__ */ React14.createElement(
559
680
  "button",
560
681
  {
561
682
  type: "button",
@@ -563,7 +684,7 @@ function SearchFiltersDialog(props = {}) {
563
684
  className: "canopy-search-filters__close"
564
685
  },
565
686
  "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(
687
+ )), /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters__body" }, Array.isArray(facets) && facets.length ? /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters__facets" }, facets.map((facet) => /* @__PURE__ */ React14.createElement(
567
688
  FacetSection,
568
689
  {
569
690
  key: facet.slug || facet.label,
@@ -571,7 +692,7 @@ function SearchFiltersDialog(props = {}) {
571
692
  selected: selectedMap,
572
693
  onToggle
573
694
  }
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(
695
+ ))) : /* @__PURE__ */ React14.createElement("p", { className: "canopy-search-filters__empty" }, "No filters are available for this collection.")), /* @__PURE__ */ React14.createElement("footer", { className: "canopy-search-filters__footer" }, /* @__PURE__ */ React14.createElement("div", null, activeCount ? `${activeCount} filter${activeCount === 1 ? "" : "s"} applied` : "No filters applied"), /* @__PURE__ */ React14.createElement("div", { className: "canopy-search-filters__footer-actions" }, /* @__PURE__ */ React14.createElement(
575
696
  "button",
576
697
  {
577
698
  type: "button",
@@ -582,7 +703,7 @@ function SearchFiltersDialog(props = {}) {
582
703
  className: "canopy-search-filters__button canopy-search-filters__button--secondary"
583
704
  },
584
705
  "Clear all"
585
- ), /* @__PURE__ */ React12.createElement(
706
+ ), /* @__PURE__ */ React14.createElement(
586
707
  "button",
587
708
  {
588
709
  type: "button",
@@ -595,14 +716,14 @@ function SearchFiltersDialog(props = {}) {
595
716
  }
596
717
 
597
718
  // ui/src/search-form/MdxSearchFormModal.jsx
598
- import React16 from "react";
719
+ import React18 from "react";
599
720
 
600
721
  // 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" }));
722
+ import React15 from "react";
723
+ var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React15.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React15.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
724
 
604
725
  // ui/src/search/SearchPanelForm.jsx
605
- import React14 from "react";
726
+ import React16 from "react";
606
727
  function readBasePath() {
607
728
  const normalize = (val) => {
608
729
  const raw = typeof val === "string" ? val.trim() : "";
@@ -661,21 +782,22 @@ function SearchPanelForm(props = {}) {
661
782
  buttonLabel = "Search",
662
783
  label,
663
784
  searchPath = "/search",
664
- inputId: inputIdProp
785
+ inputId: inputIdProp,
786
+ clearLabel = "Clear search"
665
787
  } = props || {};
666
788
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
667
- const action = React14.useMemo(
789
+ const action = React16.useMemo(
668
790
  () => resolveSearchPath(searchPath),
669
791
  [searchPath]
670
792
  );
671
- const autoId = typeof React14.useId === "function" ? React14.useId() : void 0;
672
- const [fallbackId] = React14.useState(
793
+ const autoId = typeof React16.useId === "function" ? React16.useId() : void 0;
794
+ const [fallbackId] = React16.useState(
673
795
  () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
674
796
  );
675
797
  const inputId = inputIdProp || autoId || fallbackId;
676
- const inputRef = React14.useRef(null);
677
- const [hasValue, setHasValue] = React14.useState(false);
678
- const focusInput = React14.useCallback(() => {
798
+ const inputRef = React16.useRef(null);
799
+ const [hasValue, setHasValue] = React16.useState(false);
800
+ const focusInput = React16.useCallback(() => {
679
801
  const el = inputRef.current;
680
802
  if (!el) return;
681
803
  if (document.activeElement === el) return;
@@ -688,30 +810,44 @@ function SearchPanelForm(props = {}) {
688
810
  }
689
811
  }
690
812
  }, []);
691
- const handlePointerDown = React14.useCallback(
813
+ const handlePointerDown = React16.useCallback(
692
814
  (event) => {
693
815
  const target = event.target;
694
816
  if (target && typeof target.closest === "function") {
695
817
  if (target.closest("[data-canopy-search-form-trigger]")) return;
818
+ if (target.closest("[data-canopy-search-form-clear]")) return;
696
819
  }
697
820
  event.preventDefault();
698
821
  focusInput();
699
822
  },
700
823
  [focusInput]
701
824
  );
702
- React14.useEffect(() => {
825
+ React16.useEffect(() => {
703
826
  const el = inputRef.current;
704
827
  if (!el) return;
705
828
  if (el.value && el.value.trim()) {
706
829
  setHasValue(true);
707
830
  }
708
831
  }, []);
709
- const handleInputChange = React14.useCallback((event) => {
832
+ const handleInputChange = React16.useCallback((event) => {
710
833
  var _a;
711
- const nextHasValue = Boolean(((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim());
834
+ const nextHasValue = Boolean(
835
+ ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
836
+ );
712
837
  setHasValue(nextHasValue);
713
838
  }, []);
714
- return /* @__PURE__ */ React14.createElement(
839
+ const handleClear = React16.useCallback((event) => {
840
+ }, []);
841
+ const handleClearKey = React16.useCallback(
842
+ (event) => {
843
+ if (event.key === "Enter" || event.key === " ") {
844
+ event.preventDefault();
845
+ handleClear(event);
846
+ }
847
+ },
848
+ [handleClear]
849
+ );
850
+ return /* @__PURE__ */ React16.createElement(
715
851
  "form",
716
852
  {
717
853
  action,
@@ -721,59 +857,63 @@ function SearchPanelForm(props = {}) {
721
857
  spellCheck: "false",
722
858
  className: "canopy-search-form canopy-search-form-shell",
723
859
  onPointerDown: handlePointerDown,
724
- "data-placeholder": placeholder || "",
725
860
  "data-has-value": hasValue ? "1" : "0"
726
861
  },
727
- /* @__PURE__ */ React14.createElement(
728
- "label",
862
+ /* @__PURE__ */ React16.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React16.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React16.createElement(
863
+ "input",
729
864
  {
730
- htmlFor: inputId,
731
- className: "canopy-search-form__label"
865
+ id: inputId,
866
+ type: "search",
867
+ name: "q",
868
+ inputMode: "search",
869
+ "data-canopy-search-form-input": true,
870
+ placeholder,
871
+ className: "canopy-search-form__input",
872
+ "aria-label": "Search",
873
+ ref: inputRef,
874
+ onChange: handleInputChange,
875
+ onInput: handleInputChange
876
+ }
877
+ )),
878
+ hasValue ? /* @__PURE__ */ React16.createElement(
879
+ "button",
880
+ {
881
+ type: "button",
882
+ className: "canopy-search-form__clear",
883
+ onClick: handleClear,
884
+ onPointerDown: (event) => event.stopPropagation(),
885
+ onKeyDown: handleClearKey,
886
+ "aria-label": clearLabel,
887
+ "data-canopy-search-form-clear": true
732
888
  },
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(
889
+ "\xD7"
890
+ ) : null,
891
+ /* @__PURE__ */ React16.createElement(
752
892
  "button",
753
893
  {
754
894
  type: "submit",
755
895
  "data-canopy-search-form-trigger": "submit",
756
896
  className: "canopy-search-form__submit"
757
897
  },
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"))
898
+ /* @__PURE__ */ React16.createElement("span", null, text),
899
+ /* @__PURE__ */ React16.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React16.createElement("span", null, "\u2318"), /* @__PURE__ */ React16.createElement("span", null, "K"))
760
900
  )
761
901
  );
762
902
  }
763
903
 
764
904
  // ui/src/search/SearchPanelTeaserResults.jsx
765
- import React15 from "react";
905
+ import React17 from "react";
766
906
  function SearchPanelTeaserResults(props = {}) {
767
907
  const { style, className } = props || {};
768
908
  const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
769
- return /* @__PURE__ */ React15.createElement(
909
+ return /* @__PURE__ */ React17.createElement(
770
910
  "div",
771
911
  {
772
912
  "data-canopy-search-form-panel": true,
773
913
  className: classes || void 0,
774
914
  style
775
915
  },
776
- /* @__PURE__ */ React15.createElement("div", { id: "cplist" })
916
+ /* @__PURE__ */ React17.createElement("div", { id: "cplist" })
777
917
  );
778
918
  }
779
919
 
@@ -793,11 +933,11 @@ function MdxSearchFormModal(props = {}) {
793
933
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
794
934
  const resolvedSearchPath = resolveSearchPath(searchPath);
795
935
  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) } }));
936
+ 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) } }));
797
937
  }
798
938
 
799
939
  // ui/src/search/SearchPanel.jsx
800
- import React17 from "react";
940
+ import React19 from "react";
801
941
  function SearchPanel(props = {}) {
802
942
  const {
803
943
  placeholder = "Search\u2026",
@@ -814,14 +954,16 @@ function SearchPanel(props = {}) {
814
954
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
815
955
  const resolvedSearchPath = resolveSearchPath(searchPath);
816
956
  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) } }));
957
+ return /* @__PURE__ */ React19.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React19.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React19.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React19.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React19.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
818
958
  }
819
959
  export {
960
+ AnnotationCard,
820
961
  Card,
821
962
  Grid,
822
963
  GridItem,
823
964
  HelloWorld,
824
965
  MdxRelatedItems as RelatedItems,
966
+ Scroll,
825
967
  SearchFiltersDialog,
826
968
  MdxSearchFormModal as SearchFormModal,
827
969
  SearchPanel,