@canopy-iiif/app 0.9.1 → 0.9.3

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,7 +104,7 @@ function Card({
104
104
  );
105
105
  }
106
106
 
107
- // ui/src/layout/AnnotationCard.jsx
107
+ // ui/src/layout/TextCard.jsx
108
108
  import React3, { useMemo } from "react";
109
109
  function escapeRegExp(str = "") {
110
110
  return String(str).replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
@@ -141,7 +141,7 @@ function highlightSnippet(snippet, query) {
141
141
  (part, idx) => part.toLowerCase() === termLower ? /* @__PURE__ */ React3.createElement("mark", { key: idx }, part) : /* @__PURE__ */ React3.createElement(React3.Fragment, { key: idx }, part)
142
142
  );
143
143
  }
144
- function AnnotationCard({
144
+ function TextCard({
145
145
  href = "#",
146
146
  title = "Untitled",
147
147
  annotation = "",
@@ -226,8 +226,621 @@ function Grid({
226
226
  ));
227
227
  }
228
228
 
229
+ // ui/src/layout/CanopyHeader.jsx
230
+ import React11 from "react";
231
+
232
+ // ui/src/search/SearchPanel.jsx
233
+ import React8 from "react";
234
+
235
+ // ui/src/Icons.jsx
236
+ import React5 from "react";
237
+ var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React5.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React5.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" }));
238
+
239
+ // ui/src/search/SearchPanelForm.jsx
240
+ import React6 from "react";
241
+ function readBasePath() {
242
+ const normalize = (val) => {
243
+ const raw = typeof val === "string" ? val.trim() : "";
244
+ if (!raw) return "";
245
+ return raw.replace(/\/+$/, "");
246
+ };
247
+ try {
248
+ if (typeof window !== "undefined" && window.CANOPY_BASE_PATH != null) {
249
+ const fromWindow = normalize(window.CANOPY_BASE_PATH);
250
+ if (fromWindow) return fromWindow;
251
+ }
252
+ } catch (_) {
253
+ }
254
+ try {
255
+ if (typeof globalThis !== "undefined" && globalThis.CANOPY_BASE_PATH != null) {
256
+ const fromGlobal = normalize(globalThis.CANOPY_BASE_PATH);
257
+ if (fromGlobal) return fromGlobal;
258
+ }
259
+ } catch (_) {
260
+ }
261
+ try {
262
+ if (typeof process !== "undefined" && process.env && process.env.CANOPY_BASE_PATH) {
263
+ const fromEnv = normalize(process.env.CANOPY_BASE_PATH);
264
+ if (fromEnv) return fromEnv;
265
+ }
266
+ } catch (_) {
267
+ }
268
+ return "";
269
+ }
270
+ function isAbsoluteUrl(href) {
271
+ try {
272
+ return /^https?:/i.test(String(href || ""));
273
+ } catch (_) {
274
+ return false;
275
+ }
276
+ }
277
+ function resolveSearchPath(pathValue) {
278
+ let raw = typeof pathValue === "string" ? pathValue.trim() : "";
279
+ if (!raw) raw = "/search";
280
+ if (isAbsoluteUrl(raw)) return raw;
281
+ const normalizedPath = raw.startsWith("/") ? raw : `/${raw}`;
282
+ const base = readBasePath();
283
+ if (!base) return normalizedPath;
284
+ const baseWithLead = base.startsWith("/") ? base : `/${base}`;
285
+ const baseTrimmed = baseWithLead.replace(/\/+$/, "");
286
+ if (!baseTrimmed) return normalizedPath;
287
+ if (normalizedPath === baseTrimmed || normalizedPath.startsWith(`${baseTrimmed}/`)) {
288
+ return normalizedPath;
289
+ }
290
+ const pathTrimmed = normalizedPath.replace(/^\/+/, "");
291
+ return `${baseTrimmed}/${pathTrimmed}`;
292
+ }
293
+ function SearchPanelForm(props = {}) {
294
+ const {
295
+ placeholder = "Search\u2026",
296
+ buttonLabel = "Search",
297
+ label,
298
+ searchPath = "/search",
299
+ inputId: inputIdProp,
300
+ clearLabel = "Clear search"
301
+ } = props || {};
302
+ const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
303
+ const action = React6.useMemo(
304
+ () => resolveSearchPath(searchPath),
305
+ [searchPath]
306
+ );
307
+ const autoId = typeof React6.useId === "function" ? React6.useId() : void 0;
308
+ const [fallbackId] = React6.useState(
309
+ () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
310
+ );
311
+ const inputId = inputIdProp || autoId || fallbackId;
312
+ const inputRef = React6.useRef(null);
313
+ const [hasValue, setHasValue] = React6.useState(false);
314
+ const focusInput = React6.useCallback(() => {
315
+ const el = inputRef.current;
316
+ if (!el) return;
317
+ if (document.activeElement === el) return;
318
+ try {
319
+ el.focus({ preventScroll: true });
320
+ } catch (_) {
321
+ try {
322
+ el.focus();
323
+ } catch (_2) {
324
+ }
325
+ }
326
+ }, []);
327
+ const handlePointerDown = React6.useCallback(
328
+ (event) => {
329
+ const target = event.target;
330
+ if (target && typeof target.closest === "function") {
331
+ if (target.closest("[data-canopy-search-form-trigger]")) return;
332
+ if (target.closest("[data-canopy-search-form-clear]")) return;
333
+ }
334
+ event.preventDefault();
335
+ focusInput();
336
+ },
337
+ [focusInput]
338
+ );
339
+ React6.useEffect(() => {
340
+ const el = inputRef.current;
341
+ if (!el) return;
342
+ if (el.value && el.value.trim()) {
343
+ setHasValue(true);
344
+ }
345
+ }, []);
346
+ const handleInputChange = React6.useCallback((event) => {
347
+ var _a;
348
+ const nextHasValue = Boolean(
349
+ ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
350
+ );
351
+ setHasValue(nextHasValue);
352
+ }, []);
353
+ const handleClear = React6.useCallback((event) => {
354
+ }, []);
355
+ const handleClearKey = React6.useCallback(
356
+ (event) => {
357
+ if (event.key === "Enter" || event.key === " ") {
358
+ event.preventDefault();
359
+ handleClear(event);
360
+ }
361
+ },
362
+ [handleClear]
363
+ );
364
+ return /* @__PURE__ */ React6.createElement(
365
+ "form",
366
+ {
367
+ action,
368
+ method: "get",
369
+ role: "search",
370
+ autoComplete: "off",
371
+ spellCheck: "false",
372
+ className: "canopy-search-form canopy-search-form-shell",
373
+ onPointerDown: handlePointerDown,
374
+ "data-has-value": hasValue ? "1" : "0"
375
+ },
376
+ /* @__PURE__ */ React6.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React6.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React6.createElement(
377
+ "input",
378
+ {
379
+ id: inputId,
380
+ type: "search",
381
+ name: "q",
382
+ inputMode: "search",
383
+ "data-canopy-search-form-input": true,
384
+ placeholder,
385
+ className: "canopy-search-form__input",
386
+ "aria-label": "Search",
387
+ ref: inputRef,
388
+ onChange: handleInputChange,
389
+ onInput: handleInputChange
390
+ }
391
+ )),
392
+ hasValue ? /* @__PURE__ */ React6.createElement(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ className: "canopy-search-form__clear",
397
+ onClick: handleClear,
398
+ onPointerDown: (event) => event.stopPropagation(),
399
+ onKeyDown: handleClearKey,
400
+ "aria-label": clearLabel,
401
+ "data-canopy-search-form-clear": true
402
+ },
403
+ "\xD7"
404
+ ) : null,
405
+ /* @__PURE__ */ React6.createElement(
406
+ "button",
407
+ {
408
+ type: "submit",
409
+ "data-canopy-search-form-trigger": "submit",
410
+ className: "canopy-search-form__submit"
411
+ },
412
+ /* @__PURE__ */ React6.createElement("span", null, text),
413
+ /* @__PURE__ */ React6.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React6.createElement("span", null, "\u2318"), /* @__PURE__ */ React6.createElement("span", null, "K"))
414
+ )
415
+ );
416
+ }
417
+
418
+ // ui/src/search/SearchPanelTeaserResults.jsx
419
+ import React7 from "react";
420
+ function SearchPanelTeaserResults(props = {}) {
421
+ const { style, className } = props || {};
422
+ const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
423
+ return /* @__PURE__ */ React7.createElement(
424
+ "div",
425
+ {
426
+ "data-canopy-search-form-panel": true,
427
+ className: classes || void 0,
428
+ style
429
+ },
430
+ /* @__PURE__ */ React7.createElement("div", { id: "cplist" })
431
+ );
432
+ }
433
+
434
+ // ui/src/search/SearchPanel.jsx
435
+ function SearchPanel(props = {}) {
436
+ const {
437
+ placeholder = "Search\u2026",
438
+ hotkey = "mod+k",
439
+ maxResults = 8,
440
+ groupOrder = ["work", "docs", "page"],
441
+ // Kept for backward compat; form always renders submit
442
+ button = true,
443
+ // eslint-disable-line no-unused-vars
444
+ buttonLabel = "Search",
445
+ label,
446
+ searchPath = "/search"
447
+ } = props || {};
448
+ const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
449
+ const resolvedSearchPath = resolveSearchPath(searchPath);
450
+ const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
451
+ return /* @__PURE__ */ React8.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React8.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React8.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React8.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
452
+ }
453
+
454
+ // ui/src/layout/CanopyBrand.jsx
455
+ import React9 from "react";
456
+ function CanopyBrand(props = {}) {
457
+ const {
458
+ labelId,
459
+ label = "Canopy IIIF",
460
+ href = "/",
461
+ className,
462
+ Logo
463
+ } = props || {};
464
+ const spanProps = labelId ? { id: labelId } : {};
465
+ const classes = ["canopy-logo", className].filter(Boolean).join(" ");
466
+ return /* @__PURE__ */ React9.createElement("a", { href, className: classes }, typeof Logo === "function" ? /* @__PURE__ */ React9.createElement(Logo, null) : null, /* @__PURE__ */ React9.createElement("span", { ...spanProps }, label));
467
+ }
468
+
469
+ // ui/src/layout/CanopyModal.jsx
470
+ import React10 from "react";
471
+ function CanopyModal(props = {}) {
472
+ const {
473
+ id,
474
+ variant,
475
+ open = false,
476
+ labelledBy,
477
+ label,
478
+ logo: Logo,
479
+ href = "/",
480
+ closeLabel = "Close",
481
+ closeDataAttr,
482
+ onClose,
483
+ onBackgroundClick,
484
+ bodyClassName,
485
+ padded = true,
486
+ className,
487
+ children
488
+ } = props;
489
+ const rootClassName = ["canopy-modal", variant ? `canopy-modal--${variant}` : null, className].filter(Boolean).join(" ");
490
+ const modalProps = {
491
+ id,
492
+ className: rootClassName,
493
+ role: "dialog",
494
+ "aria-modal": "true",
495
+ "aria-hidden": open ? "false" : "true",
496
+ "data-open": open ? "true" : "false"
497
+ };
498
+ if (variant) modalProps["data-canopy-modal"] = variant;
499
+ const resolvedLabelId = labelledBy || (label ? `${variant || "modal"}-label` : void 0);
500
+ if (resolvedLabelId) modalProps["aria-labelledby"] = resolvedLabelId;
501
+ if (typeof onBackgroundClick === "function") {
502
+ modalProps.onClick = (event) => {
503
+ if (event.target === event.currentTarget) onBackgroundClick(event);
504
+ };
505
+ }
506
+ const closeButtonProps = {
507
+ type: "button",
508
+ className: "canopy-modal__close",
509
+ "aria-label": closeLabel
510
+ };
511
+ if (typeof closeDataAttr === "string" && closeDataAttr) {
512
+ closeButtonProps["data-canopy-header-close"] = closeDataAttr;
513
+ }
514
+ if (typeof onClose === "function") {
515
+ closeButtonProps.onClick = onClose;
516
+ }
517
+ const bodyClasses = ["canopy-modal__body"];
518
+ if (padded) bodyClasses.push("canopy-modal__body--padded");
519
+ if (bodyClassName) bodyClasses.push(bodyClassName);
520
+ const bodyClassNameValue = bodyClasses.join(" ");
521
+ return /* @__PURE__ */ React10.createElement("div", { ...modalProps }, /* @__PURE__ */ React10.createElement("div", { className: "canopy-modal__panel" }, /* @__PURE__ */ React10.createElement("button", { ...closeButtonProps }, /* @__PURE__ */ React10.createElement(
522
+ "svg",
523
+ {
524
+ xmlns: "http://www.w3.org/2000/svg",
525
+ viewBox: "0 0 24 24",
526
+ fill: "none",
527
+ stroke: "currentColor",
528
+ strokeWidth: "1.5",
529
+ className: "canopy-modal__close-icon"
530
+ },
531
+ /* @__PURE__ */ React10.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 6l12 12M6 18L18 6" })
532
+ ), /* @__PURE__ */ React10.createElement("span", { className: "sr-only" }, closeLabel)), /* @__PURE__ */ React10.createElement("div", { className: bodyClassNameValue }, label ? /* @__PURE__ */ React10.createElement("div", { className: "canopy-modal__brand" }, /* @__PURE__ */ React10.createElement(
533
+ CanopyBrand,
534
+ {
535
+ labelId: resolvedLabelId,
536
+ label,
537
+ href,
538
+ Logo,
539
+ className: "canopy-modal__brand-link"
540
+ }
541
+ )) : null, children)));
542
+ }
543
+
544
+ // ui/src/layout/CanopyHeader.jsx
545
+ function HeaderScript() {
546
+ const code = `
547
+ (function () {
548
+ if (typeof window === 'undefined') return;
549
+
550
+ var doc = document;
551
+ var body = doc.body;
552
+ var root = doc.documentElement;
553
+
554
+ function ready(fn) {
555
+ if (doc.readyState === 'loading') {
556
+ doc.addEventListener('DOMContentLoaded', fn, { once: true });
557
+ } else {
558
+ fn();
559
+ }
560
+ }
561
+
562
+ ready(function () {
563
+ var header = doc.querySelector('.canopy-header');
564
+ if (!header) return;
565
+
566
+ var NAV_ATTR = 'data-mobile-nav';
567
+ var SEARCH_ATTR = 'data-mobile-search';
568
+
569
+ function modalFor(type) {
570
+ return doc.querySelector('[data-canopy-modal="' + type + '"]');
571
+ }
572
+
573
+ function each(list, fn) {
574
+ if (!list || typeof fn !== 'function') return;
575
+ Array.prototype.forEach.call(list, fn);
576
+ }
577
+
578
+ function setExpanded(type, expanded) {
579
+ var toggles = header.querySelectorAll('[data-canopy-header-toggle="' + type + '"]');
580
+ each(toggles, function (btn) {
581
+ btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
582
+ });
583
+ var modal = modalFor(type);
584
+ if (modal) {
585
+ modal.setAttribute('data-open', expanded ? 'true' : 'false');
586
+ modal.setAttribute('aria-hidden', expanded ? 'false' : 'true');
587
+ }
588
+ }
589
+
590
+ function lockScroll(shouldLock) {
591
+ if (!body) return;
592
+ if (shouldLock) {
593
+ if (!body.dataset.canopyScrollLock) {
594
+ body.dataset.canopyScrollPrevOverflow = body.style.overflow || '';
595
+ if (root && root.dataset) {
596
+ root.dataset.canopyScrollPrevOverflow = root.style.overflow || '';
597
+ }
598
+ }
599
+ body.dataset.canopyScrollLock = '1';
600
+ body.style.overflow = 'hidden';
601
+ if (root) root.style.overflow = 'hidden';
602
+ } else {
603
+ if (body.dataset.canopyScrollLock) {
604
+ delete body.dataset.canopyScrollLock;
605
+ body.style.overflow = body.dataset.canopyScrollPrevOverflow || '';
606
+ delete body.dataset.canopyScrollPrevOverflow;
607
+ }
608
+ if (root && root.dataset) {
609
+ root.style.overflow = root.dataset.canopyScrollPrevOverflow || '';
610
+ delete root.dataset.canopyScrollPrevOverflow;
611
+ }
612
+ }
613
+ }
614
+
615
+ function stateFor(type) {
616
+ if (type === 'nav') return header.getAttribute(NAV_ATTR);
617
+ if (type === 'search') return header.getAttribute(SEARCH_ATTR);
618
+ return 'closed';
619
+ }
620
+
621
+ function focusSearchForm() {
622
+ var input = header.querySelector('[data-canopy-search-form-input]');
623
+ if (!input) return;
624
+ var raf = typeof window !== 'undefined' && window.requestAnimationFrame;
625
+ (raf || function (fn) { return setTimeout(fn, 16); })(function () {
626
+ try {
627
+ input.focus({ preventScroll: true });
628
+ } catch (_) {
629
+ try { input.focus(); } catch (_) {}
630
+ }
631
+ });
632
+ }
633
+
634
+ function focusNavMenu() {
635
+ var modal = modalFor('nav');
636
+ if (!modal) return;
637
+ var target = modal.querySelector('button, a, input, [tabindex]:not([tabindex="-1"])');
638
+ if (!target) return;
639
+ var raf = typeof window !== 'undefined' && window.requestAnimationFrame;
640
+ (raf || function (fn) { return setTimeout(fn, 16); })(function () {
641
+ try {
642
+ target.focus({ preventScroll: true });
643
+ } catch (_) {
644
+ try { target.focus(); } catch (_) {}
645
+ }
646
+ });
647
+ }
648
+
649
+ function setState(type, next) {
650
+ if (type === 'nav') header.setAttribute(NAV_ATTR, next);
651
+ if (type === 'search') header.setAttribute(SEARCH_ATTR, next);
652
+ setExpanded(type, next === 'open');
653
+ var navOpen = header.getAttribute(NAV_ATTR) === 'open';
654
+ var searchOpen = header.getAttribute(SEARCH_ATTR) === 'open';
655
+ lockScroll(navOpen || searchOpen);
656
+ }
657
+
658
+ function toggle(type, force) {
659
+ var current = stateFor(type) === 'open';
660
+ var shouldOpen = typeof force === 'boolean' ? force : !current;
661
+ if (shouldOpen && type === 'nav') setState('search', 'closed');
662
+ if (shouldOpen && type === 'search') setState('nav', 'closed');
663
+ setState(type, shouldOpen ? 'open' : 'closed');
664
+ if (type === 'search' && shouldOpen) focusSearchForm();
665
+ if (type === 'nav' && shouldOpen) focusNavMenu();
666
+ }
667
+
668
+ each(header.querySelectorAll('[data-canopy-header-toggle]'), function (btn) {
669
+ btn.addEventListener('click', function (event) {
670
+ event.preventDefault();
671
+ var type = btn.getAttribute('data-canopy-header-toggle');
672
+ if (!type) return;
673
+ toggle(type);
674
+ });
675
+ });
676
+
677
+ each(doc.querySelectorAll('[data-canopy-header-close]'), function (btn) {
678
+ btn.addEventListener('click', function () {
679
+ var type = btn.getAttribute('data-canopy-header-close');
680
+ if (!type) return;
681
+ toggle(type, false);
682
+ });
683
+ });
684
+
685
+ var navModal = modalFor('nav');
686
+ if (navModal) {
687
+ navModal.addEventListener('click', function (event) {
688
+ if (event.target === navModal) {
689
+ toggle('nav', false);
690
+ return;
691
+ }
692
+ var target = event.target && event.target.closest && event.target.closest('a');
693
+ if (!target) return;
694
+ toggle('nav', false);
695
+ });
696
+ }
697
+
698
+ var searchModal = modalFor('search');
699
+ if (searchModal) {
700
+ searchModal.addEventListener('click', function (event) {
701
+ if (event.target === searchModal) toggle('search', false);
702
+ });
703
+ }
704
+
705
+ doc.addEventListener('keydown', function (event) {
706
+ if (event.key !== 'Escape') return;
707
+ var navOpen = header.getAttribute(NAV_ATTR) === 'open';
708
+ var searchOpen = header.getAttribute(SEARCH_ATTR) === 'open';
709
+ if (!navOpen && !searchOpen) return;
710
+ event.preventDefault();
711
+ toggle('nav', false);
712
+ toggle('search', false);
713
+ });
714
+
715
+ var mq = window.matchMedia('(min-width: 48rem)');
716
+ function syncDesktopState() {
717
+ if (mq.matches) {
718
+ setState('nav', 'closed');
719
+ setState('search', 'closed');
720
+ setExpanded('nav', false);
721
+ setExpanded('search', false);
722
+ lockScroll(false);
723
+ }
724
+ }
725
+
726
+ try {
727
+ mq.addEventListener('change', syncDesktopState);
728
+ } catch (_) {
729
+ mq.addListener(syncDesktopState);
730
+ }
731
+
732
+ syncDesktopState();
733
+ });
734
+ })();
735
+ `;
736
+ return /* @__PURE__ */ React11.createElement(
737
+ "script",
738
+ {
739
+ dangerouslySetInnerHTML: {
740
+ __html: code
741
+ }
742
+ }
743
+ );
744
+ }
745
+ function ensureArray(navLinks) {
746
+ if (!Array.isArray(navLinks)) return [];
747
+ return navLinks.filter((link) => link && typeof link === "object" && typeof link.href === "string");
748
+ }
749
+ function CanopyHeader(props = {}) {
750
+ const {
751
+ navigation: navLinksProp,
752
+ searchLabel = "Search",
753
+ searchHotkey = "mod+k",
754
+ searchPlaceholder = "Search\u2026",
755
+ brandHref = "/",
756
+ title = "Canopy IIIF",
757
+ logo: SiteLogo
758
+ } = props;
759
+ const navLinks = ensureArray(navLinksProp);
760
+ return /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement("header", { className: "canopy-header", "data-mobile-nav": "closed", "data-mobile-search": "closed" }, /* @__PURE__ */ React11.createElement("div", { className: "canopy-header__brand" }, /* @__PURE__ */ React11.createElement(
761
+ CanopyBrand,
762
+ {
763
+ label: title,
764
+ href: brandHref,
765
+ className: "canopy-header__brand-link",
766
+ Logo: SiteLogo
767
+ }
768
+ )), /* @__PURE__ */ React11.createElement("div", { className: "canopy-header__desktop-search" }, /* @__PURE__ */ React11.createElement(SearchPanel, { label: searchLabel, hotkey: searchHotkey, placeholder: searchPlaceholder })), /* @__PURE__ */ React11.createElement("nav", { className: "canopy-nav-links canopy-header__desktop-nav", "aria-label": "Primary navigation" }, navLinks.map((link) => /* @__PURE__ */ React11.createElement("a", { key: link.href, href: link.href }, link.label || link.href))), /* @__PURE__ */ React11.createElement("div", { className: "canopy-header__actions" }, /* @__PURE__ */ React11.createElement(
769
+ "button",
770
+ {
771
+ type: "button",
772
+ className: "canopy-header__icon-button canopy-header__search-trigger",
773
+ "aria-label": "Open search",
774
+ "aria-controls": "canopy-modal-search",
775
+ "aria-expanded": "false",
776
+ "data-canopy-header-toggle": "search"
777
+ },
778
+ /* @__PURE__ */ React11.createElement(
779
+ "svg",
780
+ {
781
+ xmlns: "http://www.w3.org/2000/svg",
782
+ viewBox: "0 0 24 24",
783
+ fill: "none",
784
+ stroke: "currentColor",
785
+ strokeWidth: "1.5",
786
+ className: "canopy-header__search-icon"
787
+ },
788
+ /* @__PURE__ */ React11.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "m21 21-3.8-3.8M10.5 18a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15Z" })
789
+ )
790
+ ), /* @__PURE__ */ React11.createElement(
791
+ "button",
792
+ {
793
+ type: "button",
794
+ className: "canopy-header__icon-button canopy-header__menu",
795
+ "aria-label": "Open navigation",
796
+ "aria-controls": "canopy-modal-nav",
797
+ "aria-expanded": "false",
798
+ "data-canopy-header-toggle": "nav"
799
+ },
800
+ /* @__PURE__ */ React11.createElement(
801
+ "svg",
802
+ {
803
+ xmlns: "http://www.w3.org/2000/svg",
804
+ fill: "none",
805
+ viewBox: "0 0 24 24",
806
+ strokeWidth: "1.5",
807
+ stroke: "currentColor",
808
+ className: "canopy-header__menu-icon"
809
+ },
810
+ /* @__PURE__ */ React11.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" })
811
+ )
812
+ ))), /* @__PURE__ */ React11.createElement(
813
+ CanopyModal,
814
+ {
815
+ id: "canopy-modal-nav",
816
+ variant: "nav",
817
+ labelledBy: "canopy-modal-nav-label",
818
+ label: title,
819
+ logo: SiteLogo,
820
+ href: brandHref,
821
+ closeLabel: "Close navigation",
822
+ closeDataAttr: "nav"
823
+ },
824
+ /* @__PURE__ */ React11.createElement("nav", { className: "canopy-nav-links canopy-modal__nav", "aria-label": "Primary navigation" }, navLinks.map((link) => /* @__PURE__ */ React11.createElement("a", { key: link.href, href: link.href }, link.label || link.href)))
825
+ ), /* @__PURE__ */ React11.createElement(
826
+ CanopyModal,
827
+ {
828
+ id: "canopy-modal-search",
829
+ variant: "search",
830
+ labelledBy: "canopy-modal-search-label",
831
+ label: title,
832
+ logo: SiteLogo,
833
+ href: brandHref,
834
+ closeLabel: "Close search",
835
+ closeDataAttr: "search",
836
+ bodyClassName: "canopy-modal__body--search"
837
+ },
838
+ /* @__PURE__ */ React11.createElement(SearchPanel, { label: searchLabel, hotkey: searchHotkey, placeholder: searchPlaceholder })
839
+ ), /* @__PURE__ */ React11.createElement(HeaderScript, null));
840
+ }
841
+
229
842
  // ui/src/iiif/Viewer.jsx
230
- import React5, { useEffect as useEffect2, useState as useState2 } from "react";
843
+ import React12, { useEffect as useEffect2, useState as useState2 } from "react";
231
844
  var DEFAULT_VIEWER_OPTIONS = {
232
845
  showDownload: false,
233
846
  showIIIFBadge: false,
@@ -283,7 +896,7 @@ var Viewer = (props) => {
283
896
  } catch (_) {
284
897
  json = "{}";
285
898
  }
286
- return /* @__PURE__ */ React5.createElement("div", { "data-canopy-viewer": "1", className: "not-prose" }, /* @__PURE__ */ React5.createElement(
899
+ return /* @__PURE__ */ React12.createElement("div", { "data-canopy-viewer": "1", className: "not-prose" }, /* @__PURE__ */ React12.createElement(
287
900
  "script",
288
901
  {
289
902
  type: "application/json",
@@ -291,11 +904,11 @@ var Viewer = (props) => {
291
904
  }
292
905
  ));
293
906
  }
294
- return /* @__PURE__ */ React5.createElement(CloverViewer, { ...props, options: mergedOptions });
907
+ return /* @__PURE__ */ React12.createElement(CloverViewer, { ...props, options: mergedOptions });
295
908
  };
296
909
 
297
910
  // ui/src/iiif/Slider.jsx
298
- import React6, { useEffect as useEffect3, useState as useState3 } from "react";
911
+ import React13, { useEffect as useEffect3, useState as useState3 } from "react";
299
912
  var Slider = (props) => {
300
913
  const [CloverSlider, setCloverSlider] = useState3(null);
301
914
  useEffect3(() => {
@@ -321,7 +934,7 @@ var Slider = (props) => {
321
934
  } catch (_) {
322
935
  json = "{}";
323
936
  }
324
- return /* @__PURE__ */ React6.createElement("div", { "data-canopy-slider": "1", className: "not-prose" }, /* @__PURE__ */ React6.createElement(
937
+ return /* @__PURE__ */ React13.createElement("div", { "data-canopy-slider": "1", className: "not-prose" }, /* @__PURE__ */ React13.createElement(
325
938
  "script",
326
939
  {
327
940
  type: "application/json",
@@ -329,11 +942,11 @@ var Slider = (props) => {
329
942
  }
330
943
  ));
331
944
  }
332
- return /* @__PURE__ */ React6.createElement(CloverSlider, { ...props });
945
+ return /* @__PURE__ */ React13.createElement(CloverSlider, { ...props });
333
946
  };
334
947
 
335
948
  // ui/src/iiif/Scroll.jsx
336
- import React7, { useEffect as useEffect4, useState as useState4 } from "react";
949
+ import React14, { useEffect as useEffect4, useState as useState4 } from "react";
337
950
  var Scroll = (props) => {
338
951
  const [CloverScroll, setCloverScroll] = useState4(null);
339
952
  useEffect4(() => {
@@ -358,7 +971,7 @@ var Scroll = (props) => {
358
971
  } catch (_) {
359
972
  json = "{}";
360
973
  }
361
- return /* @__PURE__ */ React7.createElement("div", { "data-canopy-scroll": "1", className: "not-prose" }, /* @__PURE__ */ React7.createElement(
974
+ return /* @__PURE__ */ React14.createElement("div", { "data-canopy-scroll": "1", className: "not-prose" }, /* @__PURE__ */ React14.createElement(
362
975
  "script",
363
976
  {
364
977
  type: "application/json",
@@ -366,11 +979,11 @@ var Scroll = (props) => {
366
979
  }
367
980
  ));
368
981
  }
369
- return /* @__PURE__ */ React7.createElement(CloverScroll, { ...props });
982
+ return /* @__PURE__ */ React14.createElement(CloverScroll, { ...props });
370
983
  };
371
984
 
372
985
  // ui/src/iiif/MdxRelatedItems.jsx
373
- import React8 from "react";
986
+ import React15 from "react";
374
987
  function MdxRelatedItems(props) {
375
988
  let json = "{}";
376
989
  try {
@@ -378,11 +991,11 @@ function MdxRelatedItems(props) {
378
991
  } catch (_) {
379
992
  json = "{}";
380
993
  }
381
- return /* @__PURE__ */ React8.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React8.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
994
+ return /* @__PURE__ */ React15.createElement("div", { "data-canopy-related-items": "1", className: "not-prose" }, /* @__PURE__ */ React15.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
382
995
  }
383
996
 
384
997
  // ui/src/search/MdxSearchResults.jsx
385
- import React9 from "react";
998
+ import React16 from "react";
386
999
  function MdxSearchResults(props) {
387
1000
  let json = "{}";
388
1001
  try {
@@ -390,11 +1003,11 @@ function MdxSearchResults(props) {
390
1003
  } catch (_) {
391
1004
  json = "{}";
392
1005
  }
393
- return /* @__PURE__ */ React9.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React9.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1006
+ return /* @__PURE__ */ React16.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React16.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
394
1007
  }
395
1008
 
396
1009
  // ui/src/search/SearchSummary.jsx
397
- import React10 from "react";
1010
+ import React17 from "react";
398
1011
  function SearchSummary(props) {
399
1012
  let json = "{}";
400
1013
  try {
@@ -402,11 +1015,11 @@ function SearchSummary(props) {
402
1015
  } catch (_) {
403
1016
  json = "{}";
404
1017
  }
405
- return /* @__PURE__ */ React10.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React10.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1018
+ return /* @__PURE__ */ React17.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
406
1019
  }
407
1020
 
408
1021
  // ui/src/search/MdxSearchTabs.jsx
409
- import React11 from "react";
1022
+ import React18 from "react";
410
1023
  function MdxSearchTabs(props) {
411
1024
  let json = "{}";
412
1025
  try {
@@ -414,11 +1027,11 @@ function MdxSearchTabs(props) {
414
1027
  } catch (_) {
415
1028
  json = "{}";
416
1029
  }
417
- return /* @__PURE__ */ React11.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React11.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1030
+ return /* @__PURE__ */ React18.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React18.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
418
1031
  }
419
1032
 
420
1033
  // ui/src/search/SearchResults.jsx
421
- import React12 from "react";
1034
+ import React19 from "react";
422
1035
  function SearchResults({
423
1036
  results = [],
424
1037
  type = "all",
@@ -426,38 +1039,48 @@ function SearchResults({
426
1039
  query = ""
427
1040
  }) {
428
1041
  if (!results.length) {
429
- return /* @__PURE__ */ React12.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React12.createElement("em", null, "No results"));
1042
+ return /* @__PURE__ */ React19.createElement("div", { className: "text-slate-600" }, /* @__PURE__ */ React19.createElement("em", null, "No results"));
430
1043
  }
431
- const isAnnotationView = String(type).toLowerCase() === "annotation";
1044
+ const normalizedType = String(type || "all").toLowerCase();
1045
+ const isAnnotationView = normalizedType === "annotation";
432
1046
  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
- );
1047
+ return /* @__PURE__ */ React19.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
1048
+ if (!r) return null;
1049
+ return renderTextCard(r, r.id || i);
447
1050
  }));
448
1051
  }
1052
+ const renderTextCard = (record, key) => {
1053
+ if (!record) return null;
1054
+ return /* @__PURE__ */ React19.createElement(
1055
+ TextCard,
1056
+ {
1057
+ key,
1058
+ href: record.href,
1059
+ title: record.title || record.href || "Untitled",
1060
+ annotation: record.annotation,
1061
+ summary: record.summary || record.summaryValue || "",
1062
+ metadata: Array.isArray(record.metadata) ? record.metadata : [],
1063
+ query
1064
+ }
1065
+ );
1066
+ };
1067
+ const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
1068
+ const shouldRenderAsTextCard = (record) => !isWorkRecord(record) || normalizedType !== "work";
449
1069
  if (layout === "list") {
450
- return /* @__PURE__ */ React12.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
1070
+ return /* @__PURE__ */ React19.createElement("ul", { id: "search-results", className: "space-y-3" }, results.map((r, i) => {
1071
+ if (shouldRenderAsTextCard(r)) {
1072
+ return /* @__PURE__ */ React19.createElement("li", { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
1073
+ }
451
1074
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
452
1075
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
453
- return /* @__PURE__ */ React12.createElement(
1076
+ return /* @__PURE__ */ React19.createElement(
454
1077
  "li",
455
1078
  {
456
1079
  key: i,
457
1080
  className: `search-result ${r.type}`,
458
1081
  "data-thumbnail-aspect-ratio": aspect
459
1082
  },
460
- /* @__PURE__ */ React12.createElement(
1083
+ /* @__PURE__ */ React19.createElement(
461
1084
  Card,
462
1085
  {
463
1086
  href: r.href,
@@ -471,17 +1094,20 @@ function SearchResults({
471
1094
  );
472
1095
  }));
473
1096
  }
474
- return /* @__PURE__ */ React12.createElement("div", { id: "search-results" }, /* @__PURE__ */ React12.createElement(Grid, null, results.map((r, i) => {
1097
+ return /* @__PURE__ */ React19.createElement("div", { id: "search-results" }, /* @__PURE__ */ React19.createElement(Grid, null, results.map((r, i) => {
1098
+ if (shouldRenderAsTextCard(r)) {
1099
+ return /* @__PURE__ */ React19.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
1100
+ }
475
1101
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
476
1102
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
477
- return /* @__PURE__ */ React12.createElement(
1103
+ return /* @__PURE__ */ React19.createElement(
478
1104
  GridItem,
479
1105
  {
480
1106
  key: i,
481
1107
  className: `search-result ${r.type}`,
482
1108
  "data-thumbnail-aspect-ratio": aspect
483
1109
  },
484
- /* @__PURE__ */ React12.createElement(
1110
+ /* @__PURE__ */ React19.createElement(
485
1111
  Card,
486
1112
  {
487
1113
  href: r.href,
@@ -497,7 +1123,7 @@ function SearchResults({
497
1123
  }
498
1124
 
499
1125
  // ui/src/search/SearchTabs.jsx
500
- import React13 from "react";
1126
+ import React20 from "react";
501
1127
  function SearchTabs({
502
1128
  type = "all",
503
1129
  onTypeChange,
@@ -512,7 +1138,7 @@ function SearchTabs({
512
1138
  const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
513
1139
  const hasFilters = typeof onOpenFilters === "function";
514
1140
  const filterBadge = activeFilterCount > 0 ? ` (${activeFilterCount})` : "";
515
- return /* @__PURE__ */ React13.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React13.createElement(
1141
+ return /* @__PURE__ */ React20.createElement("div", { className: "canopy-search-tabs-wrapper" }, /* @__PURE__ */ React20.createElement(
516
1142
  "div",
517
1143
  {
518
1144
  role: "tablist",
@@ -523,7 +1149,7 @@ function SearchTabs({
523
1149
  const active = String(type).toLowerCase() === String(t).toLowerCase();
524
1150
  const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
525
1151
  const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
526
- return /* @__PURE__ */ React13.createElement(
1152
+ return /* @__PURE__ */ React20.createElement(
527
1153
  "button",
528
1154
  {
529
1155
  key: t,
@@ -538,7 +1164,7 @@ function SearchTabs({
538
1164
  ")"
539
1165
  );
540
1166
  })
541
- ), hasFilters ? /* @__PURE__ */ React13.createElement(
1167
+ ), hasFilters ? /* @__PURE__ */ React20.createElement(
542
1168
  "button",
543
1169
  {
544
1170
  type: "button",
@@ -546,12 +1172,12 @@ function SearchTabs({
546
1172
  "aria-expanded": filtersOpen ? "true" : "false",
547
1173
  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"
548
1174
  },
549
- /* @__PURE__ */ React13.createElement("span", null, filtersLabel, filterBadge)
1175
+ /* @__PURE__ */ React20.createElement("span", null, filtersLabel, filterBadge)
550
1176
  ) : null);
551
1177
  }
552
1178
 
553
1179
  // ui/src/search/SearchFiltersDialog.jsx
554
- import React14 from "react";
1180
+ import React21 from "react";
555
1181
  function toArray(input) {
556
1182
  if (!input) return [];
557
1183
  if (Array.isArray(input)) return input;
@@ -590,20 +1216,20 @@ function FacetSection({ facet, selected, onToggle }) {
590
1216
  const selectedValues = selected.get(String(slug)) || /* @__PURE__ */ new Set();
591
1217
  const checkboxId = (valueSlug) => `filter-${slug}-${valueSlug}`;
592
1218
  const hasSelection = selectedValues.size > 0;
593
- const [quickQuery, setQuickQuery] = React14.useState("");
1219
+ const [quickQuery, setQuickQuery] = React21.useState("");
594
1220
  const hasQuery = quickQuery.trim().length > 0;
595
- const filteredValues = React14.useMemo(
1221
+ const filteredValues = React21.useMemo(
596
1222
  () => facetMatches(values, quickQuery),
597
1223
  [values, quickQuery]
598
1224
  );
599
- return /* @__PURE__ */ React14.createElement(
1225
+ return /* @__PURE__ */ React21.createElement(
600
1226
  "details",
601
1227
  {
602
1228
  className: "canopy-search-filters__facet",
603
1229
  open: hasSelection
604
1230
  },
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(
1231
+ /* @__PURE__ */ React21.createElement("summary", { className: "canopy-search-filters__facet-summary" }, /* @__PURE__ */ React21.createElement("span", null, label), /* @__PURE__ */ React21.createElement("span", { className: "canopy-search-filters__facet-count" }, values.length)),
1232
+ /* @__PURE__ */ React21.createElement("div", { className: "canopy-search-filters__facet-content" }, /* @__PURE__ */ React21.createElement("div", { className: "canopy-search-filters__quick" }, /* @__PURE__ */ React21.createElement(
607
1233
  "input",
608
1234
  {
609
1235
  type: "search",
@@ -613,7 +1239,7 @@ function FacetSection({ facet, selected, onToggle }) {
613
1239
  className: "canopy-search-filters__quick-input",
614
1240
  "aria-label": `Filter ${label} values`
615
1241
  }
616
- ), quickQuery ? /* @__PURE__ */ React14.createElement(
1242
+ ), quickQuery ? /* @__PURE__ */ React21.createElement(
617
1243
  "button",
618
1244
  {
619
1245
  type: "button",
@@ -621,11 +1247,11 @@ function FacetSection({ facet, selected, onToggle }) {
621
1247
  className: "canopy-search-filters__quick-clear"
622
1248
  },
623
1249
  "Clear"
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) => {
1250
+ ) : null), hasQuery && !filteredValues.length ? /* @__PURE__ */ React21.createElement("p", { className: "canopy-search-filters__facet-notice" }, "No matches found.") : null, /* @__PURE__ */ React21.createElement("ul", { className: "canopy-search-filters__facet-list" }, filteredValues.map((entry) => {
625
1251
  const valueSlug = String(entry.slug || entry.value || "");
626
1252
  const isChecked = selectedValues.has(valueSlug);
627
1253
  const inputId = checkboxId(valueSlug);
628
- return /* @__PURE__ */ React14.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React14.createElement(
1254
+ return /* @__PURE__ */ React21.createElement("li", { key: valueSlug, className: "canopy-search-filters__facet-item" }, /* @__PURE__ */ React21.createElement(
629
1255
  "input",
630
1256
  {
631
1257
  id: inputId,
@@ -637,15 +1263,15 @@ function FacetSection({ facet, selected, onToggle }) {
637
1263
  if (onToggle) onToggle(slug, valueSlug, nextChecked);
638
1264
  }
639
1265
  }
640
- ), /* @__PURE__ */ React14.createElement(
1266
+ ), /* @__PURE__ */ React21.createElement(
641
1267
  "label",
642
1268
  {
643
1269
  htmlFor: inputId,
644
1270
  className: "canopy-search-filters__facet-label"
645
1271
  },
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)
1272
+ /* @__PURE__ */ React21.createElement("span", null, entry.value, " ", Number.isFinite(entry.doc_count) ? /* @__PURE__ */ React21.createElement("span", { className: "canopy-search-filters__facet-count" }, "(", entry.doc_count, ")") : null)
647
1273
  ));
648
- }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React14.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
1274
+ }), !filteredValues.length && !hasQuery ? /* @__PURE__ */ React21.createElement("li", { className: "canopy-search-filters__facet-empty" }, "No values available.") : null))
649
1275
  );
650
1276
  }
651
1277
  function SearchFiltersDialog(props = {}) {
@@ -656,35 +1282,51 @@ function SearchFiltersDialog(props = {}) {
656
1282
  selected = {},
657
1283
  onToggle,
658
1284
  onClear,
659
- title = "Filters",
660
- subtitle = "Refine results by metadata"
1285
+ title,
1286
+ subtitle = "Refine results by metadata",
1287
+ brandLabel = "Canopy IIIF",
1288
+ brandHref = "/",
1289
+ logo: SiteLogo
661
1290
  } = props;
662
1291
  const selectedMap = normalizeSelected(selected);
663
1292
  const activeCount = Array.from(selectedMap.values()).reduce(
664
1293
  (total, set) => total + set.size,
665
1294
  0
666
1295
  );
1296
+ React21.useEffect(() => {
1297
+ if (!open) return void 0;
1298
+ if (typeof document === "undefined") return void 0;
1299
+ const body = document.body;
1300
+ const root = document.documentElement;
1301
+ const prevBody = body ? body.style.overflow : "";
1302
+ const prevRoot = root ? root.style.overflow : "";
1303
+ if (body) body.style.overflow = "hidden";
1304
+ if (root) root.style.overflow = "hidden";
1305
+ return () => {
1306
+ if (body) body.style.overflow = prevBody;
1307
+ if (root) root.style.overflow = prevRoot;
1308
+ };
1309
+ }, [open]);
667
1310
  if (!open) return null;
668
- return /* @__PURE__ */ React14.createElement(
669
- "div",
1311
+ const brandId = "canopy-modal-filters-label";
1312
+ const subtitleText = subtitle != null ? subtitle : title;
1313
+ return /* @__PURE__ */ React21.createElement(
1314
+ CanopyModal,
670
1315
  {
671
- role: "dialog",
672
- "aria-modal": "true",
673
- className: "canopy-search-filters-overlay",
674
- onClick: (event) => {
675
- if (event.target === event.currentTarget && onOpenChange)
676
- onOpenChange(false);
677
- }
1316
+ id: "canopy-modal-filters",
1317
+ variant: "filters",
1318
+ open: true,
1319
+ labelledBy: brandId,
1320
+ label: brandLabel,
1321
+ logo: SiteLogo,
1322
+ href: brandHref,
1323
+ closeLabel: "Close filters",
1324
+ onClose: () => onOpenChange && onOpenChange(false),
1325
+ onBackgroundClick: () => onOpenChange && onOpenChange(false),
1326
+ bodyClassName: "canopy-modal__body--filters"
678
1327
  },
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(
680
- "button",
681
- {
682
- type: "button",
683
- onClick: () => onOpenChange && onOpenChange(false),
684
- className: "canopy-search-filters__close"
685
- },
686
- "Close"
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(
1328
+ subtitleText ? /* @__PURE__ */ React21.createElement("p", { className: "canopy-search-filters__subtitle" }, subtitleText) : null,
1329
+ /* @__PURE__ */ React21.createElement("div", { className: "canopy-search-filters__body" }, Array.isArray(facets) && facets.length ? /* @__PURE__ */ React21.createElement("div", { className: "canopy-search-filters__facets" }, facets.map((facet) => /* @__PURE__ */ React21.createElement(
688
1330
  FacetSection,
689
1331
  {
690
1332
  key: facet.slug || facet.label,
@@ -692,7 +1334,8 @@ function SearchFiltersDialog(props = {}) {
692
1334
  selected: selectedMap,
693
1335
  onToggle
694
1336
  }
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(
1337
+ ))) : /* @__PURE__ */ React21.createElement("p", { className: "canopy-search-filters__empty" }, "No filters are available for this collection.")),
1338
+ /* @__PURE__ */ React21.createElement("footer", { className: "canopy-search-filters__footer" }, /* @__PURE__ */ React21.createElement("div", null, activeCount ? `${activeCount} filter${activeCount === 1 ? "" : "s"} applied` : "No filters applied"), /* @__PURE__ */ React21.createElement("div", { className: "canopy-search-filters__footer-actions" }, /* @__PURE__ */ React21.createElement(
696
1339
  "button",
697
1340
  {
698
1341
  type: "button",
@@ -703,7 +1346,7 @@ function SearchFiltersDialog(props = {}) {
703
1346
  className: "canopy-search-filters__button canopy-search-filters__button--secondary"
704
1347
  },
705
1348
  "Clear all"
706
- ), /* @__PURE__ */ React14.createElement(
1349
+ ), /* @__PURE__ */ React21.createElement(
707
1350
  "button",
708
1351
  {
709
1352
  type: "button",
@@ -711,213 +1354,12 @@ function SearchFiltersDialog(props = {}) {
711
1354
  className: "canopy-search-filters__button canopy-search-filters__button--primary"
712
1355
  },
713
1356
  "Done"
714
- ))))
715
- );
716
- }
717
-
718
- // ui/src/search-form/MdxSearchFormModal.jsx
719
- import React18 from "react";
720
-
721
- // ui/src/Icons.jsx
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" }));
724
-
725
- // ui/src/search/SearchPanelForm.jsx
726
- import React16 from "react";
727
- function readBasePath() {
728
- const normalize = (val) => {
729
- const raw = typeof val === "string" ? val.trim() : "";
730
- if (!raw) return "";
731
- return raw.replace(/\/+$/, "");
732
- };
733
- try {
734
- if (typeof window !== "undefined" && window.CANOPY_BASE_PATH != null) {
735
- const fromWindow = normalize(window.CANOPY_BASE_PATH);
736
- if (fromWindow) return fromWindow;
737
- }
738
- } catch (_) {
739
- }
740
- try {
741
- if (typeof globalThis !== "undefined" && globalThis.CANOPY_BASE_PATH != null) {
742
- const fromGlobal = normalize(globalThis.CANOPY_BASE_PATH);
743
- if (fromGlobal) return fromGlobal;
744
- }
745
- } catch (_) {
746
- }
747
- try {
748
- if (typeof process !== "undefined" && process.env && process.env.CANOPY_BASE_PATH) {
749
- const fromEnv = normalize(process.env.CANOPY_BASE_PATH);
750
- if (fromEnv) return fromEnv;
751
- }
752
- } catch (_) {
753
- }
754
- return "";
755
- }
756
- function isAbsoluteUrl(href) {
757
- try {
758
- return /^https?:/i.test(String(href || ""));
759
- } catch (_) {
760
- return false;
761
- }
762
- }
763
- function resolveSearchPath(pathValue) {
764
- let raw = typeof pathValue === "string" ? pathValue.trim() : "";
765
- if (!raw) raw = "/search";
766
- if (isAbsoluteUrl(raw)) return raw;
767
- const normalizedPath = raw.startsWith("/") ? raw : `/${raw}`;
768
- const base = readBasePath();
769
- if (!base) return normalizedPath;
770
- const baseWithLead = base.startsWith("/") ? base : `/${base}`;
771
- const baseTrimmed = baseWithLead.replace(/\/+$/, "");
772
- if (!baseTrimmed) return normalizedPath;
773
- if (normalizedPath === baseTrimmed || normalizedPath.startsWith(`${baseTrimmed}/`)) {
774
- return normalizedPath;
775
- }
776
- const pathTrimmed = normalizedPath.replace(/^\/+/, "");
777
- return `${baseTrimmed}/${pathTrimmed}`;
778
- }
779
- function SearchPanelForm(props = {}) {
780
- const {
781
- placeholder = "Search\u2026",
782
- buttonLabel = "Search",
783
- label,
784
- searchPath = "/search",
785
- inputId: inputIdProp,
786
- clearLabel = "Clear search"
787
- } = props || {};
788
- const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
789
- const action = React16.useMemo(
790
- () => resolveSearchPath(searchPath),
791
- [searchPath]
792
- );
793
- const autoId = typeof React16.useId === "function" ? React16.useId() : void 0;
794
- const [fallbackId] = React16.useState(
795
- () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
796
- );
797
- const inputId = inputIdProp || autoId || fallbackId;
798
- const inputRef = React16.useRef(null);
799
- const [hasValue, setHasValue] = React16.useState(false);
800
- const focusInput = React16.useCallback(() => {
801
- const el = inputRef.current;
802
- if (!el) return;
803
- if (document.activeElement === el) return;
804
- try {
805
- el.focus({ preventScroll: true });
806
- } catch (_) {
807
- try {
808
- el.focus();
809
- } catch (_2) {
810
- }
811
- }
812
- }, []);
813
- const handlePointerDown = React16.useCallback(
814
- (event) => {
815
- const target = event.target;
816
- if (target && typeof target.closest === "function") {
817
- if (target.closest("[data-canopy-search-form-trigger]")) return;
818
- if (target.closest("[data-canopy-search-form-clear]")) return;
819
- }
820
- event.preventDefault();
821
- focusInput();
822
- },
823
- [focusInput]
824
- );
825
- React16.useEffect(() => {
826
- const el = inputRef.current;
827
- if (!el) return;
828
- if (el.value && el.value.trim()) {
829
- setHasValue(true);
830
- }
831
- }, []);
832
- const handleInputChange = React16.useCallback((event) => {
833
- var _a;
834
- const nextHasValue = Boolean(
835
- ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
836
- );
837
- setHasValue(nextHasValue);
838
- }, []);
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(
851
- "form",
852
- {
853
- action,
854
- method: "get",
855
- role: "search",
856
- autoComplete: "off",
857
- spellCheck: "false",
858
- className: "canopy-search-form canopy-search-form-shell",
859
- onPointerDown: handlePointerDown,
860
- "data-has-value": hasValue ? "1" : "0"
861
- },
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",
864
- {
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
888
- },
889
- "\xD7"
890
- ) : null,
891
- /* @__PURE__ */ React16.createElement(
892
- "button",
893
- {
894
- type: "submit",
895
- "data-canopy-search-form-trigger": "submit",
896
- className: "canopy-search-form__submit"
897
- },
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"))
900
- )
901
- );
902
- }
903
-
904
- // ui/src/search/SearchPanelTeaserResults.jsx
905
- import React17 from "react";
906
- function SearchPanelTeaserResults(props = {}) {
907
- const { style, className } = props || {};
908
- const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
909
- return /* @__PURE__ */ React17.createElement(
910
- "div",
911
- {
912
- "data-canopy-search-form-panel": true,
913
- className: classes || void 0,
914
- style
915
- },
916
- /* @__PURE__ */ React17.createElement("div", { id: "cplist" })
1357
+ )))
917
1358
  );
918
1359
  }
919
1360
 
920
1361
  // ui/src/search-form/MdxSearchFormModal.jsx
1362
+ import React22 from "react";
921
1363
  function MdxSearchFormModal(props = {}) {
922
1364
  const {
923
1365
  placeholder = "Search\u2026",
@@ -933,31 +1375,12 @@ function MdxSearchFormModal(props = {}) {
933
1375
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
934
1376
  const resolvedSearchPath = resolveSearchPath(searchPath);
935
1377
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
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) } }));
937
- }
938
-
939
- // ui/src/search/SearchPanel.jsx
940
- import React19 from "react";
941
- function SearchPanel(props = {}) {
942
- const {
943
- placeholder = "Search\u2026",
944
- hotkey = "mod+k",
945
- maxResults = 8,
946
- groupOrder = ["work", "docs", "page"],
947
- // Kept for backward compat; form always renders submit
948
- button = true,
949
- // eslint-disable-line no-unused-vars
950
- buttonLabel = "Search",
951
- label,
952
- searchPath = "/search"
953
- } = props || {};
954
- const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
955
- const resolvedSearchPath = resolveSearchPath(searchPath);
956
- const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
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) } }));
1378
+ return /* @__PURE__ */ React22.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React22.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React22.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React22.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React22.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
958
1379
  }
959
1380
  export {
960
- AnnotationCard,
1381
+ CanopyBrand,
1382
+ CanopyHeader,
1383
+ CanopyModal,
961
1384
  Card,
962
1385
  Grid,
963
1386
  GridItem,
@@ -975,6 +1398,7 @@ export {
975
1398
  MdxSearchTabs as SearchTabs,
976
1399
  SearchTabs as SearchTabsUI,
977
1400
  Slider,
1401
+ TextCard,
978
1402
  Viewer
979
1403
  };
980
1404
  //# sourceMappingURL=index.mjs.map