@extend-ai/react-pptx 0.2.0 → 0.2.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/dist/index.js CHANGED
@@ -9242,6 +9242,25 @@ async function renderWmfToDataUrl(buffer, maxWidth, maxHeight, optionsOrScale) {
9242
9242
  }
9243
9243
  }
9244
9244
 
9245
+ // src/zoom.ts
9246
+ var MIN_VIEWER_ZOOM = 10;
9247
+ var MAX_VIEWER_ZOOM = 400;
9248
+ function normalizeViewerZoomLevel(level) {
9249
+ if (typeof level !== "number") return level;
9250
+ if (Number.isNaN(level)) return 100;
9251
+ return Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, level));
9252
+ }
9253
+ function resolveViewerZoom(level, viewportWidth, viewportHeight, contentWidth, contentHeight) {
9254
+ const normalized = normalizeViewerZoomLevel(level);
9255
+ if (typeof normalized === "number") return normalized;
9256
+ const safeContentWidth = Number.isFinite(contentWidth) && contentWidth > 0 ? contentWidth : 1;
9257
+ const safeContentHeight = Number.isFinite(contentHeight) && contentHeight > 0 ? contentHeight : 1;
9258
+ const fitWidth = (Number.isFinite(viewportWidth) && viewportWidth > 0 ? viewportWidth : safeContentWidth) / safeContentWidth * 100;
9259
+ const fitHeight = (Number.isFinite(viewportHeight) && viewportHeight > 0 ? viewportHeight : safeContentHeight) / safeContentHeight * 100;
9260
+ const resolved = normalized === "fit-width" ? fitWidth : normalized === "fit-page" ? Math.min(fitWidth, fitHeight) : Math.min(fitWidth, 100);
9261
+ return Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, resolved));
9262
+ }
9263
+
9245
9264
  // src/normalized-viewer.ts
9246
9265
  var EMU_PER_CSS_PIXEL = 9525;
9247
9266
  var DEFAULT_TEXT_HORIZONTAL_INSET_EMU = 91440;
@@ -9307,6 +9326,10 @@ function acquirePresentationAssetCache(presentation) {
9307
9326
  }
9308
9327
  var LIST_ITEM_GAP = 24;
9309
9328
  var FALLBACK_VIEWPORT_RECT = { width: 800, height: 600 };
9329
+ function pixelValue(value) {
9330
+ const parsed = Number.parseFloat(value);
9331
+ return Number.isFinite(parsed) ? parsed : 0;
9332
+ }
9310
9333
  function scaleListAnchorOffset(offset, previousSlideHeight, nextSlideHeight) {
9311
9334
  if (previousSlideHeight <= 0) return offset;
9312
9335
  if (offset <= previousSlideHeight) return offset * (nextSlideHeight / previousSlideHeight);
@@ -9852,9 +9875,11 @@ var NormalizedPresentationViewer = class {
9852
9875
  presentation;
9853
9876
  callbacks;
9854
9877
  current = 0;
9855
- zoom = 100;
9856
- fit = "contain";
9878
+ zoomLevel = "automatic";
9879
+ resolvedZoom = 100;
9857
9880
  assetCache;
9881
+ resizeObserver;
9882
+ activeViewport;
9858
9883
  listCleanup;
9859
9884
  highlight;
9860
9885
  activeMountResources;
@@ -9866,6 +9891,7 @@ var NormalizedPresentationViewer = class {
9866
9891
  renderGeneration = 0;
9867
9892
  navigationGeneration = 0;
9868
9893
  lastNotifiedSlide;
9894
+ lastZoomNotification;
9869
9895
  listState;
9870
9896
  renderState;
9871
9897
  get slideCount() {
@@ -9874,11 +9900,17 @@ var NormalizedPresentationViewer = class {
9874
9900
  get currentSlideIndex() {
9875
9901
  return this.current;
9876
9902
  }
9903
+ get zoom() {
9904
+ return this.zoomLevel;
9905
+ }
9877
9906
  get zoomPercent() {
9878
- return this.zoom;
9907
+ return this.resolvedZoom;
9908
+ }
9909
+ get resolvedZoomPercent() {
9910
+ return this.resolvedZoom;
9879
9911
  }
9880
9912
  get fitMode() {
9881
- return this.fit;
9913
+ return typeof this.zoomLevel === "number" ? "none" : "contain";
9882
9914
  }
9883
9915
  get naturalSlideWidth() {
9884
9916
  const width = this.presentation.size.widthEmu / EMU_PER_CSS_PIXEL;
@@ -9888,9 +9920,57 @@ var NormalizedPresentationViewer = class {
9888
9920
  const height = this.presentation.size.heightEmu / EMU_PER_CSS_PIXEL;
9889
9921
  return Number.isFinite(height) && height > 0 ? height : 1;
9890
9922
  }
9891
- scaleForViewport(viewportWidth) {
9892
- const fitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
9893
- return fitScale * (this.zoom / 100);
9923
+ viewportSize(viewport) {
9924
+ const rect = viewport.getBoundingClientRect();
9925
+ let width = viewport.clientWidth || rect.width;
9926
+ let height = viewport.clientHeight || rect.height;
9927
+ if (typeof getComputedStyle === "function") {
9928
+ const viewportStyle = getComputedStyle(viewport);
9929
+ width -= pixelValue(viewportStyle.paddingLeft) + pixelValue(viewportStyle.paddingRight);
9930
+ height -= pixelValue(viewportStyle.paddingTop) + pixelValue(viewportStyle.paddingBottom);
9931
+ if (viewport !== this.container) {
9932
+ const containerStyle = getComputedStyle(this.container);
9933
+ width -= pixelValue(containerStyle.paddingLeft) + pixelValue(containerStyle.paddingRight);
9934
+ height -= pixelValue(containerStyle.paddingTop) + pixelValue(containerStyle.paddingBottom);
9935
+ }
9936
+ }
9937
+ return { width: Math.max(0, width), height: Math.max(0, height) };
9938
+ }
9939
+ resolveZoom(level, viewport = this.activeViewport ?? this.container) {
9940
+ const size = this.viewportSize(viewport);
9941
+ return resolveViewerZoom(
9942
+ level,
9943
+ size.width,
9944
+ size.height,
9945
+ this.naturalSlideWidth,
9946
+ this.naturalSlideHeight
9947
+ );
9948
+ }
9949
+ scaleForViewport(viewport) {
9950
+ return this.resolveZoom(this.zoomLevel, viewport) / 100;
9951
+ }
9952
+ publishZoom(resolvedZoom) {
9953
+ const normalized = Math.max(MIN_VIEWER_ZOOM, Math.min(MAX_VIEWER_ZOOM, resolvedZoom));
9954
+ const levelChanged = this.lastZoomNotification?.level !== this.zoomLevel;
9955
+ const resolutionChanged = this.lastZoomNotification === void 0 || Math.abs(this.lastZoomNotification.resolvedZoom - normalized) > 0.01;
9956
+ this.resolvedZoom = normalized;
9957
+ if (!levelChanged && !resolutionChanged) return;
9958
+ const state = { level: this.zoomLevel, resolvedZoom: normalized };
9959
+ this.lastZoomNotification = state;
9960
+ this.callbacks.onZoomChange?.(state);
9961
+ }
9962
+ observeViewport(viewport) {
9963
+ if (this.activeViewport === viewport && this.resizeObserver) return;
9964
+ this.resizeObserver?.disconnect();
9965
+ this.resizeObserver = void 0;
9966
+ this.activeViewport = viewport;
9967
+ if (typeof ResizeObserver === "undefined") return;
9968
+ this.resizeObserver = new ResizeObserver(() => {
9969
+ if (this.destroyed || typeof this.zoomLevel === "number") return;
9970
+ void this.updateRenderedScale();
9971
+ });
9972
+ this.resizeObserver.observe(viewport);
9973
+ if (viewport !== this.container) this.resizeObserver.observe(this.container);
9894
9974
  }
9895
9975
  isRenderActive(generation) {
9896
9976
  return !this.destroyed && generation === this.renderGeneration;
@@ -9901,6 +9981,9 @@ var NormalizedPresentationViewer = class {
9901
9981
  this.navigationGeneration += 1;
9902
9982
  this.listCleanup?.();
9903
9983
  this.listCleanup = void 0;
9984
+ this.resizeObserver?.disconnect();
9985
+ this.resizeObserver = void 0;
9986
+ this.activeViewport = void 0;
9904
9987
  this.listState = void 0;
9905
9988
  this.clearSearchHighlights();
9906
9989
  return generation;
@@ -10458,7 +10541,7 @@ var NormalizedPresentationViewer = class {
10458
10541
  } finally {
10459
10542
  this.activeMountResources = previousResources;
10460
10543
  }
10461
- const effectiveScale = scale ?? this.scaleForViewport(target.clientWidth);
10544
+ const effectiveScale = scale ?? this.scaleForViewport(target);
10462
10545
  const wrapper = document.createElement("div");
10463
10546
  wrapper.dataset.rpvSlideWrapper = String(index);
10464
10547
  wrapper.style.position = "relative";
@@ -10550,6 +10633,7 @@ var NormalizedPresentationViewer = class {
10550
10633
  this.renderState = { mode: "single" };
10551
10634
  const generation = this.beginRender();
10552
10635
  if (generation === void 0) return;
10636
+ this.observeViewport(this.container);
10553
10637
  this.current = Math.max(0, Math.min(this.slideCount - 1, index));
10554
10638
  this.disposeContainerSlides();
10555
10639
  this.container.replaceChildren();
@@ -10559,6 +10643,7 @@ var NormalizedPresentationViewer = class {
10559
10643
  if (!this.isRenderActive(generation) || this.handlesByTarget.get(this.container) !== handle) {
10560
10644
  return;
10561
10645
  }
10646
+ this.publishZoom(this.scaleForViewport(this.container) * 100);
10562
10647
  this.notifySlideChange(this.current);
10563
10648
  }
10564
10649
  createListPlaceholder(index, item, generation, scale) {
@@ -10619,7 +10704,8 @@ var NormalizedPresentationViewer = class {
10619
10704
  Math.min(this.slideCount - 1, options.initialSlideIndex ?? this.current)
10620
10705
  );
10621
10706
  const scroller = options.scrollElement ?? this.container;
10622
- let effectiveScale = this.scaleForViewport(scroller.clientWidth);
10707
+ this.observeViewport(scroller);
10708
+ let effectiveScale = this.scaleForViewport(scroller);
10623
10709
  let slideHeight = this.naturalSlideHeight * effectiveScale;
10624
10710
  let itemStride = slideHeight + LIST_ITEM_GAP;
10625
10711
  if (!windowingEnabled) {
@@ -10634,6 +10720,8 @@ var NormalizedPresentationViewer = class {
10634
10720
  const state2 = { generation, options: normalizedOptions, placeholders: placeholders2 };
10635
10721
  state2.updateScale = () => {
10636
10722
  if (!this.isRenderActive(generation) || this.listState !== state2) return;
10723
+ const previousScale = effectiveScale;
10724
+ const previousScrollLeft = scroller.scrollLeft;
10637
10725
  const previousStride = itemStride;
10638
10726
  const previousSlideHeight = slideHeight;
10639
10727
  const listStart = scroller === this.container ? 0 : this.container.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
@@ -10643,13 +10731,17 @@ var NormalizedPresentationViewer = class {
10643
10731
  Math.min(this.slideCount - 1, Math.floor(listOffset / previousStride))
10644
10732
  );
10645
10733
  const anchorOffset = listOffset - anchorIndex * previousStride;
10646
- effectiveScale = this.scaleForViewport(scroller.clientWidth);
10734
+ effectiveScale = this.scaleForViewport(scroller);
10647
10735
  slideHeight = this.naturalSlideHeight * effectiveScale;
10648
10736
  itemStride = slideHeight + LIST_ITEM_GAP;
10649
10737
  placeholders2.forEach((placeholder) => {
10650
10738
  placeholder.item.style.minHeight = `${slideHeight}px`;
10651
10739
  placeholder.setScale(effectiveScale);
10652
10740
  });
10741
+ if (previousScale > 0 && previousScrollLeft > 0) {
10742
+ scroller.scrollLeft = previousScrollLeft * (effectiveScale / previousScale);
10743
+ }
10744
+ this.publishZoom(effectiveScale * 100);
10653
10745
  if (listOffset < 0) return;
10654
10746
  const nextAnchorOffset = scaleListAnchorOffset(
10655
10747
  anchorOffset,
@@ -10675,6 +10767,7 @@ var NormalizedPresentationViewer = class {
10675
10767
  initialItem.scrollIntoView?.({ behavior: "instant", block: "start" });
10676
10768
  }
10677
10769
  this.current = initialIndex;
10770
+ this.publishZoom(effectiveScale * 100);
10678
10771
  this.notifySlideChange(initialIndex);
10679
10772
  return;
10680
10773
  }
@@ -10818,6 +10911,8 @@ var NormalizedPresentationViewer = class {
10818
10911
  };
10819
10912
  state.updateScale = () => {
10820
10913
  if (!isActive()) return;
10914
+ const previousScale = effectiveScale;
10915
+ const previousScrollLeft = scroller.scrollLeft;
10821
10916
  const previousStride = itemStride;
10822
10917
  const previousSlideHeight = slideHeight;
10823
10918
  const anchorIndex = this.current;
@@ -10827,7 +10922,7 @@ var NormalizedPresentationViewer = class {
10827
10922
  const previousScrollTop = Math.abs(observedIndex - anchorIndex) > 1 && intendedIndex !== void 0 && Math.abs(intendedIndex - anchorIndex) <= 1 ? lastProgrammaticTop : observedScrollTop;
10828
10923
  const previousOffset = previousScrollTop - scrollMargin;
10829
10924
  const anchorOffset = previousOffset - anchorIndex * previousStride;
10830
- effectiveScale = this.scaleForViewport(scroller.clientWidth);
10925
+ effectiveScale = this.scaleForViewport(scroller);
10831
10926
  slideHeight = this.naturalSlideHeight * effectiveScale;
10832
10927
  itemStride = slideHeight + LIST_ITEM_GAP;
10833
10928
  sizer.style.height = `${this.slideCount * itemStride - LIST_ITEM_GAP}px`;
@@ -10836,6 +10931,9 @@ var NormalizedPresentationViewer = class {
10836
10931
  placeholder.item.style.transform = `translateY(${index * itemStride}px)`;
10837
10932
  placeholder.setScale(effectiveScale);
10838
10933
  });
10934
+ if (previousScale > 0 && previousScrollLeft > 0) {
10935
+ scroller.scrollLeft = previousScrollLeft * (effectiveScale / previousScale);
10936
+ }
10839
10937
  const nextAnchorOffset = scaleListAnchorOffset(
10840
10938
  anchorOffset,
10841
10939
  previousSlideHeight,
@@ -10848,6 +10946,7 @@ var NormalizedPresentationViewer = class {
10848
10946
  writeScrollTop(nextScrollTop);
10849
10947
  virtualizer.setOptions({ ...virtualizer.options, overscan });
10850
10948
  virtualizer.measure();
10949
+ this.publishZoom(effectiveScale * 100);
10851
10950
  };
10852
10951
  const teardown = virtualizer._didMount();
10853
10952
  this.listCleanup = teardown;
@@ -10866,6 +10965,7 @@ var NormalizedPresentationViewer = class {
10866
10965
  this.container.scrollLeft = 0;
10867
10966
  }
10868
10967
  this.current = initialIndex;
10968
+ this.publishZoom(effectiveScale * 100);
10869
10969
  this.notifySlideChange(initialIndex);
10870
10970
  }
10871
10971
  async goToSlide(index, scrollOptions) {
@@ -10902,10 +11002,7 @@ var NormalizedPresentationViewer = class {
10902
11002
  if (!changed && this.container.querySelector(`[data-rpv-slide-index="${next}"]`)) return;
10903
11003
  await this.renderSlide(next);
10904
11004
  }
10905
- async setZoom(percent) {
10906
- const next = Math.max(10, Math.min(400, percent));
10907
- if (this.zoom === next) return;
10908
- this.zoom = next;
11005
+ async updateRenderedScale() {
10909
11006
  if (this.renderState?.mode === "continuous") {
10910
11007
  const state = this.listState;
10911
11008
  if (state && this.isRenderActive(state.generation) && state.updateScale) {
@@ -10915,25 +11012,34 @@ var NormalizedPresentationViewer = class {
10915
11012
  }
10916
11013
  } else if (this.renderState) {
10917
11014
  const handle = this.handlesByTarget.get(this.container);
10918
- if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10919
- else await this.renderSlide(this.current);
11015
+ if (handle) {
11016
+ const viewport = this.activeViewport ?? this.container;
11017
+ const previousScale = this.resolvedZoom / 100;
11018
+ const previousScrollTop = viewport.scrollTop;
11019
+ const previousScrollLeft = viewport.scrollLeft;
11020
+ const nextScale = this.scaleForViewport(viewport);
11021
+ handle.setScale(nextScale);
11022
+ if (previousScale > 0) {
11023
+ viewport.scrollTop = previousScrollTop * (nextScale / previousScale);
11024
+ viewport.scrollLeft = previousScrollLeft * (nextScale / previousScale);
11025
+ }
11026
+ this.publishZoom(nextScale * 100);
11027
+ } else await this.renderSlide(this.current);
11028
+ } else {
11029
+ this.publishZoom(this.resolveZoom(this.zoomLevel));
10920
11030
  }
10921
11031
  }
10922
- async setFitMode(mode) {
10923
- if (this.fit === mode) return;
10924
- this.fit = mode;
10925
- if (this.renderState?.mode === "continuous") {
10926
- const state = this.listState;
10927
- if (state && this.isRenderActive(state.generation) && state.updateScale) {
10928
- state.updateScale();
10929
- } else {
10930
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10931
- }
10932
- } else if (this.renderState) {
10933
- const handle = this.handlesByTarget.get(this.container);
10934
- if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10935
- else await this.renderSlide(this.current);
11032
+ async setZoom(level) {
11033
+ const next = normalizeViewerZoomLevel(level);
11034
+ if (this.zoomLevel === next) {
11035
+ if (typeof next !== "number") await this.updateRenderedScale();
11036
+ return;
10936
11037
  }
11038
+ this.zoomLevel = next;
11039
+ await this.updateRenderedScale();
11040
+ }
11041
+ async setFitMode(mode) {
11042
+ await this.setZoom(mode === "contain" ? "automatic" : 100);
10937
11043
  }
10938
11044
  searchText(query, options) {
10939
11045
  return searchDocument(this.presentation, query, options);
@@ -11009,6 +11115,9 @@ var NormalizedPresentationViewer = class {
11009
11115
  this.navigationGeneration += 1;
11010
11116
  this.listCleanup?.();
11011
11117
  this.listCleanup = void 0;
11118
+ this.resizeObserver?.disconnect();
11119
+ this.resizeObserver = void 0;
11120
+ this.activeViewport = void 0;
11012
11121
  this.listState = void 0;
11013
11122
  this.clearSearchHighlights();
11014
11123
  this.assetCache.release();
@@ -11353,7 +11462,7 @@ function requestInitKey(value) {
11353
11462
  return String(value);
11354
11463
  }
11355
11464
  }
11356
- async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSlideChange, reportWarning, isCurrent) {
11465
+ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSlideChange, onZoomChange, reportWarning, isCurrent) {
11357
11466
  const fontManager = new PptxFontManager(props.fonts, (warning) => {
11358
11467
  if (isCurrent()) reportWarning(warning);
11359
11468
  });
@@ -11372,6 +11481,9 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11372
11481
  onSlideChange: (index) => {
11373
11482
  if (active()) onCurrentSlideChange(index);
11374
11483
  },
11484
+ onZoomChange: (state) => {
11485
+ if (active()) onZoomChange(state);
11486
+ },
11375
11487
  onSlideRendered: (index, element) => {
11376
11488
  if (!active()) return;
11377
11489
  fontManager.applyTo(element, index);
@@ -11413,8 +11525,8 @@ async function createAdapter(target, parsed, props, getCurrentProps, onCurrentSl
11413
11525
  );
11414
11526
  },
11415
11527
  goToSlide: (index, options) => normalized.goToSlide(index, options),
11416
- setZoom: (percent) => normalized.setZoom(percent),
11417
- setFitMode: (mode) => normalized.setFitMode(mode),
11528
+ setZoom: (level) => normalized.setZoom(level),
11529
+ resolveZoom: (level) => normalized.resolveZoom(level),
11418
11530
  search: (query, options) => normalized.searchText(query, options),
11419
11531
  highlight: (result, options) => normalized.highlightSearchResult(result, options),
11420
11532
  clearHighlights: () => normalized.clearSearchHighlights(),
@@ -11482,7 +11594,7 @@ var ReactPptxViewer = forwardRef(
11482
11594
  slideIndex: controlledSlide,
11483
11595
  initialSlide = 0,
11484
11596
  zoom: controlledZoom,
11485
- defaultZoom = 100,
11597
+ defaultZoom,
11486
11598
  fitMode = "contain",
11487
11599
  showToolbar = false,
11488
11600
  showThumbnails = false,
@@ -11513,6 +11625,7 @@ var ReactPptxViewer = forwardRef(
11513
11625
  onError,
11514
11626
  onWarning,
11515
11627
  onSlideChange: _onSlideChange,
11628
+ onZoomChange: _onZoomChange,
11516
11629
  onSlideRendered: _onSlideRendered,
11517
11630
  onSlideUnmounted: _onSlideUnmounted,
11518
11631
  onSearchResults,
@@ -11531,7 +11644,12 @@ var ReactPptxViewer = forwardRef(
11531
11644
  const [error, setError] = useState(null);
11532
11645
  const [loading, setLoading] = useState(true);
11533
11646
  const [internalSlide, setInternalSlide] = useState(initialSlide);
11534
- const [internalZoom, setInternalZoom] = useState(defaultZoom);
11647
+ const [internalZoom, setInternalZoom] = useState(
11648
+ () => normalizeViewerZoomLevel(defaultZoom ?? (fitMode === "contain" ? "automatic" : 100))
11649
+ );
11650
+ const [resolvedZoom, setResolvedZoom] = useState(
11651
+ () => typeof defaultZoom === "number" ? normalizeViewerZoomLevel(defaultZoom) : 100
11652
+ );
11535
11653
  const [results, setResults] = useState([]);
11536
11654
  const [runtimeWarnings, setRuntimeWarnings] = useState([]);
11537
11655
  const virtualizationOptions = typeof virtualization === "object" ? virtualization : {};
@@ -11545,14 +11663,33 @@ var ReactPptxViewer = forwardRef(
11545
11663
  const parseFetchInit = parseOptions?.fetchInit;
11546
11664
  const parseFetchInitKey = requestInitKey(parseFetchInit);
11547
11665
  const slide = clampSlide(controlledSlide ?? internalSlide, parsed?.document.slides.length ?? 0);
11548
- const zoom = controlledZoom ?? internalZoom;
11666
+ const zoom = normalizeViewerZoomLevel(controlledZoom ?? internalZoom);
11549
11667
  const latestSlideRef = useRef(slide);
11550
11668
  const latestZoomRef = useRef(zoom);
11551
- const latestFitModeRef = useRef(fitMode);
11669
+ const latestResolvedZoomRef = useRef(resolvedZoom);
11670
+ const latestControlledZoomRef = useRef(controlledZoom);
11671
+ const lastPublishedZoomRef = useRef(void 0);
11552
11672
  const lastViewerSlideRef = useRef(void 0);
11553
11673
  latestSlideRef.current = slide;
11554
11674
  latestZoomRef.current = zoom;
11555
- latestFitModeRef.current = fitMode;
11675
+ latestResolvedZoomRef.current = resolvedZoom;
11676
+ latestControlledZoomRef.current = controlledZoom;
11677
+ const notifyZoomChange = useCallback((state) => {
11678
+ const previous = lastPublishedZoomRef.current;
11679
+ if (previous?.level === state.level && Math.abs(previous.resolvedZoom - state.resolvedZoom) <= 0.01) {
11680
+ return;
11681
+ }
11682
+ lastPublishedZoomRef.current = state;
11683
+ latestPropsRef.current.onZoomChange?.(state);
11684
+ }, []);
11685
+ const publishZoomChange = useCallback(
11686
+ (state) => {
11687
+ setResolvedZoom(state.resolvedZoom);
11688
+ latestResolvedZoomRef.current = state.resolvedZoom;
11689
+ notifyZoomChange(state);
11690
+ },
11691
+ [notifyZoomChange]
11692
+ );
11556
11693
  const thumbnailCount = showThumbnails ? parsed?.document.slides.length ?? 0 : 0;
11557
11694
  const getThumbnailKey = useCallback(
11558
11695
  (index) => parsed?.document.slides[index]?.id ?? index,
@@ -11593,13 +11730,28 @@ var ReactPptxViewer = forwardRef(
11593
11730
  block: "center"
11594
11731
  });
11595
11732
  },
11596
- async setZoom(percent) {
11597
- const next = Math.max(10, Math.min(400, percent));
11733
+ async setZoom(level) {
11734
+ const next = normalizeViewerZoomLevel(level);
11735
+ const adapter = adapterRef.current;
11736
+ if (latestControlledZoomRef.current !== void 0) {
11737
+ notifyZoomChange({
11738
+ level: next,
11739
+ resolvedZoom: adapter?.resolveZoom(next) ?? (typeof next === "number" ? next : latestResolvedZoomRef.current)
11740
+ });
11741
+ return;
11742
+ }
11598
11743
  setInternalZoom(next);
11599
- await adapterRef.current?.setZoom(next);
11744
+ latestZoomRef.current = next;
11745
+ if (adapter) await adapter.setZoom(next);
11746
+ else {
11747
+ publishZoomChange({
11748
+ level: next,
11749
+ resolvedZoom: typeof next === "number" ? next : latestResolvedZoomRef.current
11750
+ });
11751
+ }
11600
11752
  },
11601
11753
  async setFitMode(nextMode) {
11602
- await adapterRef.current?.setFitMode(nextMode);
11754
+ await this.setZoom(nextMode === "contain" ? "automatic" : 100);
11603
11755
  },
11604
11756
  search(query, options) {
11605
11757
  return adapterRef.current?.search(query, options) ?? [];
@@ -11623,9 +11775,10 @@ var ReactPptxViewer = forwardRef(
11623
11775
  },
11624
11776
  getDocument: () => parsed?.document ?? null,
11625
11777
  getSlideIndex: () => latestSlideRef.current,
11626
- getZoom: () => latestZoomRef.current
11778
+ getZoom: () => latestZoomRef.current,
11779
+ getResolvedZoom: () => latestResolvedZoomRef.current
11627
11780
  }),
11628
- [adapterState?.generation, parsed]
11781
+ [adapterState?.generation, notifyZoomChange, parsed, publishZoomChange]
11629
11782
  );
11630
11783
  useImperativeHandle(forwardedRef, () => controller, [controller]);
11631
11784
  useEffect(() => {
@@ -11699,6 +11852,7 @@ var ReactPptxViewer = forwardRef(
11699
11852
  setInternalSlide(index);
11700
11853
  latestPropsRef.current.onSlideChange?.(index);
11701
11854
  },
11855
+ publishZoomChange,
11702
11856
  (warning) => {
11703
11857
  if (!isCurrent()) return;
11704
11858
  setRuntimeWarnings(
@@ -11720,11 +11874,6 @@ var ReactPptxViewer = forwardRef(
11720
11874
  adapter.destroy();
11721
11875
  return;
11722
11876
  }
11723
- await adapter.setFitMode(latestFitModeRef.current);
11724
- if (!isCurrent()) {
11725
- adapter.destroy();
11726
- return;
11727
- }
11728
11877
  await adapter.render(mode, latestSlideRef.current);
11729
11878
  if (!isCurrent()) {
11730
11879
  adapter.destroy();
@@ -11762,7 +11911,7 @@ var ReactPptxViewer = forwardRef(
11762
11911
  overscanViewport,
11763
11912
  batchSize,
11764
11913
  scrollElement,
11765
- width
11914
+ publishZoomChange
11766
11915
  ]);
11767
11916
  useEffect(() => {
11768
11917
  if (adapterState && controlledSlide !== void 0 && slide !== lastViewerSlideRef.current) {
@@ -11770,13 +11919,12 @@ var ReactPptxViewer = forwardRef(
11770
11919
  }
11771
11920
  }, [adapterState, controlledSlide, slide]);
11772
11921
  useEffect(() => {
11773
- if (adapterState && controlledZoom !== void 0) {
11774
- void adapterState.adapter.setZoom(zoom);
11775
- }
11776
- }, [adapterState, controlledZoom, zoom]);
11922
+ if (adapterState) void adapterState.adapter.setZoom(zoom);
11923
+ }, [adapterState, zoom]);
11777
11924
  useEffect(() => {
11778
- if (adapterState) void adapterState.adapter.setFitMode(fitMode);
11779
- }, [adapterState, fitMode]);
11925
+ if (controlledZoom !== void 0 || defaultZoom !== void 0) return;
11926
+ setInternalZoom(fitMode === "contain" ? "automatic" : 100);
11927
+ }, [controlledZoom, defaultZoom, fitMode]);
11780
11928
  useEffect(() => {
11781
11929
  const adapter = adapterState?.adapter;
11782
11930
  if (!adapter || searchQuery === void 0 || searchQuery === "") {
@@ -11853,20 +12001,20 @@ var ReactPptxViewer = forwardRef(
11853
12001
  "button",
11854
12002
  {
11855
12003
  type: "button",
11856
- onClick: () => void controller.setZoom(zoom - 10),
12004
+ onClick: () => void controller.setZoom(resolvedZoom - 10),
11857
12005
  "aria-label": "Zoom out",
11858
12006
  children: "\u2212"
11859
12007
  }
11860
12008
  ),
11861
12009
  /* @__PURE__ */ jsxs("span", { className: "rpv-toolbar__count", children: [
11862
- zoom,
12010
+ Math.round(resolvedZoom),
11863
12011
  "%"
11864
12012
  ] }),
11865
12013
  /* @__PURE__ */ jsx(
11866
12014
  "button",
11867
12015
  {
11868
12016
  type: "button",
11869
- onClick: () => void controller.setZoom(zoom + 10),
12017
+ onClick: () => void controller.setZoom(resolvedZoom + 10),
11870
12018
  "aria-label": "Zoom in",
11871
12019
  children: "+"
11872
12020
  }