@canopy-iiif/app 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
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,80 @@ 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>`);
2976
- }
2977
- chunks.push("</div></div>");
2978
- return chunks.join("");
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
+ let hadError = false;
3033
+ const render = () => {
3034
+ if (hadError) return;
3035
+ try {
3036
+ if (!root) root = createRoot(container);
3037
+ root.render(/* @__PURE__ */ React35.createElement(MapPopupContent, { marker }));
3038
+ } catch (error) {
3039
+ hadError = true;
3040
+ if (root) {
3041
+ try {
3042
+ root.unmount();
3043
+ } catch (_) {
3044
+ }
3045
+ root = null;
3046
+ }
3047
+ const fallbackTitle = marker.title || marker.summary || marker.href || "Location";
3048
+ container.innerHTML = `<div class="canopy-map__popup"><div class="canopy-map__popup-body"><span class="canopy-map__popup-title">${escapeHtml(fallbackTitle)}</span></div></div>`;
3049
+ }
3050
+ };
3051
+ const destroy = () => {
3052
+ if (!root) return;
3053
+ try {
3054
+ root.unmount();
3055
+ } catch (_) {
3056
+ }
3057
+ root = null;
3058
+ };
3059
+ render();
3060
+ return {
3061
+ element: container,
3062
+ render,
3063
+ destroy
3064
+ };
2979
3065
  }
2980
3066
  function normalizeCustomMarkers(points) {
2981
3067
  if (!Array.isArray(points)) return [];
@@ -2995,6 +3081,7 @@ function normalizeCustomMarkers(points) {
2995
3081
  thumbnail: point.thumbnail || "",
2996
3082
  thumbnailWidth: point.thumbnailWidth,
2997
3083
  thumbnailHeight: point.thumbnailHeight,
3084
+ manifests: Array.isArray(point.manifests) ? point.manifests : [],
2998
3085
  type: "custom"
2999
3086
  };
3000
3087
  }).filter(Boolean);
@@ -3072,26 +3159,26 @@ function Map2({
3072
3159
  defaultCenter = null,
3073
3160
  defaultZoom = null
3074
3161
  } = {}) {
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);
3162
+ const containerRef = React35.useRef(null);
3163
+ const mapRef = React35.useRef(null);
3164
+ const layerRef = React35.useRef(null);
3165
+ const [leafletLib, setLeafletLib] = React35.useState(() => resolveGlobalLeaflet());
3166
+ const [leafletError, setLeafletError] = React35.useState(null);
3080
3167
  const datasetInfo = navDataset && typeof navDataset === "object" ? navDataset : null;
3081
3168
  const datasetHref = datasetInfo && datasetInfo.href || "/api/navplace.json";
3082
3169
  const datasetVersion = datasetInfo && datasetInfo.version;
3083
3170
  const datasetHasFeatures = !!(datasetInfo && datasetInfo.hasFeatures);
3084
- const [navState, setNavState] = React34.useState(() => ({
3171
+ const [navState, setNavState] = React35.useState(() => ({
3085
3172
  loading: false,
3086
3173
  error: null,
3087
3174
  markers: []
3088
3175
  }));
3089
- const [iiifTargets, setIiifTargets] = React34.useState(() => ({
3176
+ const [iiifTargets, setIiifTargets] = React35.useState(() => ({
3090
3177
  loading: false,
3091
3178
  error: null,
3092
3179
  keys: []
3093
3180
  }));
3094
- React34.useEffect(() => {
3181
+ React35.useEffect(() => {
3095
3182
  if (!iiifContent) {
3096
3183
  setIiifTargets({ loading: false, error: null, keys: [] });
3097
3184
  return;
@@ -3139,7 +3226,7 @@ function Map2({
3139
3226
  const navTargets = iiifTargets.keys || [];
3140
3227
  const navTargetsKey = navTargets.join("|");
3141
3228
  const shouldFetchNav = datasetHasFeatures && navTargets.length > 0;
3142
- React34.useEffect(() => {
3229
+ React35.useEffect(() => {
3143
3230
  if (!shouldFetchNav) {
3144
3231
  setNavState({ loading: false, error: null, markers: [] });
3145
3232
  return void 0;
@@ -3171,7 +3258,7 @@ function Map2({
3171
3258
  cancelled = true;
3172
3259
  };
3173
3260
  }, [datasetHref, datasetVersion, navTargetsKey, shouldFetchNav]);
3174
- React34.useEffect(() => {
3261
+ React35.useEffect(() => {
3175
3262
  if (leafletLib) return;
3176
3263
  let cancelled = false;
3177
3264
  waitForLeaflet().then((lib) => {
@@ -3183,7 +3270,7 @@ function Map2({
3183
3270
  cancelled = true;
3184
3271
  };
3185
3272
  }, [leafletLib]);
3186
- const navMatchMap = React34.useMemo(() => {
3273
+ const navMatchMap = React35.useMemo(() => {
3187
3274
  const matchMap = createMarkerMap();
3188
3275
  (navState.markers || []).forEach((marker) => {
3189
3276
  if (!marker || !Array.isArray(marker.matchKeys)) return;
@@ -3194,7 +3281,7 @@ function Map2({
3194
3281
  });
3195
3282
  return matchMap;
3196
3283
  }, [navState.markers]);
3197
- const normalizedCustom = React34.useMemo(() => {
3284
+ const normalizedCustom = React35.useMemo(() => {
3198
3285
  return normalizeCustomMarkers(customPoints).map((point) => {
3199
3286
  if (!point || point.thumbnail || !point.href) return point;
3200
3287
  const match = navMatchMap.get(normalizeKey(point.href));
@@ -3209,11 +3296,11 @@ function Map2({
3209
3296
  };
3210
3297
  });
3211
3298
  }, [customPoints, navMatchMap]);
3212
- const allMarkers = React34.useMemo(() => {
3299
+ const allMarkers = React35.useMemo(() => {
3213
3300
  return [...navState.markers || [], ...normalizedCustom];
3214
3301
  }, [navState.markers, normalizedCustom]);
3215
- const clusterOptions = React34.useMemo(() => buildClusterOptions(leafletLib), [leafletLib]);
3216
- React34.useEffect(() => {
3302
+ const clusterOptions = React35.useMemo(() => buildClusterOptions(leafletLib), [leafletLib]);
3303
+ React35.useEffect(() => {
3217
3304
  if (!containerRef.current || mapRef.current || !leafletLib) return void 0;
3218
3305
  const map = leafletLib.map(containerRef.current, {
3219
3306
  zoomControl: true,
@@ -3251,7 +3338,7 @@ function Map2({
3251
3338
  layerRef.current = null;
3252
3339
  };
3253
3340
  }, [tileLayers, scrollWheelZoom, cluster, clusterOptions, leafletLib]);
3254
- React34.useEffect(() => {
3341
+ React35.useEffect(() => {
3255
3342
  const map = mapRef.current;
3256
3343
  const layer = layerRef.current;
3257
3344
  if (!map || !layer || !leafletLib) return;
@@ -3260,6 +3347,7 @@ function Map2({
3260
3347
  } catch (_) {
3261
3348
  }
3262
3349
  const bounds = [];
3350
+ const popupCleanups = [];
3263
3351
  allMarkers.forEach((marker) => {
3264
3352
  if (!marker || !Number.isFinite(marker.lat) || !Number.isFinite(marker.lng)) return;
3265
3353
  const latlng = leafletLib.latLng(marker.lat, marker.lng);
@@ -3267,11 +3355,30 @@ function Map2({
3267
3355
  const icon = buildMarkerIcon(marker, leafletLib);
3268
3356
  const leafletMarker = leafletLib.marker(latlng, icon ? { icon } : void 0);
3269
3357
  const popup = renderPopup(marker);
3270
- if (popup) {
3358
+ if (popup && popup.element) {
3271
3359
  try {
3272
- leafletMarker.bindPopup(popup);
3360
+ leafletMarker.bindPopup(popup.element);
3361
+ if (typeof popup.render === "function") {
3362
+ leafletMarker.on("popupopen", popup.render);
3363
+ }
3364
+ popupCleanups.push(() => {
3365
+ if (typeof popup.render === "function") {
3366
+ try {
3367
+ leafletMarker.off("popupopen", popup.render);
3368
+ } catch (_) {
3369
+ }
3370
+ }
3371
+ if (typeof popup.destroy === "function") {
3372
+ popup.destroy();
3373
+ }
3374
+ });
3273
3375
  } catch (_) {
3376
+ if (typeof popup.destroy === "function") {
3377
+ popup.destroy();
3378
+ }
3274
3379
  }
3380
+ } else if (popup && typeof popup.destroy === "function") {
3381
+ popup.destroy();
3275
3382
  }
3276
3383
  try {
3277
3384
  layer.addLayer(leafletMarker);
@@ -3314,6 +3421,14 @@ function Map2({
3314
3421
  } catch (_) {
3315
3422
  }
3316
3423
  }
3424
+ return () => {
3425
+ popupCleanups.forEach((cleanup) => {
3426
+ try {
3427
+ cleanup();
3428
+ } catch (_) {
3429
+ }
3430
+ });
3431
+ };
3317
3432
  }, [allMarkers, defaultCenter, defaultZoom, leafletLib]);
3318
3433
  const isLoadingMarkers = iiifTargets.loading || navState.loading;
3319
3434
  const hasMarkers = allMarkers.length > 0;
@@ -3327,14 +3442,14 @@ function Map2({
3327
3442
  ].filter(Boolean).join(" ");
3328
3443
  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
3444
  const showStatus = Boolean(statusLabel);
3330
- return /* @__PURE__ */ React34.createElement("div", { className: rootClass, id: id || void 0, style: style || void 0 }, /* @__PURE__ */ React34.createElement(
3445
+ return /* @__PURE__ */ React35.createElement("div", { className: rootClass, id: id || void 0, style: style || void 0 }, /* @__PURE__ */ React35.createElement(
3331
3446
  "div",
3332
3447
  {
3333
3448
  ref: containerRef,
3334
3449
  className: "canopy-map__canvas",
3335
3450
  style: { height: height || "600px" }
3336
3451
  }
3337
- ), showStatus ? /* @__PURE__ */ React34.createElement("div", { className: "canopy-map__status", "aria-live": "polite" }, statusLabel) : null);
3452
+ ), showStatus ? /* @__PURE__ */ React35.createElement("div", { className: "canopy-map__status", "aria-live": "polite" }, statusLabel) : null);
3338
3453
  }
3339
3454
 
3340
3455
  // ui/src/content/map/MapPoint.jsx