@canopy-iiif/app 0.8.3 → 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.
Files changed (36) hide show
  1. package/lib/build/build.js +2 -0
  2. package/lib/build/dev.js +38 -22
  3. package/lib/build/iiif.js +359 -83
  4. package/lib/build/mdx.js +12 -2
  5. package/lib/build/pages.js +15 -1
  6. package/lib/build/styles.js +53 -1
  7. package/lib/common.js +28 -6
  8. package/lib/components/navigation.js +308 -0
  9. package/lib/page-context.js +14 -0
  10. package/lib/search/search-app.jsx +177 -25
  11. package/lib/search/search-form-runtime.js +126 -19
  12. package/lib/search/search.js +130 -18
  13. package/package.json +4 -1
  14. package/ui/dist/index.mjs +204 -101
  15. package/ui/dist/index.mjs.map +4 -4
  16. package/ui/dist/server.mjs +167 -59
  17. package/ui/dist/server.mjs.map +4 -4
  18. package/ui/styles/_variables.scss +1 -0
  19. package/ui/styles/base/_common.scss +27 -5
  20. package/ui/styles/base/_heading.scss +2 -4
  21. package/ui/styles/base/index.scss +1 -0
  22. package/ui/styles/components/_card.scss +47 -4
  23. package/ui/styles/components/_sub-navigation.scss +76 -0
  24. package/ui/styles/components/header/_header.scss +1 -4
  25. package/ui/styles/components/header/_logo.scss +33 -10
  26. package/ui/styles/components/index.scss +1 -0
  27. package/ui/styles/components/search/_filters.scss +5 -7
  28. package/ui/styles/components/search/_form.scss +55 -17
  29. package/ui/styles/components/search/_results.scss +49 -14
  30. package/ui/styles/index.css +344 -56
  31. package/ui/styles/index.scss +2 -4
  32. package/ui/tailwind-canopy-iiif-plugin.js +10 -2
  33. package/ui/tailwind-canopy-iiif-preset.js +21 -19
  34. package/ui/theme.js +303 -0
  35. package/ui/styles/variables.emit.scss +0 -72
  36. package/ui/styles/variables.scss +0 -76
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(),
@@ -120,7 +185,7 @@ function GridItem({ children, className = "", style = {}, ...rest }) {
120
185
  }
121
186
  function Grid({
122
187
  breakpointCols,
123
- gap = "2rem",
188
+ gap = "1.618rem",
124
189
  paddingY = "0",
125
190
  className = "",
126
191
  style = {},
@@ -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,26 +475,25 @@ 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: "flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 pb-1" }, /* @__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",
398
482
  "aria-label": "Search types",
399
- className: "flex items-center gap-2"
483
+ className: "canopy-search-tabs"
400
484
  },
401
485
  orderedTypes.map((t) => {
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,
409
493
  role: "tab",
410
494
  "aria-selected": active,
411
495
  type: "button",
412
- onClick: () => onTypeChange && onTypeChange(t),
413
- className: "px-3 py-2 text-sm rounded-t-md border-b-2 -mb-px transition-colors " + (active ? "border-brand-600 text-brand-700" : "border-transparent text-slate-600 hover:text-slate-900 hover:border-slate-300")
496
+ onClick: () => onTypeChange && onTypeChange(t)
414
497
  },
415
498
  toLabel(t),
416
499
  " (",
@@ -418,7 +501,7 @@ function SearchTabs({
418
501
  ")"
419
502
  );
420
503
  })
421
- ), hasFilters ? /* @__PURE__ */ React11.createElement(
504
+ ), hasFilters ? /* @__PURE__ */ React12.createElement(
422
505
  "button",
423
506
  {
424
507
  type: "button",
@@ -426,12 +509,12 @@ function SearchTabs({
426
509
  "aria-expanded": filtersOpen ? "true" : "false",
427
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"
428
511
  },
429
- /* @__PURE__ */ React11.createElement("span", null, filtersLabel, filterBadge)
512
+ /* @__PURE__ */ React12.createElement("span", null, filtersLabel, filterBadge)
430
513
  ) : null);
431
514
  }
432
515
 
433
516
  // ui/src/search/SearchFiltersDialog.jsx
434
- import React12 from "react";
517
+ import React13 from "react";
435
518
  function toArray(input) {
436
519
  if (!input) return [];
437
520
  if (Array.isArray(input)) return input;
@@ -470,20 +553,20 @@ function FacetSection({ facet, selected, onToggle }) {
470
553
  const selectedValues = selected.get(String(slug)) || /* @__PURE__ */ new Set();
471
554
  const checkboxId = (valueSlug) => `filter-${slug}-${valueSlug}`;
472
555
  const hasSelection = selectedValues.size > 0;
473
- const [quickQuery, setQuickQuery] = React12.useState("");
556
+ const [quickQuery, setQuickQuery] = React13.useState("");
474
557
  const hasQuery = quickQuery.trim().length > 0;
475
- const filteredValues = React12.useMemo(
558
+ const filteredValues = React13.useMemo(
476
559
  () => facetMatches(values, quickQuery),
477
560
  [values, quickQuery]
478
561
  );
479
- return /* @__PURE__ */ React12.createElement(
562
+ return /* @__PURE__ */ React13.createElement(
480
563
  "details",
481
564
  {
482
565
  className: "canopy-search-filters__facet",
483
566
  open: hasSelection
484
567
  },
485
- /* @__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)),
486
- /* @__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(
487
570
  "input",
488
571
  {
489
572
  type: "search",
@@ -493,7 +576,7 @@ function FacetSection({ facet, selected, onToggle }) {
493
576
  className: "canopy-search-filters__quick-input",
494
577
  "aria-label": `Filter ${label} values`
495
578
  }
496
- ), quickQuery ? /* @__PURE__ */ React12.createElement(
579
+ ), quickQuery ? /* @__PURE__ */ React13.createElement(
497
580
  "button",
498
581
  {
499
582
  type: "button",
@@ -501,11 +584,11 @@ function FacetSection({ facet, selected, onToggle }) {
501
584
  className: "canopy-search-filters__quick-clear"
502
585
  },
503
586
  "Clear"
504
- ) : 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) => {
505
588
  const valueSlug = String(entry.slug || entry.value || "");
506
589
  const isChecked = selectedValues.has(valueSlug);
507
590
  const inputId = checkboxId(valueSlug);
508
- 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(
509
592
  "input",
510
593
  {
511
594
  id: inputId,
@@ -517,15 +600,15 @@ function FacetSection({ facet, selected, onToggle }) {
517
600
  if (onToggle) onToggle(slug, valueSlug, nextChecked);
518
601
  }
519
602
  }
520
- ), /* @__PURE__ */ React12.createElement(
603
+ ), /* @__PURE__ */ React13.createElement(
521
604
  "label",
522
605
  {
523
606
  htmlFor: inputId,
524
607
  className: "canopy-search-filters__facet-label"
525
608
  },
526
- /* @__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)
527
610
  ));
528
- }), !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))
529
612
  );
530
613
  }
531
614
  function SearchFiltersDialog(props = {}) {
@@ -545,7 +628,7 @@ function SearchFiltersDialog(props = {}) {
545
628
  0
546
629
  );
547
630
  if (!open) return null;
548
- return /* @__PURE__ */ React12.createElement(
631
+ return /* @__PURE__ */ React13.createElement(
549
632
  "div",
550
633
  {
551
634
  role: "dialog",
@@ -556,7 +639,7 @@ function SearchFiltersDialog(props = {}) {
556
639
  onOpenChange(false);
557
640
  }
558
641
  },
559
- /* @__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(
560
643
  "button",
561
644
  {
562
645
  type: "button",
@@ -564,7 +647,7 @@ function SearchFiltersDialog(props = {}) {
564
647
  className: "canopy-search-filters__close"
565
648
  },
566
649
  "Close"
567
- )), /* @__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(
568
651
  FacetSection,
569
652
  {
570
653
  key: facet.slug || facet.label,
@@ -572,7 +655,7 @@ function SearchFiltersDialog(props = {}) {
572
655
  selected: selectedMap,
573
656
  onToggle
574
657
  }
575
- ))) : /* @__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(
576
659
  "button",
577
660
  {
578
661
  type: "button",
@@ -583,7 +666,7 @@ function SearchFiltersDialog(props = {}) {
583
666
  className: "canopy-search-filters__button canopy-search-filters__button--secondary"
584
667
  },
585
668
  "Clear all"
586
- ), /* @__PURE__ */ React12.createElement(
669
+ ), /* @__PURE__ */ React13.createElement(
587
670
  "button",
588
671
  {
589
672
  type: "button",
@@ -596,14 +679,14 @@ function SearchFiltersDialog(props = {}) {
596
679
  }
597
680
 
598
681
  // ui/src/search-form/MdxSearchFormModal.jsx
599
- import React16 from "react";
682
+ import React17 from "react";
600
683
 
601
684
  // ui/src/Icons.jsx
602
- import React13 from "react";
603
- 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" }));
604
687
 
605
688
  // ui/src/search/SearchPanelForm.jsx
606
- import React14 from "react";
689
+ import React15 from "react";
607
690
  function readBasePath() {
608
691
  const normalize = (val) => {
609
692
  const raw = typeof val === "string" ? val.trim() : "";
@@ -662,21 +745,22 @@ function SearchPanelForm(props = {}) {
662
745
  buttonLabel = "Search",
663
746
  label,
664
747
  searchPath = "/search",
665
- inputId: inputIdProp
748
+ inputId: inputIdProp,
749
+ clearLabel = "Clear search"
666
750
  } = props || {};
667
751
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
668
- const action = React14.useMemo(
752
+ const action = React15.useMemo(
669
753
  () => resolveSearchPath(searchPath),
670
754
  [searchPath]
671
755
  );
672
- const autoId = typeof React14.useId === "function" ? React14.useId() : void 0;
673
- const [fallbackId] = React14.useState(
756
+ const autoId = typeof React15.useId === "function" ? React15.useId() : void 0;
757
+ const [fallbackId] = React15.useState(
674
758
  () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
675
759
  );
676
760
  const inputId = inputIdProp || autoId || fallbackId;
677
- const inputRef = React14.useRef(null);
678
- const [hasValue, setHasValue] = React14.useState(false);
679
- const focusInput = React14.useCallback(() => {
761
+ const inputRef = React15.useRef(null);
762
+ const [hasValue, setHasValue] = React15.useState(false);
763
+ const focusInput = React15.useCallback(() => {
680
764
  const el = inputRef.current;
681
765
  if (!el) return;
682
766
  if (document.activeElement === el) return;
@@ -689,30 +773,44 @@ function SearchPanelForm(props = {}) {
689
773
  }
690
774
  }
691
775
  }, []);
692
- const handlePointerDown = React14.useCallback(
776
+ const handlePointerDown = React15.useCallback(
693
777
  (event) => {
694
778
  const target = event.target;
695
779
  if (target && typeof target.closest === "function") {
696
780
  if (target.closest("[data-canopy-search-form-trigger]")) return;
781
+ if (target.closest("[data-canopy-search-form-clear]")) return;
697
782
  }
698
783
  event.preventDefault();
699
784
  focusInput();
700
785
  },
701
786
  [focusInput]
702
787
  );
703
- React14.useEffect(() => {
788
+ React15.useEffect(() => {
704
789
  const el = inputRef.current;
705
790
  if (!el) return;
706
791
  if (el.value && el.value.trim()) {
707
792
  setHasValue(true);
708
793
  }
709
794
  }, []);
710
- const handleInputChange = React14.useCallback((event) => {
795
+ const handleInputChange = React15.useCallback((event) => {
711
796
  var _a;
712
- 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
+ );
713
800
  setHasValue(nextHasValue);
714
801
  }, []);
715
- 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(
716
814
  "form",
717
815
  {
718
816
  action,
@@ -722,59 +820,63 @@ function SearchPanelForm(props = {}) {
722
820
  spellCheck: "false",
723
821
  className: "canopy-search-form canopy-search-form-shell",
724
822
  onPointerDown: handlePointerDown,
725
- "data-placeholder": placeholder || "",
726
823
  "data-has-value": hasValue ? "1" : "0"
727
824
  },
728
- /* @__PURE__ */ React14.createElement(
729
- "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",
730
827
  {
731
- htmlFor: inputId,
732
- 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
733
851
  },
734
- /* @__PURE__ */ React14.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }),
735
- /* @__PURE__ */ React14.createElement(
736
- "input",
737
- {
738
- id: inputId,
739
- type: "search",
740
- name: "q",
741
- inputMode: "search",
742
- "data-canopy-search-form-input": true,
743
- placeholder,
744
- className: "canopy-search-form__input",
745
- "aria-label": "Search",
746
- ref: inputRef,
747
- onChange: handleInputChange,
748
- onInput: handleInputChange
749
- }
750
- )
751
- ),
752
- /* @__PURE__ */ React14.createElement(
852
+ "\xD7"
853
+ ) : null,
854
+ /* @__PURE__ */ React15.createElement(
753
855
  "button",
754
856
  {
755
857
  type: "submit",
756
858
  "data-canopy-search-form-trigger": "submit",
757
859
  className: "canopy-search-form__submit"
758
860
  },
759
- /* @__PURE__ */ React14.createElement("span", null, text),
760
- /* @__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"))
761
863
  )
762
864
  );
763
865
  }
764
866
 
765
867
  // ui/src/search/SearchPanelTeaserResults.jsx
766
- import React15 from "react";
868
+ import React16 from "react";
767
869
  function SearchPanelTeaserResults(props = {}) {
768
870
  const { style, className } = props || {};
769
871
  const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
770
- return /* @__PURE__ */ React15.createElement(
872
+ return /* @__PURE__ */ React16.createElement(
771
873
  "div",
772
874
  {
773
875
  "data-canopy-search-form-panel": true,
774
876
  className: classes || void 0,
775
877
  style
776
878
  },
777
- /* @__PURE__ */ React15.createElement("div", { id: "cplist" })
879
+ /* @__PURE__ */ React16.createElement("div", { id: "cplist" })
778
880
  );
779
881
  }
780
882
 
@@ -794,11 +896,11 @@ function MdxSearchFormModal(props = {}) {
794
896
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
795
897
  const resolvedSearchPath = resolveSearchPath(searchPath);
796
898
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
797
- 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) } }));
798
900
  }
799
901
 
800
902
  // ui/src/search/SearchPanel.jsx
801
- import React17 from "react";
903
+ import React18 from "react";
802
904
  function SearchPanel(props = {}) {
803
905
  const {
804
906
  placeholder = "Search\u2026",
@@ -815,9 +917,10 @@ function SearchPanel(props = {}) {
815
917
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
816
918
  const resolvedSearchPath = resolveSearchPath(searchPath);
817
919
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
818
- 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) } }));
819
921
  }
820
922
  export {
923
+ AnnotationCard,
821
924
  Card,
822
925
  Grid,
823
926
  GridItem,