@files-preview-app/preview-file 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import DOMPurify6 from 'dompurify';
2
2
  import * as pdfjsLib from 'pdfjs-dist';
3
3
  import * as docx from 'docx-preview';
4
+ import * as fflate from 'fflate';
4
5
  import { unzipSync, strFromU8, unzip } from 'fflate';
5
6
  import * as XLSX from 'xlsx';
6
7
  import hljs from 'highlight.js';
@@ -577,8 +578,7 @@ var ICON_FULLSCREEN = `<svg width="24" height="24" viewBox="0 0 24 24" fill="non
577
578
  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>`;
578
579
  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>`;
579
580
  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>`;
580
- 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>`;
581
- 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>`;
581
+ 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>`;
582
582
  var ICON_MAP = {
583
583
  "zoom-in": ICON_ZOOM_IN,
584
584
  "zoom-out": ICON_ZOOM_OUT,
@@ -605,28 +605,177 @@ var ICON_MAP = {
605
605
  "forward-10": ICON_FAST_FORWARD,
606
606
  "rewind": ICON_REWIND,
607
607
  "replay-10": ICON_REWIND,
608
- "speed": ICON_SPEED,
609
608
  "open-window": ICON_EXTERNAL_WINDOW,
610
- "external-window": ICON_EXTERNAL_WINDOW
609
+ "external-window": ICON_EXTERNAL_WINDOW,
610
+ "openWindow": ICON_EXTERNAL_WINDOW,
611
+ "openSeparateWindow": ICON_EXTERNAL_WINDOW
611
612
  };
612
613
  var ToolbarController = class {
613
614
  el;
614
615
  toolbarEl;
615
616
  actions = [];
617
+ config = {};
616
618
  pageInputEl = null;
617
619
  pageLabelEl = null;
618
- constructor(container) {
620
+ prevPageBtn = null;
621
+ nextPageBtn = null;
622
+ constructor(container, config) {
619
623
  this.el = container;
624
+ if (config) this.config = { ...config };
620
625
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
621
626
  this.el.appendChild(this.toolbarEl);
622
627
  }
628
+ setConfig(config) {
629
+ this.config = { ...config };
630
+ this.render();
631
+ }
632
+ getConfig() {
633
+ return { ...this.config };
634
+ }
635
+ hideAction(actionId) {
636
+ this.config[actionId] = false;
637
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = false;
638
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = false;
639
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
640
+ this.config.fitPage = false;
641
+ this.config.fitToPage = false;
642
+ this.config.fitWidth = false;
643
+ }
644
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = false;
645
+ if (actionId === "fullscreen") this.config.fullscreen = false;
646
+ if (actionId === "download") this.config.download = false;
647
+ if (actionId === "print") this.config.print = false;
648
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
649
+ this.config.openWindow = false;
650
+ this.config.openSeparateWindow = false;
651
+ }
652
+ if (actionId === "copy") this.config.copy = false;
653
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
654
+ this.config.pageNav = false;
655
+ this.config.pagination = false;
656
+ }
657
+ this.render();
658
+ }
659
+ showAction(actionId) {
660
+ this.config[actionId] = true;
661
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = true;
662
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = true;
663
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
664
+ this.config.fitPage = true;
665
+ this.config.fitToPage = true;
666
+ this.config.fitWidth = true;
667
+ }
668
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = true;
669
+ if (actionId === "fullscreen") this.config.fullscreen = true;
670
+ if (actionId === "download") this.config.download = true;
671
+ if (actionId === "print") this.config.print = true;
672
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
673
+ this.config.openWindow = true;
674
+ this.config.openSeparateWindow = true;
675
+ }
676
+ if (actionId === "copy") this.config.copy = true;
677
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
678
+ this.config.pageNav = true;
679
+ this.config.pagination = true;
680
+ }
681
+ this.render();
682
+ }
683
+ normalizeActionId(actionId) {
684
+ if (actionId === "zoomIn") return "zoom-in";
685
+ if (actionId === "zoomOut") return "zoom-out";
686
+ if (actionId === "fitPage" || actionId === "fitToPage" || actionId === "fitWidth") return "fit-page";
687
+ if (actionId === "rotateCW") return "rotate-cw";
688
+ if (actionId === "rotateCCW") return "rotate-ccw";
689
+ if (actionId === "openWindow" || actionId === "openSeparateWindow") return "open-window";
690
+ if (actionId === "pageNav") return "page-nav";
691
+ return actionId;
692
+ }
693
+ enableAction(actionId) {
694
+ const norm = this.normalizeActionId(actionId);
695
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
696
+ if (btn) {
697
+ btn.disabled = false;
698
+ btn.classList.remove("disabled");
699
+ }
700
+ }
701
+ disableAction(actionId) {
702
+ const norm = this.normalizeActionId(actionId);
703
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
704
+ if (btn) {
705
+ btn.disabled = true;
706
+ btn.classList.add("disabled");
707
+ }
708
+ }
709
+ setActionActive(actionId, active) {
710
+ const norm = this.normalizeActionId(actionId);
711
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
712
+ if (btn) {
713
+ if (active) {
714
+ btn.classList.add("active");
715
+ } else {
716
+ btn.classList.remove("active");
717
+ }
718
+ }
719
+ }
720
+ isActionEnabled(action) {
721
+ const c = this.config;
722
+ const id = action.id;
723
+ if (c[id] === false) return false;
724
+ if (id === "zoom-in" && (c.zoomIn === false || c["zoom-in"] === false)) return false;
725
+ if (id === "zoom-out" && (c.zoomOut === false || c["zoom-out"] === false)) return false;
726
+ 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)) {
727
+ return false;
728
+ }
729
+ if ((id === "rotate-cw" || id === "rotate-ccw" || id === "rotate") && (c.rotate === false || c.rotateCW === false || c.rotateCCW === false || c["rotate-cw"] === false)) {
730
+ return false;
731
+ }
732
+ if (id === "fullscreen" && c.fullscreen === false) return false;
733
+ if (id === "download" && c.download === false) return false;
734
+ if (id === "print" && c.print === false) return false;
735
+ if ((id === "open-window" || id === "external-window") && (c.openWindow === false || c.openSeparateWindow === false || c["open-window"] === false)) {
736
+ return false;
737
+ }
738
+ if (id === "copy" && c.copy === false) return false;
739
+ if (id === "thumbnails" && c.thumbnails === false) return false;
740
+ if (id === "page-nav" && (c.pageNav === false || c.pagination === false || c["page-nav"] === false)) {
741
+ return false;
742
+ }
743
+ if ((id === "prev" || id === "page-prev") && (c.prevPage === false || c.prev === false || c["prev"] === false || c.pageNav === false || c.pagination === false)) {
744
+ return false;
745
+ }
746
+ if ((id === "next" || id === "page-next") && (c.nextPage === false || c.next === false || c["next"] === false || c.pageNav === false || c.pagination === false)) {
747
+ return false;
748
+ }
749
+ if (id === "play" && c.play === false) return false;
750
+ if (id === "pause" && c.pause === false) return false;
751
+ if ((id === "fast-forward" || id === "forward-10") && (c.fastForward === false || c["fast-forward"] === false)) {
752
+ return false;
753
+ }
754
+ if ((id === "rewind" || id === "replay-10") && (c.rewind === false || c["rewind"] === false)) {
755
+ return false;
756
+ }
757
+ if (id === "speed" && c.speed === false) return false;
758
+ return true;
759
+ }
623
760
  setPage(page, max) {
624
761
  if (this.pageInputEl) {
625
762
  this.pageInputEl.value = page.toString();
763
+ if (max !== void 0) {
764
+ this.pageInputEl.max = max.toString();
765
+ }
626
766
  }
627
767
  if (max !== void 0 && this.pageLabelEl) {
628
768
  this.pageLabelEl.textContent = ` / ${max}`;
629
769
  }
770
+ const currentMax = max !== void 0 ? max : parseInt(this.pageInputEl?.max || "1", 10) || 1;
771
+ if (this.prevPageBtn) {
772
+ this.prevPageBtn.disabled = page <= 1;
773
+ this.prevPageBtn.style.opacity = page <= 1 ? "0.4" : "1";
774
+ }
775
+ if (this.nextPageBtn) {
776
+ this.nextPageBtn.disabled = page >= currentMax;
777
+ this.nextPageBtn.style.opacity = page >= currentMax ? "0.4" : "1";
778
+ }
630
779
  }
631
780
  update(actions) {
632
781
  this.actions = actions;
@@ -649,8 +798,9 @@ var ToolbarController = class {
649
798
  view: [],
650
799
  actions: []
651
800
  };
652
- const hasPageNav = this.actions.some((a) => a.type === "page-nav");
653
- const effectiveActions = hasPageNav ? this.actions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : this.actions;
801
+ const enabledActions = this.actions.filter((a) => this.isActionEnabled(a));
802
+ const hasPageNav = enabledActions.some((a) => a.type === "page-nav");
803
+ const effectiveActions = hasPageNav ? enabledActions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : enabledActions;
654
804
  for (const action of effectiveActions) {
655
805
  if (groups[action.group]) {
656
806
  groups[action.group].push(action);
@@ -710,6 +860,13 @@ var ToolbarController = class {
710
860
  action.execute("go", val);
711
861
  });
712
862
  const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
863
+ const curVal = action.value ?? 1;
864
+ prevBtn.disabled = curVal <= 1;
865
+ prevBtn.style.opacity = curVal <= 1 ? "0.4" : "1";
866
+ nextBtn.disabled = curVal >= max;
867
+ nextBtn.style.opacity = curVal >= max ? "0.4" : "1";
868
+ this.prevPageBtn = prevBtn;
869
+ this.nextPageBtn = nextBtn;
713
870
  this.pageInputEl = input;
714
871
  this.pageLabelEl = label;
715
872
  groupEl.appendChild(prevBtn);
@@ -750,9 +907,11 @@ var ToolbarController = class {
750
907
  type: "button",
751
908
  "data-action-id": id
752
909
  });
753
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
754
- if (svg) {
755
- btn.innerHTML = sanitizeSVG(svg);
910
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
911
+ if (internalSvg) {
912
+ btn.innerHTML = internalSvg;
913
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
914
+ btn.innerHTML = sanitizeSVG(iconHtml);
756
915
  } else {
757
916
  btn.textContent = title || id;
758
917
  }
@@ -763,25 +922,63 @@ var ToolbarController = class {
763
922
  var ThumbnailPanel = class {
764
923
  el;
765
924
  panelEl;
925
+ listEl;
926
+ headerEl;
766
927
  thumbnails = [];
767
928
  onSelect;
929
+ onToggleCallback;
768
930
  activeIndex = 0;
769
- constructor(container) {
931
+ observer;
932
+ renderedIndices = /* @__PURE__ */ new Set();
933
+ constructor(container, onToggle) {
770
934
  this.el = container;
935
+ this.onToggleCallback = onToggle;
771
936
  this.panelEl = createElement("div", { className: "fp-thumbnail-panel hidden" });
937
+ this.panelEl.style.width = "0px";
938
+ this.panelEl.style.minWidth = "0px";
939
+ this.panelEl.style.opacity = "0";
940
+ this.headerEl = createElement("div", { className: "fp-thumbnail-header" });
941
+ const title = createElement("span", { className: "fp-thumbnail-title" }, "Pages");
942
+ const closeBtn = createElement("button", {
943
+ className: "fp-thumbnail-close-btn",
944
+ title: "Close Thumbnails",
945
+ type: "button"
946
+ });
947
+ closeBtn.innerHTML = ICON_CLOSE;
948
+ closeBtn.addEventListener("click", (e) => {
949
+ e.stopPropagation();
950
+ this.hide();
951
+ });
952
+ this.headerEl.appendChild(title);
953
+ this.headerEl.appendChild(closeBtn);
954
+ this.listEl = createElement("div", { className: "fp-thumbnail-list" });
955
+ this.panelEl.appendChild(this.headerEl);
956
+ this.panelEl.appendChild(this.listEl);
772
957
  this.el.appendChild(this.panelEl);
773
958
  }
959
+ isOpen() {
960
+ return !this.panelEl.classList.contains("hidden") && this.panelEl.style.width !== "0px";
961
+ }
774
962
  update(thumbnails, onSelect) {
775
963
  this.thumbnails = thumbnails;
776
964
  this.onSelect = onSelect;
965
+ this.renderedIndices.clear();
966
+ const titleEl = this.headerEl.querySelector(".fp-thumbnail-title");
967
+ if (titleEl) {
968
+ titleEl.textContent = `Pages (${thumbnails.length})`;
969
+ }
777
970
  this.render();
971
+ if (this.isOpen()) {
972
+ this.renderAllVisible();
973
+ }
778
974
  }
779
975
  setActive(index) {
780
976
  this.activeIndex = index;
781
- const items = this.panelEl.querySelectorAll(".fp-thumbnail-item");
977
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
782
978
  items.forEach((item, i) => {
783
979
  if (i === index) {
784
980
  item.classList.add("active");
981
+ item.scrollIntoView({ block: "nearest", behavior: "smooth" });
785
982
  } else {
786
983
  item.classList.remove("active");
787
984
  }
@@ -789,37 +986,102 @@ var ThumbnailPanel = class {
789
986
  }
790
987
  show() {
791
988
  this.panelEl.classList.remove("hidden");
989
+ this.panelEl.style.width = "200px";
990
+ this.panelEl.style.minWidth = "200px";
991
+ this.panelEl.style.opacity = "1";
992
+ this.panelEl.style.display = "flex";
993
+ this.onToggleCallback?.(true);
994
+ this.renderAllVisible();
792
995
  }
793
996
  hide() {
794
997
  this.panelEl.classList.add("hidden");
998
+ this.panelEl.style.width = "0px";
999
+ this.panelEl.style.minWidth = "0px";
1000
+ this.panelEl.style.opacity = "0";
1001
+ this.onToggleCallback?.(false);
795
1002
  }
796
1003
  toggle() {
797
- this.panelEl.classList.toggle("hidden");
1004
+ if (this.isOpen()) {
1005
+ this.hide();
1006
+ } else {
1007
+ this.show();
1008
+ }
798
1009
  }
799
1010
  destroy() {
1011
+ if (this.observer) {
1012
+ this.observer.disconnect();
1013
+ this.observer = void 0;
1014
+ }
1015
+ this.renderedIndices.clear();
800
1016
  this.el.innerHTML = "";
801
1017
  }
802
- async render() {
803
- this.panelEl.innerHTML = "";
1018
+ renderAllVisible() {
1019
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
1020
+ items.forEach((item) => {
1021
+ const idx = parseInt(item.dataset.thumbIndex || "-1", 10);
1022
+ if (idx >= 0 && !this.renderedIndices.has(idx)) {
1023
+ this.renderThumbnailItem(idx, item);
1024
+ }
1025
+ });
1026
+ }
1027
+ renderThumbnailItem(idx, itemEl) {
1028
+ if (this.renderedIndices.has(idx)) return;
1029
+ this.renderedIndices.add(idx);
1030
+ const canvas = itemEl.querySelector("canvas");
1031
+ const thumb = this.thumbnails[idx];
1032
+ if (canvas && thumb) {
1033
+ Promise.resolve(thumb.render(canvas)).catch((err) => {
1034
+ console.warn(`[FilePreview] Error rendering thumbnail ${idx}:`, err);
1035
+ });
1036
+ }
1037
+ }
1038
+ render() {
1039
+ if (this.observer) {
1040
+ this.observer.disconnect();
1041
+ }
1042
+ this.listEl.innerHTML = "";
1043
+ const useObserver = typeof IntersectionObserver !== "undefined";
1044
+ if (useObserver) {
1045
+ this.observer = new IntersectionObserver(
1046
+ (entries) => {
1047
+ for (const entry of entries) {
1048
+ if (entry.isIntersecting) {
1049
+ const target = entry.target;
1050
+ const idx = parseInt(target.dataset.thumbIndex || "-1", 10);
1051
+ if (idx >= 0) {
1052
+ this.renderThumbnailItem(idx, target);
1053
+ }
1054
+ }
1055
+ }
1056
+ },
1057
+ {
1058
+ root: this.listEl,
1059
+ rootMargin: "120px 0px 120px 0px"
1060
+ }
1061
+ );
1062
+ }
804
1063
  for (let i = 0; i < this.thumbnails.length; i++) {
805
1064
  const thumb = this.thumbnails[i];
806
- const itemEl = createElement("div", { className: "fp-thumbnail-item" });
807
- if (i === this.activeIndex) {
808
- itemEl.classList.add("active");
809
- }
1065
+ const itemEl = createElement("div", {
1066
+ className: "fp-thumbnail-item" + (i === this.activeIndex ? " active" : "")
1067
+ });
1068
+ itemEl.dataset.thumbIndex = i.toString();
810
1069
  itemEl.addEventListener("click", () => {
811
1070
  this.setActive(i);
812
1071
  this.onSelect?.(i);
813
1072
  });
814
- const canvas = createElement("canvas", { width: "150", height: "200" });
815
- const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label);
816
- itemEl.appendChild(canvas);
1073
+ const canvasWrapper = createElement("div", { className: "fp-thumbnail-canvas-wrapper" });
1074
+ const canvas = createElement("canvas", { width: "130", height: "170" });
1075
+ canvasWrapper.appendChild(canvas);
1076
+ const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label || `Page ${i + 1}`);
1077
+ itemEl.appendChild(canvasWrapper);
817
1078
  itemEl.appendChild(label);
818
- this.panelEl.appendChild(itemEl);
819
- try {
820
- await thumb.render(canvas);
821
- } catch (err) {
822
- console.error(`[FilePreview] Error rendering thumbnail ${i}:`, err);
1079
+ this.listEl.appendChild(itemEl);
1080
+ if (this.observer) {
1081
+ this.observer.observe(itemEl);
1082
+ }
1083
+ if (i < 4) {
1084
+ this.renderThumbnailItem(i, itemEl);
823
1085
  }
824
1086
  }
825
1087
  }
@@ -840,6 +1102,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
840
1102
  currentOptions = {};
841
1103
  resizeObserver = null;
842
1104
  fullscreenHandler = null;
1105
+ titleBarEl = null;
843
1106
  /**
844
1107
  * Register a preview plugin.
845
1108
  */
@@ -876,6 +1139,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
876
1139
  }
877
1140
  this.currentBuffer = buffer.slice(0);
878
1141
  this.currentMetadata = metadata;
1142
+ this.updateTitleBar(options, metadata);
879
1143
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
880
1144
  const fileInfo = { metadata, buffer };
881
1145
  const matchedPlugin = await this.findPlugin(fileInfo);
@@ -899,15 +1163,21 @@ var FilePreviewViewer = class _FilePreviewViewer {
899
1163
  if (event === "page-change" && payload && typeof payload.page === "number") {
900
1164
  const total = payload.total ?? payload.totalPages;
901
1165
  this.toolbar?.setPage(payload.page, total);
1166
+ this.thumbnailPanel?.setActive(payload.page - 1);
902
1167
  }
903
1168
  this.eventEmitter.emit(event, payload);
904
- }
1169
+ },
1170
+ toggleThumbnails: () => this.toggleThumbnails()
905
1171
  });
906
1172
  this.activeInstance = instance;
907
1173
  instance.openInSeparateWindow = () => this.openInSeparateWindow();
1174
+ instance.toggleThumbnails = () => this.toggleThumbnails();
908
1175
  this.hideLoading();
909
1176
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
910
- if (options.showToolbar !== false && this.toolbar) {
1177
+ const isToolbarVisible = options.showToolbar !== false && options.toolbar !== false;
1178
+ if (isToolbarVisible && this.toolbar) {
1179
+ const toolbarConfig = this.extractToolbarConfig(options);
1180
+ this.toolbar.setConfig(toolbarConfig);
911
1181
  const actions = matchedPlugin.getToolbarActions(instance);
912
1182
  const hasFullscreen = actions.some((a) => a.id === "fullscreen");
913
1183
  if (!hasFullscreen && this.wrapperEl) {
@@ -957,6 +1227,12 @@ var FilePreviewViewer = class _FilePreviewViewer {
957
1227
  }
958
1228
  });
959
1229
  }
1230
+ const thumbAction = actions.find((a) => a.id === "thumbnails");
1231
+ if (thumbAction) {
1232
+ thumbAction.execute = () => {
1233
+ this.toggleThumbnails();
1234
+ };
1235
+ }
960
1236
  this.toolbar.update(actions);
961
1237
  this.toolbar.show();
962
1238
  }
@@ -1041,7 +1317,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
1041
1317
  if (targetUrl) {
1042
1318
  const newWin2 = window.open(targetUrl, "_blank");
1043
1319
  if (!newWin2) {
1044
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1320
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
1045
1321
  return null;
1046
1322
  }
1047
1323
  return newWin2;
@@ -1049,7 +1325,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
1049
1325
  const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
1050
1326
  const newWin = window.open("", "_blank");
1051
1327
  if (!newWin) {
1052
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1328
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
1053
1329
  return null;
1054
1330
  }
1055
1331
  newWin.document.title = title;
@@ -1126,6 +1402,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
1126
1402
  this.currentBuffer = null;
1127
1403
  this.currentMetadata = null;
1128
1404
  this.currentContainer = null;
1405
+ this.titleBarEl = null;
1129
1406
  }
1130
1407
  /**
1131
1408
  * Get the currently active preview instance.
@@ -1133,7 +1410,218 @@ var FilePreviewViewer = class _FilePreviewViewer {
1133
1410
  getInstance() {
1134
1411
  return this.activeInstance;
1135
1412
  }
1413
+ // --- Toolbar Configuration & Feature Toggles ---
1414
+ /**
1415
+ * Configure toolbar features dynamically through options or methods.
1416
+ * e.g. viewer.setToolbarConfig({ zoomIn: false, print: false })
1417
+ */
1418
+ setToolbarConfig(config) {
1419
+ this.toolbar?.setConfig(config);
1420
+ }
1421
+ /**
1422
+ * Get current toolbar configuration.
1423
+ */
1424
+ getToolbarConfig() {
1425
+ return this.toolbar?.getConfig() ?? {};
1426
+ }
1427
+ /**
1428
+ * Hide a specific toolbar action by ID or alias (e.g. 'zoom-in', 'print', 'download').
1429
+ */
1430
+ hideToolbarAction(actionId) {
1431
+ this.toolbar?.hideAction(actionId);
1432
+ }
1433
+ /**
1434
+ * Show a specific toolbar action by ID or alias.
1435
+ */
1436
+ showToolbarAction(actionId) {
1437
+ this.toolbar?.showAction(actionId);
1438
+ }
1439
+ /**
1440
+ * Enable a specific toolbar action button.
1441
+ */
1442
+ enableToolbarAction(actionId) {
1443
+ this.toolbar?.enableAction(actionId);
1444
+ }
1445
+ /**
1446
+ * Disable a specific toolbar action button.
1447
+ */
1448
+ disableToolbarAction(actionId) {
1449
+ this.toolbar?.disableAction(actionId);
1450
+ }
1451
+ // --- Programmatic Toolbar Action Methods ---
1452
+ /**
1453
+ * Fit document to page so it fills the frame width with minimal margins.
1454
+ */
1455
+ fitToPage() {
1456
+ this.activeInstance?.fitToPage?.();
1457
+ }
1458
+ /**
1459
+ * Zoom in.
1460
+ */
1461
+ zoomIn() {
1462
+ this.activeInstance?.zoomIn?.();
1463
+ }
1464
+ /**
1465
+ * Zoom out.
1466
+ */
1467
+ zoomOut() {
1468
+ this.activeInstance?.zoomOut?.();
1469
+ }
1470
+ /**
1471
+ * Set specific zoom level.
1472
+ */
1473
+ setZoom(level) {
1474
+ this.activeInstance?.setZoom?.(level);
1475
+ }
1476
+ /**
1477
+ * Get current zoom level.
1478
+ */
1479
+ getZoom() {
1480
+ return this.activeInstance?.getZoom?.() ?? 1;
1481
+ }
1482
+ /**
1483
+ * Rotate 90 degrees clockwise.
1484
+ */
1485
+ rotateCW() {
1486
+ this.activeInstance?.rotateCW?.();
1487
+ }
1488
+ /**
1489
+ * Rotate 90 degrees counter-clockwise.
1490
+ */
1491
+ rotateCCW() {
1492
+ this.activeInstance?.rotateCCW?.();
1493
+ }
1494
+ /**
1495
+ * Navigate to a specific page number.
1496
+ */
1497
+ goToPage(page) {
1498
+ this.activeInstance?.goToPage?.(page);
1499
+ this.toolbar?.setPage(page);
1500
+ }
1501
+ /**
1502
+ * Navigate to next page.
1503
+ */
1504
+ nextPage() {
1505
+ const cur = this.getCurrentPage();
1506
+ const max = this.getPageCount();
1507
+ if (cur < max) {
1508
+ this.goToPage(cur + 1);
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Navigate to previous page.
1513
+ */
1514
+ prevPage() {
1515
+ const cur = this.getCurrentPage();
1516
+ if (cur > 1) {
1517
+ this.goToPage(cur - 1);
1518
+ }
1519
+ }
1520
+ /**
1521
+ * Get total page count.
1522
+ */
1523
+ getPageCount() {
1524
+ return this.activeInstance?.getPageCount?.() ?? 1;
1525
+ }
1526
+ /**
1527
+ * Get current page number.
1528
+ */
1529
+ getCurrentPage() {
1530
+ return this.activeInstance?.getCurrentPage?.() ?? 1;
1531
+ }
1532
+ /**
1533
+ * Download the current document.
1534
+ */
1535
+ download() {
1536
+ this.activeInstance?.download?.();
1537
+ }
1538
+ /**
1539
+ * Print the current document.
1540
+ */
1541
+ print() {
1542
+ this.activeInstance?.print?.();
1543
+ }
1544
+ /**
1545
+ * Toggle fullscreen mode.
1546
+ */
1547
+ toggleFullscreen() {
1548
+ try {
1549
+ if (!document.fullscreenElement) {
1550
+ this.wrapperEl?.requestFullscreen?.();
1551
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
1552
+ } else {
1553
+ document.exitFullscreen?.();
1554
+ this.wrapperEl?.classList.remove("fp-fullscreen-active");
1555
+ }
1556
+ } catch {
1557
+ this.wrapperEl?.classList.toggle("fp-fullscreen-active");
1558
+ }
1559
+ setTimeout(() => {
1560
+ this.activeInstance?.fitToPage?.();
1561
+ }, 120);
1562
+ }
1563
+ /**
1564
+ * Toggle thumbnails sidebar panel.
1565
+ */
1566
+ toggleThumbnails() {
1567
+ if (!this.thumbnailPanel) return;
1568
+ this.thumbnailPanel.toggle();
1569
+ const isOpen = this.thumbnailPanel.isOpen();
1570
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1571
+ setTimeout(() => {
1572
+ this.activeInstance?.fitToPage?.();
1573
+ }, 260);
1574
+ }
1575
+ /**
1576
+ * Set whether to display the file name / title bar above the toolbar.
1577
+ */
1578
+ setShowFileName(show) {
1579
+ if (this.currentOptions) {
1580
+ this.currentOptions.showFileName = show;
1581
+ }
1582
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1583
+ }
1584
+ /**
1585
+ * Set or override the displayed file name in the preview panel.
1586
+ */
1587
+ setFileName(name) {
1588
+ if (this.currentOptions) {
1589
+ this.currentOptions.fileName = name;
1590
+ }
1591
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1592
+ }
1136
1593
  // --- Private methods ---
1594
+ updateTitleBar(options, metadata) {
1595
+ if (!this.titleBarEl) return;
1596
+ const show = options?.showFileName !== false;
1597
+ if (!show) {
1598
+ this.titleBarEl.style.display = "none";
1599
+ return;
1600
+ }
1601
+ const name = options?.fileName || metadata?.name || this.currentMetadata?.name || "";
1602
+ if (!name) {
1603
+ this.titleBarEl.style.display = "none";
1604
+ return;
1605
+ }
1606
+ this.titleBarEl.style.display = "flex";
1607
+ const ext = metadata?.extension || (name.includes(".") ? "." + name.split(".").pop() : "");
1608
+ const badge = ext ? ext.replace(".", "").toUpperCase() : "";
1609
+ this.titleBarEl.innerHTML = `
1610
+ <div class="fp-titlebar-info">
1611
+ <span class="fp-titlebar-icon">\u{1F4C4}</span>
1612
+ <span class="fp-titlebar-name" title="${name}">${name}</span>
1613
+ ${badge ? `<span class="fp-titlebar-badge">${badge}</span>` : ""}
1614
+ </div>
1615
+ `;
1616
+ }
1617
+ extractToolbarConfig(options) {
1618
+ if (!options) return {};
1619
+ const nested = typeof options.toolbar === "object" ? options.toolbar : {};
1620
+ return {
1621
+ ...options,
1622
+ ...nested
1623
+ };
1624
+ }
1137
1625
  abort() {
1138
1626
  if (this.abortController) {
1139
1627
  this.abortController.abort();
@@ -1176,17 +1664,29 @@ var FilePreviewViewer = class _FilePreviewViewer {
1176
1664
  bodyEl.className = "fp-body";
1177
1665
  bodyEl.appendChild(thumbnailEl);
1178
1666
  bodyEl.appendChild(this.contentEl);
1667
+ const titleBarEl = document.createElement("div");
1668
+ titleBarEl.className = "fp-titlebar";
1669
+ this.titleBarEl = titleBarEl;
1670
+ this.updateTitleBar(options, this.currentMetadata);
1179
1671
  if (toolbarPos === "top") {
1672
+ this.wrapperEl.appendChild(titleBarEl);
1180
1673
  this.wrapperEl.appendChild(toolbarEl);
1181
1674
  this.wrapperEl.appendChild(bodyEl);
1182
1675
  } else {
1676
+ this.wrapperEl.appendChild(titleBarEl);
1183
1677
  this.wrapperEl.appendChild(bodyEl);
1184
1678
  this.wrapperEl.appendChild(toolbarEl);
1185
1679
  }
1186
1680
  container.innerHTML = "";
1187
1681
  container.appendChild(this.wrapperEl);
1188
- this.toolbar = new ToolbarController(toolbarEl);
1189
- this.thumbnailPanel = new ThumbnailPanel(thumbnailEl);
1682
+ const initialToolbarConfig = this.extractToolbarConfig(options);
1683
+ this.toolbar = new ToolbarController(toolbarEl, initialToolbarConfig);
1684
+ this.thumbnailPanel = new ThumbnailPanel(thumbnailEl, (isOpen) => {
1685
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1686
+ setTimeout(() => {
1687
+ this.activeInstance?.fitToPage?.();
1688
+ }, 260);
1689
+ });
1190
1690
  this.setupKeyboardShortcuts();
1191
1691
  this.setupDragAndDrop(container, options);
1192
1692
  this.setupResizeAndFullscreenListeners();
@@ -1576,6 +2076,14 @@ var PdfPlugin = class {
1576
2076
  group: "zoom",
1577
2077
  execute: () => instance.fitToPage?.()
1578
2078
  },
2079
+ {
2080
+ id: "fit-width",
2081
+ icon: "fit-width",
2082
+ label: "Fit to Width",
2083
+ type: "button",
2084
+ group: "zoom",
2085
+ execute: () => instance.fitToWidth?.()
2086
+ },
1579
2087
  {
1580
2088
  id: "rotate-cw",
1581
2089
  icon: "rotate-cw",
@@ -1584,6 +2092,14 @@ var PdfPlugin = class {
1584
2092
  group: "view",
1585
2093
  execute: () => instance.rotateCW?.()
1586
2094
  },
2095
+ {
2096
+ id: "rotate-ccw",
2097
+ icon: "rotate-ccw",
2098
+ label: "Rotate Counter-Clockwise",
2099
+ type: "button",
2100
+ group: "view",
2101
+ execute: () => instance.rotateCCW?.()
2102
+ },
1587
2103
  {
1588
2104
  id: "download",
1589
2105
  icon: "download",
@@ -1615,12 +2131,13 @@ var PdfPlugin = class {
1615
2131
  container.className = "fp-pdf-container";
1616
2132
  container.style.width = "100%";
1617
2133
  container.style.height = "100%";
1618
- container.style.overflow = "auto";
2134
+ container.style.overflowX = "hidden";
2135
+ container.style.overflowY = "auto";
1619
2136
  container.style.display = "flex";
1620
2137
  container.style.flexDirection = "column";
1621
2138
  container.style.alignItems = "center";
1622
2139
  container.style.justifyContent = "flex-start";
1623
- container.style.padding = "20px 16px";
2140
+ container.style.padding = "16px 8px";
1624
2141
  container.style.backgroundColor = "#0f172a";
1625
2142
  container.style.boxSizing = "border-box";
1626
2143
  container.style.position = "relative";
@@ -1631,30 +2148,12 @@ var PdfPlugin = class {
1631
2148
  pageCard.style.borderRadius = "4px";
1632
2149
  pageCard.style.overflow = "hidden";
1633
2150
  pageCard.style.lineHeight = "0";
1634
- pageCard.style.transition = "transform 0.15s ease";
1635
2151
  pageCard.style.position = "relative";
1636
2152
  pageCard.style.flexShrink = "0";
2153
+ pageCard.style.transition = "box-shadow 0.2s ease";
1637
2154
  let canvas = document.createElement("canvas");
1638
2155
  pageCard.appendChild(canvas);
1639
2156
  container.appendChild(pageCard);
1640
- const indicator = document.createElement("div");
1641
- indicator.className = "fp-pdf-page-indicator";
1642
- indicator.style.position = "sticky";
1643
- indicator.style.bottom = "16px";
1644
- indicator.style.marginTop = "16px";
1645
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1646
- indicator.style.backdropFilter = "blur(8px)";
1647
- indicator.style.color = "#f8fafc";
1648
- indicator.style.fontSize = "12px";
1649
- indicator.style.fontWeight = "600";
1650
- indicator.style.padding = "5px 14px";
1651
- indicator.style.borderRadius = "20px";
1652
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1653
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1654
- indicator.style.zIndex = "10";
1655
- indicator.style.userSelect = "none";
1656
- indicator.style.pointerEvents = "none";
1657
- container.appendChild(indicator);
1658
2157
  ctx.container.appendChild(container);
1659
2158
  const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1660
2159
  const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
@@ -1670,8 +2169,9 @@ var PdfPlugin = class {
1670
2169
  let currentPage = 1;
1671
2170
  let zoomScale = 1;
1672
2171
  let rotation = 0;
1673
- let fitMode = "page";
2172
+ let fitMode = ctx?.options?.fitMode || "width";
1674
2173
  let currentRenderTask = null;
2174
+ let textLayerDiv = null;
1675
2175
  const renderPage = async (pageNum) => {
1676
2176
  if (currentRenderTask) {
1677
2177
  try {
@@ -1683,30 +2183,37 @@ var PdfPlugin = class {
1683
2183
  const newCanvas = document.createElement("canvas");
1684
2184
  pageCard.replaceChild(newCanvas, canvas);
1685
2185
  canvas = newCanvas;
2186
+ if (textLayerDiv) {
2187
+ textLayerDiv.remove();
2188
+ textLayerDiv = null;
2189
+ }
1686
2190
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
1687
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1688
2191
  ctx.emit("page-change", { page: currentPage, total: totalPages });
1689
2192
  const page = await pdfDoc.getPage(currentPage);
1690
2193
  const containerWidth = container.clientWidth || 900;
1691
2194
  const containerHeight = container.clientHeight || 700;
1692
2195
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1693
- const availWidth = Math.max(320, containerWidth - 48);
1694
- const availHeight = Math.max(550, containerHeight - 88);
2196
+ const availWidth = Math.max(280, containerWidth - 36);
2197
+ const availHeight = Math.max(280, containerHeight - 32);
1695
2198
  const scaleW = availWidth / unscaledVp.width;
1696
2199
  const scaleH = availHeight / unscaledVp.height;
1697
2200
  let fitScale;
1698
2201
  if (fitMode === "page") {
1699
- fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
2202
+ fitScale = Math.max(0.2, Math.min(4, Math.min(scaleW, scaleH)));
1700
2203
  } else {
1701
- fitScale = Math.max(0.65, Math.min(1.25, scaleW));
2204
+ fitScale = Math.max(0.2, Math.min(4, scaleW));
1702
2205
  }
1703
2206
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1704
2207
  const pixelRatio = window.devicePixelRatio || 1;
1705
2208
  const viewport = page.getViewport({ scale: effectiveScale, rotation });
2209
+ const displayWidth = Math.floor(viewport.width);
2210
+ const displayHeight = Math.floor(viewport.height);
1706
2211
  canvas.width = Math.floor(viewport.width * pixelRatio);
1707
2212
  canvas.height = Math.floor(viewport.height * pixelRatio);
1708
- canvas.style.width = `${Math.floor(viewport.width)}px`;
1709
- canvas.style.height = `${Math.floor(viewport.height)}px`;
2213
+ canvas.style.width = `${displayWidth}px`;
2214
+ canvas.style.height = `${displayHeight}px`;
2215
+ pageCard.style.width = `${displayWidth}px`;
2216
+ pageCard.style.height = `${displayHeight}px`;
1710
2217
  const canvasCtx = canvas.getContext("2d");
1711
2218
  if (!canvasCtx) return;
1712
2219
  canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
@@ -1720,9 +2227,31 @@ var PdfPlugin = class {
1720
2227
  if (err?.name !== "RenderingCancelledException") {
1721
2228
  console.warn("[PdfPlugin] Page render warning:", err);
1722
2229
  }
2230
+ return;
1723
2231
  } finally {
1724
2232
  currentRenderTask = null;
1725
2233
  }
2234
+ try {
2235
+ textLayerDiv = document.createElement("div");
2236
+ textLayerDiv.className = "textLayer";
2237
+ textLayerDiv.style.width = `${displayWidth}px`;
2238
+ textLayerDiv.style.height = `${displayHeight}px`;
2239
+ textLayerDiv.style.position = "absolute";
2240
+ textLayerDiv.style.top = "0";
2241
+ textLayerDiv.style.left = "0";
2242
+ textLayerDiv.style.lineHeight = "1";
2243
+ pageCard.appendChild(textLayerDiv);
2244
+ if (pdfjsLib.TextLayer) {
2245
+ const textLayer = new pdfjsLib.TextLayer({
2246
+ textContentSource: page.streamTextContent(),
2247
+ container: textLayerDiv,
2248
+ viewport
2249
+ });
2250
+ await textLayer.render();
2251
+ }
2252
+ } catch (err) {
2253
+ console.debug("[PdfPlugin] TextLayer notice:", err);
2254
+ }
1726
2255
  };
1727
2256
  renderPage(1);
1728
2257
  let resizeTimer = null;
@@ -1732,11 +2261,48 @@ var PdfPlugin = class {
1732
2261
  if (Math.abs(zoomScale - 1) < 0.05) {
1733
2262
  renderPage(currentPage);
1734
2263
  }
1735
- }, 150);
2264
+ }, 120);
1736
2265
  });
1737
2266
  resizeObserver.observe(container);
2267
+ const onWheel = (e) => {
2268
+ if (e.ctrlKey || e.metaKey) {
2269
+ e.preventDefault();
2270
+ const delta = e.deltaY > 0 ? -0.15 : 0.15;
2271
+ zoomScale = Math.max(0.25, Math.min(4, Math.round((zoomScale + delta) * 100) / 100));
2272
+ renderPage(currentPage);
2273
+ }
2274
+ };
2275
+ container.addEventListener("wheel", onWheel, { passive: false });
2276
+ const onKeyDown = (e) => {
2277
+ const tag = e.target?.tagName?.toLowerCase();
2278
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
2279
+ if (e.key === "ArrowRight" || e.key === "PageDown") {
2280
+ if (currentPage < totalPages) {
2281
+ e.preventDefault();
2282
+ container.scrollTop = 0;
2283
+ renderPage(currentPage + 1);
2284
+ }
2285
+ } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
2286
+ if (currentPage > 1) {
2287
+ e.preventDefault();
2288
+ container.scrollTop = 0;
2289
+ renderPage(currentPage - 1);
2290
+ }
2291
+ } else if (e.key === "Home") {
2292
+ e.preventDefault();
2293
+ container.scrollTop = 0;
2294
+ renderPage(1);
2295
+ } else if (e.key === "End") {
2296
+ e.preventDefault();
2297
+ container.scrollTop = 0;
2298
+ renderPage(totalPages);
2299
+ }
2300
+ };
2301
+ window.addEventListener("keydown", onKeyDown);
1738
2302
  const cleanup = () => {
1739
2303
  resizeObserver.disconnect();
2304
+ window.removeEventListener("keydown", onKeyDown);
2305
+ container.removeEventListener("wheel", onWheel);
1740
2306
  if (resizeTimer) clearTimeout(resizeTimer);
1741
2307
  if (currentRenderTask) {
1742
2308
  try {
@@ -1755,22 +2321,26 @@ var PdfPlugin = class {
1755
2321
  const instance = {
1756
2322
  destroy: cleanup,
1757
2323
  zoomIn: () => {
1758
- zoomScale = Math.min(3.5, zoomScale + 0.2);
2324
+ zoomScale = Math.min(4, Math.round((zoomScale + 0.2) * 10) / 10);
1759
2325
  renderPage(currentPage);
1760
2326
  },
1761
2327
  zoomOut: () => {
1762
- zoomScale = Math.max(0.3, zoomScale - 0.2);
2328
+ zoomScale = Math.max(0.25, Math.round((zoomScale - 0.2) * 10) / 10);
1763
2329
  renderPage(currentPage);
1764
2330
  },
1765
2331
  getZoom: () => zoomScale,
1766
2332
  setZoom: (level) => {
1767
- zoomScale = Math.max(0.3, Math.min(3.5, level));
2333
+ zoomScale = Math.max(0.25, Math.min(4, level));
1768
2334
  renderPage(currentPage);
1769
2335
  },
1770
2336
  fitToPage: () => {
1771
- fitMode = fitMode === "page" ? "width" : "page";
2337
+ fitMode = "page";
2338
+ zoomScale = 1;
2339
+ renderPage(currentPage);
2340
+ },
2341
+ fitToWidth: () => {
2342
+ fitMode = "width";
1772
2343
  zoomScale = 1;
1773
- rotation = 0;
1774
2344
  renderPage(currentPage);
1775
2345
  },
1776
2346
  rotateCW: () => {
@@ -1788,6 +2358,9 @@ var PdfPlugin = class {
1788
2358
  container.scrollTop = 0;
1789
2359
  renderPage(page);
1790
2360
  },
2361
+ toggleThumbnails: () => {
2362
+ ctx?.toggleThumbnails?.();
2363
+ },
1791
2364
  download: () => {
1792
2365
  const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1793
2366
  const url = URL.createObjectURL(blob);
@@ -1821,7 +2394,7 @@ var PdfPlugin = class {
1821
2394
  },
1822
2395
  getThumbnails: async () => {
1823
2396
  const thumbnails = [];
1824
- const count = Math.min(totalPages, 50);
2397
+ const count = totalPages;
1825
2398
  for (let i = 1; i <= count; i++) {
1826
2399
  thumbnails.push({
1827
2400
  index: i,
@@ -1829,9 +2402,10 @@ var PdfPlugin = class {
1829
2402
  render: async (thumbCanvas) => {
1830
2403
  try {
1831
2404
  const p = await pdfDoc.getPage(i);
1832
- const baseVp = p.getViewport({ scale: 1 });
1833
- const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1834
- const thumbVp = p.getViewport({ scale: thumbScale });
2405
+ const baseVp = p.getViewport({ scale: 1, rotation });
2406
+ const targetW = thumbCanvas.width || 130;
2407
+ const thumbScale = targetW / baseVp.width;
2408
+ const thumbVp = p.getViewport({ scale: thumbScale, rotation });
1835
2409
  thumbCanvas.height = Math.floor(thumbVp.height);
1836
2410
  const tCtx = thumbCanvas.getContext("2d");
1837
2411
  if (tCtx) {
@@ -2175,28 +2749,26 @@ var DocxPlugin = class {
2175
2749
  getToolbarActions(instance) {
2176
2750
  const totalPages = instance.getPageCount?.() ?? 1;
2177
2751
  const actions = [];
2178
- if (totalPages > 1) {
2179
- actions.push({
2180
- id: "page-nav",
2181
- icon: "",
2182
- label: "Page Navigation",
2183
- type: "page-nav",
2184
- group: "navigation",
2185
- value: instance.getCurrentPage?.() ?? 1,
2186
- max: totalPages,
2187
- execute: (action, page) => {
2188
- const cur = instance.getCurrentPage?.() ?? 1;
2189
- const max = instance.getPageCount?.() ?? 1;
2190
- if (action === "prev") {
2191
- if (cur > 1) instance.goToPage?.(cur - 1);
2192
- } else if (action === "next") {
2193
- if (cur < max) instance.goToPage?.(cur + 1);
2194
- } else if (typeof page === "number") {
2195
- instance.goToPage?.(page);
2196
- }
2752
+ actions.push({
2753
+ id: "page-nav",
2754
+ icon: "",
2755
+ label: "Page Navigation",
2756
+ type: "page-nav",
2757
+ group: "navigation",
2758
+ value: instance.getCurrentPage?.() ?? 1,
2759
+ max: totalPages,
2760
+ execute: (action, page) => {
2761
+ const cur = instance.getCurrentPage?.() ?? 1;
2762
+ const max = instance.getPageCount?.() ?? 1;
2763
+ if (action === "prev") {
2764
+ if (cur > 1) instance.goToPage?.(cur - 1);
2765
+ } else if (action === "next") {
2766
+ if (cur < max) instance.goToPage?.(cur + 1);
2767
+ } else if (typeof page === "number") {
2768
+ instance.goToPage?.(page);
2197
2769
  }
2198
- });
2199
- }
2770
+ }
2771
+ });
2200
2772
  actions.push(
2201
2773
  {
2202
2774
  id: "zoom-out",
@@ -2262,11 +2834,17 @@ var DocxPlugin = class {
2262
2834
  wrapper.className = "fp-docx-wrapper";
2263
2835
  wrapper.style.transformOrigin = "top center";
2264
2836
  wrapper.style.transition = "transform 0.2s ease";
2265
- wrapper.style.padding = "24px 16px";
2266
- wrapper.style.maxWidth = "950px";
2837
+ wrapper.style.padding = "0";
2838
+ wrapper.style.maxWidth = "none";
2839
+ wrapper.style.width = "816px";
2267
2840
  wrapper.style.margin = "0 auto";
2841
+ wrapper.style.display = "flex";
2842
+ wrapper.style.flexDirection = "column";
2843
+ wrapper.style.alignItems = "center";
2268
2844
  wrapper.style.boxSizing = "border-box";
2269
- ctx.container.style.overflow = "auto";
2845
+ ctx.container.style.overflowX = "hidden";
2846
+ ctx.container.style.overflowY = "auto";
2847
+ ctx.container.style.padding = "16px 8px";
2270
2848
  ctx.container.style.backgroundColor = "#f1f5f9";
2271
2849
  ctx.container.appendChild(wrapper);
2272
2850
  const styleOverride = document.createElement("style");
@@ -2440,39 +3018,13 @@ var DocxPlugin = class {
2440
3018
  const pageElements = sections.length > 0 ? sections : cards;
2441
3019
  const totalPages = Math.max(1, pageElements.length);
2442
3020
  let currentPage = 1;
2443
- let indicator = null;
2444
- if (totalPages > 1) {
2445
- indicator = document.createElement("div");
2446
- indicator.className = "fp-docx-page-indicator";
2447
- indicator.style.position = "sticky";
2448
- indicator.style.bottom = "16px";
2449
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2450
- indicator.style.backdropFilter = "blur(8px)";
2451
- indicator.style.color = "#f8fafc";
2452
- indicator.style.fontSize = "12px";
2453
- indicator.style.fontWeight = "600";
2454
- indicator.style.padding = "5px 14px";
2455
- indicator.style.borderRadius = "20px";
2456
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2457
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2458
- indicator.style.zIndex = "10";
2459
- indicator.style.userSelect = "none";
2460
- indicator.style.pointerEvents = "none";
2461
- indicator.style.textAlign = "center";
2462
- indicator.style.width = "fit-content";
2463
- indicator.style.margin = "16px auto 0";
2464
- ctx.container.appendChild(indicator);
2465
- }
2466
3021
  const showPage = (pageNum) => {
2467
3022
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
2468
3023
  if (pageElements.length > 1) {
2469
3024
  pageElements.forEach((sec, idx) => {
2470
- sec.style.display = idx + 1 === currentPage ? "block" : "none";
3025
+ sec.style.display = idx + 1 === currentPage ? "" : "none";
2471
3026
  });
2472
3027
  }
2473
- if (indicator) {
2474
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2475
- }
2476
3028
  ctx.container.scrollTop = 0;
2477
3029
  ctx.emit("page-change", { page: currentPage, total: totalPages });
2478
3030
  };
@@ -2481,31 +3033,46 @@ var DocxPlugin = class {
2481
3033
  }
2482
3034
  scale = 1;
2483
3035
  let rotation = 0;
2484
- let fitMode = "width";
2485
- const calculateFitScale = (mode = fitMode) => {
2486
- const activeEl = pageElements[currentPage - 1] || wrapper.firstElementChild || wrapper;
3036
+ let fitMode = "page";
3037
+ let isUserZoomed = false;
3038
+ const calculateFitScale = (_mode = fitMode) => {
3039
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
2487
3040
  const elW = activeEl.offsetWidth || 816;
2488
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
2489
- const availW = Math.max(280, ctx.container.clientWidth - 48);
2490
- const availH = Math.max(280, ctx.container.clientHeight - 80);
3041
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
2491
3042
  const sW = availW / elW;
2492
- const sH = availH / singlePageH;
2493
- if (mode === "page") {
2494
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
2495
- }
2496
- return Math.max(0.65, Math.min(1.05, sW));
3043
+ return Math.max(0.35, Math.min(3, sW));
2497
3044
  };
2498
3045
  const applyTransform = () => {
2499
3046
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
2500
3047
  wrapper.style.transformOrigin = "top center";
3048
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
3049
+ const elH = activeEl.offsetHeight || 1056;
3050
+ if (scale > 1) {
3051
+ wrapper.style.marginBottom = `${Math.round(elH * scale - elH + 32)}px`;
3052
+ } else {
3053
+ wrapper.style.marginBottom = "32px";
3054
+ }
2501
3055
  };
3056
+ let resizeObserver = null;
3057
+ if (typeof ResizeObserver !== "undefined") {
3058
+ resizeObserver = new ResizeObserver(() => {
3059
+ if (!isUserZoomed) {
3060
+ const newFit = calculateFitScale("page");
3061
+ if (Math.abs(scale - newFit) > 0.015) {
3062
+ scale = newFit;
3063
+ applyTransform();
3064
+ }
3065
+ }
3066
+ });
3067
+ resizeObserver.observe(ctx.container);
3068
+ }
2502
3069
  setTimeout(() => {
2503
- fitMode = "width";
2504
- scale = calculateFitScale("width");
3070
+ scale = calculateFitScale("page");
2505
3071
  applyTransform();
2506
- }, 60);
3072
+ }, 40);
2507
3073
  const cleanup = () => {
2508
- indicator?.remove();
3074
+ resizeObserver?.disconnect();
3075
+ resizeObserver = null;
2509
3076
  for (const url of createdBlobUrls) {
2510
3077
  URL.revokeObjectURL(url);
2511
3078
  }
@@ -2522,21 +3089,24 @@ var DocxPlugin = class {
2522
3089
  showPage(page);
2523
3090
  },
2524
3091
  zoomIn: () => {
2525
- scale += 0.15;
3092
+ isUserZoomed = true;
3093
+ scale = Math.min(3.5, scale + 0.15);
2526
3094
  applyTransform();
2527
3095
  },
2528
3096
  zoomOut: () => {
3097
+ isUserZoomed = true;
2529
3098
  scale = Math.max(0.2, scale - 0.15);
2530
3099
  applyTransform();
2531
3100
  },
2532
3101
  getZoom: () => scale,
2533
3102
  setZoom: (level) => {
3103
+ isUserZoomed = true;
2534
3104
  scale = level;
2535
3105
  applyTransform();
2536
3106
  },
2537
3107
  fitToPage: () => {
2538
- fitMode = fitMode === "width" ? "page" : "width";
2539
- scale = calculateFitScale(fitMode);
3108
+ isUserZoomed = false;
3109
+ scale = calculateFitScale("page");
2540
3110
  rotation = 0;
2541
3111
  applyTransform();
2542
3112
  },
@@ -2926,7 +3496,7 @@ var ExcelPlugin = class {
2926
3496
  "application/vnd.ms-excel.template.macroEnabled.12",
2927
3497
  "application/vnd.oasis.opendocument.spreadsheet"
2928
3498
  ];
2929
- weight = 80;
3499
+ weight = 85;
2930
3500
  supports(file) {
2931
3501
  const ext = file.metadata.extension?.toLowerCase();
2932
3502
  const mime = file.metadata.mimeType?.toLowerCase();
@@ -2938,6 +3508,14 @@ var ExcelPlugin = class {
2938
3508
  getToolbarActions(instance) {
2939
3509
  const totalSheets = instance.getPageCount?.() ?? 1;
2940
3510
  const actions = [];
3511
+ actions.push({
3512
+ id: "thumbnails",
3513
+ icon: "thumbnails",
3514
+ label: "Sheet Thumbnails",
3515
+ type: "button",
3516
+ group: "navigation",
3517
+ execute: () => instance.toggleThumbnails?.()
3518
+ });
2941
3519
  if (totalSheets > 1) {
2942
3520
  actions.push({
2943
3521
  id: "page-nav",
@@ -3020,6 +3598,16 @@ var ExcelPlugin = class {
3020
3598
  execute: () => {
3021
3599
  instance.print?.();
3022
3600
  }
3601
+ },
3602
+ {
3603
+ id: "open-window",
3604
+ icon: "open-window",
3605
+ label: "Open in Separate Full Window",
3606
+ type: "button",
3607
+ group: "actions",
3608
+ execute: () => {
3609
+ instance.openInSeparateWindow?.();
3610
+ }
3023
3611
  }
3024
3612
  );
3025
3613
  return actions;
@@ -3185,6 +3773,63 @@ var ExcelPlugin = class {
3185
3773
  },
3186
3774
  getPageCount: () => sheetNames.length,
3187
3775
  getCurrentPage: () => currentSheetIndex,
3776
+ getThumbnails: () => {
3777
+ return sheetNames.map((name, idx) => ({
3778
+ index: idx,
3779
+ label: name,
3780
+ render: async (canvas) => {
3781
+ canvas.width = 140;
3782
+ canvas.height = 100;
3783
+ const c = canvas.getContext("2d");
3784
+ if (!c) return;
3785
+ c.fillStyle = "#ffffff";
3786
+ c.fillRect(0, 0, canvas.width, canvas.height);
3787
+ c.fillStyle = "#107c41";
3788
+ c.fillRect(0, 0, canvas.width, 16);
3789
+ c.fillStyle = "#ffffff";
3790
+ c.font = 'bold 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3791
+ c.fillText(`\u{1F4CA} ${name.slice(0, 16)}`, 6, 11);
3792
+ const startY = 17;
3793
+ const rowH = 13;
3794
+ const colW = 30;
3795
+ const colHeaders = ["A", "B", "C", "D"];
3796
+ c.fillStyle = "#f1f5f9";
3797
+ c.fillRect(0, startY, canvas.width, rowH);
3798
+ c.strokeStyle = "#cbd5e1";
3799
+ c.lineWidth = 0.8;
3800
+ c.strokeRect(0, startY, canvas.width, rowH);
3801
+ c.fillStyle = "#64748b";
3802
+ c.font = "bold 7px sans-serif";
3803
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3804
+ c.fillText(colHeaders[ci], 14 + ci * colW, startY + 9);
3805
+ }
3806
+ const ws = wb?.Sheets[name];
3807
+ for (let ri = 1; ri <= 5; ri++) {
3808
+ const y = startY + ri * rowH;
3809
+ if (y > canvas.height - 2) break;
3810
+ c.fillStyle = "#f8fafc";
3811
+ c.fillRect(0, y, 12, rowH);
3812
+ c.fillStyle = "#94a3b8";
3813
+ c.font = "6px sans-serif";
3814
+ c.fillText(String(ri), 3, y + 9);
3815
+ c.strokeStyle = "#e2e8f0";
3816
+ c.strokeRect(0, y, canvas.width, rowH);
3817
+ c.fillStyle = "#334155";
3818
+ c.font = "6px sans-serif";
3819
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3820
+ const cellRef = `${colHeaders[ci]}${ri}`;
3821
+ const cellVal = ws?.[cellRef]?.w || ws?.[cellRef]?.v;
3822
+ if (cellVal !== void 0) {
3823
+ c.fillText(String(cellVal).slice(0, 6), 14 + ci * colW, y + 9);
3824
+ }
3825
+ }
3826
+ }
3827
+ c.strokeStyle = "#cbd5e1";
3828
+ c.lineWidth = 1;
3829
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
3830
+ }
3831
+ }));
3832
+ },
3188
3833
  download: () => {
3189
3834
  const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
3190
3835
  const url = URL.createObjectURL(blob);
@@ -3217,7 +3862,39 @@ var CsvPlugin = class {
3217
3862
  return ext === ".csv" || ext === ".tsv" || mime === "text/csv" || mime === "text/tab-separated-values";
3218
3863
  }
3219
3864
  getToolbarActions(instance) {
3220
- return [
3865
+ const totalPages = instance.getPageCount?.() ?? 1;
3866
+ const actions = [];
3867
+ actions.push({
3868
+ id: "thumbnails",
3869
+ icon: "thumbnails",
3870
+ label: "Page Thumbnails",
3871
+ type: "button",
3872
+ group: "navigation",
3873
+ execute: () => instance.toggleThumbnails?.()
3874
+ });
3875
+ if (totalPages > 1) {
3876
+ actions.push({
3877
+ id: "page-nav",
3878
+ icon: "",
3879
+ label: "Page Navigation",
3880
+ type: "page-nav",
3881
+ group: "navigation",
3882
+ value: instance.getCurrentPage?.() ?? 1,
3883
+ max: totalPages,
3884
+ execute: (action, page) => {
3885
+ const cur = instance.getCurrentPage?.() ?? 1;
3886
+ const max = instance.getPageCount?.() ?? 1;
3887
+ if (action === "prev") {
3888
+ if (cur > 1) instance.goToPage?.(cur - 1);
3889
+ } else if (action === "next") {
3890
+ if (cur < max) instance.goToPage?.(cur + 1);
3891
+ } else if (typeof page === "number") {
3892
+ instance.goToPage?.(page);
3893
+ }
3894
+ }
3895
+ });
3896
+ }
3897
+ actions.push(
3221
3898
  {
3222
3899
  id: "zoom-out",
3223
3900
  icon: "zoom-out",
@@ -3238,6 +3915,16 @@ var CsvPlugin = class {
3238
3915
  instance.zoomIn?.();
3239
3916
  }
3240
3917
  },
3918
+ {
3919
+ id: "fit-page",
3920
+ icon: "fit-page",
3921
+ label: "Fit to View",
3922
+ type: "button",
3923
+ group: "zoom",
3924
+ execute: () => {
3925
+ instance.fitToPage?.();
3926
+ }
3927
+ },
3241
3928
  {
3242
3929
  id: "download",
3243
3930
  icon: "download",
@@ -3255,10 +3942,21 @@ var CsvPlugin = class {
3255
3942
  type: "button",
3256
3943
  group: "actions",
3257
3944
  execute: () => {
3258
- instance.print?.();
3945
+ instance.print?.();
3946
+ }
3947
+ },
3948
+ {
3949
+ id: "open-window",
3950
+ icon: "open-window",
3951
+ label: "Open in Separate Full Window",
3952
+ type: "button",
3953
+ group: "actions",
3954
+ execute: () => {
3955
+ instance.openInSeparateWindow?.();
3259
3956
  }
3260
3957
  }
3261
- ];
3958
+ );
3959
+ return actions;
3262
3960
  }
3263
3961
  async render(ctx) {
3264
3962
  const decoder = new TextDecoder("utf-8");
@@ -3270,33 +3968,62 @@ var CsvPlugin = class {
3270
3968
  container.style.padding = "16px";
3271
3969
  container.style.boxSizing = "border-box";
3272
3970
  container.style.transformOrigin = "top left";
3971
+ const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3972
+ const allLines = text.split(/\r?\n/).filter((r) => r.trim().length > 0);
3973
+ const headerLine = allLines[0] || "";
3974
+ const headerCells = headerLine.split(delimiter);
3975
+ const dataLines = allLines.slice(1);
3976
+ const ROWS_PER_PAGE = 100;
3977
+ const totalPages = Math.max(1, Math.ceil(dataLines.length / ROWS_PER_PAGE));
3978
+ let currentPage = 1;
3273
3979
  const table = document.createElement("table");
3274
3980
  table.style.borderCollapse = "collapse";
3275
3981
  table.style.width = "100%";
3276
- table.style.fontFamily = "monospace";
3982
+ table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace';
3277
3983
  table.style.fontSize = "13px";
3278
- const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3279
- const rows = text.split(/\r?\n/).slice(0, 500);
3280
- rows.forEach((row, rowIndex) => {
3281
- if (!row.trim()) return;
3282
- const tr = document.createElement("tr");
3283
- const cells = row.split(delimiter);
3284
- cells.forEach((cell) => {
3285
- const td = document.createElement(rowIndex === 0 ? "th" : "td");
3286
- td.textContent = cell.trim();
3287
- td.style.border = "1px solid #d0d7de";
3288
- td.style.padding = "6px 12px";
3289
- if (rowIndex === 0) {
3290
- td.style.backgroundColor = "#f6f8fa";
3291
- td.style.fontWeight = "bold";
3292
- }
3293
- tr.appendChild(td);
3984
+ const renderPage = (pageNum) => {
3985
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3986
+ table.innerHTML = "";
3987
+ const headerTr = document.createElement("tr");
3988
+ headerCells.forEach((cell) => {
3989
+ const th = document.createElement("th");
3990
+ th.textContent = cell.trim();
3991
+ th.style.border = "1px solid #d0d7de";
3992
+ th.style.padding = "8px 12px";
3993
+ th.style.backgroundColor = "#f6f8fa";
3994
+ th.style.fontWeight = "600";
3995
+ th.style.whiteSpace = "nowrap";
3996
+ headerTr.appendChild(th);
3294
3997
  });
3295
- table.appendChild(tr);
3296
- });
3998
+ table.appendChild(headerTr);
3999
+ const start = (currentPage - 1) * ROWS_PER_PAGE;
4000
+ const end = Math.min(dataLines.length, start + ROWS_PER_PAGE);
4001
+ const pageRows = dataLines.slice(start, end);
4002
+ pageRows.forEach((row, idx) => {
4003
+ const tr = document.createElement("tr");
4004
+ if (idx % 2 === 1) tr.style.backgroundColor = "#f8fafc";
4005
+ const cells = row.split(delimiter);
4006
+ cells.forEach((cell) => {
4007
+ const td = document.createElement("td");
4008
+ td.textContent = cell.trim();
4009
+ td.style.border = "1px solid #d0d7de";
4010
+ td.style.padding = "6px 12px";
4011
+ td.style.whiteSpace = "nowrap";
4012
+ tr.appendChild(td);
4013
+ });
4014
+ table.appendChild(tr);
4015
+ });
4016
+ container.scrollTop = 0;
4017
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4018
+ };
4019
+ renderPage(1);
3297
4020
  container.appendChild(table);
3298
4021
  ctx.container.appendChild(container);
3299
4022
  let scale = 1;
4023
+ const applyScale = () => {
4024
+ container.style.transform = `scale(${scale})`;
4025
+ container.style.transformOrigin = "top left";
4026
+ };
3300
4027
  const cleanup = () => {
3301
4028
  container.remove();
3302
4029
  ctx.container.innerHTML = "";
@@ -3304,22 +4031,86 @@ var CsvPlugin = class {
3304
4031
  ctx.signal.addEventListener("abort", cleanup);
3305
4032
  return {
3306
4033
  destroy: cleanup,
4034
+ getPageCount: () => totalPages,
4035
+ getCurrentPage: () => currentPage,
4036
+ goToPage: (page) => renderPage(page),
4037
+ getThumbnails: () => {
4038
+ const thumbnails = [];
4039
+ for (let i = 0; i < totalPages; i++) {
4040
+ const pageIdx = i;
4041
+ const startRow = pageIdx * ROWS_PER_PAGE + 1;
4042
+ const endRow = Math.min(dataLines.length, (pageIdx + 1) * ROWS_PER_PAGE);
4043
+ const label = totalPages === 1 ? "Table" : `Page ${pageIdx + 1} (${startRow}-${endRow})`;
4044
+ thumbnails.push({
4045
+ index: pageIdx,
4046
+ label,
4047
+ render: async (canvas) => {
4048
+ canvas.width = 140;
4049
+ canvas.height = 100;
4050
+ const c = canvas.getContext("2d");
4051
+ if (!c) return;
4052
+ c.fillStyle = "#ffffff";
4053
+ c.fillRect(0, 0, canvas.width, canvas.height);
4054
+ c.fillStyle = "#2563eb";
4055
+ c.fillRect(0, 0, canvas.width, 16);
4056
+ c.fillStyle = "#ffffff";
4057
+ c.font = "bold 8px sans-serif";
4058
+ c.fillText(`\u{1F4C8} ${label.slice(0, 18)}`, 6, 11);
4059
+ const startY = 17;
4060
+ const rowH = 13;
4061
+ const colW = 28;
4062
+ const cols = Math.min(4, headerCells.length || 4);
4063
+ c.fillStyle = "#f1f5f9";
4064
+ c.fillRect(0, startY, canvas.width, rowH);
4065
+ c.strokeStyle = "#cbd5e1";
4066
+ c.lineWidth = 0.8;
4067
+ c.strokeRect(0, startY, canvas.width, rowH);
4068
+ c.fillStyle = "#64748b";
4069
+ c.font = "bold 7px sans-serif";
4070
+ for (let ci = 0; ci < cols; ci++) {
4071
+ const hName = headerCells[ci] || `Col ${ci + 1}`;
4072
+ c.fillText(hName.slice(0, 5), 8 + ci * colW, startY + 9);
4073
+ }
4074
+ const rowsToPreview = dataLines.slice(pageIdx * ROWS_PER_PAGE, pageIdx * ROWS_PER_PAGE + 5);
4075
+ for (let ri = 0; ri < rowsToPreview.length; ri++) {
4076
+ const y = startY + (ri + 1) * rowH;
4077
+ if (y > canvas.height - 2) break;
4078
+ c.strokeStyle = "#e2e8f0";
4079
+ c.strokeRect(0, y, canvas.width, rowH);
4080
+ const cVals = rowsToPreview[ri].split(delimiter);
4081
+ c.fillStyle = "#334155";
4082
+ c.font = "6px sans-serif";
4083
+ for (let ci = 0; ci < cols; ci++) {
4084
+ const val = cVals[ci] || "";
4085
+ c.fillText(val.trim().slice(0, 6), 8 + ci * colW, y + 9);
4086
+ }
4087
+ }
4088
+ c.strokeStyle = "#cbd5e1";
4089
+ c.lineWidth = 1;
4090
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
4091
+ }
4092
+ });
4093
+ }
4094
+ return thumbnails;
4095
+ },
3307
4096
  zoomIn: () => {
3308
4097
  scale += 0.1;
3309
- container.style.transform = `scale(${scale})`;
4098
+ applyScale();
3310
4099
  },
3311
4100
  zoomOut: () => {
3312
4101
  scale = Math.max(0.2, scale - 0.1);
3313
- container.style.transform = `scale(${scale})`;
4102
+ applyScale();
3314
4103
  },
3315
4104
  getZoom: () => scale,
3316
4105
  setZoom: (level) => {
3317
4106
  scale = level;
3318
- container.style.transform = `scale(${scale})`;
4107
+ applyScale();
3319
4108
  },
3320
4109
  fitToPage: () => {
3321
- scale = 1;
3322
- container.style.transform = `scale(1)`;
4110
+ const availW = Math.max(280, (ctx.container.clientWidth || window.innerWidth) - 48);
4111
+ const tW = table.offsetWidth || 600;
4112
+ scale = Math.max(0.35, Math.min(1, availW / tW));
4113
+ applyScale();
3323
4114
  },
3324
4115
  download: () => {
3325
4116
  const blob = new Blob([ctx.buffer], { type: "text/csv" });
@@ -3383,28 +4174,34 @@ var CodePlugin = class {
3383
4174
  getToolbarActions(instance) {
3384
4175
  const totalPages = instance.getPageCount?.() ?? 1;
3385
4176
  const actions = [];
3386
- if (totalPages > 1) {
3387
- actions.push({
3388
- id: "page-nav",
3389
- icon: "",
3390
- label: "Page Navigation",
3391
- type: "page-nav",
3392
- group: "navigation",
3393
- value: instance.getCurrentPage?.() ?? 1,
3394
- max: totalPages,
3395
- execute: (action, page) => {
3396
- const cur = instance.getCurrentPage?.() ?? 1;
3397
- const max = instance.getPageCount?.() ?? 1;
3398
- if (action === "prev") {
3399
- if (cur > 1) instance.goToPage?.(cur - 1);
3400
- } else if (action === "next") {
3401
- if (cur < max) instance.goToPage?.(cur + 1);
3402
- } else if (typeof page === "number") {
3403
- instance.goToPage?.(page);
3404
- }
4177
+ actions.push({
4178
+ id: "thumbnails",
4179
+ icon: "thumbnails",
4180
+ label: "Page Thumbnails",
4181
+ type: "button",
4182
+ group: "navigation",
4183
+ execute: () => instance.toggleThumbnails?.()
4184
+ });
4185
+ actions.push({
4186
+ id: "page-nav",
4187
+ icon: "",
4188
+ label: "Page Navigation",
4189
+ type: "page-nav",
4190
+ group: "navigation",
4191
+ value: instance.getCurrentPage?.() ?? 1,
4192
+ max: totalPages,
4193
+ execute: (action, page) => {
4194
+ const cur = instance.getCurrentPage?.() ?? 1;
4195
+ const max = instance.getPageCount?.() ?? 1;
4196
+ if (action === "prev") {
4197
+ if (cur > 1) instance.goToPage?.(cur - 1);
4198
+ } else if (action === "next") {
4199
+ if (cur < max) instance.goToPage?.(cur + 1);
4200
+ } else if (typeof page === "number") {
4201
+ instance.goToPage?.(page);
3405
4202
  }
3406
- });
3407
- }
4203
+ }
4204
+ });
3408
4205
  actions.push(
3409
4206
  {
3410
4207
  id: "zoom-out",
@@ -3548,16 +4345,19 @@ var CodePlugin = class {
3548
4345
  let fontSize = 13;
3549
4346
  let rotation = 0;
3550
4347
  let zoomLevel = 1;
4348
+ let isUserZoomed = false;
3551
4349
  const pre = document.createElement("pre");
3552
4350
  const code = document.createElement("code");
3553
4351
  if (isTxt) {
4352
+ container.style.overflowX = "hidden";
4353
+ container.style.overflowY = "auto";
3554
4354
  container.style.backgroundColor = "#f1f5f9";
3555
- container.style.padding = "32px";
4355
+ container.style.padding = "16px 8px";
3556
4356
  container.style.boxSizing = "border-box";
3557
4357
  container.style.display = "flex";
3558
4358
  container.style.flexDirection = "column";
3559
4359
  container.style.alignItems = "center";
3560
- pre.style.margin = "0";
4360
+ pre.style.margin = "0 auto";
3561
4361
  pre.style.fontFamily = "Consolas, 'Courier New', monospace";
3562
4362
  pre.style.fontSize = "13px";
3563
4363
  pre.style.color = "#1e293b";
@@ -3569,10 +4369,12 @@ var CodePlugin = class {
3569
4369
  pre.style.minHeight = "1056px";
3570
4370
  pre.style.padding = "72px 56px";
3571
4371
  pre.style.boxSizing = "border-box";
3572
- pre.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
4372
+ pre.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
3573
4373
  pre.style.borderRadius = "4px";
3574
4374
  pre.style.transformOrigin = "top center";
4375
+ pre.style.transition = "transform 0.15s ease";
3575
4376
  } else {
4377
+ container.style.overflow = "auto";
3576
4378
  container.style.backgroundColor = "#1e1e1e";
3577
4379
  container.style.color = "#d4d4d4";
3578
4380
  container.style.padding = "16px";
@@ -3604,42 +4406,9 @@ var CodePlugin = class {
3604
4406
  renderCodePage(rawPages[0] || (isTxt ? "" : fullText));
3605
4407
  pre.appendChild(code);
3606
4408
  container.appendChild(pre);
3607
- let indicator = null;
3608
- if (totalPages > 1) {
3609
- indicator = document.createElement("div");
3610
- indicator.className = "fp-code-page-indicator";
3611
- indicator.style.position = "sticky";
3612
- indicator.style.bottom = "16px";
3613
- if (isTxt) {
3614
- indicator.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
3615
- indicator.style.color = "#334155";
3616
- indicator.style.border = "1px solid #e2e8f0";
3617
- indicator.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)";
3618
- } else {
3619
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3620
- indicator.style.color = "#f8fafc";
3621
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3622
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3623
- indicator.style.backdropFilter = "blur(8px)";
3624
- }
3625
- indicator.style.fontSize = "12px";
3626
- indicator.style.fontWeight = "600";
3627
- indicator.style.padding = "5px 14px";
3628
- indicator.style.borderRadius = "20px";
3629
- indicator.style.zIndex = "10";
3630
- indicator.style.userSelect = "none";
3631
- indicator.style.pointerEvents = "none";
3632
- indicator.style.textAlign = "center";
3633
- indicator.style.width = "fit-content";
3634
- indicator.style.margin = "16px auto 0";
3635
- container.appendChild(indicator);
3636
- }
3637
4409
  const showPage = (pageNum) => {
3638
4410
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
3639
4411
  renderCodePage(rawPages[currentPage - 1] || (isTxt ? "" : fullText));
3640
- if (indicator) {
3641
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3642
- }
3643
4412
  container.scrollTop = 0;
3644
4413
  ctx.emit("page-change", { page: currentPage, total: totalPages });
3645
4414
  };
@@ -3647,36 +4416,90 @@ var CodePlugin = class {
3647
4416
  showPage(1);
3648
4417
  }
3649
4418
  ctx.container.appendChild(container);
3650
- const cleanup = () => {
3651
- indicator?.remove();
3652
- container.remove();
3653
- ctx.container.innerHTML = "";
4419
+ const calculateTxtFit = () => {
4420
+ const availW = Math.max(280, container.clientWidth - 32);
4421
+ return Math.max(0.4, Math.min(3, availW / 816));
3654
4422
  };
3655
- ctx.signal.addEventListener("abort", cleanup);
3656
4423
  const updateTransform = () => {
3657
4424
  if (isTxt) {
3658
4425
  pre.style.transform = `scale(${zoomLevel}) rotate(${rotation}deg)`;
4426
+ const scaledH = 1056 * zoomLevel;
4427
+ const extraH = Math.max(0, scaledH - 1056);
4428
+ pre.style.marginBottom = `${extraH + 32}px`;
3659
4429
  } else {
3660
4430
  pre.style.transform = `rotate(${rotation}deg)`;
3661
4431
  pre.style.fontSize = `${fontSize}px`;
3662
4432
  }
3663
4433
  };
4434
+ let resizeObserver = null;
4435
+ if (isTxt) {
4436
+ setTimeout(() => {
4437
+ zoomLevel = calculateTxtFit();
4438
+ updateTransform();
4439
+ }, 60);
4440
+ resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
4441
+ if (!isUserZoomed) {
4442
+ zoomLevel = calculateTxtFit();
4443
+ updateTransform();
4444
+ }
4445
+ }) : null;
4446
+ resizeObserver?.observe(container);
4447
+ }
4448
+ const cleanup = () => {
4449
+ resizeObserver?.disconnect();
4450
+ container.remove();
4451
+ ctx.container.innerHTML = "";
4452
+ };
4453
+ ctx.signal.addEventListener("abort", cleanup);
3664
4454
  return {
3665
4455
  destroy: cleanup,
3666
4456
  getPageCount: () => totalPages,
3667
4457
  getCurrentPage: () => currentPage,
3668
4458
  goToPage: (page) => showPage(page),
4459
+ getThumbnails: () => {
4460
+ return rawPages.map((pageText, idx) => ({
4461
+ index: idx,
4462
+ label: `Page ${idx + 1}`,
4463
+ render: async (canvas) => {
4464
+ canvas.width = 140;
4465
+ canvas.height = 180;
4466
+ const c = canvas.getContext("2d");
4467
+ if (!c) return;
4468
+ c.fillStyle = "#ffffff";
4469
+ c.fillRect(0, 0, canvas.width, canvas.height);
4470
+ c.strokeStyle = "#cbd5e1";
4471
+ c.lineWidth = 1;
4472
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
4473
+ c.fillStyle = "#64748b";
4474
+ const lines = (pageText || "").split("\n").slice(0, 24);
4475
+ const lineH = 6;
4476
+ const startY = 14;
4477
+ const startX = 12;
4478
+ const maxW = canvas.width - 24;
4479
+ c.font = "5px monospace";
4480
+ for (let li = 0; li < lines.length; li++) {
4481
+ const l = lines[li].trim();
4482
+ if (!l) continue;
4483
+ const y = startY + li * lineH;
4484
+ if (y > canvas.height - 12) break;
4485
+ c.fillText(l.slice(0, 30), startX, y, maxW);
4486
+ }
4487
+ }
4488
+ }));
4489
+ },
3669
4490
  zoomIn: () => {
4491
+ isUserZoomed = true;
3670
4492
  if (isTxt) {
3671
- zoomLevel = Math.min(3, zoomLevel + 0.1);
4493
+ zoomLevel = Math.min(3.5, zoomLevel + 0.15);
3672
4494
  } else {
3673
4495
  fontSize = Math.min(32, fontSize + 2);
3674
4496
  }
3675
4497
  updateTransform();
3676
4498
  },
3677
4499
  zoomOut: () => {
4500
+ isUserZoomed = true;
3678
4501
  if (isTxt) {
3679
- zoomLevel = Math.max(0.1, zoomLevel - 0.1);
4502
+ zoomLevel = Math.max(0.1, zoomLevel - 0.15);
3680
4503
  } else {
3681
4504
  fontSize = Math.max(8, fontSize - 2);
3682
4505
  }
@@ -3684,6 +4507,7 @@ var CodePlugin = class {
3684
4507
  },
3685
4508
  getZoom: () => isTxt ? zoomLevel : fontSize / 13,
3686
4509
  setZoom: (level) => {
4510
+ isUserZoomed = true;
3687
4511
  if (isTxt) {
3688
4512
  zoomLevel = level;
3689
4513
  } else {
@@ -3692,9 +4516,10 @@ var CodePlugin = class {
3692
4516
  updateTransform();
3693
4517
  },
3694
4518
  fitToPage: () => {
4519
+ isUserZoomed = false;
3695
4520
  fontSize = 13;
3696
4521
  rotation = 0;
3697
- zoomLevel = isTxt ? Math.max(0.65, Math.min(1.05, (container.clientWidth - 48) / 816)) : 1;
4522
+ zoomLevel = isTxt ? calculateTxtFit() : 1;
3698
4523
  updateTransform();
3699
4524
  },
3700
4525
  rotateCW: () => {
@@ -4575,28 +5400,26 @@ var RtfPlugin = class {
4575
5400
  getToolbarActions(instance) {
4576
5401
  const totalPages = instance.getPageCount?.() ?? 1;
4577
5402
  const actions = [];
4578
- if (totalPages > 1) {
4579
- actions.push({
4580
- id: "page-nav",
4581
- icon: "",
4582
- label: "Page Navigation",
4583
- type: "page-nav",
4584
- group: "navigation",
4585
- value: instance.getCurrentPage?.() ?? 1,
4586
- max: totalPages,
4587
- execute: (action, page) => {
4588
- const cur = instance.getCurrentPage?.() ?? 1;
4589
- const max = instance.getPageCount?.() ?? 1;
4590
- if (action === "prev") {
4591
- if (cur > 1) instance.goToPage?.(cur - 1);
4592
- } else if (action === "next") {
4593
- if (cur < max) instance.goToPage?.(cur + 1);
4594
- } else if (typeof page === "number") {
4595
- instance.goToPage?.(page);
4596
- }
5403
+ actions.push({
5404
+ id: "page-nav",
5405
+ icon: "",
5406
+ label: "Page Navigation",
5407
+ type: "page-nav",
5408
+ group: "navigation",
5409
+ value: instance.getCurrentPage?.() ?? 1,
5410
+ max: totalPages,
5411
+ execute: (action, page) => {
5412
+ const cur = instance.getCurrentPage?.() ?? 1;
5413
+ const max = instance.getPageCount?.() ?? 1;
5414
+ if (action === "prev") {
5415
+ if (cur > 1) instance.goToPage?.(cur - 1);
5416
+ } else if (action === "next") {
5417
+ if (cur < max) instance.goToPage?.(cur + 1);
5418
+ } else if (typeof page === "number") {
5419
+ instance.goToPage?.(page);
4597
5420
  }
4598
- });
4599
- }
5421
+ }
5422
+ });
4600
5423
  actions.push(
4601
5424
  {
4602
5425
  id: "zoom-out",
@@ -4668,43 +5491,57 @@ var RtfPlugin = class {
4668
5491
  async render(ctx) {
4669
5492
  const wrapper = document.createElement("div");
4670
5493
  wrapper.className = "fp-rtf-wrapper";
4671
- wrapper.style.padding = "32px";
4672
- wrapper.style.maxWidth = "850px";
5494
+ wrapper.style.padding = "0";
5495
+ wrapper.style.width = "816px";
4673
5496
  wrapper.style.margin = "0 auto";
4674
- wrapper.style.backgroundColor = "#fff";
4675
- wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
4676
- wrapper.style.borderRadius = "4px";
4677
- wrapper.style.minHeight = "100%";
5497
+ wrapper.style.boxSizing = "border-box";
5498
+ wrapper.style.display = "flex";
5499
+ wrapper.style.flexDirection = "column";
5500
+ wrapper.style.alignItems = "center";
5501
+ wrapper.style.backgroundColor = "transparent";
4678
5502
  wrapper.style.transformOrigin = "top center";
4679
- wrapper.style.transition = "transform 0.2s ease";
4680
- ctx.container.style.overflow = "auto";
4681
- ctx.container.style.padding = "24px";
5503
+ wrapper.style.transition = "transform 0.15s ease";
5504
+ ctx.container.style.overflowX = "hidden";
5505
+ ctx.container.style.overflowY = "auto";
5506
+ ctx.container.style.padding = "16px 8px";
4682
5507
  ctx.container.style.backgroundColor = "#f1f5f9";
4683
5508
  ctx.container.appendChild(wrapper);
4684
5509
  let scale = 1;
4685
5510
  let rotation = 0;
4686
5511
  let pageElements = [];
4687
- let fitMode = "width";
5512
+ let isUserZoomed = false;
5513
+ let fitMode = ctx?.options?.fitMode || "width";
4688
5514
  const calculateFitScale = (mode = fitMode) => {
4689
5515
  const activeEl = pageElements[currentPage - 1] || wrapper;
4690
- const elW = activeEl.offsetWidth || 850;
4691
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
4692
- const availW = Math.max(280, ctx.container.clientWidth - 48);
4693
- const availH = Math.max(280, ctx.container.clientHeight - 80);
5516
+ const elW = 816;
5517
+ const singlePageH = activeEl?.offsetHeight || 1056;
5518
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
5519
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
4694
5520
  const sW = availW / elW;
4695
5521
  const sH = availH / singlePageH;
4696
5522
  if (mode === "page") {
4697
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
5523
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
4698
5524
  }
4699
- return Math.max(0.65, Math.min(1.05, sW));
5525
+ return Math.max(0.4, Math.min(3, sW));
4700
5526
  };
4701
5527
  const applyTransform = () => {
4702
5528
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
4703
5529
  wrapper.style.transformOrigin = "top center";
5530
+ const activeEl = pageElements[currentPage - 1];
5531
+ const baseH = activeEl?.offsetHeight || 1056;
5532
+ const scaledH = baseH * scale;
5533
+ const extraH = Math.max(0, scaledH - baseH);
5534
+ wrapper.style.marginBottom = `${extraH + 32}px`;
4704
5535
  };
5536
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
5537
+ if (!isUserZoomed) {
5538
+ scale = calculateFitScale(fitMode);
5539
+ applyTransform();
5540
+ }
5541
+ }) : null;
5542
+ resizeObserver?.observe(ctx.container);
4705
5543
  setTimeout(() => {
4706
- fitMode = "width";
4707
- scale = calculateFitScale("width");
5544
+ scale = calculateFitScale(fitMode);
4708
5545
  applyTransform();
4709
5546
  }, 60);
4710
5547
  try {
@@ -4714,20 +5551,34 @@ var RtfPlugin = class {
4714
5551
  const doc = new RTFJS.Document(ctx.buffer, {});
4715
5552
  const htmlElements = await doc.render();
4716
5553
  const contentNodes = [];
4717
- for (const item of htmlElements) {
4718
- if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4719
- contentNodes.push(...Array.from(item.children));
4720
- } else {
4721
- contentNodes.push(item);
5554
+ const extractBlocks = (nodes) => {
5555
+ for (const item of nodes) {
5556
+ if (!item || !(item instanceof HTMLElement)) continue;
5557
+ const tag = item.tagName.toLowerCase();
5558
+ const hasChildBlocks = Array.from(item.children).some((c) => {
5559
+ const ct = c.tagName.toLowerCase();
5560
+ return ["p", "div", "table", "h1", "h2", "h3", "h4", "h5", "h6", "section", "article", "ul", "ol"].includes(ct);
5561
+ });
5562
+ if (hasChildBlocks && !["table", "tr", "td", "th", "ul", "ol", "li"].includes(tag)) {
5563
+ extractBlocks(Array.from(item.children));
5564
+ } else {
5565
+ if (tag === "p" || tag === "div") {
5566
+ item.style.display = "block";
5567
+ item.style.marginBottom = "12px";
5568
+ item.style.lineHeight = "1.6";
5569
+ }
5570
+ contentNodes.push(item);
5571
+ }
4722
5572
  }
4723
- }
5573
+ };
5574
+ extractBlocks(htmlElements);
4724
5575
  wrapper.innerHTML = "";
4725
5576
  contentNodes.forEach((node) => wrapper.appendChild(node));
4726
5577
  const childHeights = contentNodes.map((c) => {
4727
5578
  const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4728
5579
  const offH = c.offsetHeight || 0;
4729
5580
  const textLen = c.textContent?.trim().length || 0;
4730
- const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
5581
+ const estH = Math.max(28, Math.ceil(textLen / 75) * 24 + 16);
4731
5582
  return Math.max(rectH, offH, estH);
4732
5583
  });
4733
5584
  wrapper.innerHTML = "";
@@ -4744,6 +5595,7 @@ var RtfPlugin = class {
4744
5595
  card.style.marginBottom = "24px";
4745
5596
  card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4746
5597
  card.style.lineHeight = "1.6";
5598
+ card.style.color = "#1e293b";
4747
5599
  return card;
4748
5600
  };
4749
5601
  let curCard = createRtfCard();
@@ -4779,9 +5631,8 @@ var RtfPlugin = class {
4779
5631
  pageCard.style.padding = "72px 56px";
4780
5632
  pageCard.style.width = "816px";
4781
5633
  pageCard.style.minHeight = "1056px";
4782
- pageCard.style.maxWidth = "100%";
4783
5634
  pageCard.style.boxSizing = "border-box";
4784
- pageCard.style.fontFamily = "serif";
5635
+ pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4785
5636
  pageCard.style.fontSize = "12pt";
4786
5637
  pageCard.style.lineHeight = "1.6";
4787
5638
  pageCard.style.color = "#1e293b";
@@ -4794,29 +5645,6 @@ var RtfPlugin = class {
4794
5645
  }
4795
5646
  const totalPages = Math.max(1, pageElements.length);
4796
5647
  let currentPage = 1;
4797
- let indicator = null;
4798
- if (totalPages > 1) {
4799
- indicator = document.createElement("div");
4800
- indicator.className = "fp-rtf-page-indicator";
4801
- indicator.style.position = "sticky";
4802
- indicator.style.bottom = "16px";
4803
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4804
- indicator.style.backdropFilter = "blur(8px)";
4805
- indicator.style.color = "#f8fafc";
4806
- indicator.style.fontSize = "12px";
4807
- indicator.style.fontWeight = "600";
4808
- indicator.style.padding = "5px 14px";
4809
- indicator.style.borderRadius = "20px";
4810
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4811
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4812
- indicator.style.zIndex = "10";
4813
- indicator.style.userSelect = "none";
4814
- indicator.style.pointerEvents = "none";
4815
- indicator.style.textAlign = "center";
4816
- indicator.style.width = "fit-content";
4817
- indicator.style.margin = "16px auto 0";
4818
- ctx.container.appendChild(indicator);
4819
- }
4820
5648
  const showPage = (pageNum) => {
4821
5649
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
4822
5650
  if (totalPages > 1) {
@@ -4824,9 +5652,6 @@ var RtfPlugin = class {
4824
5652
  el.style.display = idx + 1 === currentPage ? "block" : "none";
4825
5653
  });
4826
5654
  }
4827
- if (indicator) {
4828
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4829
- }
4830
5655
  ctx.container.scrollTop = 0;
4831
5656
  ctx.emit("page-change", { page: currentPage, total: totalPages });
4832
5657
  };
@@ -4834,7 +5659,7 @@ var RtfPlugin = class {
4834
5659
  showPage(1);
4835
5660
  }
4836
5661
  const cleanup = () => {
4837
- indicator?.remove();
5662
+ resizeObserver?.disconnect();
4838
5663
  wrapper.remove();
4839
5664
  ctx.container.innerHTML = "";
4840
5665
  };
@@ -4845,21 +5670,25 @@ var RtfPlugin = class {
4845
5670
  getCurrentPage: () => currentPage,
4846
5671
  goToPage: (page) => showPage(page),
4847
5672
  zoomIn: () => {
5673
+ isUserZoomed = true;
4848
5674
  scale += 0.15;
4849
5675
  applyTransform();
4850
5676
  },
4851
5677
  zoomOut: () => {
5678
+ isUserZoomed = true;
4852
5679
  scale = Math.max(0.2, scale - 0.15);
4853
5680
  applyTransform();
4854
5681
  },
4855
5682
  getZoom: () => scale,
4856
5683
  setZoom: (level) => {
5684
+ isUserZoomed = true;
4857
5685
  scale = level;
4858
5686
  applyTransform();
4859
5687
  },
4860
5688
  fitToPage: () => {
4861
- fitMode = fitMode === "width" ? "page" : "width";
4862
- scale = calculateFitScale(fitMode);
5689
+ isUserZoomed = false;
5690
+ fitMode = "width";
5691
+ scale = calculateFitScale("width");
4863
5692
  rotation = 0;
4864
5693
  applyTransform();
4865
5694
  },
@@ -5047,40 +5876,36 @@ var OpenDocumentPlugin = class {
5047
5876
  const isPresentation = instance.isPresentation;
5048
5877
  const totalPages = instance.getPageCount?.() ?? 1;
5049
5878
  const actions = [];
5050
- if (isPresentation || totalPages > 1) {
5051
- if (isPresentation) {
5052
- actions.push({
5053
- id: "thumbnails",
5054
- icon: "thumbnails",
5055
- label: "Slide Thumbnails",
5056
- type: "button",
5057
- group: "navigation",
5058
- execute: () => instance.toggleThumbnails?.()
5059
- });
5060
- }
5061
- actions.push({
5062
- id: "page-nav",
5063
- icon: "",
5064
- label: isPresentation ? "Slide Navigation" : "Page Navigation",
5065
- type: "page-nav",
5066
- group: "navigation",
5067
- value: instance.getCurrentPage?.() ?? 1,
5068
- max: totalPages,
5069
- execute: (action, page) => {
5070
- const cur = instance.getCurrentPage?.() ?? 1;
5071
- const max = instance.getPageCount?.() ?? 1;
5072
- if (action === "prev") {
5073
- if (cur > 1) instance.goToPage?.(cur - 1);
5074
- } else if (action === "next") {
5075
- if (cur < max) instance.goToPage?.(cur + 1);
5076
- } else if (typeof page === "number") {
5077
- instance.goToPage?.(page);
5078
- } else if (typeof action === "number") {
5079
- instance.goToPage?.(action);
5080
- }
5879
+ actions.push({
5880
+ id: "thumbnails",
5881
+ icon: "thumbnails",
5882
+ label: isPresentation ? "Slide Thumbnails" : "Page Thumbnails",
5883
+ type: "button",
5884
+ group: "navigation",
5885
+ execute: () => instance.toggleThumbnails?.()
5886
+ });
5887
+ actions.push({
5888
+ id: "page-nav",
5889
+ icon: "",
5890
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
5891
+ type: "page-nav",
5892
+ group: "navigation",
5893
+ value: instance.getCurrentPage?.() ?? 1,
5894
+ max: totalPages,
5895
+ execute: (action, page) => {
5896
+ const cur = instance.getCurrentPage?.() ?? 1;
5897
+ const max = instance.getPageCount?.() ?? 1;
5898
+ if (action === "prev") {
5899
+ if (cur > 1) instance.goToPage?.(cur - 1);
5900
+ } else if (action === "next") {
5901
+ if (cur < max) instance.goToPage?.(cur + 1);
5902
+ } else if (typeof page === "number") {
5903
+ instance.goToPage?.(page);
5904
+ } else if (typeof action === "number") {
5905
+ instance.goToPage?.(action);
5081
5906
  }
5082
- });
5083
- }
5907
+ }
5908
+ });
5084
5909
  actions.push(
5085
5910
  {
5086
5911
  id: "zoom-out",
@@ -5171,49 +5996,65 @@ var OpenDocumentPlugin = class {
5171
5996
  container.className = "fp-odf-container";
5172
5997
  container.style.width = "100%";
5173
5998
  container.style.height = "100%";
5174
- container.style.overflow = "auto";
5175
- container.style.padding = "24px";
5999
+ container.style.overflowX = "hidden";
6000
+ container.style.overflowY = "auto";
6001
+ container.style.padding = "16px 8px";
5176
6002
  container.style.backgroundColor = "#f1f5f9";
5177
6003
  const wrapper = document.createElement("div");
5178
6004
  wrapper.className = "fp-odf-wrapper";
5179
6005
  wrapper.style.margin = "0 auto";
6006
+ wrapper.style.width = isPresentation ? "960px" : "816px";
6007
+ wrapper.style.boxSizing = "border-box";
5180
6008
  wrapper.style.backgroundColor = "#ffffff";
5181
6009
  wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
5182
6010
  wrapper.style.borderRadius = "4px";
5183
6011
  wrapper.style.transformOrigin = "top center";
5184
- wrapper.style.transition = "transform 0.2s ease";
6012
+ wrapper.style.transition = "transform 0.15s ease";
5185
6013
  container.appendChild(wrapper);
5186
6014
  ctx.container.appendChild(container);
5187
6015
  let scale = 1;
5188
6016
  let rotation = 0;
5189
- let fitMode = "width";
6017
+ let isUserZoomed = false;
6018
+ let fitMode = ctx?.options?.fitMode || "width";
6019
+ let currentPage = 1;
6020
+ let totalPages = 1;
6021
+ let slides = [];
5190
6022
  const calculateFitScale = (mode = fitMode) => {
5191
- const elW = wrapper.offsetWidth || 850;
5192
- const singlePageH = Math.min(wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
5193
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5194
- const availH = Math.max(280, ctx.container.clientHeight - 80);
6023
+ const activeSlide = slides[currentPage - 1];
6024
+ const elW = isPresentation ? 960 : 816;
6025
+ const singlePageH = Math.min(activeSlide?.offsetHeight || wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
6026
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
6027
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5195
6028
  const sW = availW / elW;
5196
6029
  const sH = availH / (isPresentation ? 540 : singlePageH);
5197
6030
  if (isPresentation) {
5198
- return Math.min(1, Math.min(sW, sH));
6031
+ return Math.min(2.5, Math.min(sW, sH));
5199
6032
  }
5200
6033
  if (mode === "page") {
5201
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
6034
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5202
6035
  }
5203
- return Math.max(0.65, Math.min(1.05, sW));
6036
+ return Math.max(0.4, Math.min(3, sW));
5204
6037
  };
5205
6038
  const applyTransform = () => {
5206
6039
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
5207
6040
  wrapper.style.transformOrigin = "top center";
6041
+ const activeSlide = slides[currentPage - 1];
6042
+ const baseH = activeSlide?.offsetHeight || wrapper.offsetHeight || 1056;
6043
+ const scaledH = baseH * scale;
6044
+ const extraH = Math.max(0, scaledH - baseH);
6045
+ wrapper.style.marginBottom = `${extraH + 32}px`;
5208
6046
  };
6047
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
6048
+ if (!isUserZoomed) {
6049
+ scale = calculateFitScale(fitMode);
6050
+ applyTransform();
6051
+ }
6052
+ }) : null;
6053
+ resizeObserver?.observe(ctx.container);
5209
6054
  setTimeout(() => {
5210
- fitMode = "width";
5211
- scale = calculateFitScale("width");
6055
+ scale = calculateFitScale(fitMode);
5212
6056
  applyTransform();
5213
6057
  }, 60);
5214
- let currentPage = 1;
5215
- let totalPages = 1;
5216
- let slides = [];
5217
6058
  if (isPresentation) {
5218
6059
  wrapper.style.maxWidth = "960px";
5219
6060
  wrapper.style.aspectRatio = "16 / 9";
@@ -5314,24 +6155,9 @@ var OpenDocumentPlugin = class {
5314
6155
  wrapper.appendChild(page);
5315
6156
  slides.push(page);
5316
6157
  });
5317
- const pageIndicator = document.createElement("div");
5318
- pageIndicator.className = "fp-odt-page-indicator";
5319
- pageIndicator.style.position = "sticky";
5320
- pageIndicator.style.bottom = "16px";
5321
- pageIndicator.style.left = "50%";
5322
- pageIndicator.style.transform = "translateX(-50%)";
5323
- pageIndicator.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
5324
- pageIndicator.style.color = "#fff";
5325
- pageIndicator.style.padding = "6px 12px";
5326
- pageIndicator.style.borderRadius = "16px";
5327
- pageIndicator.style.fontSize = "12px";
5328
- pageIndicator.style.zIndex = "100";
5329
- pageIndicator.style.display = "inline-block";
5330
- pageIndicator.style.width = "fit-content";
5331
- pageIndicator.textContent = `Page 1 of ${totalPages}`;
5332
- container.appendChild(pageIndicator);
5333
6158
  }
5334
6159
  const cleanup = () => {
6160
+ resizeObserver?.disconnect();
5335
6161
  imageUrls.forEach((url) => URL.revokeObjectURL(url));
5336
6162
  container.remove();
5337
6163
  ctx.container.innerHTML = "";
@@ -5343,10 +6169,6 @@ var OpenDocumentPlugin = class {
5343
6169
  slides.forEach((s, idx) => {
5344
6170
  s.style.display = idx === page - 1 ? "block" : "none";
5345
6171
  });
5346
- const indicator = container.querySelector(".fp-odt-page-indicator");
5347
- if (indicator) {
5348
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5349
- }
5350
6172
  container.scrollTop = 0;
5351
6173
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5352
6174
  };
@@ -5354,21 +6176,25 @@ var OpenDocumentPlugin = class {
5354
6176
  destroy: cleanup,
5355
6177
  isPresentation,
5356
6178
  zoomIn: () => {
6179
+ isUserZoomed = true;
5357
6180
  scale += 0.15;
5358
6181
  applyTransform();
5359
6182
  },
5360
6183
  zoomOut: () => {
6184
+ isUserZoomed = true;
5361
6185
  scale = Math.max(0.2, scale - 0.15);
5362
6186
  applyTransform();
5363
6187
  },
5364
6188
  getZoom: () => scale,
5365
6189
  setZoom: (level) => {
6190
+ isUserZoomed = true;
5366
6191
  scale = level;
5367
6192
  applyTransform();
5368
6193
  },
5369
6194
  fitToPage: () => {
5370
- fitMode = fitMode === "width" ? "page" : "width";
5371
- scale = calculateFitScale(fitMode);
6195
+ isUserZoomed = false;
6196
+ fitMode = "width";
6197
+ scale = calculateFitScale("width");
5372
6198
  rotation = 0;
5373
6199
  applyTransform();
5374
6200
  },
@@ -5384,23 +6210,45 @@ var OpenDocumentPlugin = class {
5384
6210
  getPageCount: () => totalPages,
5385
6211
  getCurrentPage: () => currentPage,
5386
6212
  getThumbnails: async () => {
5387
- if (!isPresentation) return [];
5388
- return slides.map((_, idx) => ({
5389
- index: idx,
5390
- label: `Slide ${idx + 1}`,
5391
- render: async (canvas) => {
5392
- const ctx2d = canvas.getContext("2d");
5393
- if (!ctx2d) return;
5394
- canvas.width = 160;
5395
- canvas.height = 90;
5396
- ctx2d.fillStyle = "#f8fafc";
5397
- ctx2d.fillRect(0, 0, 160, 90);
5398
- ctx2d.fillStyle = "#334155";
5399
- ctx2d.font = "bold 12px sans-serif";
5400
- ctx2d.textAlign = "center";
5401
- ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
5402
- }
5403
- }));
6213
+ const count = totalPages || slides.length || 1;
6214
+ const items = [];
6215
+ for (let idx = 0; idx < count; idx++) {
6216
+ const itemIdx = idx;
6217
+ items.push({
6218
+ index: itemIdx,
6219
+ label: isPresentation ? `Slide ${itemIdx + 1}` : `Page ${itemIdx + 1}`,
6220
+ render: async (canvas) => {
6221
+ const ctx2d = canvas.getContext("2d");
6222
+ if (!ctx2d) return;
6223
+ if (isPresentation) {
6224
+ canvas.width = 160;
6225
+ canvas.height = 90;
6226
+ ctx2d.fillStyle = "#f8fafc";
6227
+ ctx2d.fillRect(0, 0, 160, 90);
6228
+ ctx2d.strokeStyle = "#cbd5e1";
6229
+ ctx2d.lineWidth = 1;
6230
+ ctx2d.strokeRect(0.5, 0.5, 159, 89);
6231
+ ctx2d.fillStyle = "#334155";
6232
+ ctx2d.font = "bold 11px sans-serif";
6233
+ ctx2d.textAlign = "center";
6234
+ ctx2d.fillText(`Slide ${itemIdx + 1}`, 80, 50);
6235
+ } else {
6236
+ canvas.width = 140;
6237
+ canvas.height = 180;
6238
+ ctx2d.fillStyle = "#ffffff";
6239
+ ctx2d.fillRect(0, 0, 140, 180);
6240
+ ctx2d.strokeStyle = "#cbd5e1";
6241
+ ctx2d.lineWidth = 1;
6242
+ ctx2d.strokeRect(0.5, 0.5, 139, 179);
6243
+ ctx2d.fillStyle = "#64748b";
6244
+ ctx2d.font = "bold 10px sans-serif";
6245
+ ctx2d.textAlign = "center";
6246
+ ctx2d.fillText(`Page ${itemIdx + 1}`, 70, 90);
6247
+ }
6248
+ }
6249
+ });
6250
+ }
6251
+ return items;
5404
6252
  },
5405
6253
  download: () => {
5406
6254
  const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
@@ -5505,23 +6353,24 @@ var OpenDocumentPlugin = class {
5505
6353
  case "h": {
5506
6354
  const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
5507
6355
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5508
- html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
6356
+ html += `<h${level} style="${style}; margin: 18px 0 8px; color: #1e3a8a; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${node.textContent || ""}</h${level}>`;
5509
6357
  break;
5510
6358
  }
5511
6359
  case "p": {
5512
6360
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5513
6361
  const inner = this.renderSpans(node, styles, images);
5514
- html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
6362
+ const isInsideLi = node.parentElement?.localName === "list-item" || node.parentElement?.tagName?.toLowerCase() === "list-item";
6363
+ html += `<p style="${style}; line-height: 1.5; margin: ${isInsideLi ? "0" : "8px 0 12px"}; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${inner}</p>`;
5515
6364
  break;
5516
6365
  }
5517
6366
  case "list": {
5518
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
6367
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
5519
6368
  Array.from(node.children).forEach((c) => walk(c));
5520
6369
  html += "</ul>";
5521
6370
  break;
5522
6371
  }
5523
6372
  case "list-item": {
5524
- html += "<li>";
6373
+ html += '<li style="margin: 4px 0; font-size: 11pt; line-height: 1.5; color: #1e293b;">';
5525
6374
  Array.from(node.children).forEach((c) => walk(c));
5526
6375
  html += "</li>";
5527
6376
  break;
@@ -5647,28 +6496,26 @@ var DocPlugin = class {
5647
6496
  getToolbarActions(instance) {
5648
6497
  const totalPages = instance.getPageCount?.() ?? 1;
5649
6498
  const actions = [];
5650
- if (totalPages > 1) {
5651
- actions.push({
5652
- id: "page-nav",
5653
- icon: "",
5654
- label: "Page Navigation",
5655
- type: "page-nav",
5656
- group: "navigation",
5657
- value: instance.getCurrentPage?.() ?? 1,
5658
- max: totalPages,
5659
- execute: (action, page) => {
5660
- const cur = instance.getCurrentPage?.() ?? 1;
5661
- const max = instance.getPageCount?.() ?? 1;
5662
- if (action === "prev") {
5663
- if (cur > 1) instance.goToPage?.(cur - 1);
5664
- } else if (action === "next") {
5665
- if (cur < max) instance.goToPage?.(cur + 1);
5666
- } else if (typeof page === "number") {
5667
- instance.goToPage?.(page);
5668
- }
6499
+ actions.push({
6500
+ id: "page-nav",
6501
+ icon: "",
6502
+ label: "Page Navigation",
6503
+ type: "page-nav",
6504
+ group: "navigation",
6505
+ value: instance.getCurrentPage?.() ?? 1,
6506
+ max: totalPages,
6507
+ execute: (action, page) => {
6508
+ const cur = instance.getCurrentPage?.() ?? 1;
6509
+ const max = instance.getPageCount?.() ?? 1;
6510
+ if (action === "prev") {
6511
+ if (cur > 1) instance.goToPage?.(cur - 1);
6512
+ } else if (action === "next") {
6513
+ if (cur < max) instance.goToPage?.(cur + 1);
6514
+ } else if (typeof page === "number") {
6515
+ instance.goToPage?.(page);
5669
6516
  }
5670
- });
5671
- }
6517
+ }
6518
+ });
5672
6519
  actions.push(
5673
6520
  {
5674
6521
  id: "zoom-out",
@@ -5742,8 +6589,9 @@ var DocPlugin = class {
5742
6589
  container.className = "fp-doc-container";
5743
6590
  container.style.width = "100%";
5744
6591
  container.style.height = "100%";
5745
- container.style.overflow = "auto";
5746
- container.style.padding = "32px 16px";
6592
+ container.style.overflowX = "hidden";
6593
+ container.style.overflowY = "auto";
6594
+ container.style.padding = "16px 8px";
5747
6595
  container.style.backgroundColor = "#f1f5f9";
5748
6596
  container.style.display = "flex";
5749
6597
  container.style.justifyContent = "center";
@@ -5752,8 +6600,17 @@ var DocPlugin = class {
5752
6600
  let scale = 1;
5753
6601
  let extractedRawText = "";
5754
6602
  let isFallback = false;
6603
+ let chartSvg = "";
5755
6604
  try {
5756
6605
  const cfbf = new CfbfReader(ctx.buffer);
6606
+ try {
6607
+ const pkg = cfbf.readStream("package_stream");
6608
+ if (pkg && pkg.length > 100) {
6609
+ chartSvg = this.parseOdfChartToSvg(pkg);
6610
+ }
6611
+ } catch (chartErr) {
6612
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
6613
+ }
5757
6614
  const wordDocStream = cfbf.readStream("WordDocument");
5758
6615
  if (!wordDocStream || wordDocStream.length < 512) {
5759
6616
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5771,7 +6628,7 @@ var DocPlugin = class {
5771
6628
  extractedRawText = fallback;
5772
6629
  isFallback = true;
5773
6630
  }
5774
- const rawPages = this.splitIntoPages(extractedRawText);
6631
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5775
6632
  const totalPages = Math.max(1, rawPages.length);
5776
6633
  let currentPage = 1;
5777
6634
  const pageCards = [];
@@ -5779,16 +6636,15 @@ var DocPlugin = class {
5779
6636
  const pageCard = document.createElement("div");
5780
6637
  pageCard.className = "fp-doc-page-card";
5781
6638
  pageCard.style.width = "816px";
5782
- pageCard.style.height = "1056px";
6639
+ pageCard.style.minHeight = "1056px";
5783
6640
  pageCard.style.backgroundColor = "#ffffff";
5784
- pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
6641
+ pageCard.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
5785
6642
  pageCard.style.borderRadius = "4px";
5786
- pageCard.style.padding = "96px 72px";
6643
+ pageCard.style.padding = "72px 56px";
5787
6644
  pageCard.style.boxSizing = "border-box";
5788
- pageCard.style.overflow = "hidden";
5789
6645
  pageCard.style.display = i === 0 ? "block" : "none";
5790
6646
  pageCard.style.transformOrigin = "top center";
5791
- pageCard.style.transition = "transform 0.2s ease";
6647
+ pageCard.style.transition = "transform 0.15s ease";
5792
6648
  pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
5793
6649
  pageCard.style.color = "#1e293b";
5794
6650
  if (isFallback && i === 0) {
@@ -5805,34 +6661,15 @@ var DocPlugin = class {
5805
6661
  container.appendChild(pageCard);
5806
6662
  pageCards.push(pageCard);
5807
6663
  }
5808
- let indicator = null;
5809
- if (totalPages > 1) {
5810
- indicator = document.createElement("div");
5811
- indicator.className = "fp-doc-page-indicator";
5812
- indicator.style.position = "fixed";
5813
- indicator.style.bottom = "16px";
5814
- indicator.style.left = "50%";
5815
- indicator.style.transform = "translateX(-50%)";
5816
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
5817
- indicator.style.backdropFilter = "blur(8px)";
5818
- indicator.style.color = "#f8fafc";
5819
- indicator.style.fontSize = "12px";
5820
- indicator.style.fontWeight = "600";
5821
- indicator.style.padding = "5px 14px";
5822
- indicator.style.borderRadius = "20px";
5823
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
5824
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
5825
- indicator.style.zIndex = "10";
5826
- indicator.style.userSelect = "none";
5827
- indicator.style.pointerEvents = "none";
5828
- indicator.style.textAlign = "center";
5829
- container.appendChild(indicator);
5830
- }
5831
6664
  let rotation = 0;
5832
6665
  const applyTransform = () => {
5833
6666
  const activeCard = pageCards[currentPage - 1];
5834
6667
  if (activeCard) {
5835
6668
  activeCard.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
6669
+ const baseH = activeCard.offsetHeight || 1056;
6670
+ const scaledH = baseH * scale;
6671
+ const extraH = Math.max(0, scaledH - baseH);
6672
+ activeCard.style.marginBottom = `${extraH + 32}px`;
5836
6673
  }
5837
6674
  };
5838
6675
  const showPage = (pageNum) => {
@@ -5845,35 +6682,39 @@ var DocPlugin = class {
5845
6682
  card.style.display = "none";
5846
6683
  }
5847
6684
  });
5848
- if (indicator) {
5849
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5850
- }
5851
6685
  container.scrollTop = 0;
5852
6686
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5853
6687
  };
5854
6688
  if (totalPages > 1) {
5855
6689
  showPage(1);
5856
6690
  }
5857
- let fitMode = "width";
6691
+ let fitMode = ctx?.options?.fitMode || "width";
6692
+ let isUserZoomed = false;
5858
6693
  const calculateFitScale = (mode = fitMode) => {
5859
6694
  const elW = 816;
5860
6695
  const elH = 1056;
5861
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5862
- const availH = Math.max(280, ctx.container.clientHeight - 80);
6696
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
6697
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5863
6698
  const sW = availW / elW;
5864
6699
  const sH = availH / elH;
5865
6700
  if (mode === "page") {
5866
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
6701
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5867
6702
  }
5868
- return Math.max(0.65, Math.min(1.05, sW));
6703
+ return Math.max(0.4, Math.min(3, sW));
5869
6704
  };
6705
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
6706
+ if (!isUserZoomed) {
6707
+ scale = calculateFitScale(fitMode);
6708
+ applyTransform();
6709
+ }
6710
+ }) : null;
6711
+ resizeObserver?.observe(ctx.container);
5870
6712
  setTimeout(() => {
5871
- fitMode = "width";
5872
- scale = calculateFitScale("width");
6713
+ scale = calculateFitScale(fitMode);
5873
6714
  applyTransform();
5874
6715
  }, 60);
5875
6716
  const cleanup = () => {
5876
- indicator?.remove();
6717
+ resizeObserver?.disconnect();
5877
6718
  container.remove();
5878
6719
  ctx.container.innerHTML = "";
5879
6720
  };
@@ -5884,21 +6725,25 @@ var DocPlugin = class {
5884
6725
  getCurrentPage: () => currentPage,
5885
6726
  goToPage: (page) => showPage(page),
5886
6727
  zoomIn: () => {
6728
+ isUserZoomed = true;
5887
6729
  scale += 0.15;
5888
6730
  applyTransform();
5889
6731
  },
5890
6732
  zoomOut: () => {
6733
+ isUserZoomed = true;
5891
6734
  scale = Math.max(0.2, scale - 0.15);
5892
6735
  applyTransform();
5893
6736
  },
5894
6737
  getZoom: () => scale,
5895
6738
  setZoom: (level) => {
6739
+ isUserZoomed = true;
5896
6740
  scale = level;
5897
6741
  applyTransform();
5898
6742
  },
5899
6743
  fitToPage: () => {
5900
- fitMode = fitMode === "width" ? "page" : "width";
5901
- scale = calculateFitScale(fitMode);
6744
+ isUserZoomed = false;
6745
+ fitMode = "width";
6746
+ scale = calculateFitScale("width");
5902
6747
  rotation = 0;
5903
6748
  applyTransform();
5904
6749
  },
@@ -6013,7 +6858,7 @@ var DocPlugin = class {
6013
6858
  for (const run of [...ansiRuns, ...utf16Runs]) {
6014
6859
  const trimmed = run.trim();
6015
6860
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
6016
- if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^[\W_0-9]+$/.test(trimmed)) {
6861
+ if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^EMBED\b/i.test(trimmed) && !trimmed.includes("ChartDocument") && !/^[\W_0-9]+$/.test(trimmed)) {
6017
6862
  seen.add(trimmed);
6018
6863
  candidateLines.push(trimmed);
6019
6864
  }
@@ -6024,9 +6869,114 @@ var DocPlugin = class {
6024
6869
  heuristicTextExtraction(buffer) {
6025
6870
  return this.extractStringsFromBytes(new Uint8Array(buffer));
6026
6871
  }
6027
- cleanWordDocFields(text) {
6872
+ /**
6873
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6874
+ */
6875
+ parseOdfChartToSvg(zipBytes) {
6876
+ try {
6877
+ const unzipped = fflate.unzipSync(zipBytes);
6878
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6879
+ if (!contentXml) return "";
6880
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6881
+ if (rowsMatch.length < 2) return "";
6882
+ const headers = [];
6883
+ const firstRow = rowsMatch[0];
6884
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6885
+ for (const h of headerCells) {
6886
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6887
+ }
6888
+ const categories = [];
6889
+ const seriesValues = headers.map(() => []);
6890
+ for (let r = 1; r < rowsMatch.length; r++) {
6891
+ const rowStr = rowsMatch[r];
6892
+ if (!rowStr) continue;
6893
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6894
+ if (cells.length > 0 && cells[0]) {
6895
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6896
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6897
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6898
+ const cellStr = cells[c];
6899
+ if (!cellStr) continue;
6900
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6901
+ const series = seriesValues[c - 1];
6902
+ if (series) {
6903
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6904
+ }
6905
+ }
6906
+ }
6907
+ }
6908
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6909
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6910
+ let cIdx = 0;
6911
+ for (const cm of colorMatches) {
6912
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6913
+ cIdx++;
6914
+ }
6915
+ let maxVal = 10;
6916
+ for (const s of seriesValues) {
6917
+ for (const v of s) {
6918
+ if (v > maxVal) maxVal = v;
6919
+ }
6920
+ }
6921
+ maxVal = Math.ceil(maxVal * 1.15);
6922
+ const width = 560;
6923
+ const height = 280;
6924
+ const padLeft = 45;
6925
+ const padRight = 100;
6926
+ const padTop = 20;
6927
+ const padBottom = 40;
6928
+ const chartW = width - padLeft - padRight;
6929
+ const chartH = height - padTop - padBottom;
6930
+ let svg = `<svg viewBox="0 0 ${width} ${height}" width="100%" height="auto" style="max-width: 560px; height: 280px; margin: 16px auto; display: block; font-family: Calibri, sans-serif; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.05);">`;
6931
+ for (let step = 0; step <= 4; step++) {
6932
+ const yVal = (maxVal / 4 * step).toFixed(1);
6933
+ const yPos = padTop + chartH - step / 4 * chartH;
6934
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6935
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6936
+ }
6937
+ const numCats = categories.length;
6938
+ const numSeries = headers.length;
6939
+ const groupW = chartW / numCats;
6940
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6941
+ const groupPad = (groupW - barW * numSeries) / 2;
6942
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6943
+ const groupX = padLeft + catIdx * groupW + groupPad;
6944
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6945
+ const val = seriesValues[sIdx][catIdx] || 0;
6946
+ const barH = val / maxVal * chartH;
6947
+ const barX = groupX + sIdx * barW;
6948
+ const barY = padTop + chartH - barH;
6949
+ const col = colors[sIdx % colors.length];
6950
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6951
+ }
6952
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6953
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6954
+ }
6955
+ let legendY = padTop + 20;
6956
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6957
+ const col = colors[sIdx % colors.length];
6958
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6959
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6960
+ legendY += 20;
6961
+ }
6962
+ svg += "</svg>";
6963
+ return svg;
6964
+ } catch (e) {
6965
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6966
+ return "";
6967
+ }
6968
+ }
6969
+ cleanWordDocFields(text, chartSvg = "") {
6028
6970
  if (!text) return "";
6029
6971
  let cleaned = text.replace(
6972
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6973
+ () => chartSvg ? `
6974
+
6975
+ ${chartSvg}
6976
+
6977
+ ` : ""
6978
+ );
6979
+ cleaned = cleaned.replace(
6030
6980
  /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6031
6981
  (_match, url, label) => {
6032
6982
  const cleanUrl = url.trim();
@@ -6034,18 +6984,23 @@ var DocPlugin = class {
6034
6984
  return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6035
6985
  }
6036
6986
  );
6037
- cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, "$1");
6987
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6988
+ if (/[\x00-\x1F]/.test(res)) return "";
6989
+ return res.trim();
6990
+ });
6038
6991
  cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6039
6992
  cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6993
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6994
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6040
6995
  return cleaned;
6041
6996
  }
6042
- splitIntoPages(text) {
6997
+ splitIntoPages(text, chartSvg = "") {
6043
6998
  if (!text) return [""];
6044
- const cleanedText = this.cleanWordDocFields(text);
6999
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6045
7000
  const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6046
7001
  const explicitParts = normalized.split(/[\x0C\f]|\n\s*[-=_]{3,}\s*(?:PAGE|Page|page break)[\s\d\w-]*[-=_]{3,}\s*\n/i).map((p) => p.trim()).filter((p) => p.length > 0);
6047
7002
  if (explicitParts.length === 0) explicitParts.push(normalized);
6048
- const maxLinesPerPage = 32;
7003
+ const maxLinesPerPage = 34;
6049
7004
  const charsPerLine = 80;
6050
7005
  const finalPages = [];
6051
7006
  for (const part of explicitParts) {
@@ -6053,8 +7008,8 @@ var DocPlugin = class {
6053
7008
  let currentLines = [];
6054
7009
  let count = 0;
6055
7010
  for (const line of lines) {
6056
- const plainLine = line.replace(/<[^>]+>/g, "");
6057
- const vLines = Math.max(1, Math.ceil((plainLine.length || 1) / charsPerLine));
7011
+ const isSvg = line.includes("<svg");
7012
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
6058
7013
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
6059
7014
  finalPages.push(currentLines.join("\n"));
6060
7015
  currentLines = [];
@@ -6094,9 +7049,44 @@ var DocPlugin = class {
6094
7049
  tableLines = [];
6095
7050
  }
6096
7051
  };
7052
+ const sanitizeOptions = {
7053
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
7054
+ ADD_ATTR: [
7055
+ "href",
7056
+ "target",
7057
+ "rel",
7058
+ "style",
7059
+ "viewBox",
7060
+ "width",
7061
+ "height",
7062
+ "x",
7063
+ "y",
7064
+ "x1",
7065
+ "y1",
7066
+ "x2",
7067
+ "y2",
7068
+ "fill",
7069
+ "stroke",
7070
+ "stroke-width",
7071
+ "stroke-dasharray",
7072
+ "rx",
7073
+ "font-size",
7074
+ "text-anchor"
7075
+ ]
7076
+ };
6097
7077
  let i = 0;
6098
7078
  while (i < lines.length) {
6099
7079
  let line = lines[i];
7080
+ if (line.includes("<svg")) {
7081
+ if (inList) {
7082
+ html += "</ul>";
7083
+ inList = false;
7084
+ }
7085
+ flushTable();
7086
+ html += line;
7087
+ i++;
7088
+ continue;
7089
+ }
6100
7090
  let tabCount = (line.match(/\t/g) || []).length;
6101
7091
  if (tabCount > 0) {
6102
7092
  let j = i;
@@ -6130,26 +7120,31 @@ var DocPlugin = class {
6130
7120
  i++;
6131
7121
  continue;
6132
7122
  }
6133
- const sanitizeOptions = { ADD_TAGS: ["a"], ADD_ATTR: ["href", "target", "rel", "style"] };
6134
- if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
7123
+ if (/^\d{1,2}$/.test(line.trim())) {
7124
+ i++;
7125
+ continue;
7126
+ }
7127
+ const isShortLine = line.length < 60 && !line.endsWith(".") && !line.endsWith(",");
7128
+ 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));
7129
+ if (isTitleLike) {
6135
7130
  if (inList) {
6136
7131
  html += "</ul>";
6137
7132
  inList = false;
6138
7133
  }
6139
- 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>`;
7134
+ 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>`;
6140
7135
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
6141
7136
  if (!inList) {
6142
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
7137
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
6143
7138
  inList = true;
6144
7139
  }
6145
7140
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
6146
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
7141
+ html += `<li style="margin: 4px 0; line-height: 1.5; font-size: 11pt; color: #1e293b;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
6147
7142
  } else {
6148
7143
  if (inList) {
6149
7144
  html += "</ul>";
6150
7145
  inList = false;
6151
7146
  }
6152
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
7147
+ 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>`;
6153
7148
  }
6154
7149
  i++;
6155
7150
  }