@canopy-iiif/app 0.9.2 → 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
@@ -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,19 +1039,19 @@ 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
1044
  const normalizedType = String(type || "all").toLowerCase();
432
1045
  const isAnnotationView = normalizedType === "annotation";
433
1046
  if (isAnnotationView) {
434
- return /* @__PURE__ */ React12.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
1047
+ return /* @__PURE__ */ React19.createElement("div", { id: "search-results", className: "space-y-4" }, results.map((r, i) => {
435
1048
  if (!r) return null;
436
1049
  return renderTextCard(r, r.id || i);
437
1050
  }));
438
1051
  }
439
1052
  const renderTextCard = (record, key) => {
440
1053
  if (!record) return null;
441
- return /* @__PURE__ */ React12.createElement(
1054
+ return /* @__PURE__ */ React19.createElement(
442
1055
  TextCard,
443
1056
  {
444
1057
  key,
@@ -454,20 +1067,20 @@ function SearchResults({
454
1067
  const isWorkRecord = (record) => String(record && record.type).toLowerCase() === "work";
455
1068
  const shouldRenderAsTextCard = (record) => !isWorkRecord(record) || normalizedType !== "work";
456
1069
  if (layout === "list") {
457
- 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) => {
458
1071
  if (shouldRenderAsTextCard(r)) {
459
- return /* @__PURE__ */ React12.createElement("li", { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
1072
+ return /* @__PURE__ */ React19.createElement("li", { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
460
1073
  }
461
1074
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
462
1075
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
463
- return /* @__PURE__ */ React12.createElement(
1076
+ return /* @__PURE__ */ React19.createElement(
464
1077
  "li",
465
1078
  {
466
1079
  key: i,
467
1080
  className: `search-result ${r.type}`,
468
1081
  "data-thumbnail-aspect-ratio": aspect
469
1082
  },
470
- /* @__PURE__ */ React12.createElement(
1083
+ /* @__PURE__ */ React19.createElement(
471
1084
  Card,
472
1085
  {
473
1086
  href: r.href,
@@ -481,20 +1094,20 @@ function SearchResults({
481
1094
  );
482
1095
  }));
483
1096
  }
484
- 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) => {
485
1098
  if (shouldRenderAsTextCard(r)) {
486
- return /* @__PURE__ */ React12.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
1099
+ return /* @__PURE__ */ React19.createElement(GridItem, { key: i, className: `search-result ${r && r.type}` }, renderTextCard(r, i));
487
1100
  }
488
1101
  const hasDims = Number.isFinite(Number(r.thumbnailWidth)) && Number(r.thumbnailWidth) > 0 && Number.isFinite(Number(r.thumbnailHeight)) && Number(r.thumbnailHeight) > 0;
489
1102
  const aspect = hasDims ? Number(r.thumbnailWidth) / Number(r.thumbnailHeight) : void 0;
490
- return /* @__PURE__ */ React12.createElement(
1103
+ return /* @__PURE__ */ React19.createElement(
491
1104
  GridItem,
492
1105
  {
493
1106
  key: i,
494
1107
  className: `search-result ${r.type}`,
495
1108
  "data-thumbnail-aspect-ratio": aspect
496
1109
  },
497
- /* @__PURE__ */ React12.createElement(
1110
+ /* @__PURE__ */ React19.createElement(
498
1111
  Card,
499
1112
  {
500
1113
  href: r.href,
@@ -510,7 +1123,7 @@ function SearchResults({
510
1123
  }
511
1124
 
512
1125
  // ui/src/search/SearchTabs.jsx
513
- import React13 from "react";
1126
+ import React20 from "react";
514
1127
  function SearchTabs({
515
1128
  type = "all",
516
1129
  onTypeChange,
@@ -525,7 +1138,7 @@ function SearchTabs({
525
1138
  const toLabel = (t) => t && t.length ? t.charAt(0).toUpperCase() + t.slice(1) : "";
526
1139
  const hasFilters = typeof onOpenFilters === "function";
527
1140
  const filterBadge = activeFilterCount > 0 ? ` (${activeFilterCount})` : "";
528
- 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(
529
1142
  "div",
530
1143
  {
531
1144
  role: "tablist",
@@ -536,7 +1149,7 @@ function SearchTabs({
536
1149
  const active = String(type).toLowerCase() === String(t).toLowerCase();
537
1150
  const cRaw = counts && Object.prototype.hasOwnProperty.call(counts, t) ? counts[t] : void 0;
538
1151
  const c = Number.isFinite(Number(cRaw)) ? Number(cRaw) : 0;
539
- return /* @__PURE__ */ React13.createElement(
1152
+ return /* @__PURE__ */ React20.createElement(
540
1153
  "button",
541
1154
  {
542
1155
  key: t,
@@ -551,7 +1164,7 @@ function SearchTabs({
551
1164
  ")"
552
1165
  );
553
1166
  })
554
- ), hasFilters ? /* @__PURE__ */ React13.createElement(
1167
+ ), hasFilters ? /* @__PURE__ */ React20.createElement(
555
1168
  "button",
556
1169
  {
557
1170
  type: "button",
@@ -559,12 +1172,12 @@ function SearchTabs({
559
1172
  "aria-expanded": filtersOpen ? "true" : "false",
560
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"
561
1174
  },
562
- /* @__PURE__ */ React13.createElement("span", null, filtersLabel, filterBadge)
1175
+ /* @__PURE__ */ React20.createElement("span", null, filtersLabel, filterBadge)
563
1176
  ) : null);
564
1177
  }
565
1178
 
566
1179
  // ui/src/search/SearchFiltersDialog.jsx
567
- import React14 from "react";
1180
+ import React21 from "react";
568
1181
  function toArray(input) {
569
1182
  if (!input) return [];
570
1183
  if (Array.isArray(input)) return input;
@@ -603,20 +1216,20 @@ function FacetSection({ facet, selected, onToggle }) {
603
1216
  const selectedValues = selected.get(String(slug)) || /* @__PURE__ */ new Set();
604
1217
  const checkboxId = (valueSlug) => `filter-${slug}-${valueSlug}`;
605
1218
  const hasSelection = selectedValues.size > 0;
606
- const [quickQuery, setQuickQuery] = React14.useState("");
1219
+ const [quickQuery, setQuickQuery] = React21.useState("");
607
1220
  const hasQuery = quickQuery.trim().length > 0;
608
- const filteredValues = React14.useMemo(
1221
+ const filteredValues = React21.useMemo(
609
1222
  () => facetMatches(values, quickQuery),
610
1223
  [values, quickQuery]
611
1224
  );
612
- return /* @__PURE__ */ React14.createElement(
1225
+ return /* @__PURE__ */ React21.createElement(
613
1226
  "details",
614
1227
  {
615
1228
  className: "canopy-search-filters__facet",
616
1229
  open: hasSelection
617
1230
  },
618
- /* @__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)),
619
- /* @__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(
620
1233
  "input",
621
1234
  {
622
1235
  type: "search",
@@ -626,7 +1239,7 @@ function FacetSection({ facet, selected, onToggle }) {
626
1239
  className: "canopy-search-filters__quick-input",
627
1240
  "aria-label": `Filter ${label} values`
628
1241
  }
629
- ), quickQuery ? /* @__PURE__ */ React14.createElement(
1242
+ ), quickQuery ? /* @__PURE__ */ React21.createElement(
630
1243
  "button",
631
1244
  {
632
1245
  type: "button",
@@ -634,11 +1247,11 @@ function FacetSection({ facet, selected, onToggle }) {
634
1247
  className: "canopy-search-filters__quick-clear"
635
1248
  },
636
1249
  "Clear"
637
- ) : 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) => {
638
1251
  const valueSlug = String(entry.slug || entry.value || "");
639
1252
  const isChecked = selectedValues.has(valueSlug);
640
1253
  const inputId = checkboxId(valueSlug);
641
- 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(
642
1255
  "input",
643
1256
  {
644
1257
  id: inputId,
@@ -650,15 +1263,15 @@ function FacetSection({ facet, selected, onToggle }) {
650
1263
  if (onToggle) onToggle(slug, valueSlug, nextChecked);
651
1264
  }
652
1265
  }
653
- ), /* @__PURE__ */ React14.createElement(
1266
+ ), /* @__PURE__ */ React21.createElement(
654
1267
  "label",
655
1268
  {
656
1269
  htmlFor: inputId,
657
1270
  className: "canopy-search-filters__facet-label"
658
1271
  },
659
- /* @__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)
660
1273
  ));
661
- }), !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))
662
1275
  );
663
1276
  }
664
1277
  function SearchFiltersDialog(props = {}) {
@@ -669,35 +1282,51 @@ function SearchFiltersDialog(props = {}) {
669
1282
  selected = {},
670
1283
  onToggle,
671
1284
  onClear,
672
- title = "Filters",
673
- subtitle = "Refine results by metadata"
1285
+ title,
1286
+ subtitle = "Refine results by metadata",
1287
+ brandLabel = "Canopy IIIF",
1288
+ brandHref = "/",
1289
+ logo: SiteLogo
674
1290
  } = props;
675
1291
  const selectedMap = normalizeSelected(selected);
676
1292
  const activeCount = Array.from(selectedMap.values()).reduce(
677
1293
  (total, set) => total + set.size,
678
1294
  0
679
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]);
680
1310
  if (!open) return null;
681
- return /* @__PURE__ */ React14.createElement(
682
- "div",
1311
+ const brandId = "canopy-modal-filters-label";
1312
+ const subtitleText = subtitle != null ? subtitle : title;
1313
+ return /* @__PURE__ */ React21.createElement(
1314
+ CanopyModal,
683
1315
  {
684
- role: "dialog",
685
- "aria-modal": "true",
686
- className: "canopy-search-filters-overlay",
687
- onClick: (event) => {
688
- if (event.target === event.currentTarget && onOpenChange)
689
- onOpenChange(false);
690
- }
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"
691
1327
  },
692
- /* @__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(
693
- "button",
694
- {
695
- type: "button",
696
- onClick: () => onOpenChange && onOpenChange(false),
697
- className: "canopy-search-filters__close"
698
- },
699
- "Close"
700
- )), /* @__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(
701
1330
  FacetSection,
702
1331
  {
703
1332
  key: facet.slug || facet.label,
@@ -705,7 +1334,8 @@ function SearchFiltersDialog(props = {}) {
705
1334
  selected: selectedMap,
706
1335
  onToggle
707
1336
  }
708
- ))) : /* @__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(
709
1339
  "button",
710
1340
  {
711
1341
  type: "button",
@@ -716,7 +1346,7 @@ function SearchFiltersDialog(props = {}) {
716
1346
  className: "canopy-search-filters__button canopy-search-filters__button--secondary"
717
1347
  },
718
1348
  "Clear all"
719
- ), /* @__PURE__ */ React14.createElement(
1349
+ ), /* @__PURE__ */ React21.createElement(
720
1350
  "button",
721
1351
  {
722
1352
  type: "button",
@@ -724,213 +1354,12 @@ function SearchFiltersDialog(props = {}) {
724
1354
  className: "canopy-search-filters__button canopy-search-filters__button--primary"
725
1355
  },
726
1356
  "Done"
727
- ))))
728
- );
729
- }
730
-
731
- // ui/src/search-form/MdxSearchFormModal.jsx
732
- import React18 from "react";
733
-
734
- // ui/src/Icons.jsx
735
- import React15 from "react";
736
- 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" }));
737
-
738
- // ui/src/search/SearchPanelForm.jsx
739
- import React16 from "react";
740
- function readBasePath() {
741
- const normalize = (val) => {
742
- const raw = typeof val === "string" ? val.trim() : "";
743
- if (!raw) return "";
744
- return raw.replace(/\/+$/, "");
745
- };
746
- try {
747
- if (typeof window !== "undefined" && window.CANOPY_BASE_PATH != null) {
748
- const fromWindow = normalize(window.CANOPY_BASE_PATH);
749
- if (fromWindow) return fromWindow;
750
- }
751
- } catch (_) {
752
- }
753
- try {
754
- if (typeof globalThis !== "undefined" && globalThis.CANOPY_BASE_PATH != null) {
755
- const fromGlobal = normalize(globalThis.CANOPY_BASE_PATH);
756
- if (fromGlobal) return fromGlobal;
757
- }
758
- } catch (_) {
759
- }
760
- try {
761
- if (typeof process !== "undefined" && process.env && process.env.CANOPY_BASE_PATH) {
762
- const fromEnv = normalize(process.env.CANOPY_BASE_PATH);
763
- if (fromEnv) return fromEnv;
764
- }
765
- } catch (_) {
766
- }
767
- return "";
768
- }
769
- function isAbsoluteUrl(href) {
770
- try {
771
- return /^https?:/i.test(String(href || ""));
772
- } catch (_) {
773
- return false;
774
- }
775
- }
776
- function resolveSearchPath(pathValue) {
777
- let raw = typeof pathValue === "string" ? pathValue.trim() : "";
778
- if (!raw) raw = "/search";
779
- if (isAbsoluteUrl(raw)) return raw;
780
- const normalizedPath = raw.startsWith("/") ? raw : `/${raw}`;
781
- const base = readBasePath();
782
- if (!base) return normalizedPath;
783
- const baseWithLead = base.startsWith("/") ? base : `/${base}`;
784
- const baseTrimmed = baseWithLead.replace(/\/+$/, "");
785
- if (!baseTrimmed) return normalizedPath;
786
- if (normalizedPath === baseTrimmed || normalizedPath.startsWith(`${baseTrimmed}/`)) {
787
- return normalizedPath;
788
- }
789
- const pathTrimmed = normalizedPath.replace(/^\/+/, "");
790
- return `${baseTrimmed}/${pathTrimmed}`;
791
- }
792
- function SearchPanelForm(props = {}) {
793
- const {
794
- placeholder = "Search\u2026",
795
- buttonLabel = "Search",
796
- label,
797
- searchPath = "/search",
798
- inputId: inputIdProp,
799
- clearLabel = "Clear search"
800
- } = props || {};
801
- const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
802
- const action = React16.useMemo(
803
- () => resolveSearchPath(searchPath),
804
- [searchPath]
805
- );
806
- const autoId = typeof React16.useId === "function" ? React16.useId() : void 0;
807
- const [fallbackId] = React16.useState(
808
- () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
809
- );
810
- const inputId = inputIdProp || autoId || fallbackId;
811
- const inputRef = React16.useRef(null);
812
- const [hasValue, setHasValue] = React16.useState(false);
813
- const focusInput = React16.useCallback(() => {
814
- const el = inputRef.current;
815
- if (!el) return;
816
- if (document.activeElement === el) return;
817
- try {
818
- el.focus({ preventScroll: true });
819
- } catch (_) {
820
- try {
821
- el.focus();
822
- } catch (_2) {
823
- }
824
- }
825
- }, []);
826
- const handlePointerDown = React16.useCallback(
827
- (event) => {
828
- const target = event.target;
829
- if (target && typeof target.closest === "function") {
830
- if (target.closest("[data-canopy-search-form-trigger]")) return;
831
- if (target.closest("[data-canopy-search-form-clear]")) return;
832
- }
833
- event.preventDefault();
834
- focusInput();
835
- },
836
- [focusInput]
837
- );
838
- React16.useEffect(() => {
839
- const el = inputRef.current;
840
- if (!el) return;
841
- if (el.value && el.value.trim()) {
842
- setHasValue(true);
843
- }
844
- }, []);
845
- const handleInputChange = React16.useCallback((event) => {
846
- var _a;
847
- const nextHasValue = Boolean(
848
- ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
849
- );
850
- setHasValue(nextHasValue);
851
- }, []);
852
- const handleClear = React16.useCallback((event) => {
853
- }, []);
854
- const handleClearKey = React16.useCallback(
855
- (event) => {
856
- if (event.key === "Enter" || event.key === " ") {
857
- event.preventDefault();
858
- handleClear(event);
859
- }
860
- },
861
- [handleClear]
862
- );
863
- return /* @__PURE__ */ React16.createElement(
864
- "form",
865
- {
866
- action,
867
- method: "get",
868
- role: "search",
869
- autoComplete: "off",
870
- spellCheck: "false",
871
- className: "canopy-search-form canopy-search-form-shell",
872
- onPointerDown: handlePointerDown,
873
- "data-has-value": hasValue ? "1" : "0"
874
- },
875
- /* @__PURE__ */ React16.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React16.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React16.createElement(
876
- "input",
877
- {
878
- id: inputId,
879
- type: "search",
880
- name: "q",
881
- inputMode: "search",
882
- "data-canopy-search-form-input": true,
883
- placeholder,
884
- className: "canopy-search-form__input",
885
- "aria-label": "Search",
886
- ref: inputRef,
887
- onChange: handleInputChange,
888
- onInput: handleInputChange
889
- }
890
- )),
891
- hasValue ? /* @__PURE__ */ React16.createElement(
892
- "button",
893
- {
894
- type: "button",
895
- className: "canopy-search-form__clear",
896
- onClick: handleClear,
897
- onPointerDown: (event) => event.stopPropagation(),
898
- onKeyDown: handleClearKey,
899
- "aria-label": clearLabel,
900
- "data-canopy-search-form-clear": true
901
- },
902
- "\xD7"
903
- ) : null,
904
- /* @__PURE__ */ React16.createElement(
905
- "button",
906
- {
907
- type: "submit",
908
- "data-canopy-search-form-trigger": "submit",
909
- className: "canopy-search-form__submit"
910
- },
911
- /* @__PURE__ */ React16.createElement("span", null, text),
912
- /* @__PURE__ */ React16.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React16.createElement("span", null, "\u2318"), /* @__PURE__ */ React16.createElement("span", null, "K"))
913
- )
914
- );
915
- }
916
-
917
- // ui/src/search/SearchPanelTeaserResults.jsx
918
- import React17 from "react";
919
- function SearchPanelTeaserResults(props = {}) {
920
- const { style, className } = props || {};
921
- const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
922
- return /* @__PURE__ */ React17.createElement(
923
- "div",
924
- {
925
- "data-canopy-search-form-panel": true,
926
- className: classes || void 0,
927
- style
928
- },
929
- /* @__PURE__ */ React17.createElement("div", { id: "cplist" })
1357
+ )))
930
1358
  );
931
1359
  }
932
1360
 
933
1361
  // ui/src/search-form/MdxSearchFormModal.jsx
1362
+ import React22 from "react";
934
1363
  function MdxSearchFormModal(props = {}) {
935
1364
  const {
936
1365
  placeholder = "Search\u2026",
@@ -946,30 +1375,12 @@ function MdxSearchFormModal(props = {}) {
946
1375
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
947
1376
  const resolvedSearchPath = resolveSearchPath(searchPath);
948
1377
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
949
- 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) } }));
950
- }
951
-
952
- // ui/src/search/SearchPanel.jsx
953
- import React19 from "react";
954
- function SearchPanel(props = {}) {
955
- const {
956
- placeholder = "Search\u2026",
957
- hotkey = "mod+k",
958
- maxResults = 8,
959
- groupOrder = ["work", "docs", "page"],
960
- // Kept for backward compat; form always renders submit
961
- button = true,
962
- // eslint-disable-line no-unused-vars
963
- buttonLabel = "Search",
964
- label,
965
- searchPath = "/search"
966
- } = props || {};
967
- const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
968
- const resolvedSearchPath = resolveSearchPath(searchPath);
969
- const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
970
- 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) } }));
971
1379
  }
972
1380
  export {
1381
+ CanopyBrand,
1382
+ CanopyHeader,
1383
+ CanopyModal,
973
1384
  Card,
974
1385
  Grid,
975
1386
  GridItem,