@canopy-iiif/app 1.5.14 → 1.5.16

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.
@@ -891,7 +891,7 @@ function NavigationTreeItem({ node, depth, nodeKey }) {
891
891
  "data-expanded": allowToggle ? defaultExpanded ? "true" : "false" : void 0,
892
892
  "data-default-expanded": allowToggle && defaultExpanded ? "true" : void 0
893
893
  },
894
- /* @__PURE__ */ React10.createElement("div", { className: "canopy-nav-tree__row" }, /* @__PURE__ */ React10.createElement(
894
+ /* @__PURE__ */ React10.createElement("div", { className: "canopy-nav-tree__row" }, /* @__PURE__ */ React10.createElement("div", { className: "canopy-nav-tree__link-wrapper" }, /* @__PURE__ */ React10.createElement(
895
895
  Tag,
896
896
  {
897
897
  className: classes.join(" "),
@@ -901,7 +901,7 @@ function NavigationTreeItem({ node, depth, nodeKey }) {
901
901
  },
902
902
  node.title || node.slug,
903
903
  isRoadmap ? /* @__PURE__ */ React10.createElement("span", { className: "canopy-nav-tree__badge" }, "Roadmap") : null
904
- ), allowToggle ? /* @__PURE__ */ React10.createElement(
904
+ )), allowToggle ? /* @__PURE__ */ React10.createElement(
905
905
  "button",
906
906
  {
907
907
  type: "button",
@@ -921,7 +921,14 @@ function NavigationTreeItem({ node, depth, nodeKey }) {
921
921
  strokeWidth: "1.5",
922
922
  className: "canopy-nav-tree__toggle-icon"
923
923
  },
924
- /* @__PURE__ */ React10.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 9l7 7 7-7" })
924
+ /* @__PURE__ */ React10.createElement(
925
+ "path",
926
+ {
927
+ strokeLinecap: "round",
928
+ strokeLinejoin: "round",
929
+ d: "M5 9l7 7 7-7"
930
+ }
931
+ )
925
932
  ),
926
933
  /* @__PURE__ */ React10.createElement("span", { className: "sr-only" }, toggleLabel)
927
934
  ) : null),
@@ -966,7 +973,14 @@ function NavigationTree({
966
973
  ...rest
967
974
  },
968
975
  heading ? /* @__PURE__ */ React10.createElement("div", { className: headingClassName }, heading) : null,
969
- /* @__PURE__ */ React10.createElement(NavigationTreeList, { nodes, depth: includeRoot ? -1 : 0, parentKey })
976
+ /* @__PURE__ */ React10.createElement(
977
+ NavigationTreeList,
978
+ {
979
+ nodes,
980
+ depth: includeRoot ? -1 : 0,
981
+ parentKey
982
+ }
983
+ )
970
984
  );
971
985
  }
972
986
 
@@ -1047,12 +1061,14 @@ import navigationHelpers3 from "@canopy-iiif/app/lib/components/navigation.js";
1047
1061
  import React12 from "react";
1048
1062
  var SCROLL_OFFSET_REM = 1.618;
1049
1063
  var MAX_HEADING_DEPTH = 3;
1050
- function depthIndex(depth) {
1051
- return Math.max(0, Math.min(5, (depth || 1) - 1));
1052
- }
1053
1064
  function resolveDepth(value, fallback = 1) {
1054
1065
  return Math.max(1, typeof value === "number" ? value : fallback);
1055
1066
  }
1067
+ function buildNodeKey(id, parentKey, index) {
1068
+ const base = id ? String(id) : `section-${parentKey || "root"}-${index}`;
1069
+ const sanitized = base.replace(/[^a-zA-Z0-9_-]/g, "-");
1070
+ return `${parentKey || "section"}-${sanitized || index}`;
1071
+ }
1056
1072
  function ContentNavigation({
1057
1073
  items = [],
1058
1074
  className = "",
@@ -1076,17 +1092,16 @@ function ContentNavigation({
1076
1092
  ].filter(Boolean).join(" ");
1077
1093
  const effectiveHeading = heading || pageTitle || null;
1078
1094
  const navLabel = ariaLabel || (effectiveHeading ? `${effectiveHeading} navigation` : "Section navigation");
1079
- const getSavedDepth = React12.useCallback(
1080
- (id, fallback) => {
1081
- if (!id) return fallback;
1082
- if (!savedDepthsRef.current) savedDepthsRef.current = /* @__PURE__ */ new Map();
1083
- const store = savedDepthsRef.current;
1084
- if (store.has(id)) return store.get(id);
1085
- store.set(id, fallback);
1086
- return fallback;
1087
- },
1088
- []
1089
- );
1095
+ const toggleSrLabel = isExpanded ? "Hide section navigation" : "Show section navigation";
1096
+ const toggleStateClass = isExpanded ? "is-expanded" : "is-collapsed";
1097
+ const getSavedDepth = React12.useCallback((id, fallback) => {
1098
+ if (!id) return fallback;
1099
+ if (!savedDepthsRef.current) savedDepthsRef.current = /* @__PURE__ */ new Map();
1100
+ const store = savedDepthsRef.current;
1101
+ if (store.has(id)) return store.get(id);
1102
+ store.set(id, fallback);
1103
+ return fallback;
1104
+ }, []);
1090
1105
  const headingEntries = React12.useMemo(() => {
1091
1106
  const entries = [];
1092
1107
  const seen = /* @__PURE__ */ new Set();
@@ -1118,6 +1133,9 @@ function ContentNavigation({
1118
1133
  const [activeId, setActiveId] = React12.useState(fallbackId);
1119
1134
  const activeIdRef = React12.useRef(activeId);
1120
1135
  React12.useEffect(() => {
1136
+ if (process.env.NODE_ENV !== "production" && activeIdRef.current !== activeId) {
1137
+ console.log("[ContentNavigation] activeId changed:", activeId);
1138
+ }
1121
1139
  activeIdRef.current = activeId;
1122
1140
  }, [activeId]);
1123
1141
  React12.useEffect(() => {
@@ -1143,16 +1161,28 @@ function ContentNavigation({
1143
1161
  (elements) => {
1144
1162
  if (!elements || !elements.length) return;
1145
1163
  const offset = computeOffsetPx();
1146
- let nextId = elements[0].id;
1147
- for (const { id, element } of elements) {
1164
+ const viewportLimit = typeof window !== "undefined" && window.innerHeight ? window.innerHeight * 0.5 : 0;
1165
+ let fallbackId2 = elements[0].id;
1166
+ let bestId = fallbackId2;
1167
+ let bestDistance = Number.POSITIVE_INFINITY;
1168
+ elements.forEach(({ id, element }) => {
1169
+ if (!element || !id) return;
1148
1170
  const rect = element.getBoundingClientRect();
1149
- if (rect.top - offset <= 0) {
1150
- nextId = id;
1151
- } else {
1152
- break;
1171
+ const relativeTop = rect.top - offset;
1172
+ if (viewportLimit > 0 && relativeTop < -viewportLimit) {
1173
+ return;
1153
1174
  }
1154
- }
1175
+ const distance = Math.abs(relativeTop);
1176
+ if (distance < bestDistance) {
1177
+ bestDistance = distance;
1178
+ bestId = id;
1179
+ }
1180
+ });
1181
+ const nextId = bestId || fallbackId2;
1155
1182
  if (nextId && nextId !== activeIdRef.current) {
1183
+ if (process.env.NODE_ENV !== "production") {
1184
+ console.log("[ContentNavigation] updateActive ->", nextId);
1185
+ }
1156
1186
  activeIdRef.current = nextId;
1157
1187
  setActiveId(nextId);
1158
1188
  }
@@ -1173,6 +1203,13 @@ function ContentNavigation({
1173
1203
  if (!ticking) {
1174
1204
  ticking = true;
1175
1205
  window.requestAnimationFrame(() => {
1206
+ if (process.env.NODE_ENV !== "production") {
1207
+ console.log(
1208
+ "[ContentNavigation] scroll event",
1209
+ window.scrollY,
1210
+ window.innerHeight
1211
+ );
1212
+ }
1176
1213
  updateActiveFromElements(elements);
1177
1214
  ticking = false;
1178
1215
  });
@@ -1189,7 +1226,8 @@ function ContentNavigation({
1189
1226
  (event, targetId, options = {}) => {
1190
1227
  var _a;
1191
1228
  try {
1192
- if (event && typeof event.preventDefault === "function") event.preventDefault();
1229
+ if (event && typeof event.preventDefault === "function")
1230
+ event.preventDefault();
1193
1231
  } catch (_) {
1194
1232
  }
1195
1233
  if (!isBrowser) return;
@@ -1216,10 +1254,10 @@ function ContentNavigation({
1216
1254
  },
1217
1255
  [computeOffsetPx, headingEntries, headingId, isBrowser]
1218
1256
  );
1219
- const renderNodes = React12.useCallback(
1220
- (nodes) => {
1221
- if (!nodes || !nodes.length) return null;
1222
- return nodes.map((node) => {
1257
+ const navTreeRoot = React12.useMemo(() => {
1258
+ function mapNodes(nodes2, parentKey = "section") {
1259
+ if (!Array.isArray(nodes2) || !nodes2.length) return [];
1260
+ return nodes2.map((node, index) => {
1223
1261
  if (!node) return null;
1224
1262
  const id = node.id ? String(node.id) : "";
1225
1263
  const depth = resolveDepth(
@@ -1227,48 +1265,34 @@ function ContentNavigation({
1227
1265
  getSavedDepth(id, 2)
1228
1266
  );
1229
1267
  if (depth > MAX_HEADING_DEPTH) return null;
1230
- const idx = depthIndex(depth);
1268
+ const key = buildNodeKey(
1269
+ id || node.title || `section-${index}`,
1270
+ parentKey,
1271
+ index
1272
+ );
1273
+ const href = id ? `#${id}` : "#";
1274
+ const childNodes = mapNodes(node.children || [], key);
1231
1275
  const isActive = id && activeId === id;
1232
- const childNodes = depth < MAX_HEADING_DEPTH ? renderNodes(node.children) : null;
1233
- return /* @__PURE__ */ React12.createElement("li", { key: id || node.title, className: "canopy-sub-navigation__item", "data-depth": idx }, /* @__PURE__ */ React12.createElement(
1234
- "a",
1235
- {
1236
- className: `canopy-sub-navigation__link depth-${idx}${isActive ? " is-active" : ""}`,
1237
- href: id ? `#${id}` : "#",
1238
- onClick: (event) => handleAnchorClick(event, id || null),
1239
- "aria-current": isActive ? "location" : void 0
1240
- },
1241
- node.title
1242
- ), childNodes ? /* @__PURE__ */ React12.createElement(
1243
- "ul",
1244
- {
1245
- className: "canopy-sub-navigation__list canopy-sub-navigation__list--nested",
1246
- role: "list"
1247
- },
1248
- childNodes
1249
- ) : null);
1250
- });
1251
- },
1252
- [handleAnchorClick, activeId, getSavedDepth]
1253
- );
1254
- const nestedItems = React12.useMemo(() => renderNodes(items), [items, renderNodes]);
1255
- const topLink = headingId ? /* @__PURE__ */ React12.createElement("li", { className: "canopy-sub-navigation__item", "data-depth": 0 }, /* @__PURE__ */ React12.createElement(
1256
- "a",
1257
- {
1258
- className: `canopy-sub-navigation__link depth-0${activeId === headingId ? " is-active" : ""}`,
1259
- href: `#${headingId}`,
1260
- onClick: (event) => handleAnchorClick(event, headingId, { scrollToTop: true }),
1261
- "aria-current": activeId === headingId ? "location" : void 0
1262
- },
1263
- effectiveHeading || pageTitle || headingId
1264
- ), nestedItems ? /* @__PURE__ */ React12.createElement(
1265
- "ul",
1266
- {
1267
- className: "canopy-sub-navigation__list canopy-sub-navigation__list--nested",
1268
- role: "list"
1269
- },
1270
- nestedItems
1271
- ) : null) : null;
1276
+ const hasActiveChild = childNodes.some(
1277
+ (child) => child && child.isActive
1278
+ );
1279
+ return {
1280
+ slug: key,
1281
+ title: node.title || node.text || id || `Section ${index + 1}`,
1282
+ href,
1283
+ children: childNodes,
1284
+ isActive: Boolean(isActive),
1285
+ isExpanded: isActive || hasActiveChild
1286
+ };
1287
+ }).filter(Boolean);
1288
+ }
1289
+ const nodes = mapNodes(items, "section");
1290
+ return {
1291
+ slug: "content-nav-root",
1292
+ title: effectiveHeading || pageTitle || "On this page",
1293
+ children: nodes
1294
+ };
1295
+ }, [items, effectiveHeading, pageTitle, activeId, getSavedDepth]);
1272
1296
  return /* @__PURE__ */ React12.createElement(
1273
1297
  "nav",
1274
1298
  {
@@ -1281,20 +1305,101 @@ function ContentNavigation({
1281
1305
  "button",
1282
1306
  {
1283
1307
  type: "button",
1284
- className: "canopy-content-navigation__toggle",
1308
+ className: `canopy-content-navigation__toggle ${toggleStateClass}`,
1285
1309
  "aria-expanded": isExpanded,
1286
- "aria-label": isExpanded ? "Hide section navigation" : "Show section navigation",
1287
- title: isExpanded ? "Hide section navigation" : "Show section navigation",
1310
+ "aria-label": toggleSrLabel,
1311
+ title: toggleSrLabel,
1288
1312
  onClick: handleToggle,
1289
1313
  "data-canopy-content-nav-toggle": "true",
1290
1314
  "data-show-label": "Show",
1291
1315
  "data-hide-label": "Hide",
1292
- "data-show-full-label": "Show section navigation",
1293
- "data-hide-full-label": "Hide section navigation"
1316
+ "data-show-full-label": "Show content navigation",
1317
+ "data-hide-full-label": "Hide content navigation"
1294
1318
  },
1295
- isExpanded ? "Hide" : "Show"
1319
+ /* @__PURE__ */ React12.createElement(
1320
+ "span",
1321
+ {
1322
+ className: "canopy-content-navigation__toggle-icon",
1323
+ "aria-hidden": "true"
1324
+ },
1325
+ /* @__PURE__ */ React12.createElement(
1326
+ "svg",
1327
+ {
1328
+ xmlns: "http://www.w3.org/2000/svg",
1329
+ class: "ionicon",
1330
+ viewBox: "0 0 512 512"
1331
+ },
1332
+ /* @__PURE__ */ React12.createElement(
1333
+ "path",
1334
+ {
1335
+ fill: "none",
1336
+ stroke: "currentColor",
1337
+ "stroke-linecap": "round",
1338
+ "stroke-linejoin": "round",
1339
+ "stroke-width": "50",
1340
+ d: "M160 144h288M160 256h288M160 368h288"
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ React12.createElement(
1344
+ "circle",
1345
+ {
1346
+ cx: "80",
1347
+ cy: "144",
1348
+ r: "16",
1349
+ fill: "none",
1350
+ stroke: "currentColor",
1351
+ "stroke-linecap": "round",
1352
+ "stroke-linejoin": "round",
1353
+ "stroke-width": "32"
1354
+ }
1355
+ ),
1356
+ /* @__PURE__ */ React12.createElement(
1357
+ "circle",
1358
+ {
1359
+ cx: "80",
1360
+ cy: "256",
1361
+ r: "16",
1362
+ fill: "none",
1363
+ stroke: "currentColor",
1364
+ "stroke-linecap": "round",
1365
+ "stroke-linejoin": "round",
1366
+ "stroke-width": "32"
1367
+ }
1368
+ ),
1369
+ /* @__PURE__ */ React12.createElement(
1370
+ "circle",
1371
+ {
1372
+ cx: "80",
1373
+ cy: "368",
1374
+ r: "16",
1375
+ fill: "none",
1376
+ stroke: "currentColor",
1377
+ "stroke-linecap": "round",
1378
+ "stroke-linejoin": "round",
1379
+ "stroke-width": "32"
1380
+ }
1381
+ )
1382
+ )
1383
+ ),
1384
+ /* @__PURE__ */ React12.createElement(
1385
+ "span",
1386
+ {
1387
+ className: "canopy-content-navigation__toggle-label",
1388
+ "data-canopy-content-nav-toggle-label": "true"
1389
+ },
1390
+ /* @__PURE__ */ React12.createElement("span", { className: "sr-only" }, toggleSrLabel)
1391
+ ),
1392
+ /* @__PURE__ */ React12.createElement("span", { className: "sr-only", "data-canopy-content-nav-toggle-sr": "true" }, toggleSrLabel)
1296
1393
  ),
1297
- /* @__PURE__ */ React12.createElement("ul", { className: "canopy-sub-navigation__list", role: "list" }, topLink || nestedItems)
1394
+ /* @__PURE__ */ React12.createElement(
1395
+ NavigationTree,
1396
+ {
1397
+ root: navTreeRoot,
1398
+ includeRoot: false,
1399
+ parentKey: "content-nav",
1400
+ className: "canopy-sub-navigation__tree canopy-content-navigation__tree"
1401
+ }
1402
+ )
1298
1403
  );
1299
1404
  }
1300
1405
 
@@ -1384,18 +1489,219 @@ function ContentNavigationScript() {
1384
1489
  var layout = root.closest('.canopy-layout');
1385
1490
  if (layout) layout.classList.toggle('canopy-layout--content-nav-collapsed', isCollapsed);
1386
1491
  var nav = root.querySelector('[data-canopy-content-nav]');
1387
- if (nav) nav.classList.toggle('canopy-content-navigation--collapsed', isCollapsed);
1492
+ if (nav) {
1493
+ nav.classList.toggle('canopy-content-navigation--collapsed', isCollapsed);
1494
+ nav.classList.toggle('canopy-content-navigation--expanded', !isCollapsed);
1495
+ nav.setAttribute('data-expanded', isCollapsed ? 'false' : 'true');
1496
+ }
1388
1497
  var toggle = root.querySelector('[data-canopy-content-nav-toggle]');
1389
1498
  if (toggle) {
1390
1499
  var showLabel = toggle.getAttribute('data-show-label') || 'Show';
1391
1500
  var hideLabel = toggle.getAttribute('data-hide-label') || 'Hide';
1392
1501
  var showFull = toggle.getAttribute('data-show-full-label') || 'Show section navigation';
1393
1502
  var hideFull = toggle.getAttribute('data-hide-full-label') || 'Hide section navigation';
1394
- toggle.textContent = isCollapsed ? showLabel : hideLabel;
1503
+ var labelNode = toggle.querySelector('[data-canopy-content-nav-toggle-label]');
1504
+ var srNode = toggle.querySelector('[data-canopy-content-nav-toggle-sr]');
1395
1505
  toggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
1396
1506
  toggle.setAttribute('aria-label', isCollapsed ? showFull : hideFull);
1397
1507
  toggle.setAttribute('title', isCollapsed ? showFull : hideFull);
1508
+ toggle.classList.toggle('is-collapsed', isCollapsed);
1509
+ toggle.classList.toggle('is-expanded', !isCollapsed);
1510
+ if (!labelNode) {
1511
+ toggle.textContent = isCollapsed ? showLabel : hideLabel;
1512
+ }
1513
+ if (srNode) {
1514
+ srNode.textContent = isCollapsed ? showFull : hideFull;
1515
+ }
1398
1516
  }
1517
+ if (root.__canopyContentNavSync) {
1518
+ try {
1519
+ root.__canopyContentNavSync();
1520
+ } catch (_) {}
1521
+ }
1522
+ }
1523
+
1524
+ function setupFloatingState(root) {
1525
+ if (!root || typeof IntersectionObserver !== 'function') return;
1526
+ if (root.__canopyContentNavFloating) return;
1527
+ var sentinel = root.querySelector('[data-canopy-content-nav-sentinel]');
1528
+ var placeholder = root.querySelector('[data-canopy-content-nav-placeholder]');
1529
+ var nav = root.querySelector('[data-canopy-content-nav]');
1530
+ if (!sentinel || !nav) return;
1531
+ root.__canopyContentNavFloating = true;
1532
+
1533
+ function syncPosition() {
1534
+ try {
1535
+ var rect = root.getBoundingClientRect();
1536
+ nav.style.setProperty('--canopy-content-nav-fixed-right', '1.618rem');
1537
+ nav.style.setProperty('--canopy-content-nav-fixed-width', rect.width + 'px');
1538
+ if (placeholder) placeholder.style.width = rect.width + 'px';
1539
+ } catch (_) {}
1540
+ }
1541
+
1542
+ var observer = new IntersectionObserver(function (entries) {
1543
+ entries.forEach(function (entry) {
1544
+ var stuck = !entry.isIntersecting && entry.boundingClientRect.top < 0;
1545
+ nav.classList.toggle('canopy-content-navigation--floating', stuck);
1546
+ nav.setAttribute('data-stuck', stuck ? 'true' : 'false');
1547
+ if (placeholder) {
1548
+ placeholder.style.height = stuck ? nav.offsetHeight + 'px' : '0px';
1549
+ }
1550
+ });
1551
+ });
1552
+
1553
+ observer.observe(sentinel);
1554
+ syncPosition();
1555
+ var handleResize = function () {
1556
+ syncPosition();
1557
+ };
1558
+ window.addEventListener('resize', handleResize);
1559
+ root.__canopyContentNavCleanup = function () {
1560
+ observer.disconnect();
1561
+ window.removeEventListener('resize', handleResize);
1562
+ };
1563
+ root.__canopyContentNavSync = syncPosition;
1564
+ }
1565
+
1566
+ function computeOffsetPx() {
1567
+ try {
1568
+ var root = document.documentElement;
1569
+ var size = root ? parseFloat(window.getComputedStyle(root).fontSize || '16') || 16 : 16;
1570
+ return size * 1.618;
1571
+ } catch (error) {
1572
+ return 0;
1573
+ }
1574
+ }
1575
+
1576
+ function setupActiveHeadingWatcher(root) {
1577
+ if (!root || root.__canopyContentNavActive) return;
1578
+ var nav = root.querySelector('[data-canopy-content-nav]');
1579
+ if (!nav) return;
1580
+ var linkNodes = Array.prototype.slice.call(
1581
+ nav.querySelectorAll('.canopy-nav-tree__link[href^="#"]')
1582
+ );
1583
+ var entries = linkNodes
1584
+ .map(function (link) {
1585
+ if (!link || !link.getAttribute) return null;
1586
+ var href = link.getAttribute('href') || '';
1587
+ if (!href || href.charAt(0) !== '#') return null;
1588
+ var id = href.slice(1);
1589
+ if (!id) return null;
1590
+ var target = document.getElementById(id);
1591
+ if (!target) return null;
1592
+ return {
1593
+ id: id,
1594
+ link: link,
1595
+ target: target,
1596
+ item: link.closest('[data-canopy-nav-item]') || null,
1597
+ };
1598
+ })
1599
+ .filter(Boolean);
1600
+ if (!entries.length) return;
1601
+ root.__canopyContentNavActive = true;
1602
+ var activeId = null;
1603
+
1604
+ function expandParents(link) {
1605
+ var parent = link ? link.closest('[data-canopy-nav-item]') : null;
1606
+ while (parent) {
1607
+ parent.setAttribute('data-expanded', 'true');
1608
+ var toggle = parent.querySelector('[data-canopy-nav-item-toggle]');
1609
+ if (toggle) {
1610
+ toggle.setAttribute('aria-expanded', 'true');
1611
+ var targetId = toggle.getAttribute('data-canopy-nav-item-toggle');
1612
+ if (targetId) {
1613
+ var panel = document.getElementById(targetId);
1614
+ if (panel) {
1615
+ panel.hidden = false;
1616
+ panel.removeAttribute('hidden');
1617
+ panel.setAttribute('aria-hidden', 'false');
1618
+ }
1619
+ }
1620
+ }
1621
+ parent = parent.parentElement
1622
+ ? parent.parentElement.closest('[data-canopy-nav-item]')
1623
+ : null;
1624
+ }
1625
+ }
1626
+
1627
+ function applyActive(id) {
1628
+ if (!id || activeId === id) return;
1629
+ activeId = id;
1630
+ var activeParents = new Set();
1631
+ entries.forEach(function (entry) {
1632
+ var isActive = entry.id === id;
1633
+ entry.link.classList.toggle('is-active', isActive);
1634
+ if (entry.item) entry.item.classList.toggle('is-active', isActive);
1635
+ if (isActive) {
1636
+ expandParents(entry.link);
1637
+ var parent = entry.link.closest('[data-canopy-nav-item]');
1638
+ while (parent) {
1639
+ activeParents.add(parent);
1640
+ parent = parent.parentElement
1641
+ ? parent.parentElement.closest('[data-canopy-nav-item]')
1642
+ : null;
1643
+ }
1644
+ }
1645
+ });
1646
+ entries.forEach(function (entry) {
1647
+ var item = entry.item;
1648
+ if (!item) return;
1649
+ if (!activeParents.has(item) && entry.id !== id) {
1650
+ item.setAttribute('data-expanded', 'false');
1651
+ var toggle = item.querySelector('[data-canopy-nav-item-toggle]');
1652
+ if (toggle) {
1653
+ toggle.setAttribute('aria-expanded', 'false');
1654
+ var targetId = toggle.getAttribute('data-canopy-nav-item-toggle');
1655
+ if (targetId) {
1656
+ var panel = document.getElementById(targetId);
1657
+ if (panel) {
1658
+ panel.hidden = true;
1659
+ panel.setAttribute('hidden', '');
1660
+ panel.setAttribute('aria-hidden', 'true');
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+ });
1666
+ }
1667
+
1668
+ function updateActive() {
1669
+ var offset = computeOffsetPx();
1670
+ var baseFont = 16;
1671
+ try {
1672
+ var root = document.documentElement;
1673
+ baseFont = parseFloat(window.getComputedStyle(root).fontSize || '16') || 16;
1674
+ } catch (_) {}
1675
+ var proximityLimit = baseFont * 5;
1676
+ var fallbackId = entries[0].id;
1677
+ var bestId = fallbackId;
1678
+ var bestDistance = Number.POSITIVE_INFINITY;
1679
+ entries.forEach(function (entry) {
1680
+ if (!entry || !entry.target) return;
1681
+ var rect = entry.target.getBoundingClientRect();
1682
+ var relativeTop = rect.top - offset;
1683
+ if (relativeTop < -proximityLimit) return;
1684
+ var distance = Math.abs(relativeTop);
1685
+ if (distance < bestDistance) {
1686
+ bestDistance = distance;
1687
+ bestId = entry.id;
1688
+ }
1689
+ });
1690
+ applyActive(bestId || fallbackId);
1691
+ }
1692
+
1693
+ updateActive();
1694
+ var ticking = false;
1695
+ function handle() {
1696
+ if (ticking) return;
1697
+ ticking = true;
1698
+ window.requestAnimationFrame(function () {
1699
+ updateActive();
1700
+ ticking = false;
1701
+ });
1702
+ }
1703
+ window.addEventListener('scroll', handle, { passive: true });
1704
+ window.addEventListener('resize', handle);
1399
1705
  }
1400
1706
 
1401
1707
  ready(function () {
@@ -1405,7 +1711,16 @@ function ContentNavigationScript() {
1405
1711
  if (!roots.length) return;
1406
1712
  var stored = getStored();
1407
1713
  var collapsed = true;
1408
- if (stored === '0' || stored === 'false') {
1714
+ var isDesktop = false;
1715
+ try {
1716
+ var bp = window.getComputedStyle(document.documentElement).getPropertyValue('--canopy-desktop-breakpoint') || '70rem';
1717
+ isDesktop = window.matchMedia('(min-width: ' + bp.trim() + ')').matches;
1718
+ } catch (_) {
1719
+ isDesktop = false;
1720
+ }
1721
+ if (!isDesktop) {
1722
+ collapsed = true;
1723
+ } else if (stored === '0' || stored === 'false') {
1409
1724
  collapsed = false;
1410
1725
  } else if (stored === '1' || stored === 'true') {
1411
1726
  collapsed = true;
@@ -1429,6 +1744,11 @@ function ContentNavigationScript() {
1429
1744
  sync(!collapsed);
1430
1745
  });
1431
1746
  });
1747
+
1748
+ roots.forEach(function (root) {
1749
+ setupFloatingState(root);
1750
+ setupActiveHeadingWatcher(root);
1751
+ });
1432
1752
  });
1433
1753
  })();
1434
1754
  `;
@@ -1497,6 +1817,13 @@ function Layout({
1497
1817
  className: contentNavigationAsideClassName,
1498
1818
  "data-canopy-content-nav-root": "true"
1499
1819
  },
1820
+ /* @__PURE__ */ React13.createElement(
1821
+ "div",
1822
+ {
1823
+ "data-canopy-content-nav-sentinel": "true",
1824
+ "aria-hidden": "true"
1825
+ }
1826
+ ),
1500
1827
  /* @__PURE__ */ React13.createElement(
1501
1828
  ContentNavigation,
1502
1829
  {
@@ -1505,6 +1832,13 @@ function Layout({
1505
1832
  headingId: headingAnchorId || void 0,
1506
1833
  pageTitle: context && context.page ? context.page.title : void 0
1507
1834
  }
1835
+ ),
1836
+ /* @__PURE__ */ React13.createElement(
1837
+ "div",
1838
+ {
1839
+ "data-canopy-content-nav-placeholder": "true",
1840
+ "aria-hidden": "true"
1841
+ }
1508
1842
  )
1509
1843
  ), /* @__PURE__ */ React13.createElement(ContentNavigationScript, null)) : null);
1510
1844
  }
@@ -2132,7 +2466,7 @@ function getSharedRoot() {
2132
2466
  function getSafePageContext() {
2133
2467
  const root = getSharedRoot();
2134
2468
  if (root && root[CONTEXT_KEY]) return root[CONTEXT_KEY];
2135
- const ctx = React20.createContext({ navigation: null, page: null });
2469
+ const ctx = React20.createContext({ navigation: null, page: null, site: null });
2136
2470
  if (root) root[CONTEXT_KEY] = ctx;
2137
2471
  return ctx;
2138
2472
  }
@@ -2199,13 +2533,18 @@ function CanopyHeader(props = {}) {
2199
2533
  searchHotkey = "mod+k",
2200
2534
  searchPlaceholder = "Search\u2026",
2201
2535
  brandHref = "/",
2202
- title = "Canopy IIIF",
2536
+ title: titleProp,
2203
2537
  logo: SiteLogo
2204
2538
  } = props;
2205
2539
  const navLinks = ensureArray(navLinksProp);
2206
2540
  const PageContext = getSafePageContext();
2207
2541
  const context = React20.useContext(PageContext);
2208
2542
  const contextNavigation = context && context.navigation ? context.navigation : null;
2543
+ const contextSite = context && context.site ? context.site : null;
2544
+ const contextSiteTitle = contextSite && typeof contextSite.title === "string" ? contextSite.title.trim() : "";
2545
+ const defaultHeaderTitle = contextSiteTitle || "Site title";
2546
+ const normalizedTitleProp = typeof titleProp === "string" ? titleProp.trim() : "";
2547
+ const resolvedTitle = normalizedTitleProp || defaultHeaderTitle;
2209
2548
  const sectionNavigation = contextNavigation && contextNavigation.root ? contextNavigation : null;
2210
2549
  const navigationRoots = contextNavigation && contextNavigation.allRoots ? contextNavigation.allRoots : null;
2211
2550
  const sectionHeading = sectionNavigation && sectionNavigation.title || (sectionNavigation && sectionNavigation.root ? sectionNavigation.root.title : "");
@@ -2234,7 +2573,7 @@ function CanopyHeader(props = {}) {
2234
2573
  /* @__PURE__ */ React20.createElement("div", { className: "canopy-header__brand" }, /* @__PURE__ */ React20.createElement(
2235
2574
  CanopyBrand,
2236
2575
  {
2237
- label: title,
2576
+ label: resolvedTitle,
2238
2577
  href: brandHref,
2239
2578
  className: "canopy-header__brand-link",
2240
2579
  Logo: SiteLogo
@@ -2329,7 +2668,7 @@ function CanopyHeader(props = {}) {
2329
2668
  id: "canopy-modal-nav",
2330
2669
  variant: "nav",
2331
2670
  labelledBy: "canopy-modal-nav-label",
2332
- label: title,
2671
+ label: resolvedTitle,
2333
2672
  logo: SiteLogo,
2334
2673
  href: brandHref,
2335
2674
  closeLabel: "Close navigation",
@@ -2424,7 +2763,7 @@ function CanopyHeader(props = {}) {
2424
2763
  id: "canopy-modal-search",
2425
2764
  variant: "search",
2426
2765
  labelledBy: "canopy-modal-search-label",
2427
- label: title,
2766
+ label: resolvedTitle,
2428
2767
  logo: SiteLogo,
2429
2768
  href: brandHref,
2430
2769
  closeLabel: "Close search",