@files-preview-app/preview-file 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -495,8 +495,7 @@ var ICON_FULLSCREEN = `<svg width="24" height="24" viewBox="0 0 24 24" fill="non
495
495
  var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
496
496
  var ICON_FAST_FORWARD = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 19 22 12 13 5 13 19"></polygon><polygon points="2 19 11 12 2 5 2 19"></polygon></svg>`;
497
497
  var ICON_REWIND = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 19 2 12 11 5 11 19"></polygon><polygon points="22 19 13 12 22 5 22 19"></polygon></svg>`;
498
- var ICON_SPEED = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
499
- var ICON_EXTERNAL_WINDOW = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`;
498
+ var ICON_EXTERNAL_WINDOW = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><line x1="3" y1="8" x2="21" y2="8"></line><circle cx="6" cy="5.5" r="0.75" fill="currentColor"></circle><circle cx="8.5" cy="5.5" r="0.75" fill="currentColor"></circle><polyline points="14 13 18 13 18 17"></polyline><line x1="11" y1="20" x2="18" y2="13"></line></svg>`;
500
499
  var ICON_MAP = {
501
500
  "zoom-in": ICON_ZOOM_IN,
502
501
  "zoom-out": ICON_ZOOM_OUT,
@@ -523,28 +522,177 @@ var ICON_MAP = {
523
522
  "forward-10": ICON_FAST_FORWARD,
524
523
  "rewind": ICON_REWIND,
525
524
  "replay-10": ICON_REWIND,
526
- "speed": ICON_SPEED,
527
525
  "open-window": ICON_EXTERNAL_WINDOW,
528
- "external-window": ICON_EXTERNAL_WINDOW
526
+ "external-window": ICON_EXTERNAL_WINDOW,
527
+ "openWindow": ICON_EXTERNAL_WINDOW,
528
+ "openSeparateWindow": ICON_EXTERNAL_WINDOW
529
529
  };
530
530
  var ToolbarController = class {
531
531
  el;
532
532
  toolbarEl;
533
533
  actions = [];
534
+ config = {};
534
535
  pageInputEl = null;
535
536
  pageLabelEl = null;
536
- constructor(container) {
537
+ prevPageBtn = null;
538
+ nextPageBtn = null;
539
+ constructor(container, config) {
537
540
  this.el = container;
541
+ if (config) this.config = { ...config };
538
542
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
539
543
  this.el.appendChild(this.toolbarEl);
540
544
  }
545
+ setConfig(config) {
546
+ this.config = { ...config };
547
+ this.render();
548
+ }
549
+ getConfig() {
550
+ return { ...this.config };
551
+ }
552
+ hideAction(actionId) {
553
+ this.config[actionId] = false;
554
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = false;
555
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = false;
556
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
557
+ this.config.fitPage = false;
558
+ this.config.fitToPage = false;
559
+ this.config.fitWidth = false;
560
+ }
561
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = false;
562
+ if (actionId === "fullscreen") this.config.fullscreen = false;
563
+ if (actionId === "download") this.config.download = false;
564
+ if (actionId === "print") this.config.print = false;
565
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
566
+ this.config.openWindow = false;
567
+ this.config.openSeparateWindow = false;
568
+ }
569
+ if (actionId === "copy") this.config.copy = false;
570
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
571
+ this.config.pageNav = false;
572
+ this.config.pagination = false;
573
+ }
574
+ this.render();
575
+ }
576
+ showAction(actionId) {
577
+ this.config[actionId] = true;
578
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = true;
579
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = true;
580
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
581
+ this.config.fitPage = true;
582
+ this.config.fitToPage = true;
583
+ this.config.fitWidth = true;
584
+ }
585
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = true;
586
+ if (actionId === "fullscreen") this.config.fullscreen = true;
587
+ if (actionId === "download") this.config.download = true;
588
+ if (actionId === "print") this.config.print = true;
589
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
590
+ this.config.openWindow = true;
591
+ this.config.openSeparateWindow = true;
592
+ }
593
+ if (actionId === "copy") this.config.copy = true;
594
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
595
+ this.config.pageNav = true;
596
+ this.config.pagination = true;
597
+ }
598
+ this.render();
599
+ }
600
+ normalizeActionId(actionId) {
601
+ if (actionId === "zoomIn") return "zoom-in";
602
+ if (actionId === "zoomOut") return "zoom-out";
603
+ if (actionId === "fitPage" || actionId === "fitToPage" || actionId === "fitWidth") return "fit-page";
604
+ if (actionId === "rotateCW") return "rotate-cw";
605
+ if (actionId === "rotateCCW") return "rotate-ccw";
606
+ if (actionId === "openWindow" || actionId === "openSeparateWindow") return "open-window";
607
+ if (actionId === "pageNav") return "page-nav";
608
+ return actionId;
609
+ }
610
+ enableAction(actionId) {
611
+ const norm = this.normalizeActionId(actionId);
612
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
613
+ if (btn) {
614
+ btn.disabled = false;
615
+ btn.classList.remove("disabled");
616
+ }
617
+ }
618
+ disableAction(actionId) {
619
+ const norm = this.normalizeActionId(actionId);
620
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
621
+ if (btn) {
622
+ btn.disabled = true;
623
+ btn.classList.add("disabled");
624
+ }
625
+ }
626
+ setActionActive(actionId, active) {
627
+ const norm = this.normalizeActionId(actionId);
628
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
629
+ if (btn) {
630
+ if (active) {
631
+ btn.classList.add("active");
632
+ } else {
633
+ btn.classList.remove("active");
634
+ }
635
+ }
636
+ }
637
+ isActionEnabled(action) {
638
+ const c = this.config;
639
+ const id = action.id;
640
+ if (c[id] === false) return false;
641
+ if (id === "zoom-in" && (c.zoomIn === false || c["zoom-in"] === false)) return false;
642
+ if (id === "zoom-out" && (c.zoomOut === false || c["zoom-out"] === false)) return false;
643
+ if ((id === "fit-page" || id === "fit-width" || id === "fit-slide") && (c.fitPage === false || c.fitToPage === false || c.fitWidth === false || c["fit-page"] === false || c["fit-width"] === false)) {
644
+ return false;
645
+ }
646
+ if ((id === "rotate-cw" || id === "rotate-ccw" || id === "rotate") && (c.rotate === false || c.rotateCW === false || c.rotateCCW === false || c["rotate-cw"] === false)) {
647
+ return false;
648
+ }
649
+ if (id === "fullscreen" && c.fullscreen === false) return false;
650
+ if (id === "download" && c.download === false) return false;
651
+ if (id === "print" && c.print === false) return false;
652
+ if ((id === "open-window" || id === "external-window") && (c.openWindow === false || c.openSeparateWindow === false || c["open-window"] === false)) {
653
+ return false;
654
+ }
655
+ if (id === "copy" && c.copy === false) return false;
656
+ if (id === "thumbnails" && c.thumbnails === false) return false;
657
+ if (id === "page-nav" && (c.pageNav === false || c.pagination === false || c["page-nav"] === false)) {
658
+ return false;
659
+ }
660
+ if ((id === "prev" || id === "page-prev") && (c.prevPage === false || c.prev === false || c["prev"] === false || c.pageNav === false || c.pagination === false)) {
661
+ return false;
662
+ }
663
+ if ((id === "next" || id === "page-next") && (c.nextPage === false || c.next === false || c["next"] === false || c.pageNav === false || c.pagination === false)) {
664
+ return false;
665
+ }
666
+ if (id === "play" && c.play === false) return false;
667
+ if (id === "pause" && c.pause === false) return false;
668
+ if ((id === "fast-forward" || id === "forward-10") && (c.fastForward === false || c["fast-forward"] === false)) {
669
+ return false;
670
+ }
671
+ if ((id === "rewind" || id === "replay-10") && (c.rewind === false || c["rewind"] === false)) {
672
+ return false;
673
+ }
674
+ if (id === "speed" && c.speed === false) return false;
675
+ return true;
676
+ }
541
677
  setPage(page, max) {
542
678
  if (this.pageInputEl) {
543
679
  this.pageInputEl.value = page.toString();
680
+ if (max !== void 0) {
681
+ this.pageInputEl.max = max.toString();
682
+ }
544
683
  }
545
684
  if (max !== void 0 && this.pageLabelEl) {
546
685
  this.pageLabelEl.textContent = ` / ${max}`;
547
686
  }
687
+ const currentMax = max !== void 0 ? max : parseInt(this.pageInputEl?.max || "1", 10) || 1;
688
+ if (this.prevPageBtn) {
689
+ this.prevPageBtn.disabled = page <= 1;
690
+ this.prevPageBtn.style.opacity = page <= 1 ? "0.4" : "1";
691
+ }
692
+ if (this.nextPageBtn) {
693
+ this.nextPageBtn.disabled = page >= currentMax;
694
+ this.nextPageBtn.style.opacity = page >= currentMax ? "0.4" : "1";
695
+ }
548
696
  }
549
697
  update(actions) {
550
698
  this.actions = actions;
@@ -567,8 +715,9 @@ var ToolbarController = class {
567
715
  view: [],
568
716
  actions: []
569
717
  };
570
- const hasPageNav = this.actions.some((a) => a.type === "page-nav");
571
- const effectiveActions = hasPageNav ? this.actions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : this.actions;
718
+ const enabledActions = this.actions.filter((a) => this.isActionEnabled(a));
719
+ const hasPageNav = enabledActions.some((a) => a.type === "page-nav");
720
+ const effectiveActions = hasPageNav ? enabledActions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : enabledActions;
572
721
  for (const action of effectiveActions) {
573
722
  if (groups[action.group]) {
574
723
  groups[action.group].push(action);
@@ -628,6 +777,13 @@ var ToolbarController = class {
628
777
  action.execute("go", val);
629
778
  });
630
779
  const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
780
+ const curVal = action.value ?? 1;
781
+ prevBtn.disabled = curVal <= 1;
782
+ prevBtn.style.opacity = curVal <= 1 ? "0.4" : "1";
783
+ nextBtn.disabled = curVal >= max;
784
+ nextBtn.style.opacity = curVal >= max ? "0.4" : "1";
785
+ this.prevPageBtn = prevBtn;
786
+ this.nextPageBtn = nextBtn;
631
787
  this.pageInputEl = input;
632
788
  this.pageLabelEl = label;
633
789
  groupEl.appendChild(prevBtn);
@@ -683,25 +839,63 @@ var ToolbarController = class {
683
839
  var ThumbnailPanel = class {
684
840
  el;
685
841
  panelEl;
842
+ listEl;
843
+ headerEl;
686
844
  thumbnails = [];
687
845
  onSelect;
846
+ onToggleCallback;
688
847
  activeIndex = 0;
689
- constructor(container) {
848
+ observer;
849
+ renderedIndices = /* @__PURE__ */ new Set();
850
+ constructor(container, onToggle) {
690
851
  this.el = container;
852
+ this.onToggleCallback = onToggle;
691
853
  this.panelEl = createElement("div", { className: "fp-thumbnail-panel hidden" });
854
+ this.panelEl.style.width = "0px";
855
+ this.panelEl.style.minWidth = "0px";
856
+ this.panelEl.style.opacity = "0";
857
+ this.headerEl = createElement("div", { className: "fp-thumbnail-header" });
858
+ const title = createElement("span", { className: "fp-thumbnail-title" }, "Pages");
859
+ const closeBtn = createElement("button", {
860
+ className: "fp-thumbnail-close-btn",
861
+ title: "Close Thumbnails",
862
+ type: "button"
863
+ });
864
+ closeBtn.innerHTML = ICON_CLOSE;
865
+ closeBtn.addEventListener("click", (e) => {
866
+ e.stopPropagation();
867
+ this.hide();
868
+ });
869
+ this.headerEl.appendChild(title);
870
+ this.headerEl.appendChild(closeBtn);
871
+ this.listEl = createElement("div", { className: "fp-thumbnail-list" });
872
+ this.panelEl.appendChild(this.headerEl);
873
+ this.panelEl.appendChild(this.listEl);
692
874
  this.el.appendChild(this.panelEl);
693
875
  }
876
+ isOpen() {
877
+ return !this.panelEl.classList.contains("hidden") && this.panelEl.style.width !== "0px";
878
+ }
694
879
  update(thumbnails, onSelect) {
695
880
  this.thumbnails = thumbnails;
696
881
  this.onSelect = onSelect;
882
+ this.renderedIndices.clear();
883
+ const titleEl = this.headerEl.querySelector(".fp-thumbnail-title");
884
+ if (titleEl) {
885
+ titleEl.textContent = `Pages (${thumbnails.length})`;
886
+ }
697
887
  this.render();
888
+ if (this.isOpen()) {
889
+ this.renderAllVisible();
890
+ }
698
891
  }
699
892
  setActive(index) {
700
893
  this.activeIndex = index;
701
- const items = this.panelEl.querySelectorAll(".fp-thumbnail-item");
894
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
702
895
  items.forEach((item, i) => {
703
896
  if (i === index) {
704
897
  item.classList.add("active");
898
+ item.scrollIntoView({ block: "nearest", behavior: "smooth" });
705
899
  } else {
706
900
  item.classList.remove("active");
707
901
  }
@@ -709,37 +903,102 @@ var ThumbnailPanel = class {
709
903
  }
710
904
  show() {
711
905
  this.panelEl.classList.remove("hidden");
906
+ this.panelEl.style.width = "200px";
907
+ this.panelEl.style.minWidth = "200px";
908
+ this.panelEl.style.opacity = "1";
909
+ this.panelEl.style.display = "flex";
910
+ this.onToggleCallback?.(true);
911
+ this.renderAllVisible();
712
912
  }
713
913
  hide() {
714
914
  this.panelEl.classList.add("hidden");
915
+ this.panelEl.style.width = "0px";
916
+ this.panelEl.style.minWidth = "0px";
917
+ this.panelEl.style.opacity = "0";
918
+ this.onToggleCallback?.(false);
715
919
  }
716
920
  toggle() {
717
- this.panelEl.classList.toggle("hidden");
921
+ if (this.isOpen()) {
922
+ this.hide();
923
+ } else {
924
+ this.show();
925
+ }
718
926
  }
719
927
  destroy() {
928
+ if (this.observer) {
929
+ this.observer.disconnect();
930
+ this.observer = void 0;
931
+ }
932
+ this.renderedIndices.clear();
720
933
  this.el.innerHTML = "";
721
934
  }
722
- async render() {
723
- this.panelEl.innerHTML = "";
935
+ renderAllVisible() {
936
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
937
+ items.forEach((item) => {
938
+ const idx = parseInt(item.dataset.thumbIndex || "-1", 10);
939
+ if (idx >= 0 && !this.renderedIndices.has(idx)) {
940
+ this.renderThumbnailItem(idx, item);
941
+ }
942
+ });
943
+ }
944
+ renderThumbnailItem(idx, itemEl) {
945
+ if (this.renderedIndices.has(idx)) return;
946
+ this.renderedIndices.add(idx);
947
+ const canvas = itemEl.querySelector("canvas");
948
+ const thumb = this.thumbnails[idx];
949
+ if (canvas && thumb) {
950
+ Promise.resolve(thumb.render(canvas)).catch((err) => {
951
+ console.warn(`[FilePreview] Error rendering thumbnail ${idx}:`, err);
952
+ });
953
+ }
954
+ }
955
+ render() {
956
+ if (this.observer) {
957
+ this.observer.disconnect();
958
+ }
959
+ this.listEl.innerHTML = "";
960
+ const useObserver = typeof IntersectionObserver !== "undefined";
961
+ if (useObserver) {
962
+ this.observer = new IntersectionObserver(
963
+ (entries) => {
964
+ for (const entry of entries) {
965
+ if (entry.isIntersecting) {
966
+ const target = entry.target;
967
+ const idx = parseInt(target.dataset.thumbIndex || "-1", 10);
968
+ if (idx >= 0) {
969
+ this.renderThumbnailItem(idx, target);
970
+ }
971
+ }
972
+ }
973
+ },
974
+ {
975
+ root: this.listEl,
976
+ rootMargin: "120px 0px 120px 0px"
977
+ }
978
+ );
979
+ }
724
980
  for (let i = 0; i < this.thumbnails.length; i++) {
725
981
  const thumb = this.thumbnails[i];
726
- const itemEl = createElement("div", { className: "fp-thumbnail-item" });
727
- if (i === this.activeIndex) {
728
- itemEl.classList.add("active");
729
- }
982
+ const itemEl = createElement("div", {
983
+ className: "fp-thumbnail-item" + (i === this.activeIndex ? " active" : "")
984
+ });
985
+ itemEl.dataset.thumbIndex = i.toString();
730
986
  itemEl.addEventListener("click", () => {
731
987
  this.setActive(i);
732
988
  this.onSelect?.(i);
733
989
  });
734
- const canvas = createElement("canvas", { width: "150", height: "200" });
735
- const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label);
736
- itemEl.appendChild(canvas);
990
+ const canvasWrapper = createElement("div", { className: "fp-thumbnail-canvas-wrapper" });
991
+ const canvas = createElement("canvas", { width: "130", height: "170" });
992
+ canvasWrapper.appendChild(canvas);
993
+ const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label || `Page ${i + 1}`);
994
+ itemEl.appendChild(canvasWrapper);
737
995
  itemEl.appendChild(label);
738
- this.panelEl.appendChild(itemEl);
739
- try {
740
- await thumb.render(canvas);
741
- } catch (err) {
742
- console.error(`[FilePreview] Error rendering thumbnail ${i}:`, err);
996
+ this.listEl.appendChild(itemEl);
997
+ if (this.observer) {
998
+ this.observer.observe(itemEl);
999
+ }
1000
+ if (i < 4) {
1001
+ this.renderThumbnailItem(i, itemEl);
743
1002
  }
744
1003
  }
745
1004
  }
@@ -760,6 +1019,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
760
1019
  currentOptions = {};
761
1020
  resizeObserver = null;
762
1021
  fullscreenHandler = null;
1022
+ titleBarEl = null;
763
1023
  /**
764
1024
  * Register a preview plugin.
765
1025
  */
@@ -796,6 +1056,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
796
1056
  }
797
1057
  this.currentBuffer = buffer.slice(0);
798
1058
  this.currentMetadata = metadata;
1059
+ this.updateTitleBar(options, metadata);
799
1060
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
800
1061
  const fileInfo = { metadata, buffer };
801
1062
  const matchedPlugin = await this.findPlugin(fileInfo);
@@ -819,15 +1080,21 @@ var FilePreviewViewer = class _FilePreviewViewer {
819
1080
  if (event === "page-change" && payload && typeof payload.page === "number") {
820
1081
  const total = payload.total ?? payload.totalPages;
821
1082
  this.toolbar?.setPage(payload.page, total);
1083
+ this.thumbnailPanel?.setActive(payload.page - 1);
822
1084
  }
823
1085
  this.eventEmitter.emit(event, payload);
824
- }
1086
+ },
1087
+ toggleThumbnails: () => this.toggleThumbnails()
825
1088
  });
826
1089
  this.activeInstance = instance;
827
1090
  instance.openInSeparateWindow = () => this.openInSeparateWindow();
1091
+ instance.toggleThumbnails = () => this.toggleThumbnails();
828
1092
  this.hideLoading();
829
1093
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
830
- if (options.showToolbar !== false && this.toolbar) {
1094
+ const isToolbarVisible = options.showToolbar !== false && options.toolbar !== false;
1095
+ if (isToolbarVisible && this.toolbar) {
1096
+ const toolbarConfig = this.extractToolbarConfig(options);
1097
+ this.toolbar.setConfig(toolbarConfig);
831
1098
  const actions = matchedPlugin.getToolbarActions(instance);
832
1099
  const hasFullscreen = actions.some((a) => a.id === "fullscreen");
833
1100
  if (!hasFullscreen && this.wrapperEl) {
@@ -877,6 +1144,12 @@ var FilePreviewViewer = class _FilePreviewViewer {
877
1144
  }
878
1145
  });
879
1146
  }
1147
+ const thumbAction = actions.find((a) => a.id === "thumbnails");
1148
+ if (thumbAction) {
1149
+ thumbAction.execute = () => {
1150
+ this.toggleThumbnails();
1151
+ };
1152
+ }
880
1153
  this.toolbar.update(actions);
881
1154
  this.toolbar.show();
882
1155
  }
@@ -961,7 +1234,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
961
1234
  if (targetUrl) {
962
1235
  const newWin2 = window.open(targetUrl, "_blank");
963
1236
  if (!newWin2) {
964
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1237
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
965
1238
  return null;
966
1239
  }
967
1240
  return newWin2;
@@ -969,7 +1242,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
969
1242
  const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
970
1243
  const newWin = window.open("", "_blank");
971
1244
  if (!newWin) {
972
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1245
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
973
1246
  return null;
974
1247
  }
975
1248
  newWin.document.title = title;
@@ -1046,6 +1319,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
1046
1319
  this.currentBuffer = null;
1047
1320
  this.currentMetadata = null;
1048
1321
  this.currentContainer = null;
1322
+ this.titleBarEl = null;
1049
1323
  }
1050
1324
  /**
1051
1325
  * Get the currently active preview instance.
@@ -1053,7 +1327,218 @@ var FilePreviewViewer = class _FilePreviewViewer {
1053
1327
  getInstance() {
1054
1328
  return this.activeInstance;
1055
1329
  }
1330
+ // --- Toolbar Configuration & Feature Toggles ---
1331
+ /**
1332
+ * Configure toolbar features dynamically through options or methods.
1333
+ * e.g. viewer.setToolbarConfig({ zoomIn: false, print: false })
1334
+ */
1335
+ setToolbarConfig(config) {
1336
+ this.toolbar?.setConfig(config);
1337
+ }
1338
+ /**
1339
+ * Get current toolbar configuration.
1340
+ */
1341
+ getToolbarConfig() {
1342
+ return this.toolbar?.getConfig() ?? {};
1343
+ }
1344
+ /**
1345
+ * Hide a specific toolbar action by ID or alias (e.g. 'zoom-in', 'print', 'download').
1346
+ */
1347
+ hideToolbarAction(actionId) {
1348
+ this.toolbar?.hideAction(actionId);
1349
+ }
1350
+ /**
1351
+ * Show a specific toolbar action by ID or alias.
1352
+ */
1353
+ showToolbarAction(actionId) {
1354
+ this.toolbar?.showAction(actionId);
1355
+ }
1356
+ /**
1357
+ * Enable a specific toolbar action button.
1358
+ */
1359
+ enableToolbarAction(actionId) {
1360
+ this.toolbar?.enableAction(actionId);
1361
+ }
1362
+ /**
1363
+ * Disable a specific toolbar action button.
1364
+ */
1365
+ disableToolbarAction(actionId) {
1366
+ this.toolbar?.disableAction(actionId);
1367
+ }
1368
+ // --- Programmatic Toolbar Action Methods ---
1369
+ /**
1370
+ * Fit document to page so it fills the frame width with minimal margins.
1371
+ */
1372
+ fitToPage() {
1373
+ this.activeInstance?.fitToPage?.();
1374
+ }
1375
+ /**
1376
+ * Zoom in.
1377
+ */
1378
+ zoomIn() {
1379
+ this.activeInstance?.zoomIn?.();
1380
+ }
1381
+ /**
1382
+ * Zoom out.
1383
+ */
1384
+ zoomOut() {
1385
+ this.activeInstance?.zoomOut?.();
1386
+ }
1387
+ /**
1388
+ * Set specific zoom level.
1389
+ */
1390
+ setZoom(level) {
1391
+ this.activeInstance?.setZoom?.(level);
1392
+ }
1393
+ /**
1394
+ * Get current zoom level.
1395
+ */
1396
+ getZoom() {
1397
+ return this.activeInstance?.getZoom?.() ?? 1;
1398
+ }
1399
+ /**
1400
+ * Rotate 90 degrees clockwise.
1401
+ */
1402
+ rotateCW() {
1403
+ this.activeInstance?.rotateCW?.();
1404
+ }
1405
+ /**
1406
+ * Rotate 90 degrees counter-clockwise.
1407
+ */
1408
+ rotateCCW() {
1409
+ this.activeInstance?.rotateCCW?.();
1410
+ }
1411
+ /**
1412
+ * Navigate to a specific page number.
1413
+ */
1414
+ goToPage(page) {
1415
+ this.activeInstance?.goToPage?.(page);
1416
+ this.toolbar?.setPage(page);
1417
+ }
1418
+ /**
1419
+ * Navigate to next page.
1420
+ */
1421
+ nextPage() {
1422
+ const cur = this.getCurrentPage();
1423
+ const max = this.getPageCount();
1424
+ if (cur < max) {
1425
+ this.goToPage(cur + 1);
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Navigate to previous page.
1430
+ */
1431
+ prevPage() {
1432
+ const cur = this.getCurrentPage();
1433
+ if (cur > 1) {
1434
+ this.goToPage(cur - 1);
1435
+ }
1436
+ }
1437
+ /**
1438
+ * Get total page count.
1439
+ */
1440
+ getPageCount() {
1441
+ return this.activeInstance?.getPageCount?.() ?? 1;
1442
+ }
1443
+ /**
1444
+ * Get current page number.
1445
+ */
1446
+ getCurrentPage() {
1447
+ return this.activeInstance?.getCurrentPage?.() ?? 1;
1448
+ }
1449
+ /**
1450
+ * Download the current document.
1451
+ */
1452
+ download() {
1453
+ this.activeInstance?.download?.();
1454
+ }
1455
+ /**
1456
+ * Print the current document.
1457
+ */
1458
+ print() {
1459
+ this.activeInstance?.print?.();
1460
+ }
1461
+ /**
1462
+ * Toggle fullscreen mode.
1463
+ */
1464
+ toggleFullscreen() {
1465
+ try {
1466
+ if (!document.fullscreenElement) {
1467
+ this.wrapperEl?.requestFullscreen?.();
1468
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
1469
+ } else {
1470
+ document.exitFullscreen?.();
1471
+ this.wrapperEl?.classList.remove("fp-fullscreen-active");
1472
+ }
1473
+ } catch {
1474
+ this.wrapperEl?.classList.toggle("fp-fullscreen-active");
1475
+ }
1476
+ setTimeout(() => {
1477
+ this.activeInstance?.fitToPage?.();
1478
+ }, 120);
1479
+ }
1480
+ /**
1481
+ * Toggle thumbnails sidebar panel.
1482
+ */
1483
+ toggleThumbnails() {
1484
+ if (!this.thumbnailPanel) return;
1485
+ this.thumbnailPanel.toggle();
1486
+ const isOpen = this.thumbnailPanel.isOpen();
1487
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1488
+ setTimeout(() => {
1489
+ this.activeInstance?.fitToPage?.();
1490
+ }, 260);
1491
+ }
1492
+ /**
1493
+ * Set whether to display the file name / title bar above the toolbar.
1494
+ */
1495
+ setShowFileName(show) {
1496
+ if (this.currentOptions) {
1497
+ this.currentOptions.showFileName = show;
1498
+ }
1499
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1500
+ }
1501
+ /**
1502
+ * Set or override the displayed file name in the preview panel.
1503
+ */
1504
+ setFileName(name) {
1505
+ if (this.currentOptions) {
1506
+ this.currentOptions.fileName = name;
1507
+ }
1508
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1509
+ }
1056
1510
  // --- Private methods ---
1511
+ updateTitleBar(options, metadata) {
1512
+ if (!this.titleBarEl) return;
1513
+ const show = options?.showFileName !== false;
1514
+ if (!show) {
1515
+ this.titleBarEl.style.display = "none";
1516
+ return;
1517
+ }
1518
+ const name = options?.fileName || metadata?.name || this.currentMetadata?.name || "";
1519
+ if (!name) {
1520
+ this.titleBarEl.style.display = "none";
1521
+ return;
1522
+ }
1523
+ this.titleBarEl.style.display = "flex";
1524
+ const ext = metadata?.extension || (name.includes(".") ? "." + name.split(".").pop() : "");
1525
+ const badge = ext ? ext.replace(".", "").toUpperCase() : "";
1526
+ this.titleBarEl.innerHTML = `
1527
+ <div class="fp-titlebar-info">
1528
+ <span class="fp-titlebar-icon">\u{1F4C4}</span>
1529
+ <span class="fp-titlebar-name" title="${name}">${name}</span>
1530
+ ${badge ? `<span class="fp-titlebar-badge">${badge}</span>` : ""}
1531
+ </div>
1532
+ `;
1533
+ }
1534
+ extractToolbarConfig(options) {
1535
+ if (!options) return {};
1536
+ const nested = typeof options.toolbar === "object" ? options.toolbar : {};
1537
+ return {
1538
+ ...options,
1539
+ ...nested
1540
+ };
1541
+ }
1057
1542
  abort() {
1058
1543
  if (this.abortController) {
1059
1544
  this.abortController.abort();
@@ -1096,17 +1581,29 @@ var FilePreviewViewer = class _FilePreviewViewer {
1096
1581
  bodyEl.className = "fp-body";
1097
1582
  bodyEl.appendChild(thumbnailEl);
1098
1583
  bodyEl.appendChild(this.contentEl);
1584
+ const titleBarEl = document.createElement("div");
1585
+ titleBarEl.className = "fp-titlebar";
1586
+ this.titleBarEl = titleBarEl;
1587
+ this.updateTitleBar(options, this.currentMetadata);
1099
1588
  if (toolbarPos === "top") {
1589
+ this.wrapperEl.appendChild(titleBarEl);
1100
1590
  this.wrapperEl.appendChild(toolbarEl);
1101
1591
  this.wrapperEl.appendChild(bodyEl);
1102
1592
  } else {
1593
+ this.wrapperEl.appendChild(titleBarEl);
1103
1594
  this.wrapperEl.appendChild(bodyEl);
1104
1595
  this.wrapperEl.appendChild(toolbarEl);
1105
1596
  }
1106
1597
  container.innerHTML = "";
1107
1598
  container.appendChild(this.wrapperEl);
1108
- this.toolbar = new ToolbarController(toolbarEl);
1109
- this.thumbnailPanel = new ThumbnailPanel(thumbnailEl);
1599
+ const initialToolbarConfig = this.extractToolbarConfig(options);
1600
+ this.toolbar = new ToolbarController(toolbarEl, initialToolbarConfig);
1601
+ this.thumbnailPanel = new ThumbnailPanel(thumbnailEl, (isOpen) => {
1602
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1603
+ setTimeout(() => {
1604
+ this.activeInstance?.fitToPage?.();
1605
+ }, 260);
1606
+ });
1110
1607
  this.setupKeyboardShortcuts();
1111
1608
  this.setupDragAndDrop(container, options);
1112
1609
  this.setupResizeAndFullscreenListeners();
@@ -1496,6 +1993,14 @@ var PdfPlugin = class {
1496
1993
  group: "zoom",
1497
1994
  execute: () => instance.fitToPage?.()
1498
1995
  },
1996
+ {
1997
+ id: "fit-width",
1998
+ icon: "fit-width",
1999
+ label: "Fit to Width",
2000
+ type: "button",
2001
+ group: "zoom",
2002
+ execute: () => instance.fitToWidth?.()
2003
+ },
1499
2004
  {
1500
2005
  id: "rotate-cw",
1501
2006
  icon: "rotate-cw",
@@ -1504,6 +2009,14 @@ var PdfPlugin = class {
1504
2009
  group: "view",
1505
2010
  execute: () => instance.rotateCW?.()
1506
2011
  },
2012
+ {
2013
+ id: "rotate-ccw",
2014
+ icon: "rotate-ccw",
2015
+ label: "Rotate Counter-Clockwise",
2016
+ type: "button",
2017
+ group: "view",
2018
+ execute: () => instance.rotateCCW?.()
2019
+ },
1507
2020
  {
1508
2021
  id: "download",
1509
2022
  icon: "download",
@@ -1535,12 +2048,13 @@ var PdfPlugin = class {
1535
2048
  container.className = "fp-pdf-container";
1536
2049
  container.style.width = "100%";
1537
2050
  container.style.height = "100%";
1538
- container.style.overflow = "auto";
2051
+ container.style.overflowX = "hidden";
2052
+ container.style.overflowY = "auto";
1539
2053
  container.style.display = "flex";
1540
2054
  container.style.flexDirection = "column";
1541
2055
  container.style.alignItems = "center";
1542
2056
  container.style.justifyContent = "flex-start";
1543
- container.style.padding = "20px 16px";
2057
+ container.style.padding = "16px 8px";
1544
2058
  container.style.backgroundColor = "#0f172a";
1545
2059
  container.style.boxSizing = "border-box";
1546
2060
  container.style.position = "relative";
@@ -1551,30 +2065,12 @@ var PdfPlugin = class {
1551
2065
  pageCard.style.borderRadius = "4px";
1552
2066
  pageCard.style.overflow = "hidden";
1553
2067
  pageCard.style.lineHeight = "0";
1554
- pageCard.style.transition = "transform 0.15s ease";
1555
2068
  pageCard.style.position = "relative";
1556
2069
  pageCard.style.flexShrink = "0";
2070
+ pageCard.style.transition = "box-shadow 0.2s ease";
1557
2071
  let canvas = document.createElement("canvas");
1558
2072
  pageCard.appendChild(canvas);
1559
2073
  container.appendChild(pageCard);
1560
- const indicator = document.createElement("div");
1561
- indicator.className = "fp-pdf-page-indicator";
1562
- indicator.style.position = "sticky";
1563
- indicator.style.bottom = "16px";
1564
- indicator.style.marginTop = "16px";
1565
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1566
- indicator.style.backdropFilter = "blur(8px)";
1567
- indicator.style.color = "#f8fafc";
1568
- indicator.style.fontSize = "12px";
1569
- indicator.style.fontWeight = "600";
1570
- indicator.style.padding = "5px 14px";
1571
- indicator.style.borderRadius = "20px";
1572
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1573
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1574
- indicator.style.zIndex = "10";
1575
- indicator.style.userSelect = "none";
1576
- indicator.style.pointerEvents = "none";
1577
- container.appendChild(indicator);
1578
2074
  ctx.container.appendChild(container);
1579
2075
  const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1580
2076
  const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
@@ -1590,8 +2086,9 @@ var PdfPlugin = class {
1590
2086
  let currentPage = 1;
1591
2087
  let zoomScale = 1;
1592
2088
  let rotation = 0;
1593
- let fitMode = "page";
2089
+ let fitMode = ctx?.options?.fitMode || "width";
1594
2090
  let currentRenderTask = null;
2091
+ let textLayerDiv = null;
1595
2092
  const renderPage = async (pageNum) => {
1596
2093
  if (currentRenderTask) {
1597
2094
  try {
@@ -1603,30 +2100,37 @@ var PdfPlugin = class {
1603
2100
  const newCanvas = document.createElement("canvas");
1604
2101
  pageCard.replaceChild(newCanvas, canvas);
1605
2102
  canvas = newCanvas;
2103
+ if (textLayerDiv) {
2104
+ textLayerDiv.remove();
2105
+ textLayerDiv = null;
2106
+ }
1606
2107
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
1607
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1608
2108
  ctx.emit("page-change", { page: currentPage, total: totalPages });
1609
2109
  const page = await pdfDoc.getPage(currentPage);
1610
2110
  const containerWidth = container.clientWidth || 900;
1611
2111
  const containerHeight = container.clientHeight || 700;
1612
2112
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1613
- const availWidth = Math.max(320, containerWidth - 48);
1614
- const availHeight = Math.max(550, containerHeight - 88);
2113
+ const availWidth = Math.max(280, containerWidth - 36);
2114
+ const availHeight = Math.max(280, containerHeight - 32);
1615
2115
  const scaleW = availWidth / unscaledVp.width;
1616
2116
  const scaleH = availHeight / unscaledVp.height;
1617
2117
  let fitScale;
1618
2118
  if (fitMode === "page") {
1619
- fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
2119
+ fitScale = Math.max(0.2, Math.min(4, Math.min(scaleW, scaleH)));
1620
2120
  } else {
1621
- fitScale = Math.max(0.65, Math.min(1.25, scaleW));
2121
+ fitScale = Math.max(0.2, Math.min(4, scaleW));
1622
2122
  }
1623
2123
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1624
2124
  const pixelRatio = window.devicePixelRatio || 1;
1625
2125
  const viewport = page.getViewport({ scale: effectiveScale, rotation });
2126
+ const displayWidth = Math.floor(viewport.width);
2127
+ const displayHeight = Math.floor(viewport.height);
1626
2128
  canvas.width = Math.floor(viewport.width * pixelRatio);
1627
2129
  canvas.height = Math.floor(viewport.height * pixelRatio);
1628
- canvas.style.width = `${Math.floor(viewport.width)}px`;
1629
- canvas.style.height = `${Math.floor(viewport.height)}px`;
2130
+ canvas.style.width = `${displayWidth}px`;
2131
+ canvas.style.height = `${displayHeight}px`;
2132
+ pageCard.style.width = `${displayWidth}px`;
2133
+ pageCard.style.height = `${displayHeight}px`;
1630
2134
  const canvasCtx = canvas.getContext("2d");
1631
2135
  if (!canvasCtx) return;
1632
2136
  canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
@@ -1640,9 +2144,31 @@ var PdfPlugin = class {
1640
2144
  if (err?.name !== "RenderingCancelledException") {
1641
2145
  console.warn("[PdfPlugin] Page render warning:", err);
1642
2146
  }
2147
+ return;
1643
2148
  } finally {
1644
2149
  currentRenderTask = null;
1645
2150
  }
2151
+ try {
2152
+ textLayerDiv = document.createElement("div");
2153
+ textLayerDiv.className = "textLayer";
2154
+ textLayerDiv.style.width = `${displayWidth}px`;
2155
+ textLayerDiv.style.height = `${displayHeight}px`;
2156
+ textLayerDiv.style.position = "absolute";
2157
+ textLayerDiv.style.top = "0";
2158
+ textLayerDiv.style.left = "0";
2159
+ textLayerDiv.style.lineHeight = "1";
2160
+ pageCard.appendChild(textLayerDiv);
2161
+ if (pdfjsLib.TextLayer) {
2162
+ const textLayer = new pdfjsLib.TextLayer({
2163
+ textContentSource: page.streamTextContent(),
2164
+ container: textLayerDiv,
2165
+ viewport
2166
+ });
2167
+ await textLayer.render();
2168
+ }
2169
+ } catch (err) {
2170
+ console.debug("[PdfPlugin] TextLayer notice:", err);
2171
+ }
1646
2172
  };
1647
2173
  renderPage(1);
1648
2174
  let resizeTimer = null;
@@ -1652,11 +2178,48 @@ var PdfPlugin = class {
1652
2178
  if (Math.abs(zoomScale - 1) < 0.05) {
1653
2179
  renderPage(currentPage);
1654
2180
  }
1655
- }, 150);
2181
+ }, 120);
1656
2182
  });
1657
2183
  resizeObserver.observe(container);
2184
+ const onWheel = (e) => {
2185
+ if (e.ctrlKey || e.metaKey) {
2186
+ e.preventDefault();
2187
+ const delta = e.deltaY > 0 ? -0.15 : 0.15;
2188
+ zoomScale = Math.max(0.25, Math.min(4, Math.round((zoomScale + delta) * 100) / 100));
2189
+ renderPage(currentPage);
2190
+ }
2191
+ };
2192
+ container.addEventListener("wheel", onWheel, { passive: false });
2193
+ const onKeyDown = (e) => {
2194
+ const tag = e.target?.tagName?.toLowerCase();
2195
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
2196
+ if (e.key === "ArrowRight" || e.key === "PageDown") {
2197
+ if (currentPage < totalPages) {
2198
+ e.preventDefault();
2199
+ container.scrollTop = 0;
2200
+ renderPage(currentPage + 1);
2201
+ }
2202
+ } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
2203
+ if (currentPage > 1) {
2204
+ e.preventDefault();
2205
+ container.scrollTop = 0;
2206
+ renderPage(currentPage - 1);
2207
+ }
2208
+ } else if (e.key === "Home") {
2209
+ e.preventDefault();
2210
+ container.scrollTop = 0;
2211
+ renderPage(1);
2212
+ } else if (e.key === "End") {
2213
+ e.preventDefault();
2214
+ container.scrollTop = 0;
2215
+ renderPage(totalPages);
2216
+ }
2217
+ };
2218
+ window.addEventListener("keydown", onKeyDown);
1658
2219
  const cleanup = () => {
1659
2220
  resizeObserver.disconnect();
2221
+ window.removeEventListener("keydown", onKeyDown);
2222
+ container.removeEventListener("wheel", onWheel);
1660
2223
  if (resizeTimer) clearTimeout(resizeTimer);
1661
2224
  if (currentRenderTask) {
1662
2225
  try {
@@ -1675,22 +2238,26 @@ var PdfPlugin = class {
1675
2238
  const instance = {
1676
2239
  destroy: cleanup,
1677
2240
  zoomIn: () => {
1678
- zoomScale = Math.min(3.5, zoomScale + 0.2);
2241
+ zoomScale = Math.min(4, Math.round((zoomScale + 0.2) * 10) / 10);
1679
2242
  renderPage(currentPage);
1680
2243
  },
1681
2244
  zoomOut: () => {
1682
- zoomScale = Math.max(0.3, zoomScale - 0.2);
2245
+ zoomScale = Math.max(0.25, Math.round((zoomScale - 0.2) * 10) / 10);
1683
2246
  renderPage(currentPage);
1684
2247
  },
1685
2248
  getZoom: () => zoomScale,
1686
2249
  setZoom: (level) => {
1687
- zoomScale = Math.max(0.3, Math.min(3.5, level));
2250
+ zoomScale = Math.max(0.25, Math.min(4, level));
1688
2251
  renderPage(currentPage);
1689
2252
  },
1690
2253
  fitToPage: () => {
1691
- fitMode = fitMode === "page" ? "width" : "page";
2254
+ fitMode = "page";
2255
+ zoomScale = 1;
2256
+ renderPage(currentPage);
2257
+ },
2258
+ fitToWidth: () => {
2259
+ fitMode = "width";
1692
2260
  zoomScale = 1;
1693
- rotation = 0;
1694
2261
  renderPage(currentPage);
1695
2262
  },
1696
2263
  rotateCW: () => {
@@ -1708,6 +2275,9 @@ var PdfPlugin = class {
1708
2275
  container.scrollTop = 0;
1709
2276
  renderPage(page);
1710
2277
  },
2278
+ toggleThumbnails: () => {
2279
+ ctx?.toggleThumbnails?.();
2280
+ },
1711
2281
  download: () => {
1712
2282
  const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1713
2283
  const url = URL.createObjectURL(blob);
@@ -1741,7 +2311,7 @@ var PdfPlugin = class {
1741
2311
  },
1742
2312
  getThumbnails: async () => {
1743
2313
  const thumbnails = [];
1744
- const count = Math.min(totalPages, 50);
2314
+ const count = totalPages;
1745
2315
  for (let i = 1; i <= count; i++) {
1746
2316
  thumbnails.push({
1747
2317
  index: i,
@@ -1749,9 +2319,10 @@ var PdfPlugin = class {
1749
2319
  render: async (thumbCanvas) => {
1750
2320
  try {
1751
2321
  const p = await pdfDoc.getPage(i);
1752
- const baseVp = p.getViewport({ scale: 1 });
1753
- const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1754
- const thumbVp = p.getViewport({ scale: thumbScale });
2322
+ const baseVp = p.getViewport({ scale: 1, rotation });
2323
+ const targetW = thumbCanvas.width || 130;
2324
+ const thumbScale = targetW / baseVp.width;
2325
+ const thumbVp = p.getViewport({ scale: thumbScale, rotation });
1755
2326
  thumbCanvas.height = Math.floor(thumbVp.height);
1756
2327
  const tCtx = thumbCanvas.getContext("2d");
1757
2328
  if (tCtx) {
@@ -2095,28 +2666,26 @@ var DocxPlugin = class {
2095
2666
  getToolbarActions(instance) {
2096
2667
  const totalPages = instance.getPageCount?.() ?? 1;
2097
2668
  const actions = [];
2098
- if (totalPages > 1) {
2099
- actions.push({
2100
- id: "page-nav",
2101
- icon: "",
2102
- label: "Page Navigation",
2103
- type: "page-nav",
2104
- group: "navigation",
2105
- value: instance.getCurrentPage?.() ?? 1,
2106
- max: totalPages,
2107
- execute: (action, page) => {
2108
- const cur = instance.getCurrentPage?.() ?? 1;
2109
- const max = instance.getPageCount?.() ?? 1;
2110
- if (action === "prev") {
2111
- if (cur > 1) instance.goToPage?.(cur - 1);
2112
- } else if (action === "next") {
2113
- if (cur < max) instance.goToPage?.(cur + 1);
2114
- } else if (typeof page === "number") {
2115
- instance.goToPage?.(page);
2116
- }
2669
+ actions.push({
2670
+ id: "page-nav",
2671
+ icon: "",
2672
+ label: "Page Navigation",
2673
+ type: "page-nav",
2674
+ group: "navigation",
2675
+ value: instance.getCurrentPage?.() ?? 1,
2676
+ max: totalPages,
2677
+ execute: (action, page) => {
2678
+ const cur = instance.getCurrentPage?.() ?? 1;
2679
+ const max = instance.getPageCount?.() ?? 1;
2680
+ if (action === "prev") {
2681
+ if (cur > 1) instance.goToPage?.(cur - 1);
2682
+ } else if (action === "next") {
2683
+ if (cur < max) instance.goToPage?.(cur + 1);
2684
+ } else if (typeof page === "number") {
2685
+ instance.goToPage?.(page);
2117
2686
  }
2118
- });
2119
- }
2687
+ }
2688
+ });
2120
2689
  actions.push(
2121
2690
  {
2122
2691
  id: "zoom-out",
@@ -2182,11 +2751,17 @@ var DocxPlugin = class {
2182
2751
  wrapper.className = "fp-docx-wrapper";
2183
2752
  wrapper.style.transformOrigin = "top center";
2184
2753
  wrapper.style.transition = "transform 0.2s ease";
2185
- wrapper.style.padding = "24px 16px";
2186
- wrapper.style.maxWidth = "950px";
2754
+ wrapper.style.padding = "0";
2755
+ wrapper.style.maxWidth = "none";
2756
+ wrapper.style.width = "816px";
2187
2757
  wrapper.style.margin = "0 auto";
2758
+ wrapper.style.display = "flex";
2759
+ wrapper.style.flexDirection = "column";
2760
+ wrapper.style.alignItems = "center";
2188
2761
  wrapper.style.boxSizing = "border-box";
2189
- ctx.container.style.overflow = "auto";
2762
+ ctx.container.style.overflowX = "hidden";
2763
+ ctx.container.style.overflowY = "auto";
2764
+ ctx.container.style.padding = "16px 8px";
2190
2765
  ctx.container.style.backgroundColor = "#f1f5f9";
2191
2766
  ctx.container.appendChild(wrapper);
2192
2767
  const styleOverride = document.createElement("style");
@@ -2360,39 +2935,13 @@ var DocxPlugin = class {
2360
2935
  const pageElements = sections.length > 0 ? sections : cards;
2361
2936
  const totalPages = Math.max(1, pageElements.length);
2362
2937
  let currentPage = 1;
2363
- let indicator = null;
2364
- if (totalPages > 1) {
2365
- indicator = document.createElement("div");
2366
- indicator.className = "fp-docx-page-indicator";
2367
- indicator.style.position = "sticky";
2368
- indicator.style.bottom = "16px";
2369
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2370
- indicator.style.backdropFilter = "blur(8px)";
2371
- indicator.style.color = "#f8fafc";
2372
- indicator.style.fontSize = "12px";
2373
- indicator.style.fontWeight = "600";
2374
- indicator.style.padding = "5px 14px";
2375
- indicator.style.borderRadius = "20px";
2376
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2377
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2378
- indicator.style.zIndex = "10";
2379
- indicator.style.userSelect = "none";
2380
- indicator.style.pointerEvents = "none";
2381
- indicator.style.textAlign = "center";
2382
- indicator.style.width = "fit-content";
2383
- indicator.style.margin = "16px auto 0";
2384
- ctx.container.appendChild(indicator);
2385
- }
2386
2938
  const showPage = (pageNum) => {
2387
2939
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
2388
2940
  if (pageElements.length > 1) {
2389
2941
  pageElements.forEach((sec, idx) => {
2390
- sec.style.display = idx + 1 === currentPage ? "block" : "none";
2942
+ sec.style.display = idx + 1 === currentPage ? "" : "none";
2391
2943
  });
2392
2944
  }
2393
- if (indicator) {
2394
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2395
- }
2396
2945
  ctx.container.scrollTop = 0;
2397
2946
  ctx.emit("page-change", { page: currentPage, total: totalPages });
2398
2947
  };
@@ -2401,31 +2950,46 @@ var DocxPlugin = class {
2401
2950
  }
2402
2951
  scale = 1;
2403
2952
  let rotation = 0;
2404
- let fitMode = "width";
2405
- const calculateFitScale = (mode = fitMode) => {
2406
- const activeEl = pageElements[currentPage - 1] || wrapper.firstElementChild || wrapper;
2953
+ let fitMode = "page";
2954
+ let isUserZoomed = false;
2955
+ const calculateFitScale = (_mode = fitMode) => {
2956
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
2407
2957
  const elW = activeEl.offsetWidth || 816;
2408
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
2409
- const availW = Math.max(280, ctx.container.clientWidth - 48);
2410
- const availH = Math.max(280, ctx.container.clientHeight - 80);
2958
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
2411
2959
  const sW = availW / elW;
2412
- const sH = availH / singlePageH;
2413
- if (mode === "page") {
2414
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
2415
- }
2416
- return Math.max(0.65, Math.min(1.05, sW));
2960
+ return Math.max(0.35, Math.min(3, sW));
2417
2961
  };
2418
2962
  const applyTransform = () => {
2419
2963
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
2420
2964
  wrapper.style.transformOrigin = "top center";
2965
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
2966
+ const elH = activeEl.offsetHeight || 1056;
2967
+ if (scale > 1) {
2968
+ wrapper.style.marginBottom = `${Math.round(elH * scale - elH + 32)}px`;
2969
+ } else {
2970
+ wrapper.style.marginBottom = "32px";
2971
+ }
2421
2972
  };
2973
+ let resizeObserver = null;
2974
+ if (typeof ResizeObserver !== "undefined") {
2975
+ resizeObserver = new ResizeObserver(() => {
2976
+ if (!isUserZoomed) {
2977
+ const newFit = calculateFitScale("page");
2978
+ if (Math.abs(scale - newFit) > 0.015) {
2979
+ scale = newFit;
2980
+ applyTransform();
2981
+ }
2982
+ }
2983
+ });
2984
+ resizeObserver.observe(ctx.container);
2985
+ }
2422
2986
  setTimeout(() => {
2423
- fitMode = "width";
2424
- scale = calculateFitScale("width");
2987
+ scale = calculateFitScale("page");
2425
2988
  applyTransform();
2426
- }, 60);
2989
+ }, 40);
2427
2990
  const cleanup = () => {
2428
- indicator?.remove();
2991
+ resizeObserver?.disconnect();
2992
+ resizeObserver = null;
2429
2993
  for (const url of createdBlobUrls) {
2430
2994
  URL.revokeObjectURL(url);
2431
2995
  }
@@ -2442,21 +3006,24 @@ var DocxPlugin = class {
2442
3006
  showPage(page);
2443
3007
  },
2444
3008
  zoomIn: () => {
2445
- scale += 0.15;
3009
+ isUserZoomed = true;
3010
+ scale = Math.min(3.5, scale + 0.15);
2446
3011
  applyTransform();
2447
3012
  },
2448
3013
  zoomOut: () => {
3014
+ isUserZoomed = true;
2449
3015
  scale = Math.max(0.2, scale - 0.15);
2450
3016
  applyTransform();
2451
3017
  },
2452
3018
  getZoom: () => scale,
2453
3019
  setZoom: (level) => {
3020
+ isUserZoomed = true;
2454
3021
  scale = level;
2455
3022
  applyTransform();
2456
3023
  },
2457
3024
  fitToPage: () => {
2458
- fitMode = fitMode === "width" ? "page" : "width";
2459
- scale = calculateFitScale(fitMode);
3025
+ isUserZoomed = false;
3026
+ scale = calculateFitScale("page");
2460
3027
  rotation = 0;
2461
3028
  applyTransform();
2462
3029
  },
@@ -2846,7 +3413,7 @@ var ExcelPlugin = class {
2846
3413
  "application/vnd.ms-excel.template.macroEnabled.12",
2847
3414
  "application/vnd.oasis.opendocument.spreadsheet"
2848
3415
  ];
2849
- weight = 80;
3416
+ weight = 85;
2850
3417
  supports(file) {
2851
3418
  const ext = file.metadata.extension?.toLowerCase();
2852
3419
  const mime = file.metadata.mimeType?.toLowerCase();
@@ -2858,6 +3425,14 @@ var ExcelPlugin = class {
2858
3425
  getToolbarActions(instance) {
2859
3426
  const totalSheets = instance.getPageCount?.() ?? 1;
2860
3427
  const actions = [];
3428
+ actions.push({
3429
+ id: "thumbnails",
3430
+ icon: "thumbnails",
3431
+ label: "Sheet Thumbnails",
3432
+ type: "button",
3433
+ group: "navigation",
3434
+ execute: () => instance.toggleThumbnails?.()
3435
+ });
2861
3436
  if (totalSheets > 1) {
2862
3437
  actions.push({
2863
3438
  id: "page-nav",
@@ -2901,26 +3476,6 @@ var ExcelPlugin = class {
2901
3476
  instance.zoomIn?.();
2902
3477
  }
2903
3478
  },
2904
- {
2905
- id: "fit-page",
2906
- icon: "fit-page",
2907
- label: "Fit to View",
2908
- type: "button",
2909
- group: "zoom",
2910
- execute: () => {
2911
- instance.fitToPage?.();
2912
- }
2913
- },
2914
- {
2915
- id: "rotate-cw",
2916
- icon: "rotate-cw",
2917
- label: "Rotate",
2918
- type: "button",
2919
- group: "view",
2920
- execute: () => {
2921
- instance.rotateCW?.();
2922
- }
2923
- },
2924
3479
  {
2925
3480
  id: "download",
2926
3481
  icon: "download",
@@ -2940,6 +3495,16 @@ var ExcelPlugin = class {
2940
3495
  execute: () => {
2941
3496
  instance.print?.();
2942
3497
  }
3498
+ },
3499
+ {
3500
+ id: "open-window",
3501
+ icon: "open-window",
3502
+ label: "Open in Separate Full Window",
3503
+ type: "button",
3504
+ group: "actions",
3505
+ execute: () => {
3506
+ instance.openInSeparateWindow?.();
3507
+ }
2943
3508
  }
2944
3509
  );
2945
3510
  return actions;
@@ -2971,12 +3536,11 @@ var ExcelPlugin = class {
2971
3536
  container.appendChild(tabsArea);
2972
3537
  ctx.container.appendChild(container);
2973
3538
  let scale = 1;
2974
- let rotation = 0;
2975
3539
  let currentSheetIndex = 1;
2976
3540
  let sheetNames = [];
2977
3541
  let wb = null;
2978
3542
  const applyTransform = () => {
2979
- contentArea.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
3543
+ contentArea.style.transform = `scale(${scale})`;
2980
3544
  contentArea.style.transformOrigin = "top left";
2981
3545
  };
2982
3546
  const calculateFitScale = () => {
@@ -2984,7 +3548,7 @@ var ExcelPlugin = class {
2984
3548
  if (!table) return 1;
2985
3549
  const availW = Math.max(280, container.clientWidth - 48);
2986
3550
  const tW = table.offsetWidth || 800;
2987
- return Math.max(0.65, Math.min(1, availW / tW));
3551
+ return Math.max(0.4, Math.min(1, availW / tW));
2988
3552
  };
2989
3553
  try {
2990
3554
  wb = XLSX.read(new Uint8Array(ctx.buffer), { type: "array", cellDates: true });
@@ -3039,7 +3603,16 @@ var ExcelPlugin = class {
3039
3603
  }
3040
3604
  });
3041
3605
  ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
3606
+ requestAnimationFrame(() => {
3607
+ scale = calculateFitScale();
3608
+ applyTransform();
3609
+ });
3042
3610
  };
3611
+ const ro = new ResizeObserver(() => {
3612
+ scale = calculateFitScale();
3613
+ applyTransform();
3614
+ });
3615
+ ro.observe(container);
3043
3616
  if (sheetNames.length > 0) {
3044
3617
  sheetNames.forEach((name, idx) => {
3045
3618
  const btn = document.createElement("button");
@@ -3066,6 +3639,7 @@ var ExcelPlugin = class {
3066
3639
  `;
3067
3640
  }
3068
3641
  const cleanup = () => {
3642
+ ro.disconnect();
3069
3643
  container.remove();
3070
3644
  ctx.container.innerHTML = "";
3071
3645
  };
@@ -3087,16 +3661,11 @@ var ExcelPlugin = class {
3087
3661
  },
3088
3662
  fitToPage: () => {
3089
3663
  scale = calculateFitScale();
3090
- rotation = 0;
3091
3664
  applyTransform();
3092
3665
  },
3093
3666
  rotateCW: () => {
3094
- rotation = (rotation + 90) % 360;
3095
- applyTransform();
3096
3667
  },
3097
3668
  rotateCCW: () => {
3098
- rotation = (rotation - 90 + 360) % 360;
3099
- applyTransform();
3100
3669
  },
3101
3670
  goToPage: (page) => {
3102
3671
  if (page > 0 && page <= sheetNames.length) {
@@ -3105,6 +3674,63 @@ var ExcelPlugin = class {
3105
3674
  },
3106
3675
  getPageCount: () => sheetNames.length,
3107
3676
  getCurrentPage: () => currentSheetIndex,
3677
+ getThumbnails: () => {
3678
+ return sheetNames.map((name, idx) => ({
3679
+ index: idx,
3680
+ label: name,
3681
+ render: async (canvas) => {
3682
+ canvas.width = 140;
3683
+ canvas.height = 100;
3684
+ const c = canvas.getContext("2d");
3685
+ if (!c) return;
3686
+ c.fillStyle = "#ffffff";
3687
+ c.fillRect(0, 0, canvas.width, canvas.height);
3688
+ c.fillStyle = "#107c41";
3689
+ c.fillRect(0, 0, canvas.width, 16);
3690
+ c.fillStyle = "#ffffff";
3691
+ c.font = 'bold 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3692
+ c.fillText(`\u{1F4CA} ${name.slice(0, 16)}`, 6, 11);
3693
+ const startY = 17;
3694
+ const rowH = 13;
3695
+ const colW = 30;
3696
+ const colHeaders = ["A", "B", "C", "D"];
3697
+ c.fillStyle = "#f1f5f9";
3698
+ c.fillRect(0, startY, canvas.width, rowH);
3699
+ c.strokeStyle = "#cbd5e1";
3700
+ c.lineWidth = 0.8;
3701
+ c.strokeRect(0, startY, canvas.width, rowH);
3702
+ c.fillStyle = "#64748b";
3703
+ c.font = "bold 7px sans-serif";
3704
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3705
+ c.fillText(colHeaders[ci], 14 + ci * colW, startY + 9);
3706
+ }
3707
+ const ws = wb?.Sheets[name];
3708
+ for (let ri = 1; ri <= 5; ri++) {
3709
+ const y = startY + ri * rowH;
3710
+ if (y > canvas.height - 2) break;
3711
+ c.fillStyle = "#f8fafc";
3712
+ c.fillRect(0, y, 12, rowH);
3713
+ c.fillStyle = "#94a3b8";
3714
+ c.font = "6px sans-serif";
3715
+ c.fillText(String(ri), 3, y + 9);
3716
+ c.strokeStyle = "#e2e8f0";
3717
+ c.strokeRect(0, y, canvas.width, rowH);
3718
+ c.fillStyle = "#334155";
3719
+ c.font = "6px sans-serif";
3720
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3721
+ const cellRef = `${colHeaders[ci]}${ri}`;
3722
+ const cellVal = ws?.[cellRef]?.w || ws?.[cellRef]?.v;
3723
+ if (cellVal !== void 0) {
3724
+ c.fillText(String(cellVal).slice(0, 6), 14 + ci * colW, y + 9);
3725
+ }
3726
+ }
3727
+ }
3728
+ c.strokeStyle = "#cbd5e1";
3729
+ c.lineWidth = 1;
3730
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
3731
+ }
3732
+ }));
3733
+ },
3108
3734
  download: () => {
3109
3735
  const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
3110
3736
  const url = URL.createObjectURL(blob);
@@ -3137,7 +3763,16 @@ var CsvPlugin = class {
3137
3763
  return ext === ".csv" || ext === ".tsv" || mime === "text/csv" || mime === "text/tab-separated-values";
3138
3764
  }
3139
3765
  getToolbarActions(instance) {
3140
- return [
3766
+ const actions = [];
3767
+ actions.push({
3768
+ id: "thumbnails",
3769
+ icon: "thumbnails",
3770
+ label: "Sheet Thumbnails",
3771
+ type: "button",
3772
+ group: "navigation",
3773
+ execute: () => instance.toggleThumbnails?.()
3774
+ });
3775
+ actions.push(
3141
3776
  {
3142
3777
  id: "zoom-out",
3143
3778
  icon: "zoom-out",
@@ -3177,8 +3812,19 @@ var CsvPlugin = class {
3177
3812
  execute: () => {
3178
3813
  instance.print?.();
3179
3814
  }
3815
+ },
3816
+ {
3817
+ id: "open-window",
3818
+ icon: "open-window",
3819
+ label: "Open in Separate Full Window",
3820
+ type: "button",
3821
+ group: "actions",
3822
+ execute: () => {
3823
+ instance.openInSeparateWindow?.();
3824
+ }
3180
3825
  }
3181
- ];
3826
+ );
3827
+ return actions;
3182
3828
  }
3183
3829
  async render(ctx) {
3184
3830
  const decoder = new TextDecoder("utf-8");
@@ -3189,57 +3835,156 @@ var CsvPlugin = class {
3189
3835
  container.style.overflow = "auto";
3190
3836
  container.style.padding = "16px";
3191
3837
  container.style.boxSizing = "border-box";
3192
- container.style.transformOrigin = "top left";
3838
+ const tableWrapper = document.createElement("div");
3839
+ tableWrapper.style.transformOrigin = "top left";
3840
+ const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3841
+ const allLines = text.split(/\r?\n/).filter((r) => r.trim().length > 0);
3842
+ const headerLine = allLines[0] || "";
3843
+ const headerCells = headerLine.split(delimiter);
3844
+ const dataLines = allLines.slice(1);
3193
3845
  const table = document.createElement("table");
3194
3846
  table.style.borderCollapse = "collapse";
3195
- table.style.width = "100%";
3196
- table.style.fontFamily = "monospace";
3847
+ table.style.minWidth = "100%";
3848
+ table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace';
3197
3849
  table.style.fontSize = "13px";
3198
- const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3199
- const rows = text.split(/\r?\n/).slice(0, 500);
3200
- rows.forEach((row, rowIndex) => {
3201
- if (!row.trim()) return;
3850
+ table.style.background = "#ffffff";
3851
+ table.style.border = "1px solid #d0d7de";
3852
+ const headerTr = document.createElement("tr");
3853
+ headerCells.forEach((cell) => {
3854
+ const th = document.createElement("th");
3855
+ th.textContent = cell.trim();
3856
+ th.style.border = "1px solid #d0d7de";
3857
+ th.style.padding = "8px 12px";
3858
+ th.style.backgroundColor = "#f6f8fa";
3859
+ th.style.fontWeight = "600";
3860
+ th.style.whiteSpace = "nowrap";
3861
+ headerTr.appendChild(th);
3862
+ });
3863
+ table.appendChild(headerTr);
3864
+ dataLines.forEach((row, idx) => {
3202
3865
  const tr = document.createElement("tr");
3866
+ if (idx % 2 === 1) tr.style.backgroundColor = "#f8fafc";
3203
3867
  const cells = row.split(delimiter);
3204
3868
  cells.forEach((cell) => {
3205
- const td = document.createElement(rowIndex === 0 ? "th" : "td");
3869
+ const td = document.createElement("td");
3206
3870
  td.textContent = cell.trim();
3207
3871
  td.style.border = "1px solid #d0d7de";
3208
3872
  td.style.padding = "6px 12px";
3209
- if (rowIndex === 0) {
3210
- td.style.backgroundColor = "#f6f8fa";
3211
- td.style.fontWeight = "bold";
3212
- }
3873
+ td.style.whiteSpace = "nowrap";
3213
3874
  tr.appendChild(td);
3214
3875
  });
3215
3876
  table.appendChild(tr);
3216
3877
  });
3217
- container.appendChild(table);
3878
+ tableWrapper.appendChild(table);
3879
+ container.appendChild(tableWrapper);
3218
3880
  ctx.container.appendChild(container);
3219
3881
  let scale = 1;
3882
+ const calculateFitScale = () => {
3883
+ const availW = Math.max(280, container.clientWidth - 48);
3884
+ const tW = table.offsetWidth || 800;
3885
+ return Math.max(0.4, Math.min(1, availW / tW));
3886
+ };
3887
+ const applyScale = () => {
3888
+ tableWrapper.style.transform = `scale(${scale})`;
3889
+ };
3890
+ requestAnimationFrame(() => {
3891
+ scale = calculateFitScale();
3892
+ applyScale();
3893
+ });
3894
+ const ro = new ResizeObserver(() => {
3895
+ scale = calculateFitScale();
3896
+ applyScale();
3897
+ });
3898
+ ro.observe(container);
3220
3899
  const cleanup = () => {
3900
+ ro.disconnect();
3221
3901
  container.remove();
3222
3902
  ctx.container.innerHTML = "";
3223
3903
  };
3224
3904
  ctx.signal.addEventListener("abort", cleanup);
3225
3905
  return {
3226
3906
  destroy: cleanup,
3907
+ getPageCount: () => 1,
3908
+ getCurrentPage: () => 1,
3909
+ goToPage: () => {
3910
+ },
3911
+ getThumbnails: () => {
3912
+ const sheetName = ctx.metadata.name?.replace(/\.[^/.]+$/, "") || "Sheet1";
3913
+ return [{
3914
+ index: 0,
3915
+ label: sheetName,
3916
+ render: async (canvas) => {
3917
+ canvas.width = 140;
3918
+ canvas.height = 100;
3919
+ const c = canvas.getContext("2d");
3920
+ if (!c) return;
3921
+ c.fillStyle = "#ffffff";
3922
+ c.fillRect(0, 0, canvas.width, canvas.height);
3923
+ c.fillStyle = "#107c41";
3924
+ c.fillRect(0, 0, canvas.width, 16);
3925
+ c.fillStyle = "#ffffff";
3926
+ c.font = 'bold 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3927
+ c.fillText(`\u{1F4CA} ${sheetName.slice(0, 16)}`, 6, 11);
3928
+ const startY = 17;
3929
+ const rowH = 13;
3930
+ const colW = 30;
3931
+ const cols = headerCells.slice(0, 4).map((h) => h.trim() || "Col");
3932
+ c.fillStyle = "#f1f5f9";
3933
+ c.fillRect(0, startY, canvas.width, rowH);
3934
+ c.strokeStyle = "#cbd5e1";
3935
+ c.lineWidth = 0.8;
3936
+ c.strokeRect(0, startY, canvas.width, rowH);
3937
+ c.fillStyle = "#64748b";
3938
+ c.font = "bold 7px sans-serif";
3939
+ for (let ci = 0; ci < cols.length; ci++) {
3940
+ c.fillText(cols[ci].slice(0, 5), 14 + ci * colW, startY + 9);
3941
+ }
3942
+ for (let ri = 0; ri < Math.min(5, dataLines.length); ri++) {
3943
+ const y = startY + (ri + 1) * rowH;
3944
+ if (y > canvas.height - 2) break;
3945
+ c.fillStyle = "#f8fafc";
3946
+ c.fillRect(0, y, 12, rowH);
3947
+ c.fillStyle = "#94a3b8";
3948
+ c.font = "6px sans-serif";
3949
+ c.fillText(String(ri + 1), 3, y + 9);
3950
+ c.strokeStyle = "#e2e8f0";
3951
+ c.strokeRect(0, y, canvas.width, rowH);
3952
+ const rowCells = dataLines[ri].split(delimiter);
3953
+ c.fillStyle = "#334155";
3954
+ c.font = "6px sans-serif";
3955
+ for (let ci = 0; ci < cols.length; ci++) {
3956
+ const val = (rowCells[ci] || "").trim();
3957
+ if (val) {
3958
+ c.fillText(val.slice(0, 6), 14 + ci * colW, y + 9);
3959
+ }
3960
+ }
3961
+ }
3962
+ c.strokeStyle = "#cbd5e1";
3963
+ c.lineWidth = 1;
3964
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
3965
+ }
3966
+ }];
3967
+ },
3227
3968
  zoomIn: () => {
3228
3969
  scale += 0.1;
3229
- container.style.transform = `scale(${scale})`;
3970
+ applyScale();
3230
3971
  },
3231
3972
  zoomOut: () => {
3232
3973
  scale = Math.max(0.2, scale - 0.1);
3233
- container.style.transform = `scale(${scale})`;
3974
+ applyScale();
3234
3975
  },
3235
3976
  getZoom: () => scale,
3236
3977
  setZoom: (level) => {
3237
3978
  scale = level;
3238
- container.style.transform = `scale(${scale})`;
3979
+ applyScale();
3239
3980
  },
3240
3981
  fitToPage: () => {
3241
- scale = 1;
3242
- container.style.transform = `scale(1)`;
3982
+ scale = calculateFitScale();
3983
+ applyScale();
3984
+ },
3985
+ rotateCW: () => {
3986
+ },
3987
+ rotateCCW: () => {
3243
3988
  },
3244
3989
  download: () => {
3245
3990
  const blob = new Blob([ctx.buffer], { type: "text/csv" });
@@ -3301,30 +4046,36 @@ var CodePlugin = class {
3301
4046
  return false;
3302
4047
  }
3303
4048
  getToolbarActions(instance) {
3304
- const totalPages = instance.getPageCount?.() ?? 1;
3305
- const actions = [];
3306
- if (totalPages > 1) {
3307
- actions.push({
3308
- id: "page-nav",
3309
- icon: "",
3310
- label: "Page Navigation",
3311
- type: "page-nav",
3312
- group: "navigation",
3313
- value: instance.getCurrentPage?.() ?? 1,
3314
- max: totalPages,
3315
- execute: (action, page) => {
3316
- const cur = instance.getCurrentPage?.() ?? 1;
3317
- const max = instance.getPageCount?.() ?? 1;
3318
- if (action === "prev") {
3319
- if (cur > 1) instance.goToPage?.(cur - 1);
3320
- } else if (action === "next") {
3321
- if (cur < max) instance.goToPage?.(cur + 1);
3322
- } else if (typeof page === "number") {
3323
- instance.goToPage?.(page);
3324
- }
4049
+ const totalPages = instance.getPageCount?.() ?? 1;
4050
+ const actions = [];
4051
+ actions.push({
4052
+ id: "thumbnails",
4053
+ icon: "thumbnails",
4054
+ label: "Page Thumbnails",
4055
+ type: "button",
4056
+ group: "navigation",
4057
+ execute: () => instance.toggleThumbnails?.()
4058
+ });
4059
+ actions.push({
4060
+ id: "page-nav",
4061
+ icon: "",
4062
+ label: "Page Navigation",
4063
+ type: "page-nav",
4064
+ group: "navigation",
4065
+ value: instance.getCurrentPage?.() ?? 1,
4066
+ max: totalPages,
4067
+ execute: (action, page) => {
4068
+ const cur = instance.getCurrentPage?.() ?? 1;
4069
+ const max = instance.getPageCount?.() ?? 1;
4070
+ if (action === "prev") {
4071
+ if (cur > 1) instance.goToPage?.(cur - 1);
4072
+ } else if (action === "next") {
4073
+ if (cur < max) instance.goToPage?.(cur + 1);
4074
+ } else if (typeof page === "number") {
4075
+ instance.goToPage?.(page);
3325
4076
  }
3326
- });
3327
- }
4077
+ }
4078
+ });
3328
4079
  actions.push(
3329
4080
  {
3330
4081
  id: "zoom-out",
@@ -3346,26 +4097,6 @@ var CodePlugin = class {
3346
4097
  instance.zoomIn?.();
3347
4098
  }
3348
4099
  },
3349
- {
3350
- id: "fit-page",
3351
- icon: "fit-page",
3352
- label: "Fit to View",
3353
- type: "button",
3354
- group: "zoom",
3355
- execute: () => {
3356
- instance.fitToPage?.();
3357
- }
3358
- },
3359
- {
3360
- id: "rotate-cw",
3361
- icon: "rotate-cw",
3362
- label: "Rotate",
3363
- type: "button",
3364
- group: "view",
3365
- execute: () => {
3366
- instance.rotateCW?.();
3367
- }
3368
- },
3369
4100
  {
3370
4101
  id: "copy",
3371
4102
  icon: "copy",
@@ -3468,16 +4199,19 @@ var CodePlugin = class {
3468
4199
  let fontSize = 13;
3469
4200
  let rotation = 0;
3470
4201
  let zoomLevel = 1;
4202
+ let isUserZoomed = false;
3471
4203
  const pre = document.createElement("pre");
3472
4204
  const code = document.createElement("code");
3473
4205
  if (isTxt) {
4206
+ container.style.overflowX = "hidden";
4207
+ container.style.overflowY = "auto";
3474
4208
  container.style.backgroundColor = "#f1f5f9";
3475
- container.style.padding = "32px";
4209
+ container.style.padding = "16px 8px";
3476
4210
  container.style.boxSizing = "border-box";
3477
4211
  container.style.display = "flex";
3478
4212
  container.style.flexDirection = "column";
3479
4213
  container.style.alignItems = "center";
3480
- pre.style.margin = "0";
4214
+ pre.style.margin = "0 auto";
3481
4215
  pre.style.fontFamily = "Consolas, 'Courier New', monospace";
3482
4216
  pre.style.fontSize = "13px";
3483
4217
  pre.style.color = "#1e293b";
@@ -3489,10 +4223,12 @@ var CodePlugin = class {
3489
4223
  pre.style.minHeight = "1056px";
3490
4224
  pre.style.padding = "72px 56px";
3491
4225
  pre.style.boxSizing = "border-box";
3492
- pre.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
4226
+ pre.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
3493
4227
  pre.style.borderRadius = "4px";
3494
4228
  pre.style.transformOrigin = "top center";
4229
+ pre.style.transition = "transform 0.15s ease";
3495
4230
  } else {
4231
+ container.style.overflow = "auto";
3496
4232
  container.style.backgroundColor = "#1e1e1e";
3497
4233
  container.style.color = "#d4d4d4";
3498
4234
  container.style.padding = "16px";
@@ -3524,42 +4260,9 @@ var CodePlugin = class {
3524
4260
  renderCodePage(rawPages[0] || (isTxt ? "" : fullText));
3525
4261
  pre.appendChild(code);
3526
4262
  container.appendChild(pre);
3527
- let indicator = null;
3528
- if (totalPages > 1) {
3529
- indicator = document.createElement("div");
3530
- indicator.className = "fp-code-page-indicator";
3531
- indicator.style.position = "sticky";
3532
- indicator.style.bottom = "16px";
3533
- if (isTxt) {
3534
- indicator.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
3535
- indicator.style.color = "#334155";
3536
- indicator.style.border = "1px solid #e2e8f0";
3537
- indicator.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)";
3538
- } else {
3539
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3540
- indicator.style.color = "#f8fafc";
3541
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3542
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3543
- indicator.style.backdropFilter = "blur(8px)";
3544
- }
3545
- indicator.style.fontSize = "12px";
3546
- indicator.style.fontWeight = "600";
3547
- indicator.style.padding = "5px 14px";
3548
- indicator.style.borderRadius = "20px";
3549
- indicator.style.zIndex = "10";
3550
- indicator.style.userSelect = "none";
3551
- indicator.style.pointerEvents = "none";
3552
- indicator.style.textAlign = "center";
3553
- indicator.style.width = "fit-content";
3554
- indicator.style.margin = "16px auto 0";
3555
- container.appendChild(indicator);
3556
- }
3557
4263
  const showPage = (pageNum) => {
3558
4264
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
3559
4265
  renderCodePage(rawPages[currentPage - 1] || (isTxt ? "" : fullText));
3560
- if (indicator) {
3561
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3562
- }
3563
4266
  container.scrollTop = 0;
3564
4267
  ctx.emit("page-change", { page: currentPage, total: totalPages });
3565
4268
  };
@@ -3567,36 +4270,90 @@ var CodePlugin = class {
3567
4270
  showPage(1);
3568
4271
  }
3569
4272
  ctx.container.appendChild(container);
3570
- const cleanup = () => {
3571
- indicator?.remove();
3572
- container.remove();
3573
- ctx.container.innerHTML = "";
4273
+ const calculateTxtFit = () => {
4274
+ const availW = Math.max(280, container.clientWidth - 32);
4275
+ return Math.max(0.4, Math.min(3, availW / 816));
3574
4276
  };
3575
- ctx.signal.addEventListener("abort", cleanup);
3576
4277
  const updateTransform = () => {
3577
4278
  if (isTxt) {
3578
4279
  pre.style.transform = `scale(${zoomLevel}) rotate(${rotation}deg)`;
4280
+ const scaledH = 1056 * zoomLevel;
4281
+ const extraH = Math.max(0, scaledH - 1056);
4282
+ pre.style.marginBottom = `${extraH + 32}px`;
3579
4283
  } else {
3580
4284
  pre.style.transform = `rotate(${rotation}deg)`;
3581
4285
  pre.style.fontSize = `${fontSize}px`;
3582
4286
  }
3583
4287
  };
4288
+ let resizeObserver = null;
4289
+ if (isTxt) {
4290
+ setTimeout(() => {
4291
+ zoomLevel = calculateTxtFit();
4292
+ updateTransform();
4293
+ }, 60);
4294
+ resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
4295
+ if (!isUserZoomed) {
4296
+ zoomLevel = calculateTxtFit();
4297
+ updateTransform();
4298
+ }
4299
+ }) : null;
4300
+ resizeObserver?.observe(container);
4301
+ }
4302
+ const cleanup = () => {
4303
+ resizeObserver?.disconnect();
4304
+ container.remove();
4305
+ ctx.container.innerHTML = "";
4306
+ };
4307
+ ctx.signal.addEventListener("abort", cleanup);
3584
4308
  return {
3585
4309
  destroy: cleanup,
3586
4310
  getPageCount: () => totalPages,
3587
4311
  getCurrentPage: () => currentPage,
3588
4312
  goToPage: (page) => showPage(page),
4313
+ getThumbnails: () => {
4314
+ return rawPages.map((pageText, idx) => ({
4315
+ index: idx,
4316
+ label: `Page ${idx + 1}`,
4317
+ render: async (canvas) => {
4318
+ canvas.width = 140;
4319
+ canvas.height = 180;
4320
+ const c = canvas.getContext("2d");
4321
+ if (!c) return;
4322
+ c.fillStyle = "#ffffff";
4323
+ c.fillRect(0, 0, canvas.width, canvas.height);
4324
+ c.strokeStyle = "#cbd5e1";
4325
+ c.lineWidth = 1;
4326
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
4327
+ c.fillStyle = "#64748b";
4328
+ const lines = (pageText || "").split("\n").slice(0, 24);
4329
+ const lineH = 6;
4330
+ const startY = 14;
4331
+ const startX = 12;
4332
+ const maxW = canvas.width - 24;
4333
+ c.font = "5px monospace";
4334
+ for (let li = 0; li < lines.length; li++) {
4335
+ const l = lines[li].trim();
4336
+ if (!l) continue;
4337
+ const y = startY + li * lineH;
4338
+ if (y > canvas.height - 12) break;
4339
+ c.fillText(l.slice(0, 30), startX, y, maxW);
4340
+ }
4341
+ }
4342
+ }));
4343
+ },
3589
4344
  zoomIn: () => {
4345
+ isUserZoomed = true;
3590
4346
  if (isTxt) {
3591
- zoomLevel = Math.min(3, zoomLevel + 0.1);
4347
+ zoomLevel = Math.min(3.5, zoomLevel + 0.15);
3592
4348
  } else {
3593
4349
  fontSize = Math.min(32, fontSize + 2);
3594
4350
  }
3595
4351
  updateTransform();
3596
4352
  },
3597
4353
  zoomOut: () => {
4354
+ isUserZoomed = true;
3598
4355
  if (isTxt) {
3599
- zoomLevel = Math.max(0.1, zoomLevel - 0.1);
4356
+ zoomLevel = Math.max(0.1, zoomLevel - 0.15);
3600
4357
  } else {
3601
4358
  fontSize = Math.max(8, fontSize - 2);
3602
4359
  }
@@ -3604,6 +4361,7 @@ var CodePlugin = class {
3604
4361
  },
3605
4362
  getZoom: () => isTxt ? zoomLevel : fontSize / 13,
3606
4363
  setZoom: (level) => {
4364
+ isUserZoomed = true;
3607
4365
  if (isTxt) {
3608
4366
  zoomLevel = level;
3609
4367
  } else {
@@ -3612,9 +4370,10 @@ var CodePlugin = class {
3612
4370
  updateTransform();
3613
4371
  },
3614
4372
  fitToPage: () => {
4373
+ isUserZoomed = false;
3615
4374
  fontSize = 13;
3616
4375
  rotation = 0;
3617
- zoomLevel = isTxt ? Math.max(0.65, Math.min(1.05, (container.clientWidth - 48) / 816)) : 1;
4376
+ zoomLevel = isTxt ? calculateTxtFit() : 1;
3618
4377
  updateTransform();
3619
4378
  },
3620
4379
  rotateCW: () => {
@@ -4481,6 +5240,148 @@ var ThreeDPlugin = class {
4481
5240
  function threeDPlugin() {
4482
5241
  return new ThreeDPlugin();
4483
5242
  }
5243
+ function cleanRtfText(raw) {
5244
+ return raw.replace(/\\bin\d+\s/g, "").replace(/{\\\*[^}]+}/g, "").replace(/\\u(-?\d+)\??/g, (_, code) => {
5245
+ let num = parseInt(code, 10);
5246
+ if (num < 0) num += 65536;
5247
+ return String.fromCharCode(num);
5248
+ }).replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => {
5249
+ return String.fromCharCode(parseInt(hex, 16));
5250
+ }).replace(/\\tab\b/g, " ").replace(/\\emdash\b/g, "\u2014").replace(/\\endash\b/g, "\u2013").replace(/\\bullet\b/g, "\u2022").replace(/\\lquote\b/g, "\u2018").replace(/\\rquote\b/g, "\u2019").replace(/\\ldblquote\b/g, "\u201C").replace(/\\rdblquote\b/g, "\u201D").replace(/\\\w+(?:-?\d+)?\s?/g, " ").replace(/[{}\r\n]/g, " ").replace(/\s+/g, " ").trim();
5251
+ }
5252
+ function parseRtfTables(rtfText) {
5253
+ const rowSegments = rtfText.split(/\\row\b/);
5254
+ if (rowSegments.length <= 1) return [];
5255
+ const rawTables = [];
5256
+ let currentTable = null;
5257
+ let charOffset = 0;
5258
+ for (let i = 0; i < rowSegments.length - 1; i++) {
5259
+ const segment = rowSegments[i];
5260
+ const segmentStart = charOffset;
5261
+ const segmentEnd = charOffset + segment.length + 4;
5262
+ const cellParts = segment.split(/\\cell\b/);
5263
+ if (cellParts.length > 1) {
5264
+ const cellxMatches = [...segment.matchAll(/\\cellx(\d+)/g)].map((m) => parseInt(m[1], 10));
5265
+ let cellWidths = [];
5266
+ if (cellxMatches.length > 0) {
5267
+ let seq = [cellxMatches[0]];
5268
+ for (let k = 1; k < cellxMatches.length; k++) {
5269
+ if (cellxMatches[k] > cellxMatches[k - 1]) seq.push(cellxMatches[k]);
5270
+ else break;
5271
+ }
5272
+ let prev = 0;
5273
+ cellWidths = seq.map((x) => {
5274
+ const w = x - prev;
5275
+ prev = x;
5276
+ return w;
5277
+ });
5278
+ }
5279
+ const cells = [];
5280
+ for (let c = 0; c < cellParts.length - 1; c++) {
5281
+ let rawCell = cellParts[c];
5282
+ if (c === 0) {
5283
+ const lastTrowd = rawCell.lastIndexOf("\\trowd");
5284
+ if (lastTrowd !== -1) rawCell = rawCell.slice(lastTrowd);
5285
+ }
5286
+ cells.push(cleanRtfText(rawCell));
5287
+ }
5288
+ const isNewTable = !currentTable || segmentStart - currentTable.lastEnd > 2500;
5289
+ if (isNewTable) {
5290
+ const prevTextSlice = rtfText.slice(Math.max(0, segmentStart - 4e3), segmentStart);
5291
+ const cleanPrev = cleanRtfText(prevTextSlice);
5292
+ const anchorBefore = cleanPrev.slice(-80).trim();
5293
+ currentTable = {
5294
+ id: `RTF_TABLE_${rawTables.length}`,
5295
+ startPos: segmentStart,
5296
+ lastEnd: segmentEnd,
5297
+ rows: [],
5298
+ columnWidths: cellWidths,
5299
+ anchorBefore,
5300
+ anchorAfter: ""
5301
+ };
5302
+ rawTables.push(currentTable);
5303
+ }
5304
+ if (currentTable) {
5305
+ currentTable.rows.push({
5306
+ cells,
5307
+ widths: cellWidths.length === cells.length ? cellWidths : currentTable.columnWidths
5308
+ });
5309
+ currentTable.lastEnd = segmentEnd;
5310
+ }
5311
+ }
5312
+ charOffset += segment.length + 4;
5313
+ }
5314
+ rawTables.forEach((tbl) => {
5315
+ const afterSlice = rtfText.slice(tbl.lastEnd, Math.min(rtfText.length, tbl.lastEnd + 2e3));
5316
+ const cleanAfter = cleanRtfText(afterSlice);
5317
+ tbl.anchorAfter = cleanAfter.slice(0, 80).trim();
5318
+ });
5319
+ return rawTables.map((tbl) => {
5320
+ const totalW = (tbl.columnWidths || []).reduce((a, b) => a + b, 0);
5321
+ const colPercents = totalW > 0 ? tbl.columnWidths.map((w) => Math.round(w / totalW * 100)) : [];
5322
+ return {
5323
+ id: tbl.id,
5324
+ rows: tbl.rows.map((r) => r.cells),
5325
+ colPercents,
5326
+ anchorBefore: tbl.anchorBefore,
5327
+ anchorAfter: tbl.anchorAfter
5328
+ };
5329
+ });
5330
+ }
5331
+ function renderRtfTableElement(table) {
5332
+ const tableEl = document.createElement("table");
5333
+ tableEl.className = "fp-rtf-table";
5334
+ tableEl.style.width = "100%";
5335
+ tableEl.style.borderCollapse = "collapse";
5336
+ tableEl.style.margin = "16px 0 20px";
5337
+ tableEl.style.fontSize = "10pt";
5338
+ tableEl.style.lineHeight = "1.5";
5339
+ tableEl.style.border = "1px solid #cbd5e1";
5340
+ tableEl.style.borderRadius = "4px";
5341
+ tableEl.style.boxSizing = "border-box";
5342
+ tableEl.style.backgroundColor = "#ffffff";
5343
+ const thead = document.createElement("thead");
5344
+ const tbody = document.createElement("tbody");
5345
+ table.rows.forEach((rowCells, rIdx) => {
5346
+ const tr = document.createElement("tr");
5347
+ const isHeader = rIdx === 0;
5348
+ if (isHeader) {
5349
+ tr.style.backgroundColor = "#f8fafc";
5350
+ tr.style.borderBottom = "2px solid #cbd5e1";
5351
+ } else {
5352
+ tr.style.borderBottom = "1px solid #e2e8f0";
5353
+ if (rIdx % 2 === 0) {
5354
+ tr.style.backgroundColor = "#fcfdfe";
5355
+ }
5356
+ }
5357
+ rowCells.forEach((cellText, cIdx) => {
5358
+ const cellEl = document.createElement(isHeader ? "th" : "td");
5359
+ cellEl.style.padding = "8px 12px";
5360
+ cellEl.style.border = "1px solid #cbd5e1";
5361
+ cellEl.style.verticalAlign = "top";
5362
+ cellEl.style.color = isHeader ? "#0f172a" : "#1e293b";
5363
+ cellEl.style.fontWeight = isHeader ? "600" : "normal";
5364
+ if (table.colPercents && table.colPercents[cIdx]) {
5365
+ cellEl.style.width = `${table.colPercents[cIdx]}%`;
5366
+ }
5367
+ if (/^\d+$/.test(cellText.trim()) || cellText.trim() === "#") {
5368
+ cellEl.style.textAlign = "center";
5369
+ } else {
5370
+ cellEl.style.textAlign = "left";
5371
+ }
5372
+ cellEl.textContent = cellText;
5373
+ tr.appendChild(cellEl);
5374
+ });
5375
+ if (isHeader) {
5376
+ thead.appendChild(tr);
5377
+ } else {
5378
+ tbody.appendChild(tr);
5379
+ }
5380
+ });
5381
+ tableEl.appendChild(thead);
5382
+ tableEl.appendChild(tbody);
5383
+ return tableEl;
5384
+ }
4484
5385
  var RtfPlugin = class {
4485
5386
  id = "rtf";
4486
5387
  name = "Rich Text Format (RTF)";
@@ -4495,29 +5396,35 @@ var RtfPlugin = class {
4495
5396
  getToolbarActions(instance) {
4496
5397
  const totalPages = instance.getPageCount?.() ?? 1;
4497
5398
  const actions = [];
4498
- if (totalPages > 1) {
4499
- actions.push({
4500
- id: "page-nav",
4501
- icon: "",
4502
- label: "Page Navigation",
4503
- type: "page-nav",
4504
- group: "navigation",
4505
- value: instance.getCurrentPage?.() ?? 1,
4506
- max: totalPages,
4507
- execute: (action, page) => {
4508
- const cur = instance.getCurrentPage?.() ?? 1;
4509
- const max = instance.getPageCount?.() ?? 1;
4510
- if (action === "prev") {
4511
- if (cur > 1) instance.goToPage?.(cur - 1);
4512
- } else if (action === "next") {
4513
- if (cur < max) instance.goToPage?.(cur + 1);
4514
- } else if (typeof page === "number") {
4515
- instance.goToPage?.(page);
4516
- }
5399
+ actions.push({
5400
+ id: "page-nav",
5401
+ icon: "",
5402
+ label: "Page Navigation",
5403
+ type: "page-nav",
5404
+ group: "navigation",
5405
+ value: instance.getCurrentPage?.() ?? 1,
5406
+ max: totalPages,
5407
+ execute: (action, page) => {
5408
+ const cur = instance.getCurrentPage?.() ?? 1;
5409
+ const max = instance.getPageCount?.() ?? 1;
5410
+ if (action === "prev") {
5411
+ if (cur > 1) instance.goToPage?.(cur - 1);
5412
+ } else if (action === "next") {
5413
+ if (cur < max) instance.goToPage?.(cur + 1);
5414
+ } else if (typeof page === "number") {
5415
+ instance.goToPage?.(page);
4517
5416
  }
4518
- });
4519
- }
5417
+ }
5418
+ });
4520
5419
  actions.push(
5420
+ {
5421
+ id: "thumbnails",
5422
+ icon: "thumbnails",
5423
+ label: "Thumbnails",
5424
+ type: "button",
5425
+ group: "view",
5426
+ execute: () => instance.toggleThumbnails?.()
5427
+ },
4521
5428
  {
4522
5429
  id: "zoom-out",
4523
5430
  icon: "zoom-out",
@@ -4588,67 +5495,143 @@ var RtfPlugin = class {
4588
5495
  async render(ctx) {
4589
5496
  const wrapper = document.createElement("div");
4590
5497
  wrapper.className = "fp-rtf-wrapper";
4591
- wrapper.style.padding = "32px";
4592
- wrapper.style.maxWidth = "850px";
5498
+ wrapper.style.padding = "0";
5499
+ wrapper.style.width = "816px";
4593
5500
  wrapper.style.margin = "0 auto";
4594
- wrapper.style.backgroundColor = "#fff";
4595
- wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
4596
- wrapper.style.borderRadius = "4px";
4597
- wrapper.style.minHeight = "100%";
5501
+ wrapper.style.boxSizing = "border-box";
5502
+ wrapper.style.display = "flex";
5503
+ wrapper.style.flexDirection = "column";
5504
+ wrapper.style.alignItems = "center";
5505
+ wrapper.style.backgroundColor = "transparent";
4598
5506
  wrapper.style.transformOrigin = "top center";
4599
- wrapper.style.transition = "transform 0.2s ease";
4600
- ctx.container.style.overflow = "auto";
4601
- ctx.container.style.padding = "24px";
5507
+ wrapper.style.transition = "transform 0.15s ease";
5508
+ ctx.container.style.overflowX = "hidden";
5509
+ ctx.container.style.overflowY = "auto";
5510
+ ctx.container.style.padding = "16px 8px";
4602
5511
  ctx.container.style.backgroundColor = "#f1f5f9";
4603
5512
  ctx.container.appendChild(wrapper);
4604
5513
  let scale = 1;
4605
5514
  let rotation = 0;
4606
5515
  let pageElements = [];
4607
- let fitMode = "width";
5516
+ let isUserZoomed = false;
5517
+ let fitMode = ctx?.options?.fitMode || "width";
4608
5518
  const calculateFitScale = (mode = fitMode) => {
4609
5519
  const activeEl = pageElements[currentPage - 1] || wrapper;
4610
- const elW = activeEl.offsetWidth || 850;
4611
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
4612
- const availW = Math.max(280, ctx.container.clientWidth - 48);
4613
- const availH = Math.max(280, ctx.container.clientHeight - 80);
5520
+ const elW = 816;
5521
+ const singlePageH = activeEl?.offsetHeight || 1056;
5522
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
5523
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
4614
5524
  const sW = availW / elW;
4615
5525
  const sH = availH / singlePageH;
4616
5526
  if (mode === "page") {
4617
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
5527
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
4618
5528
  }
4619
- return Math.max(0.65, Math.min(1.05, sW));
5529
+ return Math.max(0.4, Math.min(3, sW));
4620
5530
  };
4621
5531
  const applyTransform = () => {
4622
5532
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
4623
5533
  wrapper.style.transformOrigin = "top center";
5534
+ const activeEl = pageElements[currentPage - 1];
5535
+ const baseH = activeEl?.offsetHeight || 1056;
5536
+ const scaledH = baseH * scale;
5537
+ const extraH = Math.max(0, scaledH - baseH);
5538
+ wrapper.style.marginBottom = `${extraH + 32}px`;
4624
5539
  };
5540
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
5541
+ if (!isUserZoomed) {
5542
+ scale = calculateFitScale(fitMode);
5543
+ applyTransform();
5544
+ }
5545
+ }) : null;
5546
+ resizeObserver?.observe(ctx.container);
4625
5547
  setTimeout(() => {
4626
- fitMode = "width";
4627
- scale = calculateFitScale("width");
5548
+ scale = calculateFitScale(fitMode);
4628
5549
  applyTransform();
4629
5550
  }, 60);
4630
5551
  try {
4631
5552
  if (typeof RTFJS.loggingEnabled === "function") {
4632
5553
  RTFJS.loggingEnabled(false);
4633
5554
  }
5555
+ const rawText = new TextDecoder("latin1").decode(ctx.buffer);
5556
+ const tables = parseRtfTables(rawText);
4634
5557
  const doc = new RTFJS.Document(ctx.buffer, {});
4635
5558
  const htmlElements = await doc.render();
4636
5559
  const contentNodes = [];
4637
- for (const item of htmlElements) {
4638
- if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4639
- contentNodes.push(...Array.from(item.children));
4640
- } else {
4641
- contentNodes.push(item);
5560
+ const extractBlocks = (nodes) => {
5561
+ for (const item of nodes) {
5562
+ if (!item || !(item instanceof HTMLElement)) continue;
5563
+ const tag = item.tagName.toLowerCase();
5564
+ const hasChildBlocks = Array.from(item.children).some((c) => {
5565
+ const ct = c.tagName.toLowerCase();
5566
+ return ["p", "div", "table", "h1", "h2", "h3", "h4", "h5", "h6", "section", "article", "ul", "ol"].includes(ct);
5567
+ });
5568
+ if (hasChildBlocks && !["table", "tr", "td", "th", "ul", "ol", "li"].includes(tag)) {
5569
+ extractBlocks(Array.from(item.children));
5570
+ } else {
5571
+ const svgs = item.querySelectorAll("svg, img");
5572
+ svgs.forEach((s) => {
5573
+ const el = s;
5574
+ el.style.maxWidth = "100%";
5575
+ el.style.maxHeight = "360px";
5576
+ el.style.display = "block";
5577
+ el.style.margin = "12px auto";
5578
+ if (s.tagName.toLowerCase() === "svg") {
5579
+ s.removeAttribute("width");
5580
+ s.removeAttribute("height");
5581
+ el.style.width = "100%";
5582
+ el.style.maxHeight = "360px";
5583
+ }
5584
+ });
5585
+ if (tag === "p" || tag === "div") {
5586
+ item.style.display = "block";
5587
+ item.style.marginBottom = "12px";
5588
+ item.style.lineHeight = "1.6";
5589
+ }
5590
+ contentNodes.push(item);
5591
+ }
4642
5592
  }
4643
- }
5593
+ };
5594
+ extractBlocks(htmlElements);
5595
+ tables.forEach((table) => {
5596
+ const tableEl = renderRtfTableElement(table);
5597
+ let inserted = false;
5598
+ if (table.anchorBefore && table.anchorBefore.length > 10) {
5599
+ const keyword = table.anchorBefore.slice(-35).trim();
5600
+ for (let i = 0; i < contentNodes.length; i++) {
5601
+ if (contentNodes[i].textContent?.includes(keyword)) {
5602
+ contentNodes.splice(i + 1, 0, tableEl);
5603
+ inserted = true;
5604
+ break;
5605
+ }
5606
+ }
5607
+ }
5608
+ if (!inserted && table.anchorAfter && table.anchorAfter.length > 10) {
5609
+ const keyword = table.anchorAfter.slice(0, 35).trim();
5610
+ for (let i = 0; i < contentNodes.length; i++) {
5611
+ if (contentNodes[i].textContent?.includes(keyword)) {
5612
+ contentNodes.splice(i, 0, tableEl);
5613
+ inserted = true;
5614
+ break;
5615
+ }
5616
+ }
5617
+ }
5618
+ if (!inserted) {
5619
+ if (contentNodes.length > 2) {
5620
+ contentNodes.splice(2, 0, tableEl);
5621
+ } else {
5622
+ contentNodes.push(tableEl);
5623
+ }
5624
+ }
5625
+ });
4644
5626
  wrapper.innerHTML = "";
4645
5627
  contentNodes.forEach((node) => wrapper.appendChild(node));
4646
5628
  const childHeights = contentNodes.map((c) => {
4647
5629
  const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4648
5630
  const offH = c.offsetHeight || 0;
5631
+ const realH = Math.max(rectH, offH);
5632
+ if (realH > 0) return realH;
4649
5633
  const textLen = c.textContent?.trim().length || 0;
4650
- const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4651
- return Math.max(rectH, offH, estH);
5634
+ return Math.max(24, Math.ceil(textLen / 95) * 22 + 12);
4652
5635
  });
4653
5636
  wrapper.innerHTML = "";
4654
5637
  const createRtfCard = () => {
@@ -4657,35 +5640,49 @@ var RtfPlugin = class {
4657
5640
  card.style.backgroundColor = "#ffffff";
4658
5641
  card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4659
5642
  card.style.borderRadius = "4px";
4660
- card.style.padding = "72px 56px";
5643
+ card.style.padding = "64px 56px";
4661
5644
  card.style.width = "816px";
4662
5645
  card.style.minHeight = "1056px";
4663
5646
  card.style.boxSizing = "border-box";
4664
5647
  card.style.marginBottom = "24px";
4665
5648
  card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4666
5649
  card.style.lineHeight = "1.6";
5650
+ card.style.color = "#1e293b";
4667
5651
  return card;
4668
5652
  };
4669
5653
  let curCard = createRtfCard();
4670
5654
  wrapper.appendChild(curCard);
4671
5655
  pageElements = [curCard];
4672
5656
  let curH = 0;
4673
- const maxH = 912;
5657
+ const maxH = 928;
4674
5658
  for (let i = 0; i < contentNodes.length; i++) {
4675
5659
  const child = contentNodes[i];
4676
5660
  const chH = childHeights[i];
4677
- curCard.appendChild(child);
4678
- curH += chH;
4679
- if (curH >= maxH && i < contentNodes.length - 1) {
5661
+ const isChildEmpty = (child.textContent || "").trim().length === 0 && !child.querySelector("table, img, svg, canvas");
5662
+ if (curH === 0 && isChildEmpty) {
5663
+ continue;
5664
+ }
5665
+ if (curH + chH > maxH && curCard.childNodes.length > 0) {
4680
5666
  curCard = createRtfCard();
4681
5667
  wrapper.appendChild(curCard);
4682
5668
  pageElements.push(curCard);
4683
5669
  curH = 0;
4684
5670
  }
5671
+ curCard.appendChild(child);
5672
+ curH += chH;
5673
+ }
5674
+ pageElements = pageElements.filter((card) => {
5675
+ const hasText = (card.textContent || "").trim().length > 0;
5676
+ const hasMedia = card.querySelector("table, img, svg, canvas") !== null;
5677
+ return hasText || hasMedia;
5678
+ });
5679
+ if (pageElements.length === 0) {
5680
+ pageElements = [curCard];
4685
5681
  }
4686
5682
  } catch (err) {
4687
5683
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
4688
5684
  const text = new TextDecoder("latin1").decode(ctx.buffer);
5685
+ const tables = parseRtfTables(text);
4689
5686
  const rawPages = text.split(/\\page\b/).map((segment) => {
4690
5687
  return segment.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "").trim();
4691
5688
  }).filter((p) => p.length > 0);
@@ -4696,47 +5693,26 @@ var RtfPlugin = class {
4696
5693
  pageCard.style.backgroundColor = "#ffffff";
4697
5694
  pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4698
5695
  pageCard.style.borderRadius = "4px";
4699
- pageCard.style.padding = "72px 56px";
5696
+ pageCard.style.padding = "64px 56px";
4700
5697
  pageCard.style.width = "816px";
4701
5698
  pageCard.style.minHeight = "1056px";
4702
- pageCard.style.maxWidth = "100%";
4703
5699
  pageCard.style.boxSizing = "border-box";
4704
- pageCard.style.fontFamily = "serif";
5700
+ pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4705
5701
  pageCard.style.fontSize = "12pt";
4706
5702
  pageCard.style.lineHeight = "1.6";
4707
5703
  pageCard.style.color = "#1e293b";
4708
5704
  pageCard.style.whiteSpace = "pre-wrap";
4709
5705
  pageCard.style.display = i === 0 ? "block" : "none";
4710
5706
  pageCard.textContent = pages[i];
5707
+ if (i === 1 && tables.length > 0) {
5708
+ tables.forEach((tbl) => pageCard.appendChild(renderRtfTableElement(tbl)));
5709
+ }
4711
5710
  wrapper.appendChild(pageCard);
4712
5711
  pageElements.push(pageCard);
4713
5712
  }
4714
5713
  }
4715
5714
  const totalPages = Math.max(1, pageElements.length);
4716
5715
  let currentPage = 1;
4717
- let indicator = null;
4718
- if (totalPages > 1) {
4719
- indicator = document.createElement("div");
4720
- indicator.className = "fp-rtf-page-indicator";
4721
- indicator.style.position = "sticky";
4722
- indicator.style.bottom = "16px";
4723
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4724
- indicator.style.backdropFilter = "blur(8px)";
4725
- indicator.style.color = "#f8fafc";
4726
- indicator.style.fontSize = "12px";
4727
- indicator.style.fontWeight = "600";
4728
- indicator.style.padding = "5px 14px";
4729
- indicator.style.borderRadius = "20px";
4730
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4731
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4732
- indicator.style.zIndex = "10";
4733
- indicator.style.userSelect = "none";
4734
- indicator.style.pointerEvents = "none";
4735
- indicator.style.textAlign = "center";
4736
- indicator.style.width = "fit-content";
4737
- indicator.style.margin = "16px auto 0";
4738
- ctx.container.appendChild(indicator);
4739
- }
4740
5716
  const showPage = (pageNum) => {
4741
5717
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
4742
5718
  if (totalPages > 1) {
@@ -4744,9 +5720,6 @@ var RtfPlugin = class {
4744
5720
  el.style.display = idx + 1 === currentPage ? "block" : "none";
4745
5721
  });
4746
5722
  }
4747
- if (indicator) {
4748
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4749
- }
4750
5723
  ctx.container.scrollTop = 0;
4751
5724
  ctx.emit("page-change", { page: currentPage, total: totalPages });
4752
5725
  };
@@ -4754,7 +5727,7 @@ var RtfPlugin = class {
4754
5727
  showPage(1);
4755
5728
  }
4756
5729
  const cleanup = () => {
4757
- indicator?.remove();
5730
+ resizeObserver?.disconnect();
4758
5731
  wrapper.remove();
4759
5732
  ctx.container.innerHTML = "";
4760
5733
  };
@@ -4764,22 +5737,52 @@ var RtfPlugin = class {
4764
5737
  getPageCount: () => totalPages,
4765
5738
  getCurrentPage: () => currentPage,
4766
5739
  goToPage: (page) => showPage(page),
5740
+ getThumbnails: async () => {
5741
+ const items = [];
5742
+ for (let idx = 0; idx < totalPages; idx++) {
5743
+ const pageIdx = idx;
5744
+ items.push({
5745
+ index: pageIdx,
5746
+ label: `Page ${pageIdx + 1}`,
5747
+ render: async (canvas) => {
5748
+ const ctx2d = canvas.getContext("2d");
5749
+ if (!ctx2d) return;
5750
+ canvas.width = 140;
5751
+ canvas.height = 180;
5752
+ ctx2d.fillStyle = "#ffffff";
5753
+ ctx2d.fillRect(0, 0, 140, 180);
5754
+ ctx2d.strokeStyle = "#cbd5e1";
5755
+ ctx2d.lineWidth = 1;
5756
+ ctx2d.strokeRect(0.5, 0.5, 139, 179);
5757
+ ctx2d.fillStyle = "#334155";
5758
+ ctx2d.font = 'bold 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
5759
+ ctx2d.textAlign = "center";
5760
+ ctx2d.fillText(`Page ${pageIdx + 1}`, 70, 95);
5761
+ }
5762
+ });
5763
+ }
5764
+ return items;
5765
+ },
4767
5766
  zoomIn: () => {
5767
+ isUserZoomed = true;
4768
5768
  scale += 0.15;
4769
5769
  applyTransform();
4770
5770
  },
4771
5771
  zoomOut: () => {
5772
+ isUserZoomed = true;
4772
5773
  scale = Math.max(0.2, scale - 0.15);
4773
5774
  applyTransform();
4774
5775
  },
4775
5776
  getZoom: () => scale,
4776
5777
  setZoom: (level) => {
5778
+ isUserZoomed = true;
4777
5779
  scale = level;
4778
5780
  applyTransform();
4779
5781
  },
4780
5782
  fitToPage: () => {
4781
- fitMode = fitMode === "width" ? "page" : "width";
4782
- scale = calculateFitScale(fitMode);
5783
+ isUserZoomed = false;
5784
+ fitMode = "width";
5785
+ scale = calculateFitScale("width");
4783
5786
  rotation = 0;
4784
5787
  applyTransform();
4785
5788
  },
@@ -4967,40 +5970,36 @@ var OpenDocumentPlugin = class {
4967
5970
  const isPresentation = instance.isPresentation;
4968
5971
  const totalPages = instance.getPageCount?.() ?? 1;
4969
5972
  const actions = [];
4970
- if (isPresentation || totalPages > 1) {
4971
- if (isPresentation) {
4972
- actions.push({
4973
- id: "thumbnails",
4974
- icon: "thumbnails",
4975
- label: "Slide Thumbnails",
4976
- type: "button",
4977
- group: "navigation",
4978
- execute: () => instance.toggleThumbnails?.()
4979
- });
4980
- }
4981
- actions.push({
4982
- id: "page-nav",
4983
- icon: "",
4984
- label: isPresentation ? "Slide Navigation" : "Page Navigation",
4985
- type: "page-nav",
4986
- group: "navigation",
4987
- value: instance.getCurrentPage?.() ?? 1,
4988
- max: totalPages,
4989
- execute: (action, page) => {
4990
- const cur = instance.getCurrentPage?.() ?? 1;
4991
- const max = instance.getPageCount?.() ?? 1;
4992
- if (action === "prev") {
4993
- if (cur > 1) instance.goToPage?.(cur - 1);
4994
- } else if (action === "next") {
4995
- if (cur < max) instance.goToPage?.(cur + 1);
4996
- } else if (typeof page === "number") {
4997
- instance.goToPage?.(page);
4998
- } else if (typeof action === "number") {
4999
- instance.goToPage?.(action);
5000
- }
5973
+ actions.push({
5974
+ id: "thumbnails",
5975
+ icon: "thumbnails",
5976
+ label: isPresentation ? "Slide Thumbnails" : "Page Thumbnails",
5977
+ type: "button",
5978
+ group: "navigation",
5979
+ execute: () => instance.toggleThumbnails?.()
5980
+ });
5981
+ actions.push({
5982
+ id: "page-nav",
5983
+ icon: "",
5984
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
5985
+ type: "page-nav",
5986
+ group: "navigation",
5987
+ value: instance.getCurrentPage?.() ?? 1,
5988
+ max: totalPages,
5989
+ execute: (action, page) => {
5990
+ const cur = instance.getCurrentPage?.() ?? 1;
5991
+ const max = instance.getPageCount?.() ?? 1;
5992
+ if (action === "prev") {
5993
+ if (cur > 1) instance.goToPage?.(cur - 1);
5994
+ } else if (action === "next") {
5995
+ if (cur < max) instance.goToPage?.(cur + 1);
5996
+ } else if (typeof page === "number") {
5997
+ instance.goToPage?.(page);
5998
+ } else if (typeof action === "number") {
5999
+ instance.goToPage?.(action);
5001
6000
  }
5002
- });
5003
- }
6001
+ }
6002
+ });
5004
6003
  actions.push(
5005
6004
  {
5006
6005
  id: "zoom-out",
@@ -5091,49 +6090,65 @@ var OpenDocumentPlugin = class {
5091
6090
  container.className = "fp-odf-container";
5092
6091
  container.style.width = "100%";
5093
6092
  container.style.height = "100%";
5094
- container.style.overflow = "auto";
5095
- container.style.padding = "24px";
6093
+ container.style.overflowX = "hidden";
6094
+ container.style.overflowY = "auto";
6095
+ container.style.padding = "16px 8px";
5096
6096
  container.style.backgroundColor = "#f1f5f9";
5097
6097
  const wrapper = document.createElement("div");
5098
6098
  wrapper.className = "fp-odf-wrapper";
5099
6099
  wrapper.style.margin = "0 auto";
6100
+ wrapper.style.width = isPresentation ? "960px" : "816px";
6101
+ wrapper.style.boxSizing = "border-box";
5100
6102
  wrapper.style.backgroundColor = "#ffffff";
5101
6103
  wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
5102
6104
  wrapper.style.borderRadius = "4px";
5103
6105
  wrapper.style.transformOrigin = "top center";
5104
- wrapper.style.transition = "transform 0.2s ease";
6106
+ wrapper.style.transition = "transform 0.15s ease";
5105
6107
  container.appendChild(wrapper);
5106
6108
  ctx.container.appendChild(container);
5107
6109
  let scale = 1;
5108
6110
  let rotation = 0;
5109
- let fitMode = "width";
6111
+ let isUserZoomed = false;
6112
+ let fitMode = ctx?.options?.fitMode || "width";
6113
+ let currentPage = 1;
6114
+ let totalPages = 1;
6115
+ let slides = [];
5110
6116
  const calculateFitScale = (mode = fitMode) => {
5111
- const elW = wrapper.offsetWidth || 850;
5112
- const singlePageH = Math.min(wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
5113
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5114
- const availH = Math.max(280, ctx.container.clientHeight - 80);
6117
+ const activeSlide = slides[currentPage - 1];
6118
+ const elW = isPresentation ? 960 : 816;
6119
+ const singlePageH = Math.min(activeSlide?.offsetHeight || wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
6120
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
6121
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5115
6122
  const sW = availW / elW;
5116
6123
  const sH = availH / (isPresentation ? 540 : singlePageH);
5117
6124
  if (isPresentation) {
5118
- return Math.min(1, Math.min(sW, sH));
6125
+ return Math.min(2.5, Math.min(sW, sH));
5119
6126
  }
5120
6127
  if (mode === "page") {
5121
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
6128
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5122
6129
  }
5123
- return Math.max(0.65, Math.min(1.05, sW));
6130
+ return Math.max(0.4, Math.min(3, sW));
5124
6131
  };
5125
6132
  const applyTransform = () => {
5126
6133
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
5127
6134
  wrapper.style.transformOrigin = "top center";
6135
+ const activeSlide = slides[currentPage - 1];
6136
+ const baseH = activeSlide?.offsetHeight || wrapper.offsetHeight || 1056;
6137
+ const scaledH = baseH * scale;
6138
+ const extraH = Math.max(0, scaledH - baseH);
6139
+ wrapper.style.marginBottom = `${extraH + 32}px`;
5128
6140
  };
6141
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
6142
+ if (!isUserZoomed) {
6143
+ scale = calculateFitScale(fitMode);
6144
+ applyTransform();
6145
+ }
6146
+ }) : null;
6147
+ resizeObserver?.observe(ctx.container);
5129
6148
  setTimeout(() => {
5130
- fitMode = "width";
5131
- scale = calculateFitScale("width");
6149
+ scale = calculateFitScale(fitMode);
5132
6150
  applyTransform();
5133
6151
  }, 60);
5134
- let currentPage = 1;
5135
- let totalPages = 1;
5136
- let slides = [];
5137
6152
  if (isPresentation) {
5138
6153
  wrapper.style.maxWidth = "960px";
5139
6154
  wrapper.style.aspectRatio = "16 / 9";
@@ -5234,24 +6249,9 @@ var OpenDocumentPlugin = class {
5234
6249
  wrapper.appendChild(page);
5235
6250
  slides.push(page);
5236
6251
  });
5237
- const pageIndicator = document.createElement("div");
5238
- pageIndicator.className = "fp-odt-page-indicator";
5239
- pageIndicator.style.position = "sticky";
5240
- pageIndicator.style.bottom = "16px";
5241
- pageIndicator.style.left = "50%";
5242
- pageIndicator.style.transform = "translateX(-50%)";
5243
- pageIndicator.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
5244
- pageIndicator.style.color = "#fff";
5245
- pageIndicator.style.padding = "6px 12px";
5246
- pageIndicator.style.borderRadius = "16px";
5247
- pageIndicator.style.fontSize = "12px";
5248
- pageIndicator.style.zIndex = "100";
5249
- pageIndicator.style.display = "inline-block";
5250
- pageIndicator.style.width = "fit-content";
5251
- pageIndicator.textContent = `Page 1 of ${totalPages}`;
5252
- container.appendChild(pageIndicator);
5253
6252
  }
5254
6253
  const cleanup = () => {
6254
+ resizeObserver?.disconnect();
5255
6255
  imageUrls.forEach((url) => URL.revokeObjectURL(url));
5256
6256
  container.remove();
5257
6257
  ctx.container.innerHTML = "";
@@ -5263,10 +6263,6 @@ var OpenDocumentPlugin = class {
5263
6263
  slides.forEach((s, idx) => {
5264
6264
  s.style.display = idx === page - 1 ? "block" : "none";
5265
6265
  });
5266
- const indicator = container.querySelector(".fp-odt-page-indicator");
5267
- if (indicator) {
5268
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5269
- }
5270
6266
  container.scrollTop = 0;
5271
6267
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5272
6268
  };
@@ -5274,21 +6270,25 @@ var OpenDocumentPlugin = class {
5274
6270
  destroy: cleanup,
5275
6271
  isPresentation,
5276
6272
  zoomIn: () => {
6273
+ isUserZoomed = true;
5277
6274
  scale += 0.15;
5278
6275
  applyTransform();
5279
6276
  },
5280
6277
  zoomOut: () => {
6278
+ isUserZoomed = true;
5281
6279
  scale = Math.max(0.2, scale - 0.15);
5282
6280
  applyTransform();
5283
6281
  },
5284
6282
  getZoom: () => scale,
5285
6283
  setZoom: (level) => {
6284
+ isUserZoomed = true;
5286
6285
  scale = level;
5287
6286
  applyTransform();
5288
6287
  },
5289
6288
  fitToPage: () => {
5290
- fitMode = fitMode === "width" ? "page" : "width";
5291
- scale = calculateFitScale(fitMode);
6289
+ isUserZoomed = false;
6290
+ fitMode = "width";
6291
+ scale = calculateFitScale("width");
5292
6292
  rotation = 0;
5293
6293
  applyTransform();
5294
6294
  },
@@ -5304,23 +6304,45 @@ var OpenDocumentPlugin = class {
5304
6304
  getPageCount: () => totalPages,
5305
6305
  getCurrentPage: () => currentPage,
5306
6306
  getThumbnails: async () => {
5307
- if (!isPresentation) return [];
5308
- return slides.map((_, idx) => ({
5309
- index: idx,
5310
- label: `Slide ${idx + 1}`,
5311
- render: async (canvas) => {
5312
- const ctx2d = canvas.getContext("2d");
5313
- if (!ctx2d) return;
5314
- canvas.width = 160;
5315
- canvas.height = 90;
5316
- ctx2d.fillStyle = "#f8fafc";
5317
- ctx2d.fillRect(0, 0, 160, 90);
5318
- ctx2d.fillStyle = "#334155";
5319
- ctx2d.font = "bold 12px sans-serif";
5320
- ctx2d.textAlign = "center";
5321
- ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
5322
- }
5323
- }));
6307
+ const count = totalPages || slides.length || 1;
6308
+ const items = [];
6309
+ for (let idx = 0; idx < count; idx++) {
6310
+ const itemIdx = idx;
6311
+ items.push({
6312
+ index: itemIdx,
6313
+ label: isPresentation ? `Slide ${itemIdx + 1}` : `Page ${itemIdx + 1}`,
6314
+ render: async (canvas) => {
6315
+ const ctx2d = canvas.getContext("2d");
6316
+ if (!ctx2d) return;
6317
+ if (isPresentation) {
6318
+ canvas.width = 160;
6319
+ canvas.height = 90;
6320
+ ctx2d.fillStyle = "#f8fafc";
6321
+ ctx2d.fillRect(0, 0, 160, 90);
6322
+ ctx2d.strokeStyle = "#cbd5e1";
6323
+ ctx2d.lineWidth = 1;
6324
+ ctx2d.strokeRect(0.5, 0.5, 159, 89);
6325
+ ctx2d.fillStyle = "#334155";
6326
+ ctx2d.font = "bold 11px sans-serif";
6327
+ ctx2d.textAlign = "center";
6328
+ ctx2d.fillText(`Slide ${itemIdx + 1}`, 80, 50);
6329
+ } else {
6330
+ canvas.width = 140;
6331
+ canvas.height = 180;
6332
+ ctx2d.fillStyle = "#ffffff";
6333
+ ctx2d.fillRect(0, 0, 140, 180);
6334
+ ctx2d.strokeStyle = "#cbd5e1";
6335
+ ctx2d.lineWidth = 1;
6336
+ ctx2d.strokeRect(0.5, 0.5, 139, 179);
6337
+ ctx2d.fillStyle = "#64748b";
6338
+ ctx2d.font = "bold 10px sans-serif";
6339
+ ctx2d.textAlign = "center";
6340
+ ctx2d.fillText(`Page ${itemIdx + 1}`, 70, 90);
6341
+ }
6342
+ }
6343
+ });
6344
+ }
6345
+ return items;
5324
6346
  },
5325
6347
  download: () => {
5326
6348
  const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
@@ -5425,23 +6447,24 @@ var OpenDocumentPlugin = class {
5425
6447
  case "h": {
5426
6448
  const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
5427
6449
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5428
- html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
6450
+ html += `<h${level} style="${style}; margin: 18px 0 8px; color: #1e3a8a; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${node.textContent || ""}</h${level}>`;
5429
6451
  break;
5430
6452
  }
5431
6453
  case "p": {
5432
6454
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5433
6455
  const inner = this.renderSpans(node, styles, images);
5434
- html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
6456
+ const isInsideLi = node.parentElement?.localName === "list-item" || node.parentElement?.tagName?.toLowerCase() === "list-item";
6457
+ html += `<p style="${style}; line-height: 1.5; margin: ${isInsideLi ? "0" : "8px 0 12px"}; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${inner}</p>`;
5435
6458
  break;
5436
6459
  }
5437
6460
  case "list": {
5438
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
6461
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
5439
6462
  Array.from(node.children).forEach((c) => walk(c));
5440
6463
  html += "</ul>";
5441
6464
  break;
5442
6465
  }
5443
6466
  case "list-item": {
5444
- html += "<li>";
6467
+ html += '<li style="margin: 4px 0; font-size: 11pt; line-height: 1.5; color: #1e293b;">';
5445
6468
  Array.from(node.children).forEach((c) => walk(c));
5446
6469
  html += "</li>";
5447
6470
  break;
@@ -5567,28 +6590,26 @@ var DocPlugin = class {
5567
6590
  getToolbarActions(instance) {
5568
6591
  const totalPages = instance.getPageCount?.() ?? 1;
5569
6592
  const actions = [];
5570
- if (totalPages > 1) {
5571
- actions.push({
5572
- id: "page-nav",
5573
- icon: "",
5574
- label: "Page Navigation",
5575
- type: "page-nav",
5576
- group: "navigation",
5577
- value: instance.getCurrentPage?.() ?? 1,
5578
- max: totalPages,
5579
- execute: (action, page) => {
5580
- const cur = instance.getCurrentPage?.() ?? 1;
5581
- const max = instance.getPageCount?.() ?? 1;
5582
- if (action === "prev") {
5583
- if (cur > 1) instance.goToPage?.(cur - 1);
5584
- } else if (action === "next") {
5585
- if (cur < max) instance.goToPage?.(cur + 1);
5586
- } else if (typeof page === "number") {
5587
- instance.goToPage?.(page);
5588
- }
6593
+ actions.push({
6594
+ id: "page-nav",
6595
+ icon: "",
6596
+ label: "Page Navigation",
6597
+ type: "page-nav",
6598
+ group: "navigation",
6599
+ value: instance.getCurrentPage?.() ?? 1,
6600
+ max: totalPages,
6601
+ execute: (action, page) => {
6602
+ const cur = instance.getCurrentPage?.() ?? 1;
6603
+ const max = instance.getPageCount?.() ?? 1;
6604
+ if (action === "prev") {
6605
+ if (cur > 1) instance.goToPage?.(cur - 1);
6606
+ } else if (action === "next") {
6607
+ if (cur < max) instance.goToPage?.(cur + 1);
6608
+ } else if (typeof page === "number") {
6609
+ instance.goToPage?.(page);
5589
6610
  }
5590
- });
5591
- }
6611
+ }
6612
+ });
5592
6613
  actions.push(
5593
6614
  {
5594
6615
  id: "zoom-out",
@@ -5662,8 +6683,9 @@ var DocPlugin = class {
5662
6683
  container.className = "fp-doc-container";
5663
6684
  container.style.width = "100%";
5664
6685
  container.style.height = "100%";
5665
- container.style.overflow = "auto";
5666
- container.style.padding = "32px 16px";
6686
+ container.style.overflowX = "hidden";
6687
+ container.style.overflowY = "auto";
6688
+ container.style.padding = "16px 8px";
5667
6689
  container.style.backgroundColor = "#f1f5f9";
5668
6690
  container.style.display = "flex";
5669
6691
  container.style.justifyContent = "center";
@@ -5708,16 +6730,15 @@ var DocPlugin = class {
5708
6730
  const pageCard = document.createElement("div");
5709
6731
  pageCard.className = "fp-doc-page-card";
5710
6732
  pageCard.style.width = "816px";
5711
- pageCard.style.height = "1056px";
6733
+ pageCard.style.minHeight = "1056px";
5712
6734
  pageCard.style.backgroundColor = "#ffffff";
5713
- pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
6735
+ pageCard.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
5714
6736
  pageCard.style.borderRadius = "4px";
5715
- pageCard.style.padding = "96px 72px";
6737
+ pageCard.style.padding = "72px 56px";
5716
6738
  pageCard.style.boxSizing = "border-box";
5717
- pageCard.style.overflow = "hidden";
5718
6739
  pageCard.style.display = i === 0 ? "block" : "none";
5719
6740
  pageCard.style.transformOrigin = "top center";
5720
- pageCard.style.transition = "transform 0.2s ease";
6741
+ pageCard.style.transition = "transform 0.15s ease";
5721
6742
  pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
5722
6743
  pageCard.style.color = "#1e293b";
5723
6744
  if (isFallback && i === 0) {
@@ -5734,34 +6755,15 @@ var DocPlugin = class {
5734
6755
  container.appendChild(pageCard);
5735
6756
  pageCards.push(pageCard);
5736
6757
  }
5737
- let indicator = null;
5738
- if (totalPages > 1) {
5739
- indicator = document.createElement("div");
5740
- indicator.className = "fp-doc-page-indicator";
5741
- indicator.style.position = "fixed";
5742
- indicator.style.bottom = "16px";
5743
- indicator.style.left = "50%";
5744
- indicator.style.transform = "translateX(-50%)";
5745
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
5746
- indicator.style.backdropFilter = "blur(8px)";
5747
- indicator.style.color = "#f8fafc";
5748
- indicator.style.fontSize = "12px";
5749
- indicator.style.fontWeight = "600";
5750
- indicator.style.padding = "5px 14px";
5751
- indicator.style.borderRadius = "20px";
5752
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
5753
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
5754
- indicator.style.zIndex = "10";
5755
- indicator.style.userSelect = "none";
5756
- indicator.style.pointerEvents = "none";
5757
- indicator.style.textAlign = "center";
5758
- container.appendChild(indicator);
5759
- }
5760
6758
  let rotation = 0;
5761
6759
  const applyTransform = () => {
5762
6760
  const activeCard = pageCards[currentPage - 1];
5763
6761
  if (activeCard) {
5764
6762
  activeCard.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
6763
+ const baseH = activeCard.offsetHeight || 1056;
6764
+ const scaledH = baseH * scale;
6765
+ const extraH = Math.max(0, scaledH - baseH);
6766
+ activeCard.style.marginBottom = `${extraH + 32}px`;
5765
6767
  }
5766
6768
  };
5767
6769
  const showPage = (pageNum) => {
@@ -5774,35 +6776,39 @@ var DocPlugin = class {
5774
6776
  card.style.display = "none";
5775
6777
  }
5776
6778
  });
5777
- if (indicator) {
5778
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5779
- }
5780
6779
  container.scrollTop = 0;
5781
6780
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5782
6781
  };
5783
6782
  if (totalPages > 1) {
5784
6783
  showPage(1);
5785
6784
  }
5786
- let fitMode = "width";
6785
+ let fitMode = ctx?.options?.fitMode || "width";
6786
+ let isUserZoomed = false;
5787
6787
  const calculateFitScale = (mode = fitMode) => {
5788
6788
  const elW = 816;
5789
6789
  const elH = 1056;
5790
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5791
- const availH = Math.max(280, ctx.container.clientHeight - 80);
6790
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
6791
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5792
6792
  const sW = availW / elW;
5793
6793
  const sH = availH / elH;
5794
6794
  if (mode === "page") {
5795
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
6795
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5796
6796
  }
5797
- return Math.max(0.65, Math.min(1.05, sW));
6797
+ return Math.max(0.4, Math.min(3, sW));
5798
6798
  };
6799
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
6800
+ if (!isUserZoomed) {
6801
+ scale = calculateFitScale(fitMode);
6802
+ applyTransform();
6803
+ }
6804
+ }) : null;
6805
+ resizeObserver?.observe(ctx.container);
5799
6806
  setTimeout(() => {
5800
- fitMode = "width";
5801
- scale = calculateFitScale("width");
6807
+ scale = calculateFitScale(fitMode);
5802
6808
  applyTransform();
5803
6809
  }, 60);
5804
6810
  const cleanup = () => {
5805
- indicator?.remove();
6811
+ resizeObserver?.disconnect();
5806
6812
  container.remove();
5807
6813
  ctx.container.innerHTML = "";
5808
6814
  };
@@ -5813,21 +6819,25 @@ var DocPlugin = class {
5813
6819
  getCurrentPage: () => currentPage,
5814
6820
  goToPage: (page) => showPage(page),
5815
6821
  zoomIn: () => {
6822
+ isUserZoomed = true;
5816
6823
  scale += 0.15;
5817
6824
  applyTransform();
5818
6825
  },
5819
6826
  zoomOut: () => {
6827
+ isUserZoomed = true;
5820
6828
  scale = Math.max(0.2, scale - 0.15);
5821
6829
  applyTransform();
5822
6830
  },
5823
6831
  getZoom: () => scale,
5824
6832
  setZoom: (level) => {
6833
+ isUserZoomed = true;
5825
6834
  scale = level;
5826
6835
  applyTransform();
5827
6836
  },
5828
6837
  fitToPage: () => {
5829
- fitMode = fitMode === "width" ? "page" : "width";
5830
- scale = calculateFitScale(fitMode);
6838
+ isUserZoomed = false;
6839
+ fitMode = "width";
6840
+ scale = calculateFitScale("width");
5831
6841
  rotation = 0;
5832
6842
  applyTransform();
5833
6843
  },
@@ -6204,25 +7214,31 @@ ${chartSvg}
6204
7214
  i++;
6205
7215
  continue;
6206
7216
  }
6207
- if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
7217
+ if (/^\d{1,2}$/.test(line.trim())) {
7218
+ i++;
7219
+ continue;
7220
+ }
7221
+ const isShortLine = line.length < 60 && !line.endsWith(".") && !line.endsWith(",");
7222
+ const isTitleLike = isShortLine && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#") || /^(?:Chapter|Section|Part|\d+\.)\b/i.test(line) || /^[A-Z][a-zA-Z0-9\s#:-]{2,45}$/.test(line));
7223
+ if (isTitleLike) {
6208
7224
  if (inList) {
6209
7225
  html += "</ul>";
6210
7226
  inList = false;
6211
7227
  }
6212
- html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line, sanitizeOptions)}</h2>`;
7228
+ html += `<h2 style="font-size: 18px; font-weight: 700; color: #1e3a8a; margin: 18px 0 10px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line, sanitizeOptions)}</h2>`;
6213
7229
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
6214
7230
  if (!inList) {
6215
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
7231
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
6216
7232
  inList = true;
6217
7233
  }
6218
7234
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
6219
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
7235
+ html += `<li style="margin: 4px 0; line-height: 1.5; font-size: 11pt; color: #1e293b;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
6220
7236
  } else {
6221
7237
  if (inList) {
6222
7238
  html += "</ul>";
6223
7239
  inList = false;
6224
7240
  }
6225
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
7241
+ html += `<p style="line-height: 1.5; margin: 0 0 12px 0; font-size: 11pt; color: #1e293b; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
6226
7242
  }
6227
7243
  i++;
6228
7244
  }
@@ -6784,6 +7800,8 @@ var FilePreview = memo(forwardRef(
6784
7800
  onError,
6785
7801
  onPageChange,
6786
7802
  onZoomChange,
7803
+ onRotate,
7804
+ onDestroy,
6787
7805
  className,
6788
7806
  style
6789
7807
  }, ref) => {
@@ -6806,6 +7824,8 @@ var FilePreview = memo(forwardRef(
6806
7824
  if (onError) unsubs.push(viewer.on("error", (e) => onError(e instanceof Error ? e : new Error(String(e)))));
6807
7825
  if (onPageChange) unsubs.push(viewer.on("page-change", onPageChange));
6808
7826
  if (onZoomChange) unsubs.push(viewer.on("zoom-change", onZoomChange));
7827
+ if (onRotate) unsubs.push(viewer.on("rotate", onRotate));
7828
+ if (onDestroy) unsubs.push(viewer.on("destroy", onDestroy));
6809
7829
  return () => {
6810
7830
  unsubs.forEach((fn) => fn());
6811
7831
  viewer.destroy();