@canopy-iiif/app 0.9.2 → 0.9.4

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.
@@ -809,51 +809,18 @@ function Layout({
809
809
  )) : null);
810
810
  }
811
811
 
812
- // ui/src/search/MdxSearchResults.jsx
813
- import React10 from "react";
814
- function MdxSearchResults(props) {
815
- let json = "{}";
816
- try {
817
- json = JSON.stringify(props || {});
818
- } catch (_) {
819
- json = "{}";
820
- }
821
- return /* @__PURE__ */ React10.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React10.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
822
- }
823
-
824
- // ui/src/search/SearchSummary.jsx
825
- import React11 from "react";
826
- function SearchSummary(props) {
827
- let json = "{}";
828
- try {
829
- json = JSON.stringify(props || {});
830
- } catch (_) {
831
- json = "{}";
832
- }
833
- return /* @__PURE__ */ React11.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React11.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
834
- }
835
-
836
- // ui/src/search/MdxSearchTabs.jsx
837
- import React12 from "react";
838
- function MdxSearchTabs(props) {
839
- let json = "{}";
840
- try {
841
- json = JSON.stringify(props || {});
842
- } catch (_) {
843
- json = "{}";
844
- }
845
- return /* @__PURE__ */ React12.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React12.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
846
- }
847
-
848
- // ui/src/search-form/MdxSearchFormModal.jsx
812
+ // ui/src/layout/CanopyHeader.jsx
849
813
  import React16 from "react";
850
814
 
851
- // ui/src/Icons.jsx
815
+ // ui/src/search/SearchPanel.jsx
852
816
  import React13 from "react";
853
- var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React13.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React13.createElement("path", { d: "M456.69 421.39L362.6 327.3a173.81 173.81 0 0034.84-104.58C397.44 126.38 319.06 48 222.72 48S48 126.38 48 222.72s78.38 174.72 174.72 174.72A173.81 173.81 0 00327.3 362.6l94.09 94.09a25 25 0 0035.3-35.3zM97.92 222.72a124.8 124.8 0 11124.8 124.8 124.95 124.95 0 01-124.8-124.8z" }));
817
+
818
+ // ui/src/Icons.jsx
819
+ import React10 from "react";
820
+ var MagnifyingGlassIcon = (props) => /* @__PURE__ */ React10.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", ...props }, /* @__PURE__ */ React10.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" }));
854
821
 
855
822
  // ui/src/search/SearchPanelForm.jsx
856
- import React14 from "react";
823
+ import React11 from "react";
857
824
  function readBasePath() {
858
825
  const normalize = (val) => {
859
826
  const raw = typeof val === "string" ? val.trim() : "";
@@ -916,18 +883,18 @@ function SearchPanelForm(props = {}) {
916
883
  clearLabel = "Clear search"
917
884
  } = props || {};
918
885
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
919
- const action = React14.useMemo(
886
+ const action = React11.useMemo(
920
887
  () => resolveSearchPath(searchPath),
921
888
  [searchPath]
922
889
  );
923
- const autoId = typeof React14.useId === "function" ? React14.useId() : void 0;
924
- const [fallbackId] = React14.useState(
890
+ const autoId = typeof React11.useId === "function" ? React11.useId() : void 0;
891
+ const [fallbackId] = React11.useState(
925
892
  () => `canopy-search-form-${Math.random().toString(36).slice(2, 10)}`
926
893
  );
927
894
  const inputId = inputIdProp || autoId || fallbackId;
928
- const inputRef = React14.useRef(null);
929
- const [hasValue, setHasValue] = React14.useState(false);
930
- const focusInput = React14.useCallback(() => {
895
+ const inputRef = React11.useRef(null);
896
+ const [hasValue, setHasValue] = React11.useState(false);
897
+ const focusInput = React11.useCallback(() => {
931
898
  const el = inputRef.current;
932
899
  if (!el) return;
933
900
  if (document.activeElement === el) return;
@@ -940,7 +907,7 @@ function SearchPanelForm(props = {}) {
940
907
  }
941
908
  }
942
909
  }, []);
943
- const handlePointerDown = React14.useCallback(
910
+ const handlePointerDown = React11.useCallback(
944
911
  (event) => {
945
912
  const target = event.target;
946
913
  if (target && typeof target.closest === "function") {
@@ -952,23 +919,23 @@ function SearchPanelForm(props = {}) {
952
919
  },
953
920
  [focusInput]
954
921
  );
955
- React14.useEffect(() => {
922
+ React11.useEffect(() => {
956
923
  const el = inputRef.current;
957
924
  if (!el) return;
958
925
  if (el.value && el.value.trim()) {
959
926
  setHasValue(true);
960
927
  }
961
928
  }, []);
962
- const handleInputChange = React14.useCallback((event) => {
929
+ const handleInputChange = React11.useCallback((event) => {
963
930
  var _a;
964
931
  const nextHasValue = Boolean(
965
932
  ((_a = event == null ? void 0 : event.target) == null ? void 0 : _a.value) && event.target.value.trim()
966
933
  );
967
934
  setHasValue(nextHasValue);
968
935
  }, []);
969
- const handleClear = React14.useCallback((event) => {
936
+ const handleClear = React11.useCallback((event) => {
970
937
  }, []);
971
- const handleClearKey = React14.useCallback(
938
+ const handleClearKey = React11.useCallback(
972
939
  (event) => {
973
940
  if (event.key === "Enter" || event.key === " ") {
974
941
  event.preventDefault();
@@ -977,7 +944,7 @@ function SearchPanelForm(props = {}) {
977
944
  },
978
945
  [handleClear]
979
946
  );
980
- return /* @__PURE__ */ React14.createElement(
947
+ return /* @__PURE__ */ React11.createElement(
981
948
  "form",
982
949
  {
983
950
  action,
@@ -989,7 +956,7 @@ function SearchPanelForm(props = {}) {
989
956
  onPointerDown: handlePointerDown,
990
957
  "data-has-value": hasValue ? "1" : "0"
991
958
  },
992
- /* @__PURE__ */ React14.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React14.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React14.createElement(
959
+ /* @__PURE__ */ React11.createElement("label", { htmlFor: inputId, className: "canopy-search-form__label" }, /* @__PURE__ */ React11.createElement(MagnifyingGlassIcon, { className: "canopy-search-form__icon" }), /* @__PURE__ */ React11.createElement(
993
960
  "input",
994
961
  {
995
962
  id: inputId,
@@ -1005,7 +972,7 @@ function SearchPanelForm(props = {}) {
1005
972
  onInput: handleInputChange
1006
973
  }
1007
974
  )),
1008
- hasValue ? /* @__PURE__ */ React14.createElement(
975
+ hasValue ? /* @__PURE__ */ React11.createElement(
1009
976
  "button",
1010
977
  {
1011
978
  type: "button",
@@ -1018,44 +985,45 @@ function SearchPanelForm(props = {}) {
1018
985
  },
1019
986
  "\xD7"
1020
987
  ) : null,
1021
- /* @__PURE__ */ React14.createElement(
988
+ /* @__PURE__ */ React11.createElement(
1022
989
  "button",
1023
990
  {
1024
991
  type: "submit",
1025
992
  "data-canopy-search-form-trigger": "submit",
1026
993
  className: "canopy-search-form__submit"
1027
994
  },
1028
- /* @__PURE__ */ React14.createElement("span", null, text),
1029
- /* @__PURE__ */ React14.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React14.createElement("span", null, "\u2318"), /* @__PURE__ */ React14.createElement("span", null, "K"))
995
+ /* @__PURE__ */ React11.createElement("span", null, text),
996
+ /* @__PURE__ */ React11.createElement("span", { "aria-hidden": true, className: "canopy-search-form__shortcut" }, /* @__PURE__ */ React11.createElement("span", null, "\u2318"), /* @__PURE__ */ React11.createElement("span", null, "K"))
1030
997
  )
1031
998
  );
1032
999
  }
1033
1000
 
1034
1001
  // ui/src/search/SearchPanelTeaserResults.jsx
1035
- import React15 from "react";
1002
+ import React12 from "react";
1036
1003
  function SearchPanelTeaserResults(props = {}) {
1037
1004
  const { style, className } = props || {};
1038
1005
  const classes = ["canopy-search-teaser", className].filter(Boolean).join(" ");
1039
- return /* @__PURE__ */ React15.createElement(
1006
+ return /* @__PURE__ */ React12.createElement(
1040
1007
  "div",
1041
1008
  {
1042
1009
  "data-canopy-search-form-panel": true,
1043
1010
  className: classes || void 0,
1044
1011
  style
1045
1012
  },
1046
- /* @__PURE__ */ React15.createElement("div", { id: "cplist" })
1013
+ /* @__PURE__ */ React12.createElement("div", { id: "cplist" })
1047
1014
  );
1048
1015
  }
1049
1016
 
1050
- // ui/src/search-form/MdxSearchFormModal.jsx
1051
- function MdxSearchFormModal(props = {}) {
1017
+ // ui/src/search/SearchPanel.jsx
1018
+ function SearchPanel(props = {}) {
1052
1019
  const {
1053
1020
  placeholder = "Search\u2026",
1054
1021
  hotkey = "mod+k",
1055
1022
  maxResults = 8,
1056
- groupOrder = ["work", "page"],
1023
+ groupOrder = ["work", "docs", "page"],
1024
+ // Kept for backward compat; form always renders submit
1057
1025
  button = true,
1058
- // kept for backward compat; ignored by teaser form
1026
+ // eslint-disable-line no-unused-vars
1059
1027
  buttonLabel = "Search",
1060
1028
  label,
1061
1029
  searchPath = "/search"
@@ -1063,20 +1031,443 @@ function MdxSearchFormModal(props = {}) {
1063
1031
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
1064
1032
  const resolvedSearchPath = resolveSearchPath(searchPath);
1065
1033
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
1066
- return /* @__PURE__ */ React16.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React16.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React16.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React16.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React16.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
1034
+ return /* @__PURE__ */ React13.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React13.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React13.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React13.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React13.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
1067
1035
  }
1068
1036
 
1069
- // ui/src/search/SearchPanel.jsx
1037
+ // ui/src/layout/CanopyBrand.jsx
1038
+ import React14 from "react";
1039
+ function CanopyBrand(props = {}) {
1040
+ const {
1041
+ labelId,
1042
+ label = "Canopy IIIF",
1043
+ href = "/",
1044
+ className,
1045
+ Logo
1046
+ } = props || {};
1047
+ const spanProps = labelId ? { id: labelId } : {};
1048
+ const classes = ["canopy-logo", className].filter(Boolean).join(" ");
1049
+ return /* @__PURE__ */ React14.createElement("a", { href, className: classes }, typeof Logo === "function" ? /* @__PURE__ */ React14.createElement(Logo, null) : null, /* @__PURE__ */ React14.createElement("span", { ...spanProps }, label));
1050
+ }
1051
+
1052
+ // ui/src/layout/CanopyModal.jsx
1053
+ import React15 from "react";
1054
+ function CanopyModal(props = {}) {
1055
+ const {
1056
+ id,
1057
+ variant,
1058
+ open = false,
1059
+ labelledBy,
1060
+ label,
1061
+ logo: Logo,
1062
+ href = "/",
1063
+ closeLabel = "Close",
1064
+ closeDataAttr,
1065
+ onClose,
1066
+ onBackgroundClick,
1067
+ bodyClassName,
1068
+ padded = true,
1069
+ className,
1070
+ children
1071
+ } = props;
1072
+ const rootClassName = ["canopy-modal", variant ? `canopy-modal--${variant}` : null, className].filter(Boolean).join(" ");
1073
+ const modalProps = {
1074
+ id,
1075
+ className: rootClassName,
1076
+ role: "dialog",
1077
+ "aria-modal": "true",
1078
+ "aria-hidden": open ? "false" : "true",
1079
+ "data-open": open ? "true" : "false"
1080
+ };
1081
+ if (variant) modalProps["data-canopy-modal"] = variant;
1082
+ const resolvedLabelId = labelledBy || (label ? `${variant || "modal"}-label` : void 0);
1083
+ if (resolvedLabelId) modalProps["aria-labelledby"] = resolvedLabelId;
1084
+ if (typeof onBackgroundClick === "function") {
1085
+ modalProps.onClick = (event) => {
1086
+ if (event.target === event.currentTarget) onBackgroundClick(event);
1087
+ };
1088
+ }
1089
+ const closeButtonProps = {
1090
+ type: "button",
1091
+ className: "canopy-modal__close",
1092
+ "aria-label": closeLabel
1093
+ };
1094
+ if (typeof closeDataAttr === "string" && closeDataAttr) {
1095
+ closeButtonProps["data-canopy-header-close"] = closeDataAttr;
1096
+ }
1097
+ if (typeof onClose === "function") {
1098
+ closeButtonProps.onClick = onClose;
1099
+ }
1100
+ const bodyClasses = ["canopy-modal__body"];
1101
+ if (padded) bodyClasses.push("canopy-modal__body--padded");
1102
+ if (bodyClassName) bodyClasses.push(bodyClassName);
1103
+ const bodyClassNameValue = bodyClasses.join(" ");
1104
+ return /* @__PURE__ */ React15.createElement("div", { ...modalProps }, /* @__PURE__ */ React15.createElement("div", { className: "canopy-modal__panel" }, /* @__PURE__ */ React15.createElement("button", { ...closeButtonProps }, /* @__PURE__ */ React15.createElement(
1105
+ "svg",
1106
+ {
1107
+ xmlns: "http://www.w3.org/2000/svg",
1108
+ viewBox: "0 0 24 24",
1109
+ fill: "none",
1110
+ stroke: "currentColor",
1111
+ strokeWidth: "1.5",
1112
+ className: "canopy-modal__close-icon"
1113
+ },
1114
+ /* @__PURE__ */ React15.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 6l12 12M6 18L18 6" })
1115
+ ), /* @__PURE__ */ React15.createElement("span", { className: "sr-only" }, closeLabel)), /* @__PURE__ */ React15.createElement("div", { className: bodyClassNameValue }, label ? /* @__PURE__ */ React15.createElement("div", { className: "canopy-modal__brand" }, /* @__PURE__ */ React15.createElement(
1116
+ CanopyBrand,
1117
+ {
1118
+ labelId: resolvedLabelId,
1119
+ label,
1120
+ href,
1121
+ Logo,
1122
+ className: "canopy-modal__brand-link"
1123
+ }
1124
+ )) : null, children)));
1125
+ }
1126
+
1127
+ // ui/src/layout/CanopyHeader.jsx
1128
+ function HeaderScript() {
1129
+ const code = `
1130
+ (function () {
1131
+ if (typeof window === 'undefined') return;
1132
+
1133
+ var doc = document;
1134
+ var body = doc.body;
1135
+ var root = doc.documentElement;
1136
+
1137
+ function ready(fn) {
1138
+ if (doc.readyState === 'loading') {
1139
+ doc.addEventListener('DOMContentLoaded', fn, { once: true });
1140
+ } else {
1141
+ fn();
1142
+ }
1143
+ }
1144
+
1145
+ ready(function () {
1146
+ var header = doc.querySelector('.canopy-header');
1147
+ if (!header) return;
1148
+
1149
+ var NAV_ATTR = 'data-mobile-nav';
1150
+ var SEARCH_ATTR = 'data-mobile-search';
1151
+
1152
+ function modalFor(type) {
1153
+ return doc.querySelector('[data-canopy-modal="' + type + '"]');
1154
+ }
1155
+
1156
+ function each(list, fn) {
1157
+ if (!list || typeof fn !== 'function') return;
1158
+ Array.prototype.forEach.call(list, fn);
1159
+ }
1160
+
1161
+ function setExpanded(type, expanded) {
1162
+ var toggles = header.querySelectorAll('[data-canopy-header-toggle="' + type + '"]');
1163
+ each(toggles, function (btn) {
1164
+ btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1165
+ });
1166
+ var modal = modalFor(type);
1167
+ if (modal) {
1168
+ modal.setAttribute('data-open', expanded ? 'true' : 'false');
1169
+ modal.setAttribute('aria-hidden', expanded ? 'false' : 'true');
1170
+ }
1171
+ }
1172
+
1173
+ function lockScroll(shouldLock) {
1174
+ if (!body) return;
1175
+ if (shouldLock) {
1176
+ if (!body.dataset.canopyScrollLock) {
1177
+ body.dataset.canopyScrollPrevOverflow = body.style.overflow || '';
1178
+ if (root && root.dataset) {
1179
+ root.dataset.canopyScrollPrevOverflow = root.style.overflow || '';
1180
+ }
1181
+ }
1182
+ body.dataset.canopyScrollLock = '1';
1183
+ body.style.overflow = 'hidden';
1184
+ if (root) root.style.overflow = 'hidden';
1185
+ } else {
1186
+ if (body.dataset.canopyScrollLock) {
1187
+ delete body.dataset.canopyScrollLock;
1188
+ body.style.overflow = body.dataset.canopyScrollPrevOverflow || '';
1189
+ delete body.dataset.canopyScrollPrevOverflow;
1190
+ }
1191
+ if (root && root.dataset) {
1192
+ root.style.overflow = root.dataset.canopyScrollPrevOverflow || '';
1193
+ delete root.dataset.canopyScrollPrevOverflow;
1194
+ }
1195
+ }
1196
+ }
1197
+
1198
+ function stateFor(type) {
1199
+ if (type === 'nav') return header.getAttribute(NAV_ATTR);
1200
+ if (type === 'search') return header.getAttribute(SEARCH_ATTR);
1201
+ return 'closed';
1202
+ }
1203
+
1204
+ function focusSearchForm() {
1205
+ var input = header.querySelector('[data-canopy-search-form-input]');
1206
+ if (!input) return;
1207
+ var raf = typeof window !== 'undefined' && window.requestAnimationFrame;
1208
+ (raf || function (fn) { return setTimeout(fn, 16); })(function () {
1209
+ try {
1210
+ input.focus({ preventScroll: true });
1211
+ } catch (_) {
1212
+ try { input.focus(); } catch (_) {}
1213
+ }
1214
+ });
1215
+ }
1216
+
1217
+ function focusNavMenu() {
1218
+ var modal = modalFor('nav');
1219
+ if (!modal) return;
1220
+ var target = modal.querySelector('button, a, input, [tabindex]:not([tabindex="-1"])');
1221
+ if (!target) return;
1222
+ var raf = typeof window !== 'undefined' && window.requestAnimationFrame;
1223
+ (raf || function (fn) { return setTimeout(fn, 16); })(function () {
1224
+ try {
1225
+ target.focus({ preventScroll: true });
1226
+ } catch (_) {
1227
+ try { target.focus(); } catch (_) {}
1228
+ }
1229
+ });
1230
+ }
1231
+
1232
+ function setState(type, next) {
1233
+ if (type === 'nav') header.setAttribute(NAV_ATTR, next);
1234
+ if (type === 'search') header.setAttribute(SEARCH_ATTR, next);
1235
+ setExpanded(type, next === 'open');
1236
+ var navOpen = header.getAttribute(NAV_ATTR) === 'open';
1237
+ var searchOpen = header.getAttribute(SEARCH_ATTR) === 'open';
1238
+ lockScroll(navOpen || searchOpen);
1239
+ }
1240
+
1241
+ function toggle(type, force) {
1242
+ var current = stateFor(type) === 'open';
1243
+ var shouldOpen = typeof force === 'boolean' ? force : !current;
1244
+ if (shouldOpen && type === 'nav') setState('search', 'closed');
1245
+ if (shouldOpen && type === 'search') setState('nav', 'closed');
1246
+ setState(type, shouldOpen ? 'open' : 'closed');
1247
+ if (type === 'search' && shouldOpen) focusSearchForm();
1248
+ if (type === 'nav' && shouldOpen) focusNavMenu();
1249
+ }
1250
+
1251
+ each(header.querySelectorAll('[data-canopy-header-toggle]'), function (btn) {
1252
+ btn.addEventListener('click', function (event) {
1253
+ event.preventDefault();
1254
+ var type = btn.getAttribute('data-canopy-header-toggle');
1255
+ if (!type) return;
1256
+ toggle(type);
1257
+ });
1258
+ });
1259
+
1260
+ each(doc.querySelectorAll('[data-canopy-header-close]'), function (btn) {
1261
+ btn.addEventListener('click', function () {
1262
+ var type = btn.getAttribute('data-canopy-header-close');
1263
+ if (!type) return;
1264
+ toggle(type, false);
1265
+ });
1266
+ });
1267
+
1268
+ var navModal = modalFor('nav');
1269
+ if (navModal) {
1270
+ navModal.addEventListener('click', function (event) {
1271
+ if (event.target === navModal) {
1272
+ toggle('nav', false);
1273
+ return;
1274
+ }
1275
+ var target = event.target && event.target.closest && event.target.closest('a');
1276
+ if (!target) return;
1277
+ toggle('nav', false);
1278
+ });
1279
+ }
1280
+
1281
+ var searchModal = modalFor('search');
1282
+ if (searchModal) {
1283
+ searchModal.addEventListener('click', function (event) {
1284
+ if (event.target === searchModal) toggle('search', false);
1285
+ });
1286
+ }
1287
+
1288
+ doc.addEventListener('keydown', function (event) {
1289
+ if (event.key !== 'Escape') return;
1290
+ var navOpen = header.getAttribute(NAV_ATTR) === 'open';
1291
+ var searchOpen = header.getAttribute(SEARCH_ATTR) === 'open';
1292
+ if (!navOpen && !searchOpen) return;
1293
+ event.preventDefault();
1294
+ toggle('nav', false);
1295
+ toggle('search', false);
1296
+ });
1297
+
1298
+ var mq = window.matchMedia('(min-width: 48rem)');
1299
+ function syncDesktopState() {
1300
+ if (mq.matches) {
1301
+ setState('nav', 'closed');
1302
+ setState('search', 'closed');
1303
+ setExpanded('nav', false);
1304
+ setExpanded('search', false);
1305
+ lockScroll(false);
1306
+ }
1307
+ }
1308
+
1309
+ try {
1310
+ mq.addEventListener('change', syncDesktopState);
1311
+ } catch (_) {
1312
+ mq.addListener(syncDesktopState);
1313
+ }
1314
+
1315
+ syncDesktopState();
1316
+ });
1317
+ })();
1318
+ `;
1319
+ return /* @__PURE__ */ React16.createElement(
1320
+ "script",
1321
+ {
1322
+ dangerouslySetInnerHTML: {
1323
+ __html: code
1324
+ }
1325
+ }
1326
+ );
1327
+ }
1328
+ function ensureArray(navLinks) {
1329
+ if (!Array.isArray(navLinks)) return [];
1330
+ return navLinks.filter((link) => link && typeof link === "object" && typeof link.href === "string");
1331
+ }
1332
+ function CanopyHeader(props = {}) {
1333
+ const {
1334
+ navigation: navLinksProp,
1335
+ searchLabel = "Search",
1336
+ searchHotkey = "mod+k",
1337
+ searchPlaceholder = "Search\u2026",
1338
+ brandHref = "/",
1339
+ title = "Canopy IIIF",
1340
+ logo: SiteLogo
1341
+ } = props;
1342
+ const navLinks = ensureArray(navLinksProp);
1343
+ return /* @__PURE__ */ React16.createElement(React16.Fragment, null, /* @__PURE__ */ React16.createElement("header", { className: "canopy-header", "data-mobile-nav": "closed", "data-mobile-search": "closed" }, /* @__PURE__ */ React16.createElement("div", { className: "canopy-header__brand" }, /* @__PURE__ */ React16.createElement(
1344
+ CanopyBrand,
1345
+ {
1346
+ label: title,
1347
+ href: brandHref,
1348
+ className: "canopy-header__brand-link",
1349
+ Logo: SiteLogo
1350
+ }
1351
+ )), /* @__PURE__ */ React16.createElement("div", { className: "canopy-header__desktop-search" }, /* @__PURE__ */ React16.createElement(SearchPanel, { label: searchLabel, hotkey: searchHotkey, placeholder: searchPlaceholder })), /* @__PURE__ */ React16.createElement("nav", { className: "canopy-nav-links canopy-header__desktop-nav", "aria-label": "Primary navigation" }, navLinks.map((link) => /* @__PURE__ */ React16.createElement("a", { key: link.href, href: link.href }, link.label || link.href))), /* @__PURE__ */ React16.createElement("div", { className: "canopy-header__actions" }, /* @__PURE__ */ React16.createElement(
1352
+ "button",
1353
+ {
1354
+ type: "button",
1355
+ className: "canopy-header__icon-button canopy-header__search-trigger",
1356
+ "aria-label": "Open search",
1357
+ "aria-controls": "canopy-modal-search",
1358
+ "aria-expanded": "false",
1359
+ "data-canopy-header-toggle": "search"
1360
+ },
1361
+ /* @__PURE__ */ React16.createElement(
1362
+ "svg",
1363
+ {
1364
+ xmlns: "http://www.w3.org/2000/svg",
1365
+ viewBox: "0 0 24 24",
1366
+ fill: "none",
1367
+ stroke: "currentColor",
1368
+ strokeWidth: "1.5",
1369
+ className: "canopy-header__search-icon"
1370
+ },
1371
+ /* @__PURE__ */ React16.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" })
1372
+ )
1373
+ ), /* @__PURE__ */ React16.createElement(
1374
+ "button",
1375
+ {
1376
+ type: "button",
1377
+ className: "canopy-header__icon-button canopy-header__menu",
1378
+ "aria-label": "Open navigation",
1379
+ "aria-controls": "canopy-modal-nav",
1380
+ "aria-expanded": "false",
1381
+ "data-canopy-header-toggle": "nav"
1382
+ },
1383
+ /* @__PURE__ */ React16.createElement(
1384
+ "svg",
1385
+ {
1386
+ xmlns: "http://www.w3.org/2000/svg",
1387
+ fill: "none",
1388
+ viewBox: "0 0 24 24",
1389
+ strokeWidth: "1.5",
1390
+ stroke: "currentColor",
1391
+ className: "canopy-header__menu-icon"
1392
+ },
1393
+ /* @__PURE__ */ React16.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" })
1394
+ )
1395
+ ))), /* @__PURE__ */ React16.createElement(
1396
+ CanopyModal,
1397
+ {
1398
+ id: "canopy-modal-nav",
1399
+ variant: "nav",
1400
+ labelledBy: "canopy-modal-nav-label",
1401
+ label: title,
1402
+ logo: SiteLogo,
1403
+ href: brandHref,
1404
+ closeLabel: "Close navigation",
1405
+ closeDataAttr: "nav"
1406
+ },
1407
+ /* @__PURE__ */ React16.createElement("nav", { className: "canopy-nav-links canopy-modal__nav", "aria-label": "Primary navigation" }, navLinks.map((link) => /* @__PURE__ */ React16.createElement("a", { key: link.href, href: link.href }, link.label || link.href)))
1408
+ ), /* @__PURE__ */ React16.createElement(
1409
+ CanopyModal,
1410
+ {
1411
+ id: "canopy-modal-search",
1412
+ variant: "search",
1413
+ labelledBy: "canopy-modal-search-label",
1414
+ label: title,
1415
+ logo: SiteLogo,
1416
+ href: brandHref,
1417
+ closeLabel: "Close search",
1418
+ closeDataAttr: "search",
1419
+ bodyClassName: "canopy-modal__body--search"
1420
+ },
1421
+ /* @__PURE__ */ React16.createElement(SearchPanel, { label: searchLabel, hotkey: searchHotkey, placeholder: searchPlaceholder })
1422
+ ), /* @__PURE__ */ React16.createElement(HeaderScript, null));
1423
+ }
1424
+
1425
+ // ui/src/search/MdxSearchResults.jsx
1070
1426
  import React17 from "react";
1071
- function SearchPanel(props = {}) {
1427
+ function MdxSearchResults(props) {
1428
+ let json = "{}";
1429
+ try {
1430
+ json = JSON.stringify(props || {});
1431
+ } catch (_) {
1432
+ json = "{}";
1433
+ }
1434
+ return /* @__PURE__ */ React17.createElement("div", { "data-canopy-search-results": "1" }, /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1435
+ }
1436
+
1437
+ // ui/src/search/SearchSummary.jsx
1438
+ import React18 from "react";
1439
+ function SearchSummary(props) {
1440
+ let json = "{}";
1441
+ try {
1442
+ json = JSON.stringify(props || {});
1443
+ } catch (_) {
1444
+ json = "{}";
1445
+ }
1446
+ return /* @__PURE__ */ React18.createElement("div", { "data-canopy-search-summary": "1" }, /* @__PURE__ */ React18.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1447
+ }
1448
+
1449
+ // ui/src/search/MdxSearchTabs.jsx
1450
+ import React19 from "react";
1451
+ function MdxSearchTabs(props) {
1452
+ let json = "{}";
1453
+ try {
1454
+ json = JSON.stringify(props || {});
1455
+ } catch (_) {
1456
+ json = "{}";
1457
+ }
1458
+ return /* @__PURE__ */ React19.createElement("div", { "data-canopy-search-tabs": "1" }, /* @__PURE__ */ React19.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: json } }));
1459
+ }
1460
+
1461
+ // ui/src/search-form/MdxSearchFormModal.jsx
1462
+ import React20 from "react";
1463
+ function MdxSearchFormModal(props = {}) {
1072
1464
  const {
1073
1465
  placeholder = "Search\u2026",
1074
1466
  hotkey = "mod+k",
1075
1467
  maxResults = 8,
1076
- groupOrder = ["work", "docs", "page"],
1077
- // Kept for backward compat; form always renders submit
1468
+ groupOrder = ["work", "page"],
1078
1469
  button = true,
1079
- // eslint-disable-line no-unused-vars
1470
+ // kept for backward compat; ignored by teaser form
1080
1471
  buttonLabel = "Search",
1081
1472
  label,
1082
1473
  searchPath = "/search"
@@ -1084,11 +1475,11 @@ function SearchPanel(props = {}) {
1084
1475
  const text = typeof label === "string" && label.trim() ? label.trim() : buttonLabel;
1085
1476
  const resolvedSearchPath = resolveSearchPath(searchPath);
1086
1477
  const data = { placeholder, hotkey, maxResults, groupOrder, label: text, searchPath: resolvedSearchPath };
1087
- return /* @__PURE__ */ React17.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React17.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React17.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React17.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React17.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
1478
+ return /* @__PURE__ */ React20.createElement("div", { "data-canopy-search-form": true, className: "flex-1 min-w-0" }, /* @__PURE__ */ React20.createElement("div", { className: "relative w-full" }, /* @__PURE__ */ React20.createElement(SearchPanelForm, { placeholder, buttonLabel, label, searchPath: resolvedSearchPath }), /* @__PURE__ */ React20.createElement(SearchPanelTeaserResults, null)), /* @__PURE__ */ React20.createElement("script", { type: "application/json", dangerouslySetInnerHTML: { __html: JSON.stringify(data) } }));
1088
1479
  }
1089
1480
 
1090
1481
  // ui/src/iiif/ManifestPrimitives.jsx
1091
- import React18 from "react";
1482
+ import React21 from "react";
1092
1483
  import {
1093
1484
  Label as CloverLabel,
1094
1485
  Metadata as CloverMetadata,
@@ -1113,26 +1504,199 @@ function ensureMetadata(items) {
1113
1504
  function Label({ manifest, label, ...rest }) {
1114
1505
  const intl = label || manifest && manifest.label;
1115
1506
  if (!hasInternationalValue(intl)) return null;
1116
- return /* @__PURE__ */ React18.createElement(CloverLabel, { label: intl, ...rest });
1507
+ return /* @__PURE__ */ React21.createElement(CloverLabel, { label: intl, ...rest });
1117
1508
  }
1118
1509
  function Summary({ manifest, summary, ...rest }) {
1119
1510
  const intl = summary || manifest && manifest.summary;
1120
1511
  if (!hasInternationalValue(intl)) return null;
1121
- return /* @__PURE__ */ React18.createElement(CloverSummary, { summary: intl, ...rest });
1512
+ return /* @__PURE__ */ React21.createElement(CloverSummary, { summary: intl, ...rest });
1122
1513
  }
1123
1514
  function Metadata({ manifest, metadata, ...rest }) {
1124
1515
  const items = ensureMetadata(metadata || manifest && manifest.metadata);
1125
1516
  if (!items.length) return null;
1126
- return /* @__PURE__ */ React18.createElement(CloverMetadata, { metadata: items, ...rest });
1517
+ return /* @__PURE__ */ React21.createElement(CloverMetadata, { metadata: items, ...rest });
1127
1518
  }
1128
1519
  function RequiredStatement({ manifest, requiredStatement, ...rest }) {
1129
1520
  const stmt = requiredStatement || manifest && manifest.requiredStatement;
1130
1521
  if (!stmt || !hasInternationalValue(stmt.label) || !hasInternationalValue(stmt.value)) {
1131
1522
  return null;
1132
1523
  }
1133
- return /* @__PURE__ */ React18.createElement(CloverRequiredStatement, { requiredStatement: stmt, ...rest });
1524
+ return /* @__PURE__ */ React21.createElement(CloverRequiredStatement, { requiredStatement: stmt, ...rest });
1525
+ }
1526
+
1527
+ // ui/src/docs/CodeBlock.jsx
1528
+ import React22 from "react";
1529
+ function parseHighlightAttr(attr) {
1530
+ if (!attr) return /* @__PURE__ */ new Set();
1531
+ const cleaned = String(attr || "").trim();
1532
+ if (!cleaned) return /* @__PURE__ */ new Set();
1533
+ const segments = cleaned.split(",").map((segment) => segment.trim()).filter(Boolean);
1534
+ const lines = /* @__PURE__ */ new Set();
1535
+ for (const segment of segments) {
1536
+ if (!segment) continue;
1537
+ if (/^\d+-\d+$/.test(segment)) {
1538
+ const [startRaw, endRaw] = segment.split("-");
1539
+ const start = Number(startRaw);
1540
+ const end = Number(endRaw);
1541
+ if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
1542
+ for (let i = start; i <= end; i += 1) {
1543
+ lines.add(i);
1544
+ }
1545
+ }
1546
+ } else if (/^\d+$/.test(segment)) {
1547
+ const value = Number(segment);
1548
+ if (Number.isFinite(value)) lines.add(value);
1549
+ }
1550
+ }
1551
+ return lines;
1552
+ }
1553
+ function normaliseCode(children) {
1554
+ if (children == null) return "";
1555
+ if (typeof children === "string") return children;
1556
+ if (Array.isArray(children)) {
1557
+ return children.map((child) => typeof child === "string" ? child : "").join("");
1558
+ }
1559
+ if (typeof children === "object" && typeof children.toString === "function") {
1560
+ return children.toString();
1561
+ }
1562
+ return "";
1563
+ }
1564
+ var baseLineStyle = {
1565
+ display: "block",
1566
+ padding: "0.125rem 1.25rem",
1567
+ boxSizing: "border-box"
1568
+ };
1569
+ var highlightBaseStyle = {
1570
+ background: "linear-gradient(to right, var(--color-brand-200, #bfdbfe), var(--color-brand-100, #bfdbfe))"
1571
+ };
1572
+ function DocsCodeBlock(props = {}) {
1573
+ const { children, ...rest } = props;
1574
+ const childArray = React22.Children.toArray(children);
1575
+ const codeElement = childArray.find((el) => React22.isValidElement(el));
1576
+ if (!codeElement || !codeElement.props) {
1577
+ return React22.createElement("pre", props);
1578
+ }
1579
+ const {
1580
+ className = "",
1581
+ children: codeChildren,
1582
+ ...codeProps
1583
+ } = codeElement.props;
1584
+ const rawCode = normaliseCode(codeChildren);
1585
+ const trimmedCode = rawCode.endsWith("\n") ? rawCode.slice(0, -1) : rawCode;
1586
+ const lines = trimmedCode.split("\n");
1587
+ const filename = codeProps["data-filename"] || "";
1588
+ const highlightAttr = codeProps["data-highlight"] || "";
1589
+ const highlightSet = parseHighlightAttr(highlightAttr);
1590
+ const copyAttr = codeProps["data-copy"];
1591
+ const enableCopy = copyAttr !== void 0 ? copyAttr === true || copyAttr === "true" || copyAttr === "" : false;
1592
+ const [copied, setCopied] = React22.useState(false);
1593
+ const handleCopy = React22.useCallback(async () => {
1594
+ const text = rawCode;
1595
+ try {
1596
+ if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
1597
+ await navigator.clipboard.writeText(text);
1598
+ } else {
1599
+ const textarea = document.createElement("textarea");
1600
+ textarea.value = text;
1601
+ textarea.setAttribute("readonly", "");
1602
+ textarea.style.position = "absolute";
1603
+ textarea.style.left = "-9999px";
1604
+ document.body.appendChild(textarea);
1605
+ textarea.select();
1606
+ document.execCommand("copy");
1607
+ document.body.removeChild(textarea);
1608
+ }
1609
+ setCopied(true);
1610
+ setTimeout(() => setCopied(false), 1500);
1611
+ } catch (_) {
1612
+ setCopied(false);
1613
+ }
1614
+ }, [rawCode]);
1615
+ const containerStyle = {
1616
+ borderRadius: "12px",
1617
+ overflow: "hidden",
1618
+ margin: "1.5rem 0",
1619
+ background: "var(--color-brand-100, #e0f2ff)",
1620
+ fontFamily: "var(--font-mono)",
1621
+ fontSize: "0.85rem"
1622
+ };
1623
+ const headerStyle = {
1624
+ display: "flex",
1625
+ alignItems: "center",
1626
+ justifyContent: "space-between",
1627
+ padding: "1rem 1.25rem",
1628
+ fontWeight: 700,
1629
+ background: "var(--color-brand-200, #dbeafe)",
1630
+ color: "var(--color-brand-900)"
1631
+ };
1632
+ const preStyle = {
1633
+ margin: 0,
1634
+ background: "var(--color-brand-100, #e0f2ff)",
1635
+ color: "var(--color-brand-800, #1f2d5c)",
1636
+ lineHeight: 1.55,
1637
+ padding: "1rem 0",
1638
+ overflowX: "auto"
1639
+ };
1640
+ const codeStyle = {
1641
+ display: "block",
1642
+ padding: 0
1643
+ };
1644
+ const lineContentStyle = {
1645
+ whiteSpace: "pre",
1646
+ display: "inline"
1647
+ };
1648
+ const showFilename = Boolean(filename);
1649
+ const { style: preStyleOverride, className: preClassName, ...preRest } = rest;
1650
+ const mergedPreStyle = Object.assign({}, preStyle, preStyleOverride || {});
1651
+ const lineElements = lines.map((line, index) => {
1652
+ const lineNumber = index + 1;
1653
+ const highlight = highlightSet.has(lineNumber);
1654
+ const style = highlight ? { ...baseLineStyle, ...highlightBaseStyle } : baseLineStyle;
1655
+ const displayLine = line === "" ? " " : line;
1656
+ return React22.createElement(
1657
+ "span",
1658
+ { key: lineNumber, style },
1659
+ React22.createElement("span", { style: lineContentStyle }, displayLine)
1660
+ );
1661
+ });
1662
+ return React22.createElement(
1663
+ "div",
1664
+ { style: containerStyle },
1665
+ React22.createElement(
1666
+ "div",
1667
+ { style: headerStyle },
1668
+ React22.createElement("span", null, showFilename ? filename : null),
1669
+ enableCopy ? React22.createElement(
1670
+ "button",
1671
+ {
1672
+ type: "button",
1673
+ onClick: handleCopy,
1674
+ style: {
1675
+ border: "1px solid var(--color-brand-300, #bfdbfe)",
1676
+ borderRadius: "6px",
1677
+ padding: "0.2rem 0.55rem",
1678
+ fontSize: "0.7rem",
1679
+ fontWeight: 500,
1680
+ background: "color-mix(in srgb, var(--color-brand-100, #e0f2ff) 55%, #ffffff)",
1681
+ color: "var(--color-brand-700, #1d4ed8)",
1682
+ cursor: "pointer"
1683
+ }
1684
+ },
1685
+ copied ? "Copied" : "Copy"
1686
+ ) : null
1687
+ ),
1688
+ React22.createElement(
1689
+ "pre",
1690
+ { ...preRest, className: preClassName, style: mergedPreStyle },
1691
+ React22.createElement("code", { style: codeStyle }, lineElements)
1692
+ )
1693
+ );
1134
1694
  }
1135
1695
  export {
1696
+ CanopyBrand,
1697
+ CanopyHeader,
1698
+ CanopyModal,
1699
+ DocsCodeBlock,
1136
1700
  HelloWorld,
1137
1701
  interstitials_exports as Interstitials,
1138
1702
  Label,