@extend-ai/react-pptx 0.1.0 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,7 +1,16 @@
1
1
  # @extend-ai/react-pptx
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Improve PowerPoint import fidelity for word spacing, hyperlink styling, diagonal stripe shapes, and straight connectors, including zero-width vertical timeline stems.
8
+
3
9
  ## Unreleased
4
10
 
11
+ - Make slide navigation observable before off-window rendering completes, isolate resource
12
+ readiness per slide mount, update zoom and fit geometry without rebuilding mounted slide DOM,
13
+ and reduce thumbnail work outside the visible rail.
5
14
  - Expose consumer-owned slide thumbnail rails through `usePptxViewerThumbnails`, including stable
6
15
  container refs, resolution bounds, render status, imperative rendering, and cleanup. Add
7
16
  DOCX-style visible/prefetch render windows and virtualize the built-in thumbnail scrollport.
package/dist/index.js CHANGED
@@ -117,8 +117,6 @@ var UNIVERSAL_SANS_FALLBACKS = [
117
117
  "Noto Sans Arabic",
118
118
  "Noto Sans Hebrew",
119
119
  "Segoe UI Symbol",
120
- "Apple Color Emoji",
121
- "Segoe UI Emoji",
122
120
  "sans-serif"
123
121
  ];
124
122
  function normalizeFamily(value) {
@@ -9251,6 +9249,11 @@ var DEFAULT_TEXT_VERTICAL_INSET_EMU = 45720;
9251
9249
  var POWERPOINT_SINGLE_LINE_HEIGHT = 1.15;
9252
9250
  var LIST_ITEM_GAP = 24;
9253
9251
  var FALLBACK_VIEWPORT_RECT = { width: 800, height: 600 };
9252
+ function scaleListAnchorOffset(offset, previousSlideHeight, nextSlideHeight) {
9253
+ if (previousSlideHeight <= 0) return offset;
9254
+ if (offset <= previousSlideHeight) return offset * (nextSlideHeight / previousSlideHeight);
9255
+ return nextSlideHeight + (offset - previousSlideHeight);
9256
+ }
9254
9257
  var METAFILE_FONT_MAP = Object.fromEntries(
9255
9258
  Object.entries(OFFICE_FONT_FALLBACKS).map(([family, fallbacks]) => [
9256
9259
  family,
@@ -9407,6 +9410,8 @@ function createTextRunElement(run, paragraph, fontScale) {
9407
9410
  span.href = href;
9408
9411
  span.target = "_blank";
9409
9412
  span.rel = "noreferrer noopener";
9413
+ if (!run.color) span.style.color = "inherit";
9414
+ if (!decorations.length) span.style.textDecoration = "none";
9410
9415
  }
9411
9416
  return span;
9412
9417
  }
@@ -9553,6 +9558,8 @@ function applyLine(element, line) {
9553
9558
  }
9554
9559
  function normalizedPresetGeometryPath(preset) {
9555
9560
  switch (preset) {
9561
+ case "straightConnector1":
9562
+ return "M 0 0 L 1 1";
9556
9563
  case "heart":
9557
9564
  return "M 0.5 0.25 C 0.7083333333 -0.3333333333 1.5208333333 0.25 0.5 1 C -0.5208333333 0.25 0.2916666667 -0.3333333333 0.5 0.25 Z";
9558
9565
  default:
@@ -9579,6 +9586,9 @@ function applyGeometry(element, preset) {
9579
9586
  case "parallelogram":
9580
9587
  element.style.clipPath = "polygon(20% 0, 100% 0, 80% 100%, 0 100%)";
9581
9588
  break;
9589
+ case "diagStripe":
9590
+ element.style.clipPath = "polygon(0 50%, 50% 0, 100% 0, 0 100%)";
9591
+ break;
9582
9592
  case "hexagon":
9583
9593
  element.style.clipPath = "polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%)";
9584
9594
  break;
@@ -9790,7 +9800,7 @@ var NormalizedPresentationViewer = class {
9790
9800
  metafileUrls = /* @__PURE__ */ new Map();
9791
9801
  listCleanup;
9792
9802
  highlight;
9793
- pendingResources = /* @__PURE__ */ new Set();
9803
+ activeMountResources;
9794
9804
  mountedHandles = /* @__PURE__ */ new Set();
9795
9805
  handlesByTarget = /* @__PURE__ */ new Map();
9796
9806
  destroyed = false;
@@ -9821,6 +9831,10 @@ var NormalizedPresentationViewer = class {
9821
9831
  const height = this.presentation.size.heightEmu / EMU_PER_CSS_PIXEL;
9822
9832
  return Number.isFinite(height) && height > 0 ? height : 1;
9823
9833
  }
9834
+ scaleForViewport(viewportWidth) {
9835
+ const fitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
9836
+ return fitScale * (this.zoom / 100);
9837
+ }
9824
9838
  isRenderActive(generation) {
9825
9839
  return !this.destroyed && generation === this.renderGeneration;
9826
9840
  }
@@ -9873,12 +9887,13 @@ var NormalizedPresentationViewer = class {
9873
9887
  applyAsset(assetId, apply, nodeId) {
9874
9888
  const asset = this.presentation.assets[assetId];
9875
9889
  if (!asset) return;
9890
+ const resources = this.activeMountResources;
9876
9891
  const pending = this.assetUrl(asset).then((url) => {
9877
9892
  if (url && !this.destroyed) apply(url);
9878
9893
  }).catch((error) => {
9879
9894
  if (!this.destroyed) this.callbacks.onNodeError?.(nodeId, error);
9880
- }).finally(() => this.pendingResources.delete(pending));
9881
- this.pendingResources.add(pending);
9895
+ });
9896
+ resources?.add(pending);
9882
9897
  }
9883
9898
  /**
9884
9899
  * Applies DrawingML picture recolor effects (`a:biLevel`, `a:grayscl`,
@@ -9964,8 +9979,20 @@ var NormalizedPresentationViewer = class {
9964
9979
  svg.style.height = "100%";
9965
9980
  svg.style.overflow = "visible";
9966
9981
  svg.style.pointerEvents = "none";
9982
+ let renderedPathData = pathData;
9983
+ if (node.geometry.preset === "straightConnector1") {
9984
+ if (node.transform.width === 0 && node.transform.height !== 0) {
9985
+ svg.style.left = "-0.5px";
9986
+ svg.style.width = "1px";
9987
+ renderedPathData = "M 0.5 0 L 0.5 1";
9988
+ } else if (node.transform.height === 0 && node.transform.width !== 0) {
9989
+ svg.style.top = "-0.5px";
9990
+ svg.style.height = "1px";
9991
+ renderedPathData = "M 0 0.5 L 1 0.5";
9992
+ }
9993
+ }
9967
9994
  const path = document.createElementNS(namespace, "path");
9968
- path.setAttribute("d", pathData);
9995
+ path.setAttribute("d", renderedPathData);
9969
9996
  path.setAttribute("vector-effect", "non-scaling-stroke");
9970
9997
  path.style.fill = "none";
9971
9998
  const fillStyle = node.fill;
@@ -10022,11 +10049,7 @@ var NormalizedPresentationViewer = class {
10022
10049
  image.setAttribute("preserveAspectRatio", "none");
10023
10050
  image.setAttribute("clip-path", `url(#${clipId})`);
10024
10051
  image.style.opacity = String(Math.max(0, Math.min(1, fillStyle.opacity ?? 1)));
10025
- this.applyAsset(
10026
- fillStyle.assetId,
10027
- (url) => image.setAttribute("href", url),
10028
- node.id
10029
- );
10052
+ this.applyAsset(fillStyle.assetId, (url) => image.setAttribute("href", url), node.id);
10030
10053
  svg.append(image);
10031
10054
  }
10032
10055
  if (node.line) {
@@ -10172,6 +10195,8 @@ var NormalizedPresentationViewer = class {
10172
10195
  element.href = href;
10173
10196
  element.target = "_blank";
10174
10197
  element.rel = "noreferrer noopener";
10198
+ element.style.color = "inherit";
10199
+ element.style.textDecoration = "none";
10175
10200
  }
10176
10201
  if (node.type === "shape") {
10177
10202
  const presetPath = normalizedPresetGeometryPath(node.geometry.preset);
@@ -10387,23 +10412,35 @@ var NormalizedPresentationViewer = class {
10387
10412
  element,
10388
10413
  target,
10389
10414
  ready: Promise.resolve(),
10415
+ setScale: () => {
10416
+ },
10390
10417
  dispose: () => element.remove()
10391
10418
  };
10392
10419
  }
10393
10420
  mount(index, target, scale) {
10394
10421
  if (this.destroyed) return this.emptyHandle(target);
10395
10422
  this.handlesByTarget.get(target)?.dispose();
10396
- const slide = this.createSlide(index);
10397
- const fitScale = this.fit === "contain" ? Math.min(1, (target.clientWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10398
- const effectiveScale = scale ?? fitScale * (this.zoom / 100);
10423
+ const resources = /* @__PURE__ */ new Set();
10424
+ const previousResources = this.activeMountResources;
10425
+ this.activeMountResources = resources;
10426
+ let slide;
10427
+ try {
10428
+ slide = this.createSlide(index);
10429
+ } finally {
10430
+ this.activeMountResources = previousResources;
10431
+ }
10432
+ const effectiveScale = scale ?? this.scaleForViewport(target.clientWidth);
10399
10433
  const wrapper = document.createElement("div");
10400
10434
  wrapper.dataset.rpvSlideWrapper = String(index);
10401
10435
  wrapper.style.position = "relative";
10402
- wrapper.style.width = `${this.naturalSlideWidth * effectiveScale}px`;
10403
- wrapper.style.height = `${this.naturalSlideHeight * effectiveScale}px`;
10404
10436
  wrapper.style.margin = "0 auto";
10405
10437
  slide.style.transformOrigin = "top left";
10406
- slide.style.transform = `scale(${effectiveScale})`;
10438
+ const setScale = (nextScale) => {
10439
+ wrapper.style.width = `${this.naturalSlideWidth * nextScale}px`;
10440
+ wrapper.style.height = `${this.naturalSlideHeight * nextScale}px`;
10441
+ slide.style.transform = `scale(${nextScale})`;
10442
+ };
10443
+ setScale(effectiveScale);
10407
10444
  wrapper.append(slide);
10408
10445
  target.replaceChildren(wrapper);
10409
10446
  this.callbacks.onSlideRendered?.(index, slide);
@@ -10411,7 +10448,8 @@ var NormalizedPresentationViewer = class {
10411
10448
  const handle = {
10412
10449
  element: slide,
10413
10450
  target,
10414
- ready: Promise.all([...this.pendingResources]).then(() => void 0),
10451
+ ready: Promise.all([...resources]).then(() => void 0),
10452
+ setScale,
10415
10453
  dispose: () => {
10416
10454
  if (disposed) return;
10417
10455
  disposed = true;
@@ -10439,6 +10477,32 @@ var NormalizedPresentationViewer = class {
10439
10477
  await Promise.resolve();
10440
10478
  }
10441
10479
  }
10480
+ async yieldNavigationPaint() {
10481
+ if (typeof requestAnimationFrame !== "undefined") {
10482
+ await new Promise((resolve) => {
10483
+ let settled = false;
10484
+ let firstFrame;
10485
+ let secondFrame;
10486
+ const complete = () => {
10487
+ if (settled) return;
10488
+ settled = true;
10489
+ clearTimeout(timeout2);
10490
+ if (typeof cancelAnimationFrame !== "undefined") {
10491
+ if (firstFrame !== void 0) cancelAnimationFrame(firstFrame);
10492
+ if (secondFrame !== void 0) cancelAnimationFrame(secondFrame);
10493
+ }
10494
+ resolve();
10495
+ };
10496
+ const timeout2 = setTimeout(complete, 100);
10497
+ firstFrame = requestAnimationFrame(() => {
10498
+ if (settled) return;
10499
+ secondFrame = requestAnimationFrame(complete);
10500
+ });
10501
+ });
10502
+ } else {
10503
+ await new Promise((resolve) => setTimeout(resolve, 0));
10504
+ }
10505
+ }
10442
10506
  async mountInBatches(placeholders, indices, batchSize, generation) {
10443
10507
  const uniqueIndices = [...new Set(indices)].filter(
10444
10508
  (index) => index >= 0 && index < placeholders.length
@@ -10471,12 +10535,13 @@ var NormalizedPresentationViewer = class {
10471
10535
  createListPlaceholder(index, item, generation, scale) {
10472
10536
  let handle;
10473
10537
  let mountPromise;
10538
+ let effectiveScale = scale;
10474
10539
  const isActive = () => this.isRenderActive(generation) && this.listState?.generation === generation;
10475
10540
  const mount = () => {
10476
10541
  if (!isActive()) return Promise.resolve();
10477
10542
  if (handle) return handle.ready;
10478
10543
  if (mountPromise) return mountPromise;
10479
- const nextHandle = this.mount(index, item, scale);
10544
+ const nextHandle = this.mount(index, item, effectiveScale);
10480
10545
  handle = nextHandle;
10481
10546
  const pending = nextHandle.ready.then(() => {
10482
10547
  if (!isActive() || handle !== nextHandle) {
@@ -10492,11 +10557,16 @@ var NormalizedPresentationViewer = class {
10492
10557
  const unmount = () => {
10493
10558
  const mounted = handle;
10494
10559
  handle = void 0;
10560
+ mountPromise = void 0;
10495
10561
  if (mounted) {
10496
10562
  mounted.dispose();
10497
10563
  }
10498
10564
  };
10499
- return { item, mount, unmount, isMounted: () => Boolean(handle) };
10565
+ const setScale = (nextScale) => {
10566
+ effectiveScale = nextScale;
10567
+ handle?.setScale(nextScale);
10568
+ };
10569
+ return { item, mount, unmount, isMounted: () => Boolean(handle), setScale };
10500
10570
  }
10501
10571
  async renderList(options = {}) {
10502
10572
  const windowingEnabled = options.enabled !== false;
@@ -10520,11 +10590,9 @@ var NormalizedPresentationViewer = class {
10520
10590
  Math.min(this.slideCount - 1, options.initialSlideIndex ?? this.current)
10521
10591
  );
10522
10592
  const scroller = options.scrollElement ?? this.container;
10523
- const viewportWidth = scroller.clientWidth;
10524
- const estimatedFitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10525
- const effectiveScale = estimatedFitScale * (this.zoom / 100);
10526
- const slideHeight = this.naturalSlideHeight * effectiveScale;
10527
- const itemStride = slideHeight + LIST_ITEM_GAP;
10593
+ let effectiveScale = this.scaleForViewport(scroller.clientWidth);
10594
+ let slideHeight = this.naturalSlideHeight * effectiveScale;
10595
+ let itemStride = slideHeight + LIST_ITEM_GAP;
10528
10596
  if (!windowingEnabled) {
10529
10597
  const placeholders2 = this.presentation.slides.map((_, index) => {
10530
10598
  const item = document.createElement("div");
@@ -10532,9 +10600,35 @@ var NormalizedPresentationViewer = class {
10532
10600
  item.style.minHeight = `${slideHeight}px`;
10533
10601
  item.style.margin = `0 auto ${LIST_ITEM_GAP}px`;
10534
10602
  this.container.append(item);
10535
- return this.createListPlaceholder(index, item, generation);
10603
+ return this.createListPlaceholder(index, item, generation, effectiveScale);
10536
10604
  });
10537
10605
  const state2 = { generation, options: normalizedOptions, placeholders: placeholders2 };
10606
+ state2.updateScale = () => {
10607
+ if (!this.isRenderActive(generation) || this.listState !== state2) return;
10608
+ const previousStride = itemStride;
10609
+ const previousSlideHeight = slideHeight;
10610
+ const listStart = scroller === this.container ? 0 : this.container.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
10611
+ const listOffset = scroller.scrollTop - listStart;
10612
+ const anchorIndex = Math.max(
10613
+ 0,
10614
+ Math.min(this.slideCount - 1, Math.floor(listOffset / previousStride))
10615
+ );
10616
+ const anchorOffset = listOffset - anchorIndex * previousStride;
10617
+ effectiveScale = this.scaleForViewport(scroller.clientWidth);
10618
+ slideHeight = this.naturalSlideHeight * effectiveScale;
10619
+ itemStride = slideHeight + LIST_ITEM_GAP;
10620
+ placeholders2.forEach((placeholder) => {
10621
+ placeholder.item.style.minHeight = `${slideHeight}px`;
10622
+ placeholder.setScale(effectiveScale);
10623
+ });
10624
+ if (listOffset < 0) return;
10625
+ const nextAnchorOffset = scaleListAnchorOffset(
10626
+ anchorOffset,
10627
+ previousSlideHeight,
10628
+ slideHeight
10629
+ );
10630
+ scroller.scrollTop = Math.max(0, listStart + anchorIndex * itemStride + nextAnchorOffset);
10631
+ };
10538
10632
  this.listState = state2;
10539
10633
  await this.mountInBatches(
10540
10634
  placeholders2,
@@ -10576,6 +10670,20 @@ var NormalizedPresentationViewer = class {
10576
10670
  const state = { generation, options: normalizedOptions, placeholders };
10577
10671
  this.listState = state;
10578
10672
  const isActive = () => this.isRenderActive(generation) && this.listState === state;
10673
+ const deferredMounts = /* @__PURE__ */ new Map();
10674
+ let virtualWindow = /* @__PURE__ */ new Set();
10675
+ state.deferMount = (index) => {
10676
+ deferredMounts.set(index, (deferredMounts.get(index) ?? 0) + 1);
10677
+ };
10678
+ state.resumeMount = (index) => {
10679
+ const remaining = (deferredMounts.get(index) ?? 0) - 1;
10680
+ if (remaining > 0) {
10681
+ deferredMounts.set(index, remaining);
10682
+ return;
10683
+ }
10684
+ deferredMounts.delete(index);
10685
+ if (virtualWindow.has(index)) void placeholders[index]?.mount();
10686
+ };
10579
10687
  const measureViewportRect = () => {
10580
10688
  const rect = scroller.getBoundingClientRect();
10581
10689
  if (rect.width > 0 && rect.height > 0) return { width: rect.width, height: rect.height };
@@ -10587,16 +10695,25 @@ var NormalizedPresentationViewer = class {
10587
10695
  const initialRect = measureViewportRect();
10588
10696
  const requestedOverscan = options.overscanViewport ?? 1.5;
10589
10697
  const overscanViewports = Number.isFinite(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 1.5;
10590
- const overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10698
+ let overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10591
10699
  const measureScrollMargin = () => sizer.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
10592
10700
  let scrollMargin = measureScrollMargin();
10593
10701
  let needsSettleAlignment = initialIndex > 0;
10594
10702
  let lastProgrammaticTop;
10703
+ const writeScrollTop = (top, behavior) => {
10704
+ lastProgrammaticTop = top;
10705
+ if (typeof scroller.scrollTo === "function") {
10706
+ scroller.scrollTo(behavior ? { top, behavior } : { top });
10707
+ } else {
10708
+ scroller.scrollTop = top;
10709
+ }
10710
+ };
10595
10711
  const reconcile = (instance) => {
10596
10712
  if (!isActive()) return;
10597
10713
  const windowed = new Set(instance.getVirtualItems().map((item) => item.index));
10714
+ virtualWindow = windowed;
10598
10715
  placeholders.forEach((placeholder, index) => {
10599
- if (windowed.has(index)) void placeholder.mount();
10716
+ if (windowed.has(index) && !deferredMounts.has(index)) void placeholder.mount();
10600
10717
  else placeholder.unmount();
10601
10718
  });
10602
10719
  const offset = (instance.scrollOffset ?? 0) - scrollMargin;
@@ -10659,12 +10776,7 @@ var NormalizedPresentationViewer = class {
10659
10776
  },
10660
10777
  scrollToFn: (offset, { adjustments, behavior }) => {
10661
10778
  const top = offset + (adjustments ?? 0);
10662
- lastProgrammaticTop = top;
10663
- if (typeof scroller.scrollTo === "function") {
10664
- scroller.scrollTo(behavior ? { top, behavior } : { top });
10665
- } else {
10666
- scroller.scrollTop = top;
10667
- }
10779
+ writeScrollTop(top, behavior);
10668
10780
  },
10669
10781
  onChange: (instance) => reconcile(instance)
10670
10782
  });
@@ -10675,6 +10787,39 @@ var NormalizedPresentationViewer = class {
10675
10787
  behavior: scrollOptions?.behavior === "smooth" ? "smooth" : "auto"
10676
10788
  });
10677
10789
  };
10790
+ state.updateScale = () => {
10791
+ if (!isActive()) return;
10792
+ const previousStride = itemStride;
10793
+ const previousSlideHeight = slideHeight;
10794
+ const anchorIndex = this.current;
10795
+ const observedScrollTop = virtualizer.scrollOffset ?? scroller.scrollTop;
10796
+ const observedIndex = Math.floor((observedScrollTop - scrollMargin) / previousStride);
10797
+ const intendedIndex = lastProgrammaticTop === void 0 ? void 0 : Math.floor((lastProgrammaticTop - scrollMargin) / previousStride);
10798
+ const previousScrollTop = Math.abs(observedIndex - anchorIndex) > 1 && intendedIndex !== void 0 && Math.abs(intendedIndex - anchorIndex) <= 1 ? lastProgrammaticTop : observedScrollTop;
10799
+ const previousOffset = previousScrollTop - scrollMargin;
10800
+ const anchorOffset = previousOffset - anchorIndex * previousStride;
10801
+ effectiveScale = this.scaleForViewport(scroller.clientWidth);
10802
+ slideHeight = this.naturalSlideHeight * effectiveScale;
10803
+ itemStride = slideHeight + LIST_ITEM_GAP;
10804
+ sizer.style.height = `${this.slideCount * itemStride - LIST_ITEM_GAP}px`;
10805
+ placeholders.forEach((placeholder, index) => {
10806
+ placeholder.item.style.height = `${slideHeight}px`;
10807
+ placeholder.item.style.transform = `translateY(${index * itemStride}px)`;
10808
+ placeholder.setScale(effectiveScale);
10809
+ });
10810
+ const nextAnchorOffset = scaleListAnchorOffset(
10811
+ anchorOffset,
10812
+ previousSlideHeight,
10813
+ slideHeight
10814
+ );
10815
+ const nextScrollTop = previousOffset < 0 ? previousScrollTop : Math.max(0, scrollMargin + anchorIndex * itemStride + nextAnchorOffset);
10816
+ const viewportRect = measureViewportRect();
10817
+ overscan = Math.max(1, Math.ceil(overscanViewports * viewportRect.height / itemStride));
10818
+ virtualizer.scrollOffset = nextScrollTop;
10819
+ writeScrollTop(nextScrollTop);
10820
+ virtualizer.setOptions({ ...virtualizer.options, overscan });
10821
+ virtualizer.measure();
10822
+ };
10678
10823
  const teardown = virtualizer._didMount();
10679
10824
  this.listCleanup = teardown;
10680
10825
  virtualizer._willUpdate();
@@ -10702,16 +10847,27 @@ var NormalizedPresentationViewer = class {
10702
10847
  if (state && this.isRenderActive(state.generation)) {
10703
10848
  const navigation = ++this.navigationGeneration;
10704
10849
  const placeholder = state.placeholders[next];
10705
- await placeholder?.mount();
10706
- if (this.destroyed || navigation !== this.navigationGeneration || !this.isRenderActive(state.generation) || this.listState !== state) {
10707
- return;
10708
- }
10709
- if (changed) {
10710
- if (state.scrollToIndex) state.scrollToIndex(next, scrollOptions);
10711
- else placeholder?.item.scrollIntoView?.(scrollOptions);
10850
+ let mountDeferred = Boolean(state.deferMount && placeholder && !placeholder.isMounted());
10851
+ if (mountDeferred) state.deferMount?.(next);
10852
+ try {
10853
+ if (changed) {
10854
+ if (state.scrollToIndex) state.scrollToIndex(next, scrollOptions);
10855
+ else placeholder?.item.scrollIntoView?.(scrollOptions);
10856
+ this.current = next;
10857
+ this.notifySlideChange(next);
10858
+ }
10859
+ if (mountDeferred) await this.yieldNavigationPaint();
10860
+ if (this.destroyed || navigation !== this.navigationGeneration || !this.isRenderActive(state.generation) || this.listState !== state) {
10861
+ return;
10862
+ }
10863
+ if (mountDeferred) {
10864
+ state.resumeMount?.(next);
10865
+ mountDeferred = false;
10866
+ }
10867
+ await placeholder?.mount();
10868
+ } finally {
10869
+ if (mountDeferred) state.resumeMount?.(next);
10712
10870
  }
10713
- this.current = next;
10714
- if (changed) this.notifySlideChange(next);
10715
10871
  return;
10716
10872
  }
10717
10873
  if (!changed && this.container.querySelector(`[data-rpv-slide-index="${next}"]`)) return;
@@ -10722,15 +10878,33 @@ var NormalizedPresentationViewer = class {
10722
10878
  if (this.zoom === next) return;
10723
10879
  this.zoom = next;
10724
10880
  if (this.renderState?.mode === "continuous") {
10725
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10726
- } else if (this.renderState) await this.renderSlide(this.current);
10881
+ const state = this.listState;
10882
+ if (state && this.isRenderActive(state.generation) && state.updateScale) {
10883
+ state.updateScale();
10884
+ } else {
10885
+ await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10886
+ }
10887
+ } else if (this.renderState) {
10888
+ const handle = this.handlesByTarget.get(this.container);
10889
+ if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10890
+ else await this.renderSlide(this.current);
10891
+ }
10727
10892
  }
10728
10893
  async setFitMode(mode) {
10729
10894
  if (this.fit === mode) return;
10730
10895
  this.fit = mode;
10731
10896
  if (this.renderState?.mode === "continuous") {
10732
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10733
- } else if (this.renderState) await this.renderSlide(this.current);
10897
+ const state = this.listState;
10898
+ if (state && this.isRenderActive(state.generation) && state.updateScale) {
10899
+ state.updateScale();
10900
+ } else {
10901
+ await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10902
+ }
10903
+ } else if (this.renderState) {
10904
+ const handle = this.handlesByTarget.get(this.container);
10905
+ if (handle) handle.setScale(this.scaleForViewport(this.container.clientWidth));
10906
+ else await this.renderSlide(this.current);
10907
+ }
10734
10908
  }
10735
10909
  searchText(query, options) {
10736
10910
  return searchDocument(this.presentation, query, options);
@@ -10812,7 +10986,7 @@ var NormalizedPresentationViewer = class {
10812
10986
  this.objectUrls.clear();
10813
10987
  this.assetUrls.clear();
10814
10988
  this.metafileUrls.clear();
10815
- this.pendingResources.clear();
10989
+ this.activeMountResources = void 0;
10816
10990
  for (const handle of [...this.mountedHandles]) handle.dispose();
10817
10991
  this.handlesByTarget.clear();
10818
10992
  this.renderState = void 0;
@@ -11080,7 +11254,7 @@ var presentationParsingDefaults = Object.freeze({
11080
11254
  maxInputBytes: DEFAULT_MAX_INPUT_BYTES
11081
11255
  });
11082
11256
  var THUMBNAIL_ROW_ESTIMATE = 123;
11083
- var THUMBNAIL_OVERSCAN = 3;
11257
+ var THUMBNAIL_OVERSCAN = 1;
11084
11258
  var THUMBNAIL_INITIAL_RECT = { height: 780, width: 184 };
11085
11259
  var observeThumbnailFilmstripRect = (instance, callback) => {
11086
11260
  const element = instance.scrollElement;
@@ -11801,9 +11975,12 @@ function usePptxViewer() {
11801
11975
  }
11802
11976
  var EMU_PER_CSS_PIXEL2 = 9525;
11803
11977
  var DEFAULT_MAX_DIMENSION2 = 160;
11804
- function normalizeSlideIndexes(indexes, slideCount) {
11978
+ function normalizeSlideIndexes(indexes, slideCount, sort = true) {
11805
11979
  if (!indexes?.length || slideCount <= 0) return [];
11806
- return [...new Set(indexes)].filter((index) => Number.isInteger(index) && index >= 0 && index < slideCount).sort((left, right) => left - right);
11980
+ const normalized = [...new Set(indexes)].filter(
11981
+ (index) => Number.isInteger(index) && index >= 0 && index < slideCount
11982
+ );
11983
+ return sort ? normalized.sort((left, right) => left - right) : normalized;
11807
11984
  }
11808
11985
  function finitePositive2(value) {
11809
11986
  return value !== void 0 && Number.isFinite(value) && value > 0 ? value : void 0;
@@ -11841,6 +12018,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11841
12018
  const attachedElementsRef = useRef(/* @__PURE__ */ new Map());
11842
12019
  const cleanupsRef = useRef(/* @__PURE__ */ new Map());
11843
12020
  const prefetchedRef = useRef(/* @__PURE__ */ new Map());
12021
+ const prefetchInFlightRef = useRef(/* @__PURE__ */ new Map());
11844
12022
  const renderGenerationRef = useRef(/* @__PURE__ */ new Map());
11845
12023
  const refCallbacksRef = useRef(/* @__PURE__ */ new Map());
11846
12024
  const attachRef = useRef(() => {
@@ -11853,7 +12031,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11853
12031
  [options.renderWindow?.visibleSlideIndexes, slideCount]
11854
12032
  );
11855
12033
  const prefetchSlideIndexes = useMemo(
11856
- () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount),
12034
+ () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount, false),
11857
12035
  [options.renderWindow?.prefetchSlideIndexes, slideCount]
11858
12036
  );
11859
12037
  const visibleSlideIndexesKey = visibleSlideIndexes.join(",");
@@ -11968,28 +12146,46 @@ function usePptxViewerThumbnails(controller, options = {}) {
11968
12146
  }, [renderThumbnail]);
11969
12147
  const prefetchThumbnail = useCallback(
11970
12148
  async (slideIndex) => {
11971
- if (disabled || !controller?.isReady() || attachedElementsRef.current.has(slideIndex) || prefetchedRef.current.has(slideIndex)) {
11972
- return;
11973
- }
11974
- const generation = (renderGenerationRef.current.get(slideIndex) ?? 0) + 1;
11975
- renderGenerationRef.current.set(slideIndex, generation);
11976
- const element = globalThis.document.createElement("div");
11977
- setThumbnailState(slideIndex, { status: "rendering" });
11978
- try {
11979
- const cleanup = await controller.renderThumbnail(slideIndex, element, {
11980
- width: size.width
11981
- });
11982
- if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation || attachedElementsRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
11983
- cleanup();
11984
- return;
12149
+ while (true) {
12150
+ const existing = prefetchInFlightRef.current.get(slideIndex);
12151
+ if (existing) {
12152
+ await existing;
12153
+ continue;
11985
12154
  }
11986
- prefetchedRef.current.set(slideIndex, { cleanup, element, width: size.width });
11987
- setThumbnailState(slideIndex, { status: "ready" });
11988
- } catch (reason) {
11989
- if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation) {
12155
+ if (disabled || !controller?.isReady() || attachedElementsRef.current.has(slideIndex) || prefetchedRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
11990
12156
  return;
11991
12157
  }
11992
- setThumbnailState(slideIndex, { error: asError(reason), status: "error" });
12158
+ const generation = (renderGenerationRef.current.get(slideIndex) ?? 0) + 1;
12159
+ renderGenerationRef.current.set(slideIndex, generation);
12160
+ const element = globalThis.document.createElement("div");
12161
+ setThumbnailState(slideIndex, { status: "rendering" });
12162
+ const pending = (async () => {
12163
+ try {
12164
+ const cleanup = await controller.renderThumbnail(slideIndex, element, {
12165
+ width: size.width
12166
+ });
12167
+ if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation || attachedElementsRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
12168
+ cleanup();
12169
+ return;
12170
+ }
12171
+ prefetchedRef.current.set(slideIndex, { cleanup, element, width: size.width });
12172
+ setThumbnailState(slideIndex, { status: "ready" });
12173
+ } catch (reason) {
12174
+ if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation) {
12175
+ return;
12176
+ }
12177
+ setThumbnailState(slideIndex, { error: asError(reason), status: "error" });
12178
+ }
12179
+ })();
12180
+ prefetchInFlightRef.current.set(slideIndex, pending);
12181
+ try {
12182
+ await pending;
12183
+ } finally {
12184
+ if (prefetchInFlightRef.current.get(slideIndex) === pending) {
12185
+ prefetchInFlightRef.current.delete(slideIndex);
12186
+ }
12187
+ }
12188
+ return;
11993
12189
  }
11994
12190
  },
11995
12191
  [controller, disabled, setThumbnailState, size.width]
@@ -12005,14 +12201,17 @@ function usePptxViewerThumbnails(controller, options = {}) {
12005
12201
  cleanupsRef.current.clear();
12006
12202
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12007
12203
  prefetchedRef.current.clear();
12204
+ prefetchInFlightRef.current.clear();
12008
12205
  };
12009
12206
  }, []);
12010
12207
  useEffect(
12011
12208
  () => () => {
12209
+ for (const index of prefetchInFlightRef.current.keys()) invalidateThumbnail(index);
12210
+ prefetchInFlightRef.current.clear();
12012
12211
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12013
12212
  prefetchedRef.current.clear();
12014
12213
  },
12015
- [controller, size.width]
12214
+ [controller, invalidateThumbnail, size.width]
12016
12215
  );
12017
12216
  useEffect(() => {
12018
12217
  void rerenderAttachedThumbnails();
@@ -12024,6 +12223,13 @@ function usePptxViewerThumbnails(controller, options = {}) {
12024
12223
  if (!attachedElementsRef.current.has(index)) setThumbnailState(index, { status: "idle" });
12025
12224
  }
12026
12225
  }
12226
+ for (const index of prefetchInFlightRef.current.keys()) {
12227
+ if (disabled || attachedElementsRef.current.has(index) || !requestedPrefetchSlideIndexSetRef.current.has(index)) {
12228
+ invalidateThumbnail(index);
12229
+ prefetchInFlightRef.current.delete(index);
12230
+ if (!attachedElementsRef.current.has(index)) setThumbnailState(index, { status: "idle" });
12231
+ }
12232
+ }
12027
12233
  if (disabled) return;
12028
12234
  let cancelled = false;
12029
12235
  void (async () => {
@@ -12038,6 +12244,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
12038
12244
  }, [
12039
12245
  disabled,
12040
12246
  disposePrefetchedThumbnail,
12247
+ invalidateThumbnail,
12041
12248
  prefetchSlideIndexesKey,
12042
12249
  prefetchThumbnail,
12043
12250
  setThumbnailState