@canopy-iiif/app 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -2075,7 +2075,7 @@ function CanopyDiagram() {
2075
2075
  }
2076
2076
 
2077
2077
  // ui/src/content/timeline/Timeline.jsx
2078
- import React33 from "react";
2078
+ import React34 from "react";
2079
2079
 
2080
2080
  // ui/src/content/timeline/date-utils.js
2081
2081
  var FALLBACK_LOCALE = (() => {
@@ -2227,6 +2227,56 @@ function clampProgress(value) {
2227
2227
  return value;
2228
2228
  }
2229
2229
 
2230
+ // ui/src/layout/ReferencedManifestCard.jsx
2231
+ import React33 from "react";
2232
+ function normalizeMetadata(metadata, summary) {
2233
+ if (Array.isArray(metadata) && metadata.length) {
2234
+ return metadata.filter(Boolean);
2235
+ }
2236
+ if (summary) return [summary];
2237
+ return [];
2238
+ }
2239
+ function ReferencedManifestCard({
2240
+ manifest = null,
2241
+ href,
2242
+ title,
2243
+ summary,
2244
+ metadata,
2245
+ thumbnail,
2246
+ type,
2247
+ className = "",
2248
+ ...rest
2249
+ }) {
2250
+ var _a, _b, _c, _d, _e, _f;
2251
+ const record = manifest || {};
2252
+ const resolvedHref = (_a = href != null ? href : record.href) != null ? _a : "";
2253
+ const resolvedTitle = (_c = (_b = title != null ? title : record.title) != null ? _b : record.href) != null ? _c : "";
2254
+ const resolvedSummary = (_d = summary != null ? summary : record.summary) != null ? _d : "";
2255
+ const resolvedMetadata = normalizeMetadata(
2256
+ metadata != null ? metadata : record.metadata,
2257
+ resolvedSummary
2258
+ );
2259
+ const resolvedThumbnail = (_e = thumbnail != null ? thumbnail : record.thumbnail) != null ? _e : null;
2260
+ const resolvedType = (_f = type != null ? type : record.type) != null ? _f : "work";
2261
+ const classes = [
2262
+ "canopy-referenced-manifest-card",
2263
+ className
2264
+ ].filter(Boolean).join(" ");
2265
+ return /* @__PURE__ */ React33.createElement(
2266
+ TeaserCard,
2267
+ {
2268
+ href: resolvedHref || void 0,
2269
+ title: resolvedTitle || resolvedHref || "",
2270
+ summary: resolvedSummary,
2271
+ metadata: resolvedMetadata,
2272
+ thumbnail: resolvedThumbnail,
2273
+ type: resolvedType,
2274
+ className: classes,
2275
+ ...rest
2276
+ }
2277
+ );
2278
+ }
2279
+
2230
2280
  // ui/src/content/timeline/Timeline.jsx
2231
2281
  var DAY_MS = 24 * 60 * 60 * 1e3;
2232
2282
  var DEFAULT_TRACK_HEIGHT = 640;
@@ -2394,24 +2444,14 @@ function TimelineConnector({ side, isActive, highlight }) {
2394
2444
  "canopy-timeline__connector-dot",
2395
2445
  highlight || isActive ? "is-active" : ""
2396
2446
  ].filter(Boolean).join(" ");
2397
- return /* @__PURE__ */ React33.createElement("span", { className: connectorClasses, "aria-hidden": "true" }, side === "left" ? /* @__PURE__ */ React33.createElement(React33.Fragment, null, /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__connector-line" }), /* @__PURE__ */ React33.createElement("span", { className: dotClasses })) : /* @__PURE__ */ React33.createElement(React33.Fragment, null, /* @__PURE__ */ React33.createElement("span", { className: dotClasses }), /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__connector-line" })));
2447
+ return /* @__PURE__ */ React34.createElement("span", { className: connectorClasses, "aria-hidden": "true" }, side === "left" ? /* @__PURE__ */ React34.createElement(React34.Fragment, null, /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__connector-line" }), /* @__PURE__ */ React34.createElement("span", { className: dotClasses })) : /* @__PURE__ */ React34.createElement(React34.Fragment, null, /* @__PURE__ */ React34.createElement("span", { className: dotClasses }), /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__connector-line" })));
2398
2448
  }
2399
2449
  function renderResourceSection(point) {
2400
2450
  if (!point) return null;
2401
2451
  const manifestCards = Array.isArray(point.manifests) ? point.manifests.filter(Boolean) : [];
2402
2452
  const legacyResources = Array.isArray(point.resources) ? point.resources.filter(Boolean) : [];
2403
2453
  if (!manifestCards.length && !legacyResources.length) return null;
2404
- return /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__resources" }, /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__resources-list" }, manifestCards.map((manifest) => /* @__PURE__ */ React33.createElement("div", { key: manifest.id || manifest.href }, /* @__PURE__ */ React33.createElement(
2405
- TeaserCard,
2406
- {
2407
- href: manifest.href,
2408
- title: manifest.title || manifest.href,
2409
- summary: manifest.summary,
2410
- metadata: Array.isArray(manifest.metadata) && manifest.metadata.length ? manifest.metadata : manifest.summary ? [manifest.summary] : [],
2411
- thumbnail: manifest.thumbnail,
2412
- type: manifest.type || "work"
2413
- }
2414
- ))), legacyResources.map((resource, idx) => /* @__PURE__ */ React33.createElement("div", { key: resource.id || resource.href || `legacy-${idx}` }, /* @__PURE__ */ React33.createElement(
2454
+ return /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__resources" }, /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__resources-list" }, manifestCards.map((manifest) => /* @__PURE__ */ React34.createElement("div", { key: manifest.id || manifest.href }, /* @__PURE__ */ React34.createElement(ReferencedManifestCard, { manifest }))), legacyResources.map((resource, idx) => /* @__PURE__ */ React34.createElement("div", { key: resource.id || resource.href || `legacy-${idx}` }, /* @__PURE__ */ React34.createElement(
2415
2455
  TeaserCard,
2416
2456
  {
2417
2457
  href: resource.href,
@@ -2436,26 +2476,26 @@ function Timeline({
2436
2476
  ...rest
2437
2477
  }) {
2438
2478
  const payloadPoints = payload && Array.isArray(payload.points) ? payload.points : null;
2439
- const rawPoints = React33.useMemo(() => {
2479
+ const rawPoints = React34.useMemo(() => {
2440
2480
  if (Array.isArray(pointsProp) && pointsProp.length) return pointsProp;
2441
2481
  if (payloadPoints && payloadPoints.length) return payloadPoints;
2442
2482
  return [];
2443
2483
  }, [pointsProp, payloadPoints]);
2444
- const sanitizedPoints = React33.useMemo(
2484
+ const sanitizedPoints = React34.useMemo(
2445
2485
  () => sanitizePoints(rawPoints),
2446
2486
  [rawPoints]
2447
2487
  );
2448
2488
  const localeValue = payload && payload.locale ? payload.locale : localeProp;
2449
- const baseLocale = React33.useMemo(
2489
+ const baseLocale = React34.useMemo(
2450
2490
  () => createLocale(localeValue),
2451
2491
  [localeValue]
2452
2492
  );
2453
2493
  const rangeInput = payload && payload.range ? payload.range : rangeProp || {};
2454
- const rangeOverrides = React33.useMemo(
2494
+ const rangeOverrides = React34.useMemo(
2455
2495
  () => deriveRangeOverrides(sanitizedPoints, rangeInput),
2456
2496
  [sanitizedPoints, rangeInput]
2457
2497
  );
2458
- const effectiveRange = React33.useMemo(
2498
+ const effectiveRange = React34.useMemo(
2459
2499
  () => normalizeRange({
2460
2500
  ...rangeOverrides,
2461
2501
  locale: baseLocale
@@ -2464,7 +2504,7 @@ function Timeline({
2464
2504
  );
2465
2505
  const spanStart = effectiveRange.startDate.getTime();
2466
2506
  const span = effectiveRange.span;
2467
- const pointsWithPosition = React33.useMemo(() => {
2507
+ const pointsWithPosition = React34.useMemo(() => {
2468
2508
  if (!sanitizedPoints.length) return [];
2469
2509
  return sanitizedPoints.map((point, index) => {
2470
2510
  const timestamp = point.meta.timestamp;
@@ -2478,29 +2518,29 @@ function Timeline({
2478
2518
  };
2479
2519
  });
2480
2520
  }, [sanitizedPoints, spanStart, span]);
2481
- const [activeId, setActiveId] = React33.useState(
2521
+ const [activeId, setActiveId] = React34.useState(
2482
2522
  () => getActivePointId(pointsWithPosition)
2483
2523
  );
2484
- React33.useEffect(() => {
2524
+ React34.useEffect(() => {
2485
2525
  setActiveId(getActivePointId(pointsWithPosition));
2486
2526
  }, [pointsWithPosition]);
2487
2527
  const thresholdValue = typeof thresholdProp === "number" ? thresholdProp : payload && payload.threshold != null ? payload.threshold : null;
2488
2528
  const stepsValue = typeof steps === "number" ? Number(steps) : payload && typeof payload.steps === "number" ? Number(payload.steps) : null;
2489
- const thresholdMs = React33.useMemo(
2529
+ const thresholdMs = React34.useMemo(
2490
2530
  () => getThresholdMs(thresholdValue, effectiveRange.granularity),
2491
2531
  [thresholdValue, effectiveRange.granularity]
2492
2532
  );
2493
- const groupedEntries = React33.useMemo(
2533
+ const groupedEntries = React34.useMemo(
2494
2534
  () => buildGroupedEntries(pointsWithPosition, thresholdMs, {
2495
2535
  granularity: effectiveRange.granularity,
2496
2536
  locale: baseLocale
2497
2537
  }),
2498
2538
  [pointsWithPosition, thresholdMs, effectiveRange.granularity, baseLocale]
2499
2539
  );
2500
- const [expandedGroupIds, setExpandedGroupIds] = React33.useState(
2540
+ const [expandedGroupIds, setExpandedGroupIds] = React34.useState(
2501
2541
  () => /* @__PURE__ */ new Set()
2502
2542
  );
2503
- React33.useEffect(() => {
2543
+ React34.useEffect(() => {
2504
2544
  setExpandedGroupIds((prev) => {
2505
2545
  if (!prev || prev.size === 0) return prev;
2506
2546
  const validIds = new Set(
@@ -2515,7 +2555,7 @@ function Timeline({
2515
2555
  return changed ? next : prev;
2516
2556
  });
2517
2557
  }, [groupedEntries]);
2518
- const toggleGroup = React33.useCallback((groupId) => {
2558
+ const toggleGroup = React34.useCallback((groupId) => {
2519
2559
  setExpandedGroupIds((prev) => {
2520
2560
  const next = new Set(prev || []);
2521
2561
  if (next.has(groupId)) next.delete(groupId);
@@ -2538,7 +2578,7 @@ function Timeline({
2538
2578
  point.id === activeId ? "is-active" : "",
2539
2579
  point.highlight ? "is-highlighted" : ""
2540
2580
  ].filter(Boolean).join(" ");
2541
- const connector = /* @__PURE__ */ React33.createElement(
2581
+ const connector = /* @__PURE__ */ React34.createElement(
2542
2582
  TimelineConnector,
2543
2583
  {
2544
2584
  side: point.side,
@@ -2546,9 +2586,9 @@ function Timeline({
2546
2586
  highlight: point.highlight
2547
2587
  }
2548
2588
  );
2549
- const body = /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__point-body" }, /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__point-date" }, point.meta.label), /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__point-title" }, point.title), point.summary ? /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__point-summary" }, point.summary) : null);
2589
+ const body = /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__point-body" }, /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__point-date" }, point.meta.label), /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__point-title" }, point.title), point.summary ? /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__point-summary" }, point.summary) : null);
2550
2590
  const resourceSection = renderResourceSection(point);
2551
- return /* @__PURE__ */ React33.createElement(
2591
+ return /* @__PURE__ */ React34.createElement(
2552
2592
  "div",
2553
2593
  {
2554
2594
  key: point.id,
@@ -2556,7 +2596,7 @@ function Timeline({
2556
2596
  style: wrapperStyle,
2557
2597
  role: "listitem"
2558
2598
  },
2559
- point.side === "left" ? /* @__PURE__ */ React33.createElement(React33.Fragment, null, /* @__PURE__ */ React33.createElement("div", { className: cardClasses }, body, resourceSection), connector) : /* @__PURE__ */ React33.createElement(React33.Fragment, null, connector, /* @__PURE__ */ React33.createElement("div", { className: cardClasses }, body, resourceSection))
2599
+ point.side === "left" ? /* @__PURE__ */ React34.createElement(React34.Fragment, null, /* @__PURE__ */ React34.createElement("div", { className: cardClasses }, body, resourceSection), connector) : /* @__PURE__ */ React34.createElement(React34.Fragment, null, connector, /* @__PURE__ */ React34.createElement("div", { className: cardClasses }, body, resourceSection))
2560
2600
  );
2561
2601
  }
2562
2602
  function renderGroupEntry(entry) {
@@ -2567,7 +2607,7 @@ function Timeline({
2567
2607
  const wrapperStyle = { top: `${entry.progress * 100}%` };
2568
2608
  const isExpanded = expandedGroupIds.has(entry.id);
2569
2609
  const hasActivePoint = entry.points.some((point) => point.id === activeId);
2570
- const connector = /* @__PURE__ */ React33.createElement(
2610
+ const connector = /* @__PURE__ */ React34.createElement(
2571
2611
  TimelineConnector,
2572
2612
  {
2573
2613
  side: entry.side,
@@ -2581,7 +2621,7 @@ function Timeline({
2581
2621
  hasActivePoint ? "is-active" : ""
2582
2622
  ].filter(Boolean).join(" ");
2583
2623
  const countLabel = `${entry.count} event${entry.count > 1 ? "s" : ""}`;
2584
- const header = /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__group-header" }, /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__group-summary" }, /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__point-date" }, entry.label), /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__group-count" }, countLabel)), /* @__PURE__ */ React33.createElement(
2624
+ const header = /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__group-header" }, /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__group-summary" }, /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__point-date" }, entry.label), /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__group-count" }, countLabel)), /* @__PURE__ */ React34.createElement(
2585
2625
  "button",
2586
2626
  {
2587
2627
  type: "button",
@@ -2591,7 +2631,7 @@ function Timeline({
2591
2631
  },
2592
2632
  isExpanded ? "Hide details" : "Show details"
2593
2633
  ));
2594
- const groupPoints = isExpanded ? /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__group-points" }, entry.points.map((point) => /* @__PURE__ */ React33.createElement(
2634
+ const groupPoints = isExpanded ? /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__group-points" }, entry.points.map((point) => /* @__PURE__ */ React34.createElement(
2595
2635
  "button",
2596
2636
  {
2597
2637
  key: point.id,
@@ -2602,11 +2642,11 @@ function Timeline({
2602
2642
  ].filter(Boolean).join(" "),
2603
2643
  onClick: () => setActiveId(point.id)
2604
2644
  },
2605
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__point-date" }, point.meta.label),
2606
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__group-point-title" }, point.title)
2645
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__point-date" }, point.meta.label),
2646
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__group-point-title" }, point.title)
2607
2647
  ))) : null;
2608
- const groupCard = /* @__PURE__ */ React33.createElement("div", { className: groupClasses }, header, groupPoints);
2609
- return /* @__PURE__ */ React33.createElement(
2648
+ const groupCard = /* @__PURE__ */ React34.createElement("div", { className: groupClasses }, header, groupPoints);
2649
+ return /* @__PURE__ */ React34.createElement(
2610
2650
  "div",
2611
2651
  {
2612
2652
  key: entry.id,
@@ -2614,17 +2654,17 @@ function Timeline({
2614
2654
  style: wrapperStyle,
2615
2655
  role: "listitem"
2616
2656
  },
2617
- entry.side === "left" ? /* @__PURE__ */ React33.createElement(React33.Fragment, null, groupCard, connector) : /* @__PURE__ */ React33.createElement(React33.Fragment, null, connector, groupCard)
2657
+ entry.side === "left" ? /* @__PURE__ */ React34.createElement(React34.Fragment, null, groupCard, connector) : /* @__PURE__ */ React34.createElement(React34.Fragment, null, connector, groupCard)
2618
2658
  );
2619
2659
  }
2620
- return /* @__PURE__ */ React33.createElement("section", { className: containerClasses, ...rest }, title ? /* @__PURE__ */ React33.createElement("h2", { className: "canopy-timeline__title" }, title) : null, description ? /* @__PURE__ */ React33.createElement("p", { className: "canopy-timeline__description" }, description) : null, rangeLabel ? /* @__PURE__ */ React33.createElement("p", { className: "canopy-timeline__range", "aria-live": "polite" }, rangeLabel) : null, /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__body" }, /* @__PURE__ */ React33.createElement(
2660
+ return /* @__PURE__ */ React34.createElement("section", { className: containerClasses, ...rest }, title ? /* @__PURE__ */ React34.createElement("h2", { className: "canopy-timeline__title" }, title) : null, description ? /* @__PURE__ */ React34.createElement("p", { className: "canopy-timeline__description" }, description) : null, rangeLabel ? /* @__PURE__ */ React34.createElement("p", { className: "canopy-timeline__range", "aria-live": "polite" }, rangeLabel) : null, /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__body" }, /* @__PURE__ */ React34.createElement(
2621
2661
  "div",
2622
2662
  {
2623
2663
  className: "canopy-timeline__list",
2624
2664
  role: "list",
2625
2665
  style: { minHeight: trackHeight }
2626
2666
  },
2627
- /* @__PURE__ */ React33.createElement("div", { className: "canopy-timeline__spine", "aria-hidden": "true" }),
2667
+ /* @__PURE__ */ React34.createElement("div", { className: "canopy-timeline__spine", "aria-hidden": "true" }),
2628
2668
  renderSteps(stepsValue, effectiveRange),
2629
2669
  groupedEntries.map((entry) => {
2630
2670
  if (entry.type === "group") return renderGroupEntry(entry);
@@ -2639,7 +2679,7 @@ function renderSteps(stepSize, range) {
2639
2679
  const markers = [];
2640
2680
  if (startYear < endYear) {
2641
2681
  markers.push(
2642
- /* @__PURE__ */ React33.createElement(
2682
+ /* @__PURE__ */ React34.createElement(
2643
2683
  "span",
2644
2684
  {
2645
2685
  key: "timeline-step-start",
@@ -2647,12 +2687,12 @@ function renderSteps(stepSize, range) {
2647
2687
  style: { top: "0%" },
2648
2688
  "aria-hidden": "true"
2649
2689
  },
2650
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-line" }),
2651
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-label" }, startYear)
2690
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-line" }),
2691
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-label" }, startYear)
2652
2692
  )
2653
2693
  );
2654
2694
  markers.push(
2655
- /* @__PURE__ */ React33.createElement(
2695
+ /* @__PURE__ */ React34.createElement(
2656
2696
  "span",
2657
2697
  {
2658
2698
  key: "timeline-step-end",
@@ -2660,8 +2700,8 @@ function renderSteps(stepSize, range) {
2660
2700
  style: { top: "100%" },
2661
2701
  "aria-hidden": "true"
2662
2702
  },
2663
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-line" }),
2664
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-label" }, endYear)
2703
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-line" }),
2704
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-label" }, endYear)
2665
2705
  )
2666
2706
  );
2667
2707
  }
@@ -2671,7 +2711,7 @@ function renderSteps(stepSize, range) {
2671
2711
  const progress = (timestamp - range.startDate.getTime()) / range.span;
2672
2712
  if (progress <= 0 || progress >= 1) continue;
2673
2713
  markers.push(
2674
- /* @__PURE__ */ React33.createElement(
2714
+ /* @__PURE__ */ React34.createElement(
2675
2715
  "span",
2676
2716
  {
2677
2717
  key: `timeline-step-${year}`,
@@ -2679,8 +2719,8 @@ function renderSteps(stepSize, range) {
2679
2719
  style: { top: `calc(${progress * 100}% - 0.5px)` },
2680
2720
  "aria-hidden": "true"
2681
2721
  },
2682
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-line" }),
2683
- /* @__PURE__ */ React33.createElement("span", { className: "canopy-timeline__step-label" }, year)
2722
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-line" }),
2723
+ /* @__PURE__ */ React34.createElement("span", { className: "canopy-timeline__step-label" }, year)
2684
2724
  )
2685
2725
  );
2686
2726
  }
@@ -2694,7 +2734,8 @@ function TimelinePoint() {
2694
2734
  TimelinePoint.displayName = "TimelinePoint";
2695
2735
 
2696
2736
  // ui/src/content/map/Map.jsx
2697
- import React34 from "react";
2737
+ import React35 from "react";
2738
+ import { createRoot } from "react-dom/client";
2698
2739
  var DEFAULT_TILE_LAYERS = [
2699
2740
  {
2700
2741
  name: "OpenStreetMap",
@@ -2947,35 +2988,72 @@ function escapeHtml(value) {
2947
2988
  if (value == null) return "";
2948
2989
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2949
2990
  }
2950
- function renderPopup(marker) {
2951
- if (!marker) return "";
2991
+ function MapPopupContent({ marker }) {
2992
+ if (!marker) return null;
2952
2993
  const title = marker.title || marker.manifestTitle || "";
2953
2994
  const summary = marker.summary || marker.manifestSummary || "";
2954
2995
  const href = marker.href ? withBasePath(marker.href) : "";
2955
2996
  const thumbnail = marker.thumbnail || "";
2956
2997
  const thumbWidth = marker.thumbnailWidth;
2957
2998
  const thumbHeight = marker.thumbnailHeight;
2958
- const chunks = ['<div class="canopy-map__popup">'];
2959
- if (thumbnail) {
2960
- const sizeAttrs = [];
2961
- if (typeof thumbWidth === "number" && thumbWidth > 0) sizeAttrs.push(`width="${thumbWidth}"`);
2962
- if (typeof thumbHeight === "number" && thumbHeight > 0) sizeAttrs.push(`height="${thumbHeight}"`);
2963
- chunks.push(
2964
- `<div class="canopy-map__popup-media"><img src="${escapeHtml(thumbnail)}" alt="" loading="lazy" ${sizeAttrs.join(" ")} /></div>`
2965
- );
2966
- }
2967
- chunks.push('<div class="canopy-map__popup-body">');
2968
- if (title) {
2969
- const heading = href ? `<a href="${escapeHtml(href)}" class="canopy-map__popup-title">${escapeHtml(title)}</a>` : `<span class="canopy-map__popup-title">${escapeHtml(title)}</span>`;
2970
- chunks.push(heading);
2971
- }
2972
- if (summary) chunks.push(`<p class="canopy-map__popup-summary">${escapeHtml(summary)}</p>`);
2973
- if (marker.detailsHtml) chunks.push(`<div class="canopy-map__popup-details">${marker.detailsHtml}</div>`);
2974
- if (!summary && !marker.detailsHtml && href && !title) {
2975
- chunks.push(`<a href="${escapeHtml(href)}" class="canopy-map__popup-link">View item</a>`);
2999
+ const manifestLinks = Array.isArray(marker.manifests) ? marker.manifests.filter((entry) => entry && (entry.href || entry.title)) : [];
3000
+ const normalizedManifests = manifestLinks.map((manifest) => ({
3001
+ ...manifest,
3002
+ href: manifest.href ? withBasePath(manifest.href) : manifest.href || ""
3003
+ }));
3004
+ return /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__popup" }, thumbnail ? /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__popup-media" }, /* @__PURE__ */ React35.createElement(
3005
+ "img",
3006
+ {
3007
+ src: thumbnail,
3008
+ alt: "",
3009
+ loading: "lazy",
3010
+ width: typeof thumbWidth === "number" && thumbWidth > 0 ? thumbWidth : void 0,
3011
+ height: typeof thumbHeight === "number" && thumbHeight > 0 ? thumbHeight : void 0
3012
+ }
3013
+ )) : null, /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__popup-body" }, title ? href ? /* @__PURE__ */ React35.createElement("a", { href, className: "canopy-map__popup-title" }, title) : /* @__PURE__ */ React35.createElement("span", { className: "canopy-map__popup-title" }, title) : null, summary ? /* @__PURE__ */ React35.createElement("p", { className: "canopy-map__popup-summary" }, summary) : null, marker.detailsHtml ? /* @__PURE__ */ React35.createElement(
3014
+ "div",
3015
+ {
3016
+ className: "canopy-map__popup-details",
3017
+ dangerouslySetInnerHTML: { __html: marker.detailsHtml }
3018
+ }
3019
+ ) : null, !summary && !marker.detailsHtml && href && !title ? /* @__PURE__ */ React35.createElement("a", { href, className: "canopy-map__popup-link" }, "View item") : null, normalizedManifests.length ? /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__popup-manifests" }, /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__popup-manifests-list" }, normalizedManifests.map((manifest, index) => /* @__PURE__ */ React35.createElement(
3020
+ "div",
3021
+ {
3022
+ key: manifest.id || manifest.href || `manifest-${index}`,
3023
+ className: "canopy-map__popup-manifests-item"
3024
+ },
3025
+ /* @__PURE__ */ React35.createElement(ReferencedManifestCard, { manifest })
3026
+ )))) : null));
3027
+ }
3028
+ function renderPopup(marker) {
3029
+ if (!marker || typeof document === "undefined") return null;
3030
+ const container = document.createElement("div");
3031
+ let root = null;
3032
+ try {
3033
+ root = createRoot(container);
3034
+ root.render(/* @__PURE__ */ React35.createElement(MapPopupContent, { marker }));
3035
+ } catch (error) {
3036
+ if (root) {
3037
+ try {
3038
+ root.unmount();
3039
+ } catch (_) {
3040
+ }
3041
+ root = null;
3042
+ }
3043
+ const fallbackTitle = marker.title || marker.summary || marker.href || "Location";
3044
+ container.innerHTML = `<div class="canopy-map__popup"><div class="canopy-map__popup-body"><span class="canopy-map__popup-title">${escapeHtml(fallbackTitle)}</span></div></div>`;
2976
3045
  }
2977
- chunks.push("</div></div>");
2978
- return chunks.join("");
3046
+ return {
3047
+ element: container,
3048
+ destroy() {
3049
+ if (!root) return;
3050
+ try {
3051
+ root.unmount();
3052
+ } catch (_) {
3053
+ }
3054
+ root = null;
3055
+ }
3056
+ };
2979
3057
  }
2980
3058
  function normalizeCustomMarkers(points) {
2981
3059
  if (!Array.isArray(points)) return [];
@@ -2995,6 +3073,7 @@ function normalizeCustomMarkers(points) {
2995
3073
  thumbnail: point.thumbnail || "",
2996
3074
  thumbnailWidth: point.thumbnailWidth,
2997
3075
  thumbnailHeight: point.thumbnailHeight,
3076
+ manifests: Array.isArray(point.manifests) ? point.manifests : [],
2998
3077
  type: "custom"
2999
3078
  };
3000
3079
  }).filter(Boolean);
@@ -3072,26 +3151,26 @@ function Map2({
3072
3151
  defaultCenter = null,
3073
3152
  defaultZoom = null
3074
3153
  } = {}) {
3075
- const containerRef = React34.useRef(null);
3076
- const mapRef = React34.useRef(null);
3077
- const layerRef = React34.useRef(null);
3078
- const [leafletLib, setLeafletLib] = React34.useState(() => resolveGlobalLeaflet());
3079
- const [leafletError, setLeafletError] = React34.useState(null);
3154
+ const containerRef = React35.useRef(null);
3155
+ const mapRef = React35.useRef(null);
3156
+ const layerRef = React35.useRef(null);
3157
+ const [leafletLib, setLeafletLib] = React35.useState(() => resolveGlobalLeaflet());
3158
+ const [leafletError, setLeafletError] = React35.useState(null);
3080
3159
  const datasetInfo = navDataset && typeof navDataset === "object" ? navDataset : null;
3081
3160
  const datasetHref = datasetInfo && datasetInfo.href || "/api/navplace.json";
3082
3161
  const datasetVersion = datasetInfo && datasetInfo.version;
3083
3162
  const datasetHasFeatures = !!(datasetInfo && datasetInfo.hasFeatures);
3084
- const [navState, setNavState] = React34.useState(() => ({
3163
+ const [navState, setNavState] = React35.useState(() => ({
3085
3164
  loading: false,
3086
3165
  error: null,
3087
3166
  markers: []
3088
3167
  }));
3089
- const [iiifTargets, setIiifTargets] = React34.useState(() => ({
3168
+ const [iiifTargets, setIiifTargets] = React35.useState(() => ({
3090
3169
  loading: false,
3091
3170
  error: null,
3092
3171
  keys: []
3093
3172
  }));
3094
- React34.useEffect(() => {
3173
+ React35.useEffect(() => {
3095
3174
  if (!iiifContent) {
3096
3175
  setIiifTargets({ loading: false, error: null, keys: [] });
3097
3176
  return;
@@ -3139,7 +3218,7 @@ function Map2({
3139
3218
  const navTargets = iiifTargets.keys || [];
3140
3219
  const navTargetsKey = navTargets.join("|");
3141
3220
  const shouldFetchNav = datasetHasFeatures && navTargets.length > 0;
3142
- React34.useEffect(() => {
3221
+ React35.useEffect(() => {
3143
3222
  if (!shouldFetchNav) {
3144
3223
  setNavState({ loading: false, error: null, markers: [] });
3145
3224
  return void 0;
@@ -3171,7 +3250,7 @@ function Map2({
3171
3250
  cancelled = true;
3172
3251
  };
3173
3252
  }, [datasetHref, datasetVersion, navTargetsKey, shouldFetchNav]);
3174
- React34.useEffect(() => {
3253
+ React35.useEffect(() => {
3175
3254
  if (leafletLib) return;
3176
3255
  let cancelled = false;
3177
3256
  waitForLeaflet().then((lib) => {
@@ -3183,7 +3262,7 @@ function Map2({
3183
3262
  cancelled = true;
3184
3263
  };
3185
3264
  }, [leafletLib]);
3186
- const navMatchMap = React34.useMemo(() => {
3265
+ const navMatchMap = React35.useMemo(() => {
3187
3266
  const matchMap = createMarkerMap();
3188
3267
  (navState.markers || []).forEach((marker) => {
3189
3268
  if (!marker || !Array.isArray(marker.matchKeys)) return;
@@ -3194,7 +3273,7 @@ function Map2({
3194
3273
  });
3195
3274
  return matchMap;
3196
3275
  }, [navState.markers]);
3197
- const normalizedCustom = React34.useMemo(() => {
3276
+ const normalizedCustom = React35.useMemo(() => {
3198
3277
  return normalizeCustomMarkers(customPoints).map((point) => {
3199
3278
  if (!point || point.thumbnail || !point.href) return point;
3200
3279
  const match = navMatchMap.get(normalizeKey(point.href));
@@ -3209,11 +3288,11 @@ function Map2({
3209
3288
  };
3210
3289
  });
3211
3290
  }, [customPoints, navMatchMap]);
3212
- const allMarkers = React34.useMemo(() => {
3291
+ const allMarkers = React35.useMemo(() => {
3213
3292
  return [...navState.markers || [], ...normalizedCustom];
3214
3293
  }, [navState.markers, normalizedCustom]);
3215
- const clusterOptions = React34.useMemo(() => buildClusterOptions(leafletLib), [leafletLib]);
3216
- React34.useEffect(() => {
3294
+ const clusterOptions = React35.useMemo(() => buildClusterOptions(leafletLib), [leafletLib]);
3295
+ React35.useEffect(() => {
3217
3296
  if (!containerRef.current || mapRef.current || !leafletLib) return void 0;
3218
3297
  const map = leafletLib.map(containerRef.current, {
3219
3298
  zoomControl: true,
@@ -3251,7 +3330,7 @@ function Map2({
3251
3330
  layerRef.current = null;
3252
3331
  };
3253
3332
  }, [tileLayers, scrollWheelZoom, cluster, clusterOptions, leafletLib]);
3254
- React34.useEffect(() => {
3333
+ React35.useEffect(() => {
3255
3334
  const map = mapRef.current;
3256
3335
  const layer = layerRef.current;
3257
3336
  if (!map || !layer || !leafletLib) return;
@@ -3267,11 +3346,33 @@ function Map2({
3267
3346
  const icon = buildMarkerIcon(marker, leafletLib);
3268
3347
  const leafletMarker = leafletLib.marker(latlng, icon ? { icon } : void 0);
3269
3348
  const popup = renderPopup(marker);
3270
- if (popup) {
3349
+ if (popup && popup.element) {
3271
3350
  try {
3272
- leafletMarker.bindPopup(popup);
3351
+ leafletMarker.bindPopup(popup.element);
3352
+ if (typeof popup.destroy === "function") {
3353
+ let disposed = false;
3354
+ const cleanup = () => {
3355
+ if (disposed) return;
3356
+ disposed = true;
3357
+ try {
3358
+ leafletMarker.off("popupclose", cleanup);
3359
+ leafletMarker.off("popupremove", cleanup);
3360
+ leafletMarker.off("remove", cleanup);
3361
+ } catch (_) {
3362
+ }
3363
+ popup.destroy();
3364
+ };
3365
+ leafletMarker.on("popupclose", cleanup);
3366
+ leafletMarker.on("popupremove", cleanup);
3367
+ leafletMarker.on("remove", cleanup);
3368
+ }
3273
3369
  } catch (_) {
3370
+ if (typeof popup.destroy === "function") {
3371
+ popup.destroy();
3372
+ }
3274
3373
  }
3374
+ } else if (popup && typeof popup.destroy === "function") {
3375
+ popup.destroy();
3275
3376
  }
3276
3377
  try {
3277
3378
  layer.addLayer(leafletMarker);
@@ -3327,14 +3428,14 @@ function Map2({
3327
3428
  ].filter(Boolean).join(" ");
3328
3429
  const statusLabel = leafletError ? leafletError.message || "Failed to load map library" : !leafletLib ? "Loading map\u2026" : iiifTargets.error ? iiifTargets.error : datasetUnavailable ? "Map data is unavailable for this site." : navState.error ? navState.error : isLoadingMarkers ? "Loading map data\u2026" : !iiifContent && !hasCustomPoints ? "Add iiifContent or MapPoint markers to populate this map." : !hasMarkers ? "No map locations available." : "";
3329
3430
  const showStatus = Boolean(statusLabel);
3330
- return /* @__PURE__ */ React34.createElement("div", { className: rootClass, id: id || void 0, style: style || void 0 }, /* @__PURE__ */ React34.createElement(
3431
+ return /* @__PURE__ */ React35.createElement("div", { className: rootClass, id: id || void 0, style: style || void 0 }, /* @__PURE__ */ React35.createElement(
3331
3432
  "div",
3332
3433
  {
3333
3434
  ref: containerRef,
3334
3435
  className: "canopy-map__canvas",
3335
3436
  style: { height: height || "600px" }
3336
3437
  }
3337
- ), showStatus ? /* @__PURE__ */ React34.createElement("div", { className: "canopy-map__status", "aria-live": "polite" }, statusLabel) : null);
3438
+ ), showStatus ? /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__status", "aria-live": "polite" }, statusLabel) : null);
3338
3439
  }
3339
3440
 
3340
3441
  // ui/src/content/map/MapPoint.jsx