@canopy-iiif/app 1.10.0 → 1.10.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.
@@ -656,6 +656,7 @@ async function mountImageStory(element, props = {}) {
656
656
  // ui/src/iiif/ImageStory.jsx
657
657
  var DEFAULT_IMAGE_STORY_HEIGHT = 600;
658
658
  var NUMERIC_HEIGHT_PATTERN = /^[+-]?(?:\d+|\d*\.\d+)$/;
659
+ var SIZE_EPSILON = 1;
659
660
  function resolveContainerHeight(value) {
660
661
  if (typeof value === "number" && Number.isFinite(value)) {
661
662
  return `${value}px`;
@@ -706,6 +707,7 @@ var ImageStory = (props = {}) => {
706
707
  let mounted = false;
707
708
  let resizeObserver = null;
708
709
  let pollId = null;
710
+ let lastKnownSize = null;
709
711
  const payload = sanitizeImageStoryProps({
710
712
  iiifContent,
711
713
  disablePanAndZoom,
@@ -734,12 +736,39 @@ var ImageStory = (props = {}) => {
734
736
  pollId = null;
735
737
  }
736
738
  };
737
- const hasUsableSize = () => {
738
- if (!node) return false;
739
+ const measureSize = () => {
740
+ if (!node) return null;
739
741
  const rect = node.getBoundingClientRect();
740
- const width = (rect == null ? void 0 : rect.width) || node.offsetWidth || node.clientWidth;
741
- const height2 = (rect == null ? void 0 : rect.height) || node.offsetHeight || node.clientHeight;
742
- return width > 2 && height2 > 2;
742
+ const width = (rect == null ? void 0 : rect.width) || node.offsetWidth || node.clientWidth || 0;
743
+ const height2 = (rect == null ? void 0 : rect.height) || node.offsetHeight || node.clientHeight || 0;
744
+ return { width, height: height2 };
745
+ };
746
+ const hasUsableSize = () => {
747
+ const size = measureSize();
748
+ if (!size) return false;
749
+ const usable = size.width > 2 && size.height > 2;
750
+ if (usable) {
751
+ lastKnownSize = size;
752
+ }
753
+ return usable;
754
+ };
755
+ const hasMeaningfulSizeChange = () => {
756
+ const size = measureSize();
757
+ if (!size) return false;
758
+ if (size.width <= 2 || size.height <= 2) {
759
+ return true;
760
+ }
761
+ if (!lastKnownSize) {
762
+ lastKnownSize = size;
763
+ return true;
764
+ }
765
+ const widthDelta = Math.abs(size.width - lastKnownSize.width);
766
+ const heightDelta = Math.abs(size.height - lastKnownSize.height);
767
+ if (widthDelta > SIZE_EPSILON || heightDelta > SIZE_EPSILON) {
768
+ lastKnownSize = size;
769
+ return true;
770
+ }
771
+ return false;
743
772
  };
744
773
  const mountViewer = () => {
745
774
  if (!node || mounted || cancelled) return false;
@@ -755,8 +784,9 @@ var ImageStory = (props = {}) => {
755
784
  });
756
785
  return true;
757
786
  };
758
- if (!mountViewer()) {
759
- if (typeof window !== "undefined" && typeof window.ResizeObserver === "function") {
787
+ const scheduleWatchers = () => {
788
+ if (mounted || cancelled) return;
789
+ if (!resizeObserver && typeof window !== "undefined" && typeof window.ResizeObserver === "function") {
760
790
  resizeObserver = new window.ResizeObserver(() => {
761
791
  if (mounted || cancelled) return;
762
792
  mountViewer();
@@ -775,12 +805,48 @@ var ImageStory = (props = {}) => {
775
805
  }
776
806
  }, 200);
777
807
  };
778
- schedulePoll();
808
+ if (!pollId) {
809
+ schedulePoll();
810
+ }
811
+ };
812
+ const beginMounting = () => {
813
+ if (!mountViewer()) {
814
+ scheduleWatchers();
815
+ }
816
+ };
817
+ const remountViewer = () => {
818
+ if (cancelled) return;
819
+ if (mounted) {
820
+ mounted = false;
821
+ destroyCleanup();
822
+ }
823
+ beginMounting();
824
+ };
825
+ beginMounting();
826
+ const handleGalleryModalChange = (event) => {
827
+ if (!node || !event || typeof document === "undefined") return;
828
+ const detail = event.detail || {};
829
+ if (detail.state !== "open") return;
830
+ const modal = detail.modal || (detail.modalId ? document.getElementById(detail.modalId) : null);
831
+ if (!modal || !modal.contains(node)) return;
832
+ if (!mounted) return;
833
+ if (hasMeaningfulSizeChange()) {
834
+ remountViewer();
835
+ }
836
+ };
837
+ if (typeof window !== "undefined" && window.addEventListener) {
838
+ window.addEventListener("canopy:gallery:modal-change", handleGalleryModalChange);
779
839
  }
780
840
  return () => {
781
841
  cancelled = true;
782
842
  disconnectWatchers();
783
843
  destroyCleanup();
844
+ if (typeof window !== "undefined" && window.removeEventListener) {
845
+ window.removeEventListener(
846
+ "canopy:gallery:modal-change",
847
+ handleGalleryModalChange
848
+ );
849
+ }
784
850
  };
785
851
  }, [iiifContent, disablePanAndZoom, pointOfInterestSvgUrl, viewerOptions]);
786
852
  return /* @__PURE__ */ React6.createElement(
@@ -4630,6 +4696,15 @@ function ReferencedManifestCard({
4630
4696
  var DAY_MS = 24 * 60 * 60 * 1e3;
4631
4697
  var DEFAULT_TRACK_HEIGHT = 640;
4632
4698
  var MIN_HEIGHT_PER_POINT = 220;
4699
+ var SCALE_MODES = {
4700
+ TIME: "time",
4701
+ UNIFORM: "uniform"
4702
+ };
4703
+ var ALIGN_OPTIONS = {
4704
+ CENTER: "center",
4705
+ LEFT: "left",
4706
+ RIGHT: "right"
4707
+ };
4633
4708
  function getThresholdMs(threshold, granularity) {
4634
4709
  const value = Number(threshold);
4635
4710
  if (!Number.isFinite(value) || value <= 0) return 0;
@@ -4819,6 +4894,8 @@ function Timeline({
4819
4894
  locale: localeProp = "en-US",
4820
4895
  height = DEFAULT_TRACK_HEIGHT,
4821
4896
  threshold: thresholdProp = null,
4897
+ scale = SCALE_MODES.TIME,
4898
+ align = ALIGN_OPTIONS.CENTER,
4822
4899
  steps = null,
4823
4900
  points: pointsProp,
4824
4901
  __canopyTimeline: payload = null,
@@ -4853,20 +4930,24 @@ function Timeline({
4853
4930
  );
4854
4931
  const spanStart = effectiveRange.startDate.getTime();
4855
4932
  const span = effectiveRange.span;
4933
+ const scaleValue = scale === SCALE_MODES.UNIFORM ? SCALE_MODES.UNIFORM : SCALE_MODES.TIME;
4934
+ const useUniformSpacing = scaleValue === SCALE_MODES.UNIFORM;
4935
+ const alignValue = align === ALIGN_OPTIONS.LEFT ? ALIGN_OPTIONS.LEFT : align === ALIGN_OPTIONS.RIGHT ? ALIGN_OPTIONS.RIGHT : ALIGN_OPTIONS.CENTER;
4936
+ const enforcedSide = alignValue === ALIGN_OPTIONS.LEFT ? "right" : alignValue === ALIGN_OPTIONS.RIGHT ? "left" : null;
4856
4937
  const pointsWithPosition = React37.useMemo(() => {
4857
4938
  if (!sanitizedPoints.length) return [];
4858
4939
  return sanitizedPoints.map((point, index) => {
4859
4940
  const timestamp = point.meta.timestamp;
4860
4941
  const fallbackProgress = sanitizedPoints.length > 1 ? index / (sanitizedPoints.length - 1) : 0;
4861
- const progress = Number.isFinite(timestamp) ? clampProgress((timestamp - spanStart) / span) : fallbackProgress;
4862
- const side = point.side || (index % 2 === 0 ? "left" : "right");
4942
+ const progress = useUniformSpacing ? fallbackProgress : Number.isFinite(timestamp) ? clampProgress((timestamp - spanStart) / span) : fallbackProgress;
4943
+ const side = enforcedSide || point.side || (index % 2 === 0 ? "left" : "right");
4863
4944
  return {
4864
4945
  ...point,
4865
4946
  progress,
4866
4947
  side
4867
4948
  };
4868
4949
  });
4869
- }, [sanitizedPoints, spanStart, span]);
4950
+ }, [sanitizedPoints, spanStart, span, useUniformSpacing, enforcedSide]);
4870
4951
  const [activeId, setActiveId] = React37.useState(
4871
4952
  () => getActivePointId(pointsWithPosition)
4872
4953
  );
@@ -4913,7 +4994,11 @@ function Timeline({
4913
4994
  });
4914
4995
  }, []);
4915
4996
  const trackHeight = resolveTrackHeight(height, pointsWithPosition.length);
4916
- const containerClasses = ["canopy-timeline", className].filter(Boolean).join(" ");
4997
+ const containerClasses = [
4998
+ "canopy-timeline",
4999
+ alignValue ? `canopy-timeline--align-${alignValue}` : "",
5000
+ className
5001
+ ].filter(Boolean).join(" ");
4917
5002
  const rangeLabel = formatRangeLabel(effectiveRange);
4918
5003
  function renderPointEntry(point) {
4919
5004
  if (!point) return null;
@@ -5677,6 +5762,23 @@ var INLINE_SCRIPT2 = `(() => {
5677
5762
  const NAV_OPTION_SELECTOR = '[data-canopy-gallery-nav-option]';
5678
5763
  const NAV_ITEM_SELECTOR = '[data-canopy-gallery-nav-item]';
5679
5764
 
5765
+ function emitModalState(modal, state) {
5766
+ if (!modal || typeof window === 'undefined') return;
5767
+ const detail = { modalId: modal.id || '', modal, state };
5768
+ try {
5769
+ const EventCtor = window.CustomEvent || CustomEvent;
5770
+ if (typeof EventCtor === 'function') {
5771
+ window.dispatchEvent(new EventCtor('canopy:gallery:modal-change', { detail }));
5772
+ return;
5773
+ }
5774
+ } catch (_) {}
5775
+ try {
5776
+ const fallback = document.createEvent('CustomEvent');
5777
+ fallback.initCustomEvent('canopy:gallery:modal-change', true, true, detail);
5778
+ window.dispatchEvent(fallback);
5779
+ } catch (_) {}
5780
+ }
5781
+
5680
5782
  function isVisible(node) {
5681
5783
  return !!(node && (node.offsetWidth || node.offsetHeight || node.getClientRects().length));
5682
5784
  }
@@ -5807,6 +5909,7 @@ var INLINE_SCRIPT2 = `(() => {
5807
5909
  lockScroll();
5808
5910
  document.addEventListener('keydown', handleKeydown, true);
5809
5911
  } else if (activeModal !== modal) {
5912
+ emitModalState(activeModal, 'close');
5810
5913
  activeModal.removeAttribute('data-canopy-gallery-active');
5811
5914
  }
5812
5915
  activeModal = modal;
@@ -5815,6 +5918,7 @@ var INLINE_SCRIPT2 = `(() => {
5815
5918
  if (!focusActiveNav(modal)) {
5816
5919
  focusInitial(modal);
5817
5920
  }
5921
+ emitModalState(modal, 'open');
5818
5922
  return;
5819
5923
  }
5820
5924
  if (!activeModal) return;
@@ -5848,6 +5952,7 @@ var INLINE_SCRIPT2 = `(() => {
5848
5952
  }
5849
5953
  });
5850
5954
  }
5955
+ emitModalState(previous, 'close');
5851
5956
  }
5852
5957
 
5853
5958
  function modalFromHash() {
@@ -6306,6 +6411,7 @@ function buildCaptionContent(itemProps) {
6306
6411
  ));
6307
6412
  }
6308
6413
  function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
6414
+ const { getString, formatString } = useLocale();
6309
6415
  const {
6310
6416
  props,
6311
6417
  modalId,
@@ -6318,6 +6424,12 @@ function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
6318
6424
  const kicker = props.kicker || props.label || props.eyebrow;
6319
6425
  const summary = props.popupDescription || props.modalDescription || props.description || props.summary || null;
6320
6426
  const modalTitle = props.popupTitle || props.modalTitle || props.title || `Item ${index + 1}`;
6427
+ const closeButtonText = getString("common.actions.close", "Close");
6428
+ const closeButtonLabel = formatString(
6429
+ "common.phrases.close_content",
6430
+ "Close {content}",
6431
+ { content: modalTitle }
6432
+ );
6321
6433
  return /* @__PURE__ */ React41.createElement(
6322
6434
  "div",
6323
6435
  {
@@ -6339,13 +6451,14 @@ function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
6339
6451
  groupName: `${navGroupName || "canopy-gallery"}-${modalId}`
6340
6452
  }
6341
6453
  ), /* @__PURE__ */ React41.createElement(
6342
- "a",
6454
+ Button,
6343
6455
  {
6344
6456
  className: "canopy-gallery__modal-close",
6345
6457
  href: `#${closeTargetId}`,
6346
- "aria-label": `Close popup for ${modalTitle}`
6347
- },
6348
- "X"
6458
+ label: closeButtonText,
6459
+ "aria-label": closeButtonLabel,
6460
+ variant: "secondary"
6461
+ }
6349
6462
  )), /* @__PURE__ */ React41.createElement("div", { className: "canopy-gallery__modal-panel" }, /* @__PURE__ */ React41.createElement(
6350
6463
  "button",
6351
6464
  {