@extend-ai/react-pptx 0.1.1 → 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
@@ -8,6 +8,9 @@
8
8
 
9
9
  ## Unreleased
10
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.
11
14
  - Expose consumer-owned slide thumbnail rails through `usePptxViewerThumbnails`, including stable
12
15
  container refs, resolution bounds, render status, imperative rendering, and cleanup. Add
13
16
  DOCX-style visible/prefetch render windows and virtualize the built-in thumbnail scrollport.
package/dist/index.js CHANGED
@@ -9249,6 +9249,11 @@ var DEFAULT_TEXT_VERTICAL_INSET_EMU = 45720;
9249
9249
  var POWERPOINT_SINGLE_LINE_HEIGHT = 1.15;
9250
9250
  var LIST_ITEM_GAP = 24;
9251
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
+ }
9252
9257
  var METAFILE_FONT_MAP = Object.fromEntries(
9253
9258
  Object.entries(OFFICE_FONT_FALLBACKS).map(([family, fallbacks]) => [
9254
9259
  family,
@@ -9795,7 +9800,7 @@ var NormalizedPresentationViewer = class {
9795
9800
  metafileUrls = /* @__PURE__ */ new Map();
9796
9801
  listCleanup;
9797
9802
  highlight;
9798
- pendingResources = /* @__PURE__ */ new Set();
9803
+ activeMountResources;
9799
9804
  mountedHandles = /* @__PURE__ */ new Set();
9800
9805
  handlesByTarget = /* @__PURE__ */ new Map();
9801
9806
  destroyed = false;
@@ -9826,6 +9831,10 @@ var NormalizedPresentationViewer = class {
9826
9831
  const height = this.presentation.size.heightEmu / EMU_PER_CSS_PIXEL;
9827
9832
  return Number.isFinite(height) && height > 0 ? height : 1;
9828
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
+ }
9829
9838
  isRenderActive(generation) {
9830
9839
  return !this.destroyed && generation === this.renderGeneration;
9831
9840
  }
@@ -9878,12 +9887,13 @@ var NormalizedPresentationViewer = class {
9878
9887
  applyAsset(assetId, apply, nodeId) {
9879
9888
  const asset = this.presentation.assets[assetId];
9880
9889
  if (!asset) return;
9890
+ const resources = this.activeMountResources;
9881
9891
  const pending = this.assetUrl(asset).then((url) => {
9882
9892
  if (url && !this.destroyed) apply(url);
9883
9893
  }).catch((error) => {
9884
9894
  if (!this.destroyed) this.callbacks.onNodeError?.(nodeId, error);
9885
- }).finally(() => this.pendingResources.delete(pending));
9886
- this.pendingResources.add(pending);
9895
+ });
9896
+ resources?.add(pending);
9887
9897
  }
9888
9898
  /**
9889
9899
  * Applies DrawingML picture recolor effects (`a:biLevel`, `a:grayscl`,
@@ -10039,11 +10049,7 @@ var NormalizedPresentationViewer = class {
10039
10049
  image.setAttribute("preserveAspectRatio", "none");
10040
10050
  image.setAttribute("clip-path", `url(#${clipId})`);
10041
10051
  image.style.opacity = String(Math.max(0, Math.min(1, fillStyle.opacity ?? 1)));
10042
- this.applyAsset(
10043
- fillStyle.assetId,
10044
- (url) => image.setAttribute("href", url),
10045
- node.id
10046
- );
10052
+ this.applyAsset(fillStyle.assetId, (url) => image.setAttribute("href", url), node.id);
10047
10053
  svg.append(image);
10048
10054
  }
10049
10055
  if (node.line) {
@@ -10406,23 +10412,35 @@ var NormalizedPresentationViewer = class {
10406
10412
  element,
10407
10413
  target,
10408
10414
  ready: Promise.resolve(),
10415
+ setScale: () => {
10416
+ },
10409
10417
  dispose: () => element.remove()
10410
10418
  };
10411
10419
  }
10412
10420
  mount(index, target, scale) {
10413
10421
  if (this.destroyed) return this.emptyHandle(target);
10414
10422
  this.handlesByTarget.get(target)?.dispose();
10415
- const slide = this.createSlide(index);
10416
- const fitScale = this.fit === "contain" ? Math.min(1, (target.clientWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10417
- 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);
10418
10433
  const wrapper = document.createElement("div");
10419
10434
  wrapper.dataset.rpvSlideWrapper = String(index);
10420
10435
  wrapper.style.position = "relative";
10421
- wrapper.style.width = `${this.naturalSlideWidth * effectiveScale}px`;
10422
- wrapper.style.height = `${this.naturalSlideHeight * effectiveScale}px`;
10423
10436
  wrapper.style.margin = "0 auto";
10424
10437
  slide.style.transformOrigin = "top left";
10425
- 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);
10426
10444
  wrapper.append(slide);
10427
10445
  target.replaceChildren(wrapper);
10428
10446
  this.callbacks.onSlideRendered?.(index, slide);
@@ -10430,7 +10448,8 @@ var NormalizedPresentationViewer = class {
10430
10448
  const handle = {
10431
10449
  element: slide,
10432
10450
  target,
10433
- ready: Promise.all([...this.pendingResources]).then(() => void 0),
10451
+ ready: Promise.all([...resources]).then(() => void 0),
10452
+ setScale,
10434
10453
  dispose: () => {
10435
10454
  if (disposed) return;
10436
10455
  disposed = true;
@@ -10458,6 +10477,32 @@ var NormalizedPresentationViewer = class {
10458
10477
  await Promise.resolve();
10459
10478
  }
10460
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
+ }
10461
10506
  async mountInBatches(placeholders, indices, batchSize, generation) {
10462
10507
  const uniqueIndices = [...new Set(indices)].filter(
10463
10508
  (index) => index >= 0 && index < placeholders.length
@@ -10490,12 +10535,13 @@ var NormalizedPresentationViewer = class {
10490
10535
  createListPlaceholder(index, item, generation, scale) {
10491
10536
  let handle;
10492
10537
  let mountPromise;
10538
+ let effectiveScale = scale;
10493
10539
  const isActive = () => this.isRenderActive(generation) && this.listState?.generation === generation;
10494
10540
  const mount = () => {
10495
10541
  if (!isActive()) return Promise.resolve();
10496
10542
  if (handle) return handle.ready;
10497
10543
  if (mountPromise) return mountPromise;
10498
- const nextHandle = this.mount(index, item, scale);
10544
+ const nextHandle = this.mount(index, item, effectiveScale);
10499
10545
  handle = nextHandle;
10500
10546
  const pending = nextHandle.ready.then(() => {
10501
10547
  if (!isActive() || handle !== nextHandle) {
@@ -10511,11 +10557,16 @@ var NormalizedPresentationViewer = class {
10511
10557
  const unmount = () => {
10512
10558
  const mounted = handle;
10513
10559
  handle = void 0;
10560
+ mountPromise = void 0;
10514
10561
  if (mounted) {
10515
10562
  mounted.dispose();
10516
10563
  }
10517
10564
  };
10518
- 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 };
10519
10570
  }
10520
10571
  async renderList(options = {}) {
10521
10572
  const windowingEnabled = options.enabled !== false;
@@ -10539,11 +10590,9 @@ var NormalizedPresentationViewer = class {
10539
10590
  Math.min(this.slideCount - 1, options.initialSlideIndex ?? this.current)
10540
10591
  );
10541
10592
  const scroller = options.scrollElement ?? this.container;
10542
- const viewportWidth = scroller.clientWidth;
10543
- const estimatedFitScale = this.fit === "contain" ? Math.min(1, (viewportWidth || this.naturalSlideWidth) / this.naturalSlideWidth) : 1;
10544
- const effectiveScale = estimatedFitScale * (this.zoom / 100);
10545
- const slideHeight = this.naturalSlideHeight * effectiveScale;
10546
- 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;
10547
10596
  if (!windowingEnabled) {
10548
10597
  const placeholders2 = this.presentation.slides.map((_, index) => {
10549
10598
  const item = document.createElement("div");
@@ -10551,9 +10600,35 @@ var NormalizedPresentationViewer = class {
10551
10600
  item.style.minHeight = `${slideHeight}px`;
10552
10601
  item.style.margin = `0 auto ${LIST_ITEM_GAP}px`;
10553
10602
  this.container.append(item);
10554
- return this.createListPlaceholder(index, item, generation);
10603
+ return this.createListPlaceholder(index, item, generation, effectiveScale);
10555
10604
  });
10556
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
+ };
10557
10632
  this.listState = state2;
10558
10633
  await this.mountInBatches(
10559
10634
  placeholders2,
@@ -10595,6 +10670,20 @@ var NormalizedPresentationViewer = class {
10595
10670
  const state = { generation, options: normalizedOptions, placeholders };
10596
10671
  this.listState = state;
10597
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
+ };
10598
10687
  const measureViewportRect = () => {
10599
10688
  const rect = scroller.getBoundingClientRect();
10600
10689
  if (rect.width > 0 && rect.height > 0) return { width: rect.width, height: rect.height };
@@ -10606,16 +10695,25 @@ var NormalizedPresentationViewer = class {
10606
10695
  const initialRect = measureViewportRect();
10607
10696
  const requestedOverscan = options.overscanViewport ?? 1.5;
10608
10697
  const overscanViewports = Number.isFinite(requestedOverscan) && requestedOverscan >= 0 ? requestedOverscan : 1.5;
10609
- const overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10698
+ let overscan = Math.max(1, Math.ceil(overscanViewports * initialRect.height / itemStride));
10610
10699
  const measureScrollMargin = () => sizer.getBoundingClientRect().top - scroller.getBoundingClientRect().top + scroller.scrollTop;
10611
10700
  let scrollMargin = measureScrollMargin();
10612
10701
  let needsSettleAlignment = initialIndex > 0;
10613
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
+ };
10614
10711
  const reconcile = (instance) => {
10615
10712
  if (!isActive()) return;
10616
10713
  const windowed = new Set(instance.getVirtualItems().map((item) => item.index));
10714
+ virtualWindow = windowed;
10617
10715
  placeholders.forEach((placeholder, index) => {
10618
- if (windowed.has(index)) void placeholder.mount();
10716
+ if (windowed.has(index) && !deferredMounts.has(index)) void placeholder.mount();
10619
10717
  else placeholder.unmount();
10620
10718
  });
10621
10719
  const offset = (instance.scrollOffset ?? 0) - scrollMargin;
@@ -10678,12 +10776,7 @@ var NormalizedPresentationViewer = class {
10678
10776
  },
10679
10777
  scrollToFn: (offset, { adjustments, behavior }) => {
10680
10778
  const top = offset + (adjustments ?? 0);
10681
- lastProgrammaticTop = top;
10682
- if (typeof scroller.scrollTo === "function") {
10683
- scroller.scrollTo(behavior ? { top, behavior } : { top });
10684
- } else {
10685
- scroller.scrollTop = top;
10686
- }
10779
+ writeScrollTop(top, behavior);
10687
10780
  },
10688
10781
  onChange: (instance) => reconcile(instance)
10689
10782
  });
@@ -10694,6 +10787,39 @@ var NormalizedPresentationViewer = class {
10694
10787
  behavior: scrollOptions?.behavior === "smooth" ? "smooth" : "auto"
10695
10788
  });
10696
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
+ };
10697
10823
  const teardown = virtualizer._didMount();
10698
10824
  this.listCleanup = teardown;
10699
10825
  virtualizer._willUpdate();
@@ -10721,16 +10847,27 @@ var NormalizedPresentationViewer = class {
10721
10847
  if (state && this.isRenderActive(state.generation)) {
10722
10848
  const navigation = ++this.navigationGeneration;
10723
10849
  const placeholder = state.placeholders[next];
10724
- await placeholder?.mount();
10725
- if (this.destroyed || navigation !== this.navigationGeneration || !this.isRenderActive(state.generation) || this.listState !== state) {
10726
- return;
10727
- }
10728
- if (changed) {
10729
- if (state.scrollToIndex) state.scrollToIndex(next, scrollOptions);
10730
- 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);
10731
10870
  }
10732
- this.current = next;
10733
- if (changed) this.notifySlideChange(next);
10734
10871
  return;
10735
10872
  }
10736
10873
  if (!changed && this.container.querySelector(`[data-rpv-slide-index="${next}"]`)) return;
@@ -10741,15 +10878,33 @@ var NormalizedPresentationViewer = class {
10741
10878
  if (this.zoom === next) return;
10742
10879
  this.zoom = next;
10743
10880
  if (this.renderState?.mode === "continuous") {
10744
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10745
- } 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
+ }
10746
10892
  }
10747
10893
  async setFitMode(mode) {
10748
10894
  if (this.fit === mode) return;
10749
10895
  this.fit = mode;
10750
10896
  if (this.renderState?.mode === "continuous") {
10751
- await this.renderList({ ...this.renderState.options, initialSlideIndex: this.current });
10752
- } 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
+ }
10753
10908
  }
10754
10909
  searchText(query, options) {
10755
10910
  return searchDocument(this.presentation, query, options);
@@ -10831,7 +10986,7 @@ var NormalizedPresentationViewer = class {
10831
10986
  this.objectUrls.clear();
10832
10987
  this.assetUrls.clear();
10833
10988
  this.metafileUrls.clear();
10834
- this.pendingResources.clear();
10989
+ this.activeMountResources = void 0;
10835
10990
  for (const handle of [...this.mountedHandles]) handle.dispose();
10836
10991
  this.handlesByTarget.clear();
10837
10992
  this.renderState = void 0;
@@ -11099,7 +11254,7 @@ var presentationParsingDefaults = Object.freeze({
11099
11254
  maxInputBytes: DEFAULT_MAX_INPUT_BYTES
11100
11255
  });
11101
11256
  var THUMBNAIL_ROW_ESTIMATE = 123;
11102
- var THUMBNAIL_OVERSCAN = 3;
11257
+ var THUMBNAIL_OVERSCAN = 1;
11103
11258
  var THUMBNAIL_INITIAL_RECT = { height: 780, width: 184 };
11104
11259
  var observeThumbnailFilmstripRect = (instance, callback) => {
11105
11260
  const element = instance.scrollElement;
@@ -11820,9 +11975,12 @@ function usePptxViewer() {
11820
11975
  }
11821
11976
  var EMU_PER_CSS_PIXEL2 = 9525;
11822
11977
  var DEFAULT_MAX_DIMENSION2 = 160;
11823
- function normalizeSlideIndexes(indexes, slideCount) {
11978
+ function normalizeSlideIndexes(indexes, slideCount, sort = true) {
11824
11979
  if (!indexes?.length || slideCount <= 0) return [];
11825
- 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;
11826
11984
  }
11827
11985
  function finitePositive2(value) {
11828
11986
  return value !== void 0 && Number.isFinite(value) && value > 0 ? value : void 0;
@@ -11860,6 +12018,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11860
12018
  const attachedElementsRef = useRef(/* @__PURE__ */ new Map());
11861
12019
  const cleanupsRef = useRef(/* @__PURE__ */ new Map());
11862
12020
  const prefetchedRef = useRef(/* @__PURE__ */ new Map());
12021
+ const prefetchInFlightRef = useRef(/* @__PURE__ */ new Map());
11863
12022
  const renderGenerationRef = useRef(/* @__PURE__ */ new Map());
11864
12023
  const refCallbacksRef = useRef(/* @__PURE__ */ new Map());
11865
12024
  const attachRef = useRef(() => {
@@ -11872,7 +12031,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
11872
12031
  [options.renderWindow?.visibleSlideIndexes, slideCount]
11873
12032
  );
11874
12033
  const prefetchSlideIndexes = useMemo(
11875
- () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount),
12034
+ () => normalizeSlideIndexes(options.renderWindow?.prefetchSlideIndexes, slideCount, false),
11876
12035
  [options.renderWindow?.prefetchSlideIndexes, slideCount]
11877
12036
  );
11878
12037
  const visibleSlideIndexesKey = visibleSlideIndexes.join(",");
@@ -11987,28 +12146,46 @@ function usePptxViewerThumbnails(controller, options = {}) {
11987
12146
  }, [renderThumbnail]);
11988
12147
  const prefetchThumbnail = useCallback(
11989
12148
  async (slideIndex) => {
11990
- if (disabled || !controller?.isReady() || attachedElementsRef.current.has(slideIndex) || prefetchedRef.current.has(slideIndex)) {
11991
- return;
11992
- }
11993
- const generation = (renderGenerationRef.current.get(slideIndex) ?? 0) + 1;
11994
- renderGenerationRef.current.set(slideIndex, generation);
11995
- const element = globalThis.document.createElement("div");
11996
- setThumbnailState(slideIndex, { status: "rendering" });
11997
- try {
11998
- const cleanup = await controller.renderThumbnail(slideIndex, element, {
11999
- width: size.width
12000
- });
12001
- if (!mountedRef.current || renderGenerationRef.current.get(slideIndex) !== generation || attachedElementsRef.current.has(slideIndex) || !requestedPrefetchSlideIndexSetRef.current.has(slideIndex)) {
12002
- cleanup();
12003
- return;
12149
+ while (true) {
12150
+ const existing = prefetchInFlightRef.current.get(slideIndex);
12151
+ if (existing) {
12152
+ await existing;
12153
+ continue;
12004
12154
  }
12005
- prefetchedRef.current.set(slideIndex, { cleanup, element, width: size.width });
12006
- setThumbnailState(slideIndex, { status: "ready" });
12007
- } catch (reason) {
12008
- 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)) {
12009
12156
  return;
12010
12157
  }
12011
- 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;
12012
12189
  }
12013
12190
  },
12014
12191
  [controller, disabled, setThumbnailState, size.width]
@@ -12024,14 +12201,17 @@ function usePptxViewerThumbnails(controller, options = {}) {
12024
12201
  cleanupsRef.current.clear();
12025
12202
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12026
12203
  prefetchedRef.current.clear();
12204
+ prefetchInFlightRef.current.clear();
12027
12205
  };
12028
12206
  }, []);
12029
12207
  useEffect(
12030
12208
  () => () => {
12209
+ for (const index of prefetchInFlightRef.current.keys()) invalidateThumbnail(index);
12210
+ prefetchInFlightRef.current.clear();
12031
12211
  for (const prefetched of prefetchedRef.current.values()) prefetched.cleanup();
12032
12212
  prefetchedRef.current.clear();
12033
12213
  },
12034
- [controller, size.width]
12214
+ [controller, invalidateThumbnail, size.width]
12035
12215
  );
12036
12216
  useEffect(() => {
12037
12217
  void rerenderAttachedThumbnails();
@@ -12043,6 +12223,13 @@ function usePptxViewerThumbnails(controller, options = {}) {
12043
12223
  if (!attachedElementsRef.current.has(index)) setThumbnailState(index, { status: "idle" });
12044
12224
  }
12045
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
+ }
12046
12233
  if (disabled) return;
12047
12234
  let cancelled = false;
12048
12235
  void (async () => {
@@ -12057,6 +12244,7 @@ function usePptxViewerThumbnails(controller, options = {}) {
12057
12244
  }, [
12058
12245
  disabled,
12059
12246
  disposePrefetchedThumbnail,
12247
+ invalidateThumbnail,
12060
12248
  prefetchSlideIndexesKey,
12061
12249
  prefetchThumbnail,
12062
12250
  setThumbnailState