@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/vue.js CHANGED
@@ -2,6 +2,7 @@ import { defineComponent, ref, onMounted, watch, onBeforeUnmount, h } from 'vue'
2
2
  import DOMPurify6 from 'dompurify';
3
3
  import * as pdfjsLib from 'pdfjs-dist';
4
4
  import * as docx from 'docx-preview';
5
+ import * as fflate from 'fflate';
5
6
  import { unzipSync, strFromU8, unzip } from 'fflate';
6
7
  import * as XLSX from 'xlsx';
7
8
  import hljs from 'highlight.js';
@@ -493,8 +494,7 @@ var ICON_FULLSCREEN = `<svg width="24" height="24" viewBox="0 0 24 24" fill="non
493
494
  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>`;
494
495
  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>`;
495
496
  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>`;
496
- 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>`;
497
- 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>`;
497
+ 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>`;
498
498
  var ICON_MAP = {
499
499
  "zoom-in": ICON_ZOOM_IN,
500
500
  "zoom-out": ICON_ZOOM_OUT,
@@ -521,28 +521,177 @@ var ICON_MAP = {
521
521
  "forward-10": ICON_FAST_FORWARD,
522
522
  "rewind": ICON_REWIND,
523
523
  "replay-10": ICON_REWIND,
524
- "speed": ICON_SPEED,
525
524
  "open-window": ICON_EXTERNAL_WINDOW,
526
- "external-window": ICON_EXTERNAL_WINDOW
525
+ "external-window": ICON_EXTERNAL_WINDOW,
526
+ "openWindow": ICON_EXTERNAL_WINDOW,
527
+ "openSeparateWindow": ICON_EXTERNAL_WINDOW
527
528
  };
528
529
  var ToolbarController = class {
529
530
  el;
530
531
  toolbarEl;
531
532
  actions = [];
533
+ config = {};
532
534
  pageInputEl = null;
533
535
  pageLabelEl = null;
534
- constructor(container) {
536
+ prevPageBtn = null;
537
+ nextPageBtn = null;
538
+ constructor(container, config) {
535
539
  this.el = container;
540
+ if (config) this.config = { ...config };
536
541
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
537
542
  this.el.appendChild(this.toolbarEl);
538
543
  }
544
+ setConfig(config) {
545
+ this.config = { ...config };
546
+ this.render();
547
+ }
548
+ getConfig() {
549
+ return { ...this.config };
550
+ }
551
+ hideAction(actionId) {
552
+ this.config[actionId] = false;
553
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = false;
554
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = false;
555
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
556
+ this.config.fitPage = false;
557
+ this.config.fitToPage = false;
558
+ this.config.fitWidth = false;
559
+ }
560
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = false;
561
+ if (actionId === "fullscreen") this.config.fullscreen = false;
562
+ if (actionId === "download") this.config.download = false;
563
+ if (actionId === "print") this.config.print = false;
564
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
565
+ this.config.openWindow = false;
566
+ this.config.openSeparateWindow = false;
567
+ }
568
+ if (actionId === "copy") this.config.copy = false;
569
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
570
+ this.config.pageNav = false;
571
+ this.config.pagination = false;
572
+ }
573
+ this.render();
574
+ }
575
+ showAction(actionId) {
576
+ this.config[actionId] = true;
577
+ if (actionId === "zoom-in" || actionId === "zoomIn") this.config.zoomIn = true;
578
+ if (actionId === "zoom-out" || actionId === "zoomOut") this.config.zoomOut = true;
579
+ if (actionId === "fit-page" || actionId === "fitPage" || actionId === "fitToPage" || actionId === "fit-width") {
580
+ this.config.fitPage = true;
581
+ this.config.fitToPage = true;
582
+ this.config.fitWidth = true;
583
+ }
584
+ if (actionId === "rotate-cw" || actionId === "rotate" || actionId === "rotateCW") this.config.rotate = true;
585
+ if (actionId === "fullscreen") this.config.fullscreen = true;
586
+ if (actionId === "download") this.config.download = true;
587
+ if (actionId === "print") this.config.print = true;
588
+ if (actionId === "open-window" || actionId === "openWindow" || actionId === "openSeparateWindow") {
589
+ this.config.openWindow = true;
590
+ this.config.openSeparateWindow = true;
591
+ }
592
+ if (actionId === "copy") this.config.copy = true;
593
+ if (actionId === "page-nav" || actionId === "pageNav" || actionId === "pagination") {
594
+ this.config.pageNav = true;
595
+ this.config.pagination = true;
596
+ }
597
+ this.render();
598
+ }
599
+ normalizeActionId(actionId) {
600
+ if (actionId === "zoomIn") return "zoom-in";
601
+ if (actionId === "zoomOut") return "zoom-out";
602
+ if (actionId === "fitPage" || actionId === "fitToPage" || actionId === "fitWidth") return "fit-page";
603
+ if (actionId === "rotateCW") return "rotate-cw";
604
+ if (actionId === "rotateCCW") return "rotate-ccw";
605
+ if (actionId === "openWindow" || actionId === "openSeparateWindow") return "open-window";
606
+ if (actionId === "pageNav") return "page-nav";
607
+ return actionId;
608
+ }
609
+ enableAction(actionId) {
610
+ const norm = this.normalizeActionId(actionId);
611
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
612
+ if (btn) {
613
+ btn.disabled = false;
614
+ btn.classList.remove("disabled");
615
+ }
616
+ }
617
+ disableAction(actionId) {
618
+ const norm = this.normalizeActionId(actionId);
619
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
620
+ if (btn) {
621
+ btn.disabled = true;
622
+ btn.classList.add("disabled");
623
+ }
624
+ }
625
+ setActionActive(actionId, active) {
626
+ const norm = this.normalizeActionId(actionId);
627
+ const btn = this.toolbarEl.querySelector(`button[data-action-id="${actionId}"], button[data-action-id="${norm}"]`);
628
+ if (btn) {
629
+ if (active) {
630
+ btn.classList.add("active");
631
+ } else {
632
+ btn.classList.remove("active");
633
+ }
634
+ }
635
+ }
636
+ isActionEnabled(action) {
637
+ const c = this.config;
638
+ const id = action.id;
639
+ if (c[id] === false) return false;
640
+ if (id === "zoom-in" && (c.zoomIn === false || c["zoom-in"] === false)) return false;
641
+ if (id === "zoom-out" && (c.zoomOut === false || c["zoom-out"] === false)) return false;
642
+ 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)) {
643
+ return false;
644
+ }
645
+ if ((id === "rotate-cw" || id === "rotate-ccw" || id === "rotate") && (c.rotate === false || c.rotateCW === false || c.rotateCCW === false || c["rotate-cw"] === false)) {
646
+ return false;
647
+ }
648
+ if (id === "fullscreen" && c.fullscreen === false) return false;
649
+ if (id === "download" && c.download === false) return false;
650
+ if (id === "print" && c.print === false) return false;
651
+ if ((id === "open-window" || id === "external-window") && (c.openWindow === false || c.openSeparateWindow === false || c["open-window"] === false)) {
652
+ return false;
653
+ }
654
+ if (id === "copy" && c.copy === false) return false;
655
+ if (id === "thumbnails" && c.thumbnails === false) return false;
656
+ if (id === "page-nav" && (c.pageNav === false || c.pagination === false || c["page-nav"] === false)) {
657
+ return false;
658
+ }
659
+ if ((id === "prev" || id === "page-prev") && (c.prevPage === false || c.prev === false || c["prev"] === false || c.pageNav === false || c.pagination === false)) {
660
+ return false;
661
+ }
662
+ if ((id === "next" || id === "page-next") && (c.nextPage === false || c.next === false || c["next"] === false || c.pageNav === false || c.pagination === false)) {
663
+ return false;
664
+ }
665
+ if (id === "play" && c.play === false) return false;
666
+ if (id === "pause" && c.pause === false) return false;
667
+ if ((id === "fast-forward" || id === "forward-10") && (c.fastForward === false || c["fast-forward"] === false)) {
668
+ return false;
669
+ }
670
+ if ((id === "rewind" || id === "replay-10") && (c.rewind === false || c["rewind"] === false)) {
671
+ return false;
672
+ }
673
+ if (id === "speed" && c.speed === false) return false;
674
+ return true;
675
+ }
539
676
  setPage(page, max) {
540
677
  if (this.pageInputEl) {
541
678
  this.pageInputEl.value = page.toString();
679
+ if (max !== void 0) {
680
+ this.pageInputEl.max = max.toString();
681
+ }
542
682
  }
543
683
  if (max !== void 0 && this.pageLabelEl) {
544
684
  this.pageLabelEl.textContent = ` / ${max}`;
545
685
  }
686
+ const currentMax = max !== void 0 ? max : parseInt(this.pageInputEl?.max || "1", 10) || 1;
687
+ if (this.prevPageBtn) {
688
+ this.prevPageBtn.disabled = page <= 1;
689
+ this.prevPageBtn.style.opacity = page <= 1 ? "0.4" : "1";
690
+ }
691
+ if (this.nextPageBtn) {
692
+ this.nextPageBtn.disabled = page >= currentMax;
693
+ this.nextPageBtn.style.opacity = page >= currentMax ? "0.4" : "1";
694
+ }
546
695
  }
547
696
  update(actions) {
548
697
  this.actions = actions;
@@ -565,8 +714,9 @@ var ToolbarController = class {
565
714
  view: [],
566
715
  actions: []
567
716
  };
568
- const hasPageNav = this.actions.some((a) => a.type === "page-nav");
569
- const effectiveActions = hasPageNav ? this.actions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : this.actions;
717
+ const enabledActions = this.actions.filter((a) => this.isActionEnabled(a));
718
+ const hasPageNav = enabledActions.some((a) => a.type === "page-nav");
719
+ const effectiveActions = hasPageNav ? enabledActions.filter((a) => a.id !== "page-prev" && a.id !== "page-next" && a.id !== "prev" && a.id !== "next") : enabledActions;
570
720
  for (const action of effectiveActions) {
571
721
  if (groups[action.group]) {
572
722
  groups[action.group].push(action);
@@ -626,6 +776,13 @@ var ToolbarController = class {
626
776
  action.execute("go", val);
627
777
  });
628
778
  const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
779
+ const curVal = action.value ?? 1;
780
+ prevBtn.disabled = curVal <= 1;
781
+ prevBtn.style.opacity = curVal <= 1 ? "0.4" : "1";
782
+ nextBtn.disabled = curVal >= max;
783
+ nextBtn.style.opacity = curVal >= max ? "0.4" : "1";
784
+ this.prevPageBtn = prevBtn;
785
+ this.nextPageBtn = nextBtn;
629
786
  this.pageInputEl = input;
630
787
  this.pageLabelEl = label;
631
788
  groupEl.appendChild(prevBtn);
@@ -666,9 +823,11 @@ var ToolbarController = class {
666
823
  type: "button",
667
824
  "data-action-id": id
668
825
  });
669
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
670
- if (svg) {
671
- btn.innerHTML = sanitizeSVG(svg);
826
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
827
+ if (internalSvg) {
828
+ btn.innerHTML = internalSvg;
829
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
830
+ btn.innerHTML = sanitizeSVG(iconHtml);
672
831
  } else {
673
832
  btn.textContent = title || id;
674
833
  }
@@ -679,25 +838,63 @@ var ToolbarController = class {
679
838
  var ThumbnailPanel = class {
680
839
  el;
681
840
  panelEl;
841
+ listEl;
842
+ headerEl;
682
843
  thumbnails = [];
683
844
  onSelect;
845
+ onToggleCallback;
684
846
  activeIndex = 0;
685
- constructor(container) {
847
+ observer;
848
+ renderedIndices = /* @__PURE__ */ new Set();
849
+ constructor(container, onToggle) {
686
850
  this.el = container;
851
+ this.onToggleCallback = onToggle;
687
852
  this.panelEl = createElement("div", { className: "fp-thumbnail-panel hidden" });
853
+ this.panelEl.style.width = "0px";
854
+ this.panelEl.style.minWidth = "0px";
855
+ this.panelEl.style.opacity = "0";
856
+ this.headerEl = createElement("div", { className: "fp-thumbnail-header" });
857
+ const title = createElement("span", { className: "fp-thumbnail-title" }, "Pages");
858
+ const closeBtn = createElement("button", {
859
+ className: "fp-thumbnail-close-btn",
860
+ title: "Close Thumbnails",
861
+ type: "button"
862
+ });
863
+ closeBtn.innerHTML = ICON_CLOSE;
864
+ closeBtn.addEventListener("click", (e) => {
865
+ e.stopPropagation();
866
+ this.hide();
867
+ });
868
+ this.headerEl.appendChild(title);
869
+ this.headerEl.appendChild(closeBtn);
870
+ this.listEl = createElement("div", { className: "fp-thumbnail-list" });
871
+ this.panelEl.appendChild(this.headerEl);
872
+ this.panelEl.appendChild(this.listEl);
688
873
  this.el.appendChild(this.panelEl);
689
874
  }
875
+ isOpen() {
876
+ return !this.panelEl.classList.contains("hidden") && this.panelEl.style.width !== "0px";
877
+ }
690
878
  update(thumbnails, onSelect) {
691
879
  this.thumbnails = thumbnails;
692
880
  this.onSelect = onSelect;
881
+ this.renderedIndices.clear();
882
+ const titleEl = this.headerEl.querySelector(".fp-thumbnail-title");
883
+ if (titleEl) {
884
+ titleEl.textContent = `Pages (${thumbnails.length})`;
885
+ }
693
886
  this.render();
887
+ if (this.isOpen()) {
888
+ this.renderAllVisible();
889
+ }
694
890
  }
695
891
  setActive(index) {
696
892
  this.activeIndex = index;
697
- const items = this.panelEl.querySelectorAll(".fp-thumbnail-item");
893
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
698
894
  items.forEach((item, i) => {
699
895
  if (i === index) {
700
896
  item.classList.add("active");
897
+ item.scrollIntoView({ block: "nearest", behavior: "smooth" });
701
898
  } else {
702
899
  item.classList.remove("active");
703
900
  }
@@ -705,37 +902,102 @@ var ThumbnailPanel = class {
705
902
  }
706
903
  show() {
707
904
  this.panelEl.classList.remove("hidden");
905
+ this.panelEl.style.width = "200px";
906
+ this.panelEl.style.minWidth = "200px";
907
+ this.panelEl.style.opacity = "1";
908
+ this.panelEl.style.display = "flex";
909
+ this.onToggleCallback?.(true);
910
+ this.renderAllVisible();
708
911
  }
709
912
  hide() {
710
913
  this.panelEl.classList.add("hidden");
914
+ this.panelEl.style.width = "0px";
915
+ this.panelEl.style.minWidth = "0px";
916
+ this.panelEl.style.opacity = "0";
917
+ this.onToggleCallback?.(false);
711
918
  }
712
919
  toggle() {
713
- this.panelEl.classList.toggle("hidden");
920
+ if (this.isOpen()) {
921
+ this.hide();
922
+ } else {
923
+ this.show();
924
+ }
714
925
  }
715
926
  destroy() {
927
+ if (this.observer) {
928
+ this.observer.disconnect();
929
+ this.observer = void 0;
930
+ }
931
+ this.renderedIndices.clear();
716
932
  this.el.innerHTML = "";
717
933
  }
718
- async render() {
719
- this.panelEl.innerHTML = "";
934
+ renderAllVisible() {
935
+ const items = this.listEl.querySelectorAll(".fp-thumbnail-item");
936
+ items.forEach((item) => {
937
+ const idx = parseInt(item.dataset.thumbIndex || "-1", 10);
938
+ if (idx >= 0 && !this.renderedIndices.has(idx)) {
939
+ this.renderThumbnailItem(idx, item);
940
+ }
941
+ });
942
+ }
943
+ renderThumbnailItem(idx, itemEl) {
944
+ if (this.renderedIndices.has(idx)) return;
945
+ this.renderedIndices.add(idx);
946
+ const canvas = itemEl.querySelector("canvas");
947
+ const thumb = this.thumbnails[idx];
948
+ if (canvas && thumb) {
949
+ Promise.resolve(thumb.render(canvas)).catch((err) => {
950
+ console.warn(`[FilePreview] Error rendering thumbnail ${idx}:`, err);
951
+ });
952
+ }
953
+ }
954
+ render() {
955
+ if (this.observer) {
956
+ this.observer.disconnect();
957
+ }
958
+ this.listEl.innerHTML = "";
959
+ const useObserver = typeof IntersectionObserver !== "undefined";
960
+ if (useObserver) {
961
+ this.observer = new IntersectionObserver(
962
+ (entries) => {
963
+ for (const entry of entries) {
964
+ if (entry.isIntersecting) {
965
+ const target = entry.target;
966
+ const idx = parseInt(target.dataset.thumbIndex || "-1", 10);
967
+ if (idx >= 0) {
968
+ this.renderThumbnailItem(idx, target);
969
+ }
970
+ }
971
+ }
972
+ },
973
+ {
974
+ root: this.listEl,
975
+ rootMargin: "120px 0px 120px 0px"
976
+ }
977
+ );
978
+ }
720
979
  for (let i = 0; i < this.thumbnails.length; i++) {
721
980
  const thumb = this.thumbnails[i];
722
- const itemEl = createElement("div", { className: "fp-thumbnail-item" });
723
- if (i === this.activeIndex) {
724
- itemEl.classList.add("active");
725
- }
981
+ const itemEl = createElement("div", {
982
+ className: "fp-thumbnail-item" + (i === this.activeIndex ? " active" : "")
983
+ });
984
+ itemEl.dataset.thumbIndex = i.toString();
726
985
  itemEl.addEventListener("click", () => {
727
986
  this.setActive(i);
728
987
  this.onSelect?.(i);
729
988
  });
730
- const canvas = createElement("canvas", { width: "150", height: "200" });
731
- const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label);
732
- itemEl.appendChild(canvas);
989
+ const canvasWrapper = createElement("div", { className: "fp-thumbnail-canvas-wrapper" });
990
+ const canvas = createElement("canvas", { width: "130", height: "170" });
991
+ canvasWrapper.appendChild(canvas);
992
+ const label = createElement("span", { className: "fp-thumbnail-label" }, thumb.label || `Page ${i + 1}`);
993
+ itemEl.appendChild(canvasWrapper);
733
994
  itemEl.appendChild(label);
734
- this.panelEl.appendChild(itemEl);
735
- try {
736
- await thumb.render(canvas);
737
- } catch (err) {
738
- console.error(`[FilePreview] Error rendering thumbnail ${i}:`, err);
995
+ this.listEl.appendChild(itemEl);
996
+ if (this.observer) {
997
+ this.observer.observe(itemEl);
998
+ }
999
+ if (i < 4) {
1000
+ this.renderThumbnailItem(i, itemEl);
739
1001
  }
740
1002
  }
741
1003
  }
@@ -756,6 +1018,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
756
1018
  currentOptions = {};
757
1019
  resizeObserver = null;
758
1020
  fullscreenHandler = null;
1021
+ titleBarEl = null;
759
1022
  /**
760
1023
  * Register a preview plugin.
761
1024
  */
@@ -792,6 +1055,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
792
1055
  }
793
1056
  this.currentBuffer = buffer.slice(0);
794
1057
  this.currentMetadata = metadata;
1058
+ this.updateTitleBar(options, metadata);
795
1059
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
796
1060
  const fileInfo = { metadata, buffer };
797
1061
  const matchedPlugin = await this.findPlugin(fileInfo);
@@ -815,15 +1079,21 @@ var FilePreviewViewer = class _FilePreviewViewer {
815
1079
  if (event === "page-change" && payload && typeof payload.page === "number") {
816
1080
  const total = payload.total ?? payload.totalPages;
817
1081
  this.toolbar?.setPage(payload.page, total);
1082
+ this.thumbnailPanel?.setActive(payload.page - 1);
818
1083
  }
819
1084
  this.eventEmitter.emit(event, payload);
820
- }
1085
+ },
1086
+ toggleThumbnails: () => this.toggleThumbnails()
821
1087
  });
822
1088
  this.activeInstance = instance;
823
1089
  instance.openInSeparateWindow = () => this.openInSeparateWindow();
1090
+ instance.toggleThumbnails = () => this.toggleThumbnails();
824
1091
  this.hideLoading();
825
1092
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
826
- if (options.showToolbar !== false && this.toolbar) {
1093
+ const isToolbarVisible = options.showToolbar !== false && options.toolbar !== false;
1094
+ if (isToolbarVisible && this.toolbar) {
1095
+ const toolbarConfig = this.extractToolbarConfig(options);
1096
+ this.toolbar.setConfig(toolbarConfig);
827
1097
  const actions = matchedPlugin.getToolbarActions(instance);
828
1098
  const hasFullscreen = actions.some((a) => a.id === "fullscreen");
829
1099
  if (!hasFullscreen && this.wrapperEl) {
@@ -873,6 +1143,12 @@ var FilePreviewViewer = class _FilePreviewViewer {
873
1143
  }
874
1144
  });
875
1145
  }
1146
+ const thumbAction = actions.find((a) => a.id === "thumbnails");
1147
+ if (thumbAction) {
1148
+ thumbAction.execute = () => {
1149
+ this.toggleThumbnails();
1150
+ };
1151
+ }
876
1152
  this.toolbar.update(actions);
877
1153
  this.toolbar.show();
878
1154
  }
@@ -957,7 +1233,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
957
1233
  if (targetUrl) {
958
1234
  const newWin2 = window.open(targetUrl, "_blank");
959
1235
  if (!newWin2) {
960
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1236
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
961
1237
  return null;
962
1238
  }
963
1239
  return newWin2;
@@ -965,7 +1241,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
965
1241
  const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
966
1242
  const newWin = window.open("", "_blank");
967
1243
  if (!newWin) {
968
- alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1244
+ console.warn("[FilePreviewViewer] Popup blocker prevented opening the preview in a separate window.");
969
1245
  return null;
970
1246
  }
971
1247
  newWin.document.title = title;
@@ -1042,6 +1318,7 @@ var FilePreviewViewer = class _FilePreviewViewer {
1042
1318
  this.currentBuffer = null;
1043
1319
  this.currentMetadata = null;
1044
1320
  this.currentContainer = null;
1321
+ this.titleBarEl = null;
1045
1322
  }
1046
1323
  /**
1047
1324
  * Get the currently active preview instance.
@@ -1049,7 +1326,218 @@ var FilePreviewViewer = class _FilePreviewViewer {
1049
1326
  getInstance() {
1050
1327
  return this.activeInstance;
1051
1328
  }
1329
+ // --- Toolbar Configuration & Feature Toggles ---
1330
+ /**
1331
+ * Configure toolbar features dynamically through options or methods.
1332
+ * e.g. viewer.setToolbarConfig({ zoomIn: false, print: false })
1333
+ */
1334
+ setToolbarConfig(config) {
1335
+ this.toolbar?.setConfig(config);
1336
+ }
1337
+ /**
1338
+ * Get current toolbar configuration.
1339
+ */
1340
+ getToolbarConfig() {
1341
+ return this.toolbar?.getConfig() ?? {};
1342
+ }
1343
+ /**
1344
+ * Hide a specific toolbar action by ID or alias (e.g. 'zoom-in', 'print', 'download').
1345
+ */
1346
+ hideToolbarAction(actionId) {
1347
+ this.toolbar?.hideAction(actionId);
1348
+ }
1349
+ /**
1350
+ * Show a specific toolbar action by ID or alias.
1351
+ */
1352
+ showToolbarAction(actionId) {
1353
+ this.toolbar?.showAction(actionId);
1354
+ }
1355
+ /**
1356
+ * Enable a specific toolbar action button.
1357
+ */
1358
+ enableToolbarAction(actionId) {
1359
+ this.toolbar?.enableAction(actionId);
1360
+ }
1361
+ /**
1362
+ * Disable a specific toolbar action button.
1363
+ */
1364
+ disableToolbarAction(actionId) {
1365
+ this.toolbar?.disableAction(actionId);
1366
+ }
1367
+ // --- Programmatic Toolbar Action Methods ---
1368
+ /**
1369
+ * Fit document to page so it fills the frame width with minimal margins.
1370
+ */
1371
+ fitToPage() {
1372
+ this.activeInstance?.fitToPage?.();
1373
+ }
1374
+ /**
1375
+ * Zoom in.
1376
+ */
1377
+ zoomIn() {
1378
+ this.activeInstance?.zoomIn?.();
1379
+ }
1380
+ /**
1381
+ * Zoom out.
1382
+ */
1383
+ zoomOut() {
1384
+ this.activeInstance?.zoomOut?.();
1385
+ }
1386
+ /**
1387
+ * Set specific zoom level.
1388
+ */
1389
+ setZoom(level) {
1390
+ this.activeInstance?.setZoom?.(level);
1391
+ }
1392
+ /**
1393
+ * Get current zoom level.
1394
+ */
1395
+ getZoom() {
1396
+ return this.activeInstance?.getZoom?.() ?? 1;
1397
+ }
1398
+ /**
1399
+ * Rotate 90 degrees clockwise.
1400
+ */
1401
+ rotateCW() {
1402
+ this.activeInstance?.rotateCW?.();
1403
+ }
1404
+ /**
1405
+ * Rotate 90 degrees counter-clockwise.
1406
+ */
1407
+ rotateCCW() {
1408
+ this.activeInstance?.rotateCCW?.();
1409
+ }
1410
+ /**
1411
+ * Navigate to a specific page number.
1412
+ */
1413
+ goToPage(page) {
1414
+ this.activeInstance?.goToPage?.(page);
1415
+ this.toolbar?.setPage(page);
1416
+ }
1417
+ /**
1418
+ * Navigate to next page.
1419
+ */
1420
+ nextPage() {
1421
+ const cur = this.getCurrentPage();
1422
+ const max = this.getPageCount();
1423
+ if (cur < max) {
1424
+ this.goToPage(cur + 1);
1425
+ }
1426
+ }
1427
+ /**
1428
+ * Navigate to previous page.
1429
+ */
1430
+ prevPage() {
1431
+ const cur = this.getCurrentPage();
1432
+ if (cur > 1) {
1433
+ this.goToPage(cur - 1);
1434
+ }
1435
+ }
1436
+ /**
1437
+ * Get total page count.
1438
+ */
1439
+ getPageCount() {
1440
+ return this.activeInstance?.getPageCount?.() ?? 1;
1441
+ }
1442
+ /**
1443
+ * Get current page number.
1444
+ */
1445
+ getCurrentPage() {
1446
+ return this.activeInstance?.getCurrentPage?.() ?? 1;
1447
+ }
1448
+ /**
1449
+ * Download the current document.
1450
+ */
1451
+ download() {
1452
+ this.activeInstance?.download?.();
1453
+ }
1454
+ /**
1455
+ * Print the current document.
1456
+ */
1457
+ print() {
1458
+ this.activeInstance?.print?.();
1459
+ }
1460
+ /**
1461
+ * Toggle fullscreen mode.
1462
+ */
1463
+ toggleFullscreen() {
1464
+ try {
1465
+ if (!document.fullscreenElement) {
1466
+ this.wrapperEl?.requestFullscreen?.();
1467
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
1468
+ } else {
1469
+ document.exitFullscreen?.();
1470
+ this.wrapperEl?.classList.remove("fp-fullscreen-active");
1471
+ }
1472
+ } catch {
1473
+ this.wrapperEl?.classList.toggle("fp-fullscreen-active");
1474
+ }
1475
+ setTimeout(() => {
1476
+ this.activeInstance?.fitToPage?.();
1477
+ }, 120);
1478
+ }
1479
+ /**
1480
+ * Toggle thumbnails sidebar panel.
1481
+ */
1482
+ toggleThumbnails() {
1483
+ if (!this.thumbnailPanel) return;
1484
+ this.thumbnailPanel.toggle();
1485
+ const isOpen = this.thumbnailPanel.isOpen();
1486
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1487
+ setTimeout(() => {
1488
+ this.activeInstance?.fitToPage?.();
1489
+ }, 260);
1490
+ }
1491
+ /**
1492
+ * Set whether to display the file name / title bar above the toolbar.
1493
+ */
1494
+ setShowFileName(show) {
1495
+ if (this.currentOptions) {
1496
+ this.currentOptions.showFileName = show;
1497
+ }
1498
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1499
+ }
1500
+ /**
1501
+ * Set or override the displayed file name in the preview panel.
1502
+ */
1503
+ setFileName(name) {
1504
+ if (this.currentOptions) {
1505
+ this.currentOptions.fileName = name;
1506
+ }
1507
+ this.updateTitleBar(this.currentOptions, this.currentMetadata);
1508
+ }
1052
1509
  // --- Private methods ---
1510
+ updateTitleBar(options, metadata) {
1511
+ if (!this.titleBarEl) return;
1512
+ const show = options?.showFileName !== false;
1513
+ if (!show) {
1514
+ this.titleBarEl.style.display = "none";
1515
+ return;
1516
+ }
1517
+ const name = options?.fileName || metadata?.name || this.currentMetadata?.name || "";
1518
+ if (!name) {
1519
+ this.titleBarEl.style.display = "none";
1520
+ return;
1521
+ }
1522
+ this.titleBarEl.style.display = "flex";
1523
+ const ext = metadata?.extension || (name.includes(".") ? "." + name.split(".").pop() : "");
1524
+ const badge = ext ? ext.replace(".", "").toUpperCase() : "";
1525
+ this.titleBarEl.innerHTML = `
1526
+ <div class="fp-titlebar-info">
1527
+ <span class="fp-titlebar-icon">\u{1F4C4}</span>
1528
+ <span class="fp-titlebar-name" title="${name}">${name}</span>
1529
+ ${badge ? `<span class="fp-titlebar-badge">${badge}</span>` : ""}
1530
+ </div>
1531
+ `;
1532
+ }
1533
+ extractToolbarConfig(options) {
1534
+ if (!options) return {};
1535
+ const nested = typeof options.toolbar === "object" ? options.toolbar : {};
1536
+ return {
1537
+ ...options,
1538
+ ...nested
1539
+ };
1540
+ }
1053
1541
  abort() {
1054
1542
  if (this.abortController) {
1055
1543
  this.abortController.abort();
@@ -1092,17 +1580,29 @@ var FilePreviewViewer = class _FilePreviewViewer {
1092
1580
  bodyEl.className = "fp-body";
1093
1581
  bodyEl.appendChild(thumbnailEl);
1094
1582
  bodyEl.appendChild(this.contentEl);
1583
+ const titleBarEl = document.createElement("div");
1584
+ titleBarEl.className = "fp-titlebar";
1585
+ this.titleBarEl = titleBarEl;
1586
+ this.updateTitleBar(options, this.currentMetadata);
1095
1587
  if (toolbarPos === "top") {
1588
+ this.wrapperEl.appendChild(titleBarEl);
1096
1589
  this.wrapperEl.appendChild(toolbarEl);
1097
1590
  this.wrapperEl.appendChild(bodyEl);
1098
1591
  } else {
1592
+ this.wrapperEl.appendChild(titleBarEl);
1099
1593
  this.wrapperEl.appendChild(bodyEl);
1100
1594
  this.wrapperEl.appendChild(toolbarEl);
1101
1595
  }
1102
1596
  container.innerHTML = "";
1103
1597
  container.appendChild(this.wrapperEl);
1104
- this.toolbar = new ToolbarController(toolbarEl);
1105
- this.thumbnailPanel = new ThumbnailPanel(thumbnailEl);
1598
+ const initialToolbarConfig = this.extractToolbarConfig(options);
1599
+ this.toolbar = new ToolbarController(toolbarEl, initialToolbarConfig);
1600
+ this.thumbnailPanel = new ThumbnailPanel(thumbnailEl, (isOpen) => {
1601
+ this.toolbar?.setActionActive("thumbnails", isOpen);
1602
+ setTimeout(() => {
1603
+ this.activeInstance?.fitToPage?.();
1604
+ }, 260);
1605
+ });
1106
1606
  this.setupKeyboardShortcuts();
1107
1607
  this.setupDragAndDrop(container, options);
1108
1608
  this.setupResizeAndFullscreenListeners();
@@ -1492,6 +1992,14 @@ var PdfPlugin = class {
1492
1992
  group: "zoom",
1493
1993
  execute: () => instance.fitToPage?.()
1494
1994
  },
1995
+ {
1996
+ id: "fit-width",
1997
+ icon: "fit-width",
1998
+ label: "Fit to Width",
1999
+ type: "button",
2000
+ group: "zoom",
2001
+ execute: () => instance.fitToWidth?.()
2002
+ },
1495
2003
  {
1496
2004
  id: "rotate-cw",
1497
2005
  icon: "rotate-cw",
@@ -1500,6 +2008,14 @@ var PdfPlugin = class {
1500
2008
  group: "view",
1501
2009
  execute: () => instance.rotateCW?.()
1502
2010
  },
2011
+ {
2012
+ id: "rotate-ccw",
2013
+ icon: "rotate-ccw",
2014
+ label: "Rotate Counter-Clockwise",
2015
+ type: "button",
2016
+ group: "view",
2017
+ execute: () => instance.rotateCCW?.()
2018
+ },
1503
2019
  {
1504
2020
  id: "download",
1505
2021
  icon: "download",
@@ -1531,12 +2047,13 @@ var PdfPlugin = class {
1531
2047
  container.className = "fp-pdf-container";
1532
2048
  container.style.width = "100%";
1533
2049
  container.style.height = "100%";
1534
- container.style.overflow = "auto";
2050
+ container.style.overflowX = "hidden";
2051
+ container.style.overflowY = "auto";
1535
2052
  container.style.display = "flex";
1536
2053
  container.style.flexDirection = "column";
1537
2054
  container.style.alignItems = "center";
1538
2055
  container.style.justifyContent = "flex-start";
1539
- container.style.padding = "20px 16px";
2056
+ container.style.padding = "16px 8px";
1540
2057
  container.style.backgroundColor = "#0f172a";
1541
2058
  container.style.boxSizing = "border-box";
1542
2059
  container.style.position = "relative";
@@ -1547,30 +2064,12 @@ var PdfPlugin = class {
1547
2064
  pageCard.style.borderRadius = "4px";
1548
2065
  pageCard.style.overflow = "hidden";
1549
2066
  pageCard.style.lineHeight = "0";
1550
- pageCard.style.transition = "transform 0.15s ease";
1551
2067
  pageCard.style.position = "relative";
1552
2068
  pageCard.style.flexShrink = "0";
2069
+ pageCard.style.transition = "box-shadow 0.2s ease";
1553
2070
  let canvas = document.createElement("canvas");
1554
2071
  pageCard.appendChild(canvas);
1555
2072
  container.appendChild(pageCard);
1556
- const indicator = document.createElement("div");
1557
- indicator.className = "fp-pdf-page-indicator";
1558
- indicator.style.position = "sticky";
1559
- indicator.style.bottom = "16px";
1560
- indicator.style.marginTop = "16px";
1561
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1562
- indicator.style.backdropFilter = "blur(8px)";
1563
- indicator.style.color = "#f8fafc";
1564
- indicator.style.fontSize = "12px";
1565
- indicator.style.fontWeight = "600";
1566
- indicator.style.padding = "5px 14px";
1567
- indicator.style.borderRadius = "20px";
1568
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1569
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1570
- indicator.style.zIndex = "10";
1571
- indicator.style.userSelect = "none";
1572
- indicator.style.pointerEvents = "none";
1573
- container.appendChild(indicator);
1574
2073
  ctx.container.appendChild(container);
1575
2074
  const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1576
2075
  const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
@@ -1586,8 +2085,9 @@ var PdfPlugin = class {
1586
2085
  let currentPage = 1;
1587
2086
  let zoomScale = 1;
1588
2087
  let rotation = 0;
1589
- let fitMode = "page";
2088
+ let fitMode = ctx?.options?.fitMode || "width";
1590
2089
  let currentRenderTask = null;
2090
+ let textLayerDiv = null;
1591
2091
  const renderPage = async (pageNum) => {
1592
2092
  if (currentRenderTask) {
1593
2093
  try {
@@ -1599,30 +2099,37 @@ var PdfPlugin = class {
1599
2099
  const newCanvas = document.createElement("canvas");
1600
2100
  pageCard.replaceChild(newCanvas, canvas);
1601
2101
  canvas = newCanvas;
2102
+ if (textLayerDiv) {
2103
+ textLayerDiv.remove();
2104
+ textLayerDiv = null;
2105
+ }
1602
2106
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
1603
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1604
2107
  ctx.emit("page-change", { page: currentPage, total: totalPages });
1605
2108
  const page = await pdfDoc.getPage(currentPage);
1606
2109
  const containerWidth = container.clientWidth || 900;
1607
2110
  const containerHeight = container.clientHeight || 700;
1608
2111
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1609
- const availWidth = Math.max(320, containerWidth - 48);
1610
- const availHeight = Math.max(550, containerHeight - 88);
2112
+ const availWidth = Math.max(280, containerWidth - 36);
2113
+ const availHeight = Math.max(280, containerHeight - 32);
1611
2114
  const scaleW = availWidth / unscaledVp.width;
1612
2115
  const scaleH = availHeight / unscaledVp.height;
1613
2116
  let fitScale;
1614
2117
  if (fitMode === "page") {
1615
- fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
2118
+ fitScale = Math.max(0.2, Math.min(4, Math.min(scaleW, scaleH)));
1616
2119
  } else {
1617
- fitScale = Math.max(0.65, Math.min(1.25, scaleW));
2120
+ fitScale = Math.max(0.2, Math.min(4, scaleW));
1618
2121
  }
1619
2122
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1620
2123
  const pixelRatio = window.devicePixelRatio || 1;
1621
2124
  const viewport = page.getViewport({ scale: effectiveScale, rotation });
2125
+ const displayWidth = Math.floor(viewport.width);
2126
+ const displayHeight = Math.floor(viewport.height);
1622
2127
  canvas.width = Math.floor(viewport.width * pixelRatio);
1623
2128
  canvas.height = Math.floor(viewport.height * pixelRatio);
1624
- canvas.style.width = `${Math.floor(viewport.width)}px`;
1625
- canvas.style.height = `${Math.floor(viewport.height)}px`;
2129
+ canvas.style.width = `${displayWidth}px`;
2130
+ canvas.style.height = `${displayHeight}px`;
2131
+ pageCard.style.width = `${displayWidth}px`;
2132
+ pageCard.style.height = `${displayHeight}px`;
1626
2133
  const canvasCtx = canvas.getContext("2d");
1627
2134
  if (!canvasCtx) return;
1628
2135
  canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
@@ -1636,9 +2143,31 @@ var PdfPlugin = class {
1636
2143
  if (err?.name !== "RenderingCancelledException") {
1637
2144
  console.warn("[PdfPlugin] Page render warning:", err);
1638
2145
  }
2146
+ return;
1639
2147
  } finally {
1640
2148
  currentRenderTask = null;
1641
2149
  }
2150
+ try {
2151
+ textLayerDiv = document.createElement("div");
2152
+ textLayerDiv.className = "textLayer";
2153
+ textLayerDiv.style.width = `${displayWidth}px`;
2154
+ textLayerDiv.style.height = `${displayHeight}px`;
2155
+ textLayerDiv.style.position = "absolute";
2156
+ textLayerDiv.style.top = "0";
2157
+ textLayerDiv.style.left = "0";
2158
+ textLayerDiv.style.lineHeight = "1";
2159
+ pageCard.appendChild(textLayerDiv);
2160
+ if (pdfjsLib.TextLayer) {
2161
+ const textLayer = new pdfjsLib.TextLayer({
2162
+ textContentSource: page.streamTextContent(),
2163
+ container: textLayerDiv,
2164
+ viewport
2165
+ });
2166
+ await textLayer.render();
2167
+ }
2168
+ } catch (err) {
2169
+ console.debug("[PdfPlugin] TextLayer notice:", err);
2170
+ }
1642
2171
  };
1643
2172
  renderPage(1);
1644
2173
  let resizeTimer = null;
@@ -1648,11 +2177,48 @@ var PdfPlugin = class {
1648
2177
  if (Math.abs(zoomScale - 1) < 0.05) {
1649
2178
  renderPage(currentPage);
1650
2179
  }
1651
- }, 150);
2180
+ }, 120);
1652
2181
  });
1653
2182
  resizeObserver.observe(container);
2183
+ const onWheel = (e) => {
2184
+ if (e.ctrlKey || e.metaKey) {
2185
+ e.preventDefault();
2186
+ const delta = e.deltaY > 0 ? -0.15 : 0.15;
2187
+ zoomScale = Math.max(0.25, Math.min(4, Math.round((zoomScale + delta) * 100) / 100));
2188
+ renderPage(currentPage);
2189
+ }
2190
+ };
2191
+ container.addEventListener("wheel", onWheel, { passive: false });
2192
+ const onKeyDown = (e) => {
2193
+ const tag = e.target?.tagName?.toLowerCase();
2194
+ if (tag === "input" || tag === "textarea" || tag === "select") return;
2195
+ if (e.key === "ArrowRight" || e.key === "PageDown") {
2196
+ if (currentPage < totalPages) {
2197
+ e.preventDefault();
2198
+ container.scrollTop = 0;
2199
+ renderPage(currentPage + 1);
2200
+ }
2201
+ } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
2202
+ if (currentPage > 1) {
2203
+ e.preventDefault();
2204
+ container.scrollTop = 0;
2205
+ renderPage(currentPage - 1);
2206
+ }
2207
+ } else if (e.key === "Home") {
2208
+ e.preventDefault();
2209
+ container.scrollTop = 0;
2210
+ renderPage(1);
2211
+ } else if (e.key === "End") {
2212
+ e.preventDefault();
2213
+ container.scrollTop = 0;
2214
+ renderPage(totalPages);
2215
+ }
2216
+ };
2217
+ window.addEventListener("keydown", onKeyDown);
1654
2218
  const cleanup = () => {
1655
2219
  resizeObserver.disconnect();
2220
+ window.removeEventListener("keydown", onKeyDown);
2221
+ container.removeEventListener("wheel", onWheel);
1656
2222
  if (resizeTimer) clearTimeout(resizeTimer);
1657
2223
  if (currentRenderTask) {
1658
2224
  try {
@@ -1671,22 +2237,26 @@ var PdfPlugin = class {
1671
2237
  const instance = {
1672
2238
  destroy: cleanup,
1673
2239
  zoomIn: () => {
1674
- zoomScale = Math.min(3.5, zoomScale + 0.2);
2240
+ zoomScale = Math.min(4, Math.round((zoomScale + 0.2) * 10) / 10);
1675
2241
  renderPage(currentPage);
1676
2242
  },
1677
2243
  zoomOut: () => {
1678
- zoomScale = Math.max(0.3, zoomScale - 0.2);
2244
+ zoomScale = Math.max(0.25, Math.round((zoomScale - 0.2) * 10) / 10);
1679
2245
  renderPage(currentPage);
1680
2246
  },
1681
2247
  getZoom: () => zoomScale,
1682
2248
  setZoom: (level) => {
1683
- zoomScale = Math.max(0.3, Math.min(3.5, level));
2249
+ zoomScale = Math.max(0.25, Math.min(4, level));
1684
2250
  renderPage(currentPage);
1685
2251
  },
1686
2252
  fitToPage: () => {
1687
- fitMode = fitMode === "page" ? "width" : "page";
2253
+ fitMode = "page";
2254
+ zoomScale = 1;
2255
+ renderPage(currentPage);
2256
+ },
2257
+ fitToWidth: () => {
2258
+ fitMode = "width";
1688
2259
  zoomScale = 1;
1689
- rotation = 0;
1690
2260
  renderPage(currentPage);
1691
2261
  },
1692
2262
  rotateCW: () => {
@@ -1704,6 +2274,9 @@ var PdfPlugin = class {
1704
2274
  container.scrollTop = 0;
1705
2275
  renderPage(page);
1706
2276
  },
2277
+ toggleThumbnails: () => {
2278
+ ctx?.toggleThumbnails?.();
2279
+ },
1707
2280
  download: () => {
1708
2281
  const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1709
2282
  const url = URL.createObjectURL(blob);
@@ -1737,7 +2310,7 @@ var PdfPlugin = class {
1737
2310
  },
1738
2311
  getThumbnails: async () => {
1739
2312
  const thumbnails = [];
1740
- const count = Math.min(totalPages, 50);
2313
+ const count = totalPages;
1741
2314
  for (let i = 1; i <= count; i++) {
1742
2315
  thumbnails.push({
1743
2316
  index: i,
@@ -1745,9 +2318,10 @@ var PdfPlugin = class {
1745
2318
  render: async (thumbCanvas) => {
1746
2319
  try {
1747
2320
  const p = await pdfDoc.getPage(i);
1748
- const baseVp = p.getViewport({ scale: 1 });
1749
- const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1750
- const thumbVp = p.getViewport({ scale: thumbScale });
2321
+ const baseVp = p.getViewport({ scale: 1, rotation });
2322
+ const targetW = thumbCanvas.width || 130;
2323
+ const thumbScale = targetW / baseVp.width;
2324
+ const thumbVp = p.getViewport({ scale: thumbScale, rotation });
1751
2325
  thumbCanvas.height = Math.floor(thumbVp.height);
1752
2326
  const tCtx = thumbCanvas.getContext("2d");
1753
2327
  if (tCtx) {
@@ -2091,28 +2665,26 @@ var DocxPlugin = class {
2091
2665
  getToolbarActions(instance) {
2092
2666
  const totalPages = instance.getPageCount?.() ?? 1;
2093
2667
  const actions = [];
2094
- if (totalPages > 1) {
2095
- actions.push({
2096
- id: "page-nav",
2097
- icon: "",
2098
- label: "Page Navigation",
2099
- type: "page-nav",
2100
- group: "navigation",
2101
- value: instance.getCurrentPage?.() ?? 1,
2102
- max: totalPages,
2103
- execute: (action, page) => {
2104
- const cur = instance.getCurrentPage?.() ?? 1;
2105
- const max = instance.getPageCount?.() ?? 1;
2106
- if (action === "prev") {
2107
- if (cur > 1) instance.goToPage?.(cur - 1);
2108
- } else if (action === "next") {
2109
- if (cur < max) instance.goToPage?.(cur + 1);
2110
- } else if (typeof page === "number") {
2111
- instance.goToPage?.(page);
2112
- }
2668
+ actions.push({
2669
+ id: "page-nav",
2670
+ icon: "",
2671
+ label: "Page Navigation",
2672
+ type: "page-nav",
2673
+ group: "navigation",
2674
+ value: instance.getCurrentPage?.() ?? 1,
2675
+ max: totalPages,
2676
+ execute: (action, page) => {
2677
+ const cur = instance.getCurrentPage?.() ?? 1;
2678
+ const max = instance.getPageCount?.() ?? 1;
2679
+ if (action === "prev") {
2680
+ if (cur > 1) instance.goToPage?.(cur - 1);
2681
+ } else if (action === "next") {
2682
+ if (cur < max) instance.goToPage?.(cur + 1);
2683
+ } else if (typeof page === "number") {
2684
+ instance.goToPage?.(page);
2113
2685
  }
2114
- });
2115
- }
2686
+ }
2687
+ });
2116
2688
  actions.push(
2117
2689
  {
2118
2690
  id: "zoom-out",
@@ -2178,11 +2750,17 @@ var DocxPlugin = class {
2178
2750
  wrapper.className = "fp-docx-wrapper";
2179
2751
  wrapper.style.transformOrigin = "top center";
2180
2752
  wrapper.style.transition = "transform 0.2s ease";
2181
- wrapper.style.padding = "24px 16px";
2182
- wrapper.style.maxWidth = "950px";
2753
+ wrapper.style.padding = "0";
2754
+ wrapper.style.maxWidth = "none";
2755
+ wrapper.style.width = "816px";
2183
2756
  wrapper.style.margin = "0 auto";
2757
+ wrapper.style.display = "flex";
2758
+ wrapper.style.flexDirection = "column";
2759
+ wrapper.style.alignItems = "center";
2184
2760
  wrapper.style.boxSizing = "border-box";
2185
- ctx.container.style.overflow = "auto";
2761
+ ctx.container.style.overflowX = "hidden";
2762
+ ctx.container.style.overflowY = "auto";
2763
+ ctx.container.style.padding = "16px 8px";
2186
2764
  ctx.container.style.backgroundColor = "#f1f5f9";
2187
2765
  ctx.container.appendChild(wrapper);
2188
2766
  const styleOverride = document.createElement("style");
@@ -2356,39 +2934,13 @@ var DocxPlugin = class {
2356
2934
  const pageElements = sections.length > 0 ? sections : cards;
2357
2935
  const totalPages = Math.max(1, pageElements.length);
2358
2936
  let currentPage = 1;
2359
- let indicator = null;
2360
- if (totalPages > 1) {
2361
- indicator = document.createElement("div");
2362
- indicator.className = "fp-docx-page-indicator";
2363
- indicator.style.position = "sticky";
2364
- indicator.style.bottom = "16px";
2365
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2366
- indicator.style.backdropFilter = "blur(8px)";
2367
- indicator.style.color = "#f8fafc";
2368
- indicator.style.fontSize = "12px";
2369
- indicator.style.fontWeight = "600";
2370
- indicator.style.padding = "5px 14px";
2371
- indicator.style.borderRadius = "20px";
2372
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2373
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2374
- indicator.style.zIndex = "10";
2375
- indicator.style.userSelect = "none";
2376
- indicator.style.pointerEvents = "none";
2377
- indicator.style.textAlign = "center";
2378
- indicator.style.width = "fit-content";
2379
- indicator.style.margin = "16px auto 0";
2380
- ctx.container.appendChild(indicator);
2381
- }
2382
2937
  const showPage = (pageNum) => {
2383
2938
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
2384
2939
  if (pageElements.length > 1) {
2385
2940
  pageElements.forEach((sec, idx) => {
2386
- sec.style.display = idx + 1 === currentPage ? "block" : "none";
2941
+ sec.style.display = idx + 1 === currentPage ? "" : "none";
2387
2942
  });
2388
2943
  }
2389
- if (indicator) {
2390
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2391
- }
2392
2944
  ctx.container.scrollTop = 0;
2393
2945
  ctx.emit("page-change", { page: currentPage, total: totalPages });
2394
2946
  };
@@ -2397,31 +2949,46 @@ var DocxPlugin = class {
2397
2949
  }
2398
2950
  scale = 1;
2399
2951
  let rotation = 0;
2400
- let fitMode = "width";
2401
- const calculateFitScale = (mode = fitMode) => {
2402
- const activeEl = pageElements[currentPage - 1] || wrapper.firstElementChild || wrapper;
2952
+ let fitMode = "page";
2953
+ let isUserZoomed = false;
2954
+ const calculateFitScale = (_mode = fitMode) => {
2955
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
2403
2956
  const elW = activeEl.offsetWidth || 816;
2404
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
2405
- const availW = Math.max(280, ctx.container.clientWidth - 48);
2406
- const availH = Math.max(280, ctx.container.clientHeight - 80);
2957
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
2407
2958
  const sW = availW / elW;
2408
- const sH = availH / singlePageH;
2409
- if (mode === "page") {
2410
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
2411
- }
2412
- return Math.max(0.65, Math.min(1.05, sW));
2959
+ return Math.max(0.35, Math.min(3, sW));
2413
2960
  };
2414
2961
  const applyTransform = () => {
2415
2962
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
2416
2963
  wrapper.style.transformOrigin = "top center";
2964
+ const activeEl = pageElements[currentPage - 1] || wrapper.querySelector("section.docx") || wrapper;
2965
+ const elH = activeEl.offsetHeight || 1056;
2966
+ if (scale > 1) {
2967
+ wrapper.style.marginBottom = `${Math.round(elH * scale - elH + 32)}px`;
2968
+ } else {
2969
+ wrapper.style.marginBottom = "32px";
2970
+ }
2417
2971
  };
2972
+ let resizeObserver = null;
2973
+ if (typeof ResizeObserver !== "undefined") {
2974
+ resizeObserver = new ResizeObserver(() => {
2975
+ if (!isUserZoomed) {
2976
+ const newFit = calculateFitScale("page");
2977
+ if (Math.abs(scale - newFit) > 0.015) {
2978
+ scale = newFit;
2979
+ applyTransform();
2980
+ }
2981
+ }
2982
+ });
2983
+ resizeObserver.observe(ctx.container);
2984
+ }
2418
2985
  setTimeout(() => {
2419
- fitMode = "width";
2420
- scale = calculateFitScale("width");
2986
+ scale = calculateFitScale("page");
2421
2987
  applyTransform();
2422
- }, 60);
2988
+ }, 40);
2423
2989
  const cleanup = () => {
2424
- indicator?.remove();
2990
+ resizeObserver?.disconnect();
2991
+ resizeObserver = null;
2425
2992
  for (const url of createdBlobUrls) {
2426
2993
  URL.revokeObjectURL(url);
2427
2994
  }
@@ -2438,21 +3005,24 @@ var DocxPlugin = class {
2438
3005
  showPage(page);
2439
3006
  },
2440
3007
  zoomIn: () => {
2441
- scale += 0.15;
3008
+ isUserZoomed = true;
3009
+ scale = Math.min(3.5, scale + 0.15);
2442
3010
  applyTransform();
2443
3011
  },
2444
3012
  zoomOut: () => {
3013
+ isUserZoomed = true;
2445
3014
  scale = Math.max(0.2, scale - 0.15);
2446
3015
  applyTransform();
2447
3016
  },
2448
3017
  getZoom: () => scale,
2449
3018
  setZoom: (level) => {
3019
+ isUserZoomed = true;
2450
3020
  scale = level;
2451
3021
  applyTransform();
2452
3022
  },
2453
3023
  fitToPage: () => {
2454
- fitMode = fitMode === "width" ? "page" : "width";
2455
- scale = calculateFitScale(fitMode);
3024
+ isUserZoomed = false;
3025
+ scale = calculateFitScale("page");
2456
3026
  rotation = 0;
2457
3027
  applyTransform();
2458
3028
  },
@@ -2842,7 +3412,7 @@ var ExcelPlugin = class {
2842
3412
  "application/vnd.ms-excel.template.macroEnabled.12",
2843
3413
  "application/vnd.oasis.opendocument.spreadsheet"
2844
3414
  ];
2845
- weight = 80;
3415
+ weight = 85;
2846
3416
  supports(file) {
2847
3417
  const ext = file.metadata.extension?.toLowerCase();
2848
3418
  const mime = file.metadata.mimeType?.toLowerCase();
@@ -2854,6 +3424,14 @@ var ExcelPlugin = class {
2854
3424
  getToolbarActions(instance) {
2855
3425
  const totalSheets = instance.getPageCount?.() ?? 1;
2856
3426
  const actions = [];
3427
+ actions.push({
3428
+ id: "thumbnails",
3429
+ icon: "thumbnails",
3430
+ label: "Sheet Thumbnails",
3431
+ type: "button",
3432
+ group: "navigation",
3433
+ execute: () => instance.toggleThumbnails?.()
3434
+ });
2857
3435
  if (totalSheets > 1) {
2858
3436
  actions.push({
2859
3437
  id: "page-nav",
@@ -2936,6 +3514,16 @@ var ExcelPlugin = class {
2936
3514
  execute: () => {
2937
3515
  instance.print?.();
2938
3516
  }
3517
+ },
3518
+ {
3519
+ id: "open-window",
3520
+ icon: "open-window",
3521
+ label: "Open in Separate Full Window",
3522
+ type: "button",
3523
+ group: "actions",
3524
+ execute: () => {
3525
+ instance.openInSeparateWindow?.();
3526
+ }
2939
3527
  }
2940
3528
  );
2941
3529
  return actions;
@@ -3101,6 +3689,63 @@ var ExcelPlugin = class {
3101
3689
  },
3102
3690
  getPageCount: () => sheetNames.length,
3103
3691
  getCurrentPage: () => currentSheetIndex,
3692
+ getThumbnails: () => {
3693
+ return sheetNames.map((name, idx) => ({
3694
+ index: idx,
3695
+ label: name,
3696
+ render: async (canvas) => {
3697
+ canvas.width = 140;
3698
+ canvas.height = 100;
3699
+ const c = canvas.getContext("2d");
3700
+ if (!c) return;
3701
+ c.fillStyle = "#ffffff";
3702
+ c.fillRect(0, 0, canvas.width, canvas.height);
3703
+ c.fillStyle = "#107c41";
3704
+ c.fillRect(0, 0, canvas.width, 16);
3705
+ c.fillStyle = "#ffffff";
3706
+ c.font = 'bold 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
3707
+ c.fillText(`\u{1F4CA} ${name.slice(0, 16)}`, 6, 11);
3708
+ const startY = 17;
3709
+ const rowH = 13;
3710
+ const colW = 30;
3711
+ const colHeaders = ["A", "B", "C", "D"];
3712
+ c.fillStyle = "#f1f5f9";
3713
+ c.fillRect(0, startY, canvas.width, rowH);
3714
+ c.strokeStyle = "#cbd5e1";
3715
+ c.lineWidth = 0.8;
3716
+ c.strokeRect(0, startY, canvas.width, rowH);
3717
+ c.fillStyle = "#64748b";
3718
+ c.font = "bold 7px sans-serif";
3719
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3720
+ c.fillText(colHeaders[ci], 14 + ci * colW, startY + 9);
3721
+ }
3722
+ const ws = wb?.Sheets[name];
3723
+ for (let ri = 1; ri <= 5; ri++) {
3724
+ const y = startY + ri * rowH;
3725
+ if (y > canvas.height - 2) break;
3726
+ c.fillStyle = "#f8fafc";
3727
+ c.fillRect(0, y, 12, rowH);
3728
+ c.fillStyle = "#94a3b8";
3729
+ c.font = "6px sans-serif";
3730
+ c.fillText(String(ri), 3, y + 9);
3731
+ c.strokeStyle = "#e2e8f0";
3732
+ c.strokeRect(0, y, canvas.width, rowH);
3733
+ c.fillStyle = "#334155";
3734
+ c.font = "6px sans-serif";
3735
+ for (let ci = 0; ci < colHeaders.length; ci++) {
3736
+ const cellRef = `${colHeaders[ci]}${ri}`;
3737
+ const cellVal = ws?.[cellRef]?.w || ws?.[cellRef]?.v;
3738
+ if (cellVal !== void 0) {
3739
+ c.fillText(String(cellVal).slice(0, 6), 14 + ci * colW, y + 9);
3740
+ }
3741
+ }
3742
+ }
3743
+ c.strokeStyle = "#cbd5e1";
3744
+ c.lineWidth = 1;
3745
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
3746
+ }
3747
+ }));
3748
+ },
3104
3749
  download: () => {
3105
3750
  const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
3106
3751
  const url = URL.createObjectURL(blob);
@@ -3133,7 +3778,39 @@ var CsvPlugin = class {
3133
3778
  return ext === ".csv" || ext === ".tsv" || mime === "text/csv" || mime === "text/tab-separated-values";
3134
3779
  }
3135
3780
  getToolbarActions(instance) {
3136
- return [
3781
+ const totalPages = instance.getPageCount?.() ?? 1;
3782
+ const actions = [];
3783
+ actions.push({
3784
+ id: "thumbnails",
3785
+ icon: "thumbnails",
3786
+ label: "Page Thumbnails",
3787
+ type: "button",
3788
+ group: "navigation",
3789
+ execute: () => instance.toggleThumbnails?.()
3790
+ });
3791
+ if (totalPages > 1) {
3792
+ actions.push({
3793
+ id: "page-nav",
3794
+ icon: "",
3795
+ label: "Page Navigation",
3796
+ type: "page-nav",
3797
+ group: "navigation",
3798
+ value: instance.getCurrentPage?.() ?? 1,
3799
+ max: totalPages,
3800
+ execute: (action, page) => {
3801
+ const cur = instance.getCurrentPage?.() ?? 1;
3802
+ const max = instance.getPageCount?.() ?? 1;
3803
+ if (action === "prev") {
3804
+ if (cur > 1) instance.goToPage?.(cur - 1);
3805
+ } else if (action === "next") {
3806
+ if (cur < max) instance.goToPage?.(cur + 1);
3807
+ } else if (typeof page === "number") {
3808
+ instance.goToPage?.(page);
3809
+ }
3810
+ }
3811
+ });
3812
+ }
3813
+ actions.push(
3137
3814
  {
3138
3815
  id: "zoom-out",
3139
3816
  icon: "zoom-out",
@@ -3154,6 +3831,16 @@ var CsvPlugin = class {
3154
3831
  instance.zoomIn?.();
3155
3832
  }
3156
3833
  },
3834
+ {
3835
+ id: "fit-page",
3836
+ icon: "fit-page",
3837
+ label: "Fit to View",
3838
+ type: "button",
3839
+ group: "zoom",
3840
+ execute: () => {
3841
+ instance.fitToPage?.();
3842
+ }
3843
+ },
3157
3844
  {
3158
3845
  id: "download",
3159
3846
  icon: "download",
@@ -3171,10 +3858,21 @@ var CsvPlugin = class {
3171
3858
  type: "button",
3172
3859
  group: "actions",
3173
3860
  execute: () => {
3174
- instance.print?.();
3861
+ instance.print?.();
3862
+ }
3863
+ },
3864
+ {
3865
+ id: "open-window",
3866
+ icon: "open-window",
3867
+ label: "Open in Separate Full Window",
3868
+ type: "button",
3869
+ group: "actions",
3870
+ execute: () => {
3871
+ instance.openInSeparateWindow?.();
3175
3872
  }
3176
3873
  }
3177
- ];
3874
+ );
3875
+ return actions;
3178
3876
  }
3179
3877
  async render(ctx) {
3180
3878
  const decoder = new TextDecoder("utf-8");
@@ -3186,33 +3884,62 @@ var CsvPlugin = class {
3186
3884
  container.style.padding = "16px";
3187
3885
  container.style.boxSizing = "border-box";
3188
3886
  container.style.transformOrigin = "top left";
3887
+ const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3888
+ const allLines = text.split(/\r?\n/).filter((r) => r.trim().length > 0);
3889
+ const headerLine = allLines[0] || "";
3890
+ const headerCells = headerLine.split(delimiter);
3891
+ const dataLines = allLines.slice(1);
3892
+ const ROWS_PER_PAGE = 100;
3893
+ const totalPages = Math.max(1, Math.ceil(dataLines.length / ROWS_PER_PAGE));
3894
+ let currentPage = 1;
3189
3895
  const table = document.createElement("table");
3190
3896
  table.style.borderCollapse = "collapse";
3191
3897
  table.style.width = "100%";
3192
- table.style.fontFamily = "monospace";
3898
+ table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace';
3193
3899
  table.style.fontSize = "13px";
3194
- const delimiter = ctx.metadata.extension === ".tsv" ? " " : ",";
3195
- const rows = text.split(/\r?\n/).slice(0, 500);
3196
- rows.forEach((row, rowIndex) => {
3197
- if (!row.trim()) return;
3198
- const tr = document.createElement("tr");
3199
- const cells = row.split(delimiter);
3200
- cells.forEach((cell) => {
3201
- const td = document.createElement(rowIndex === 0 ? "th" : "td");
3202
- td.textContent = cell.trim();
3203
- td.style.border = "1px solid #d0d7de";
3204
- td.style.padding = "6px 12px";
3205
- if (rowIndex === 0) {
3206
- td.style.backgroundColor = "#f6f8fa";
3207
- td.style.fontWeight = "bold";
3208
- }
3209
- tr.appendChild(td);
3900
+ const renderPage = (pageNum) => {
3901
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3902
+ table.innerHTML = "";
3903
+ const headerTr = document.createElement("tr");
3904
+ headerCells.forEach((cell) => {
3905
+ const th = document.createElement("th");
3906
+ th.textContent = cell.trim();
3907
+ th.style.border = "1px solid #d0d7de";
3908
+ th.style.padding = "8px 12px";
3909
+ th.style.backgroundColor = "#f6f8fa";
3910
+ th.style.fontWeight = "600";
3911
+ th.style.whiteSpace = "nowrap";
3912
+ headerTr.appendChild(th);
3210
3913
  });
3211
- table.appendChild(tr);
3212
- });
3914
+ table.appendChild(headerTr);
3915
+ const start = (currentPage - 1) * ROWS_PER_PAGE;
3916
+ const end = Math.min(dataLines.length, start + ROWS_PER_PAGE);
3917
+ const pageRows = dataLines.slice(start, end);
3918
+ pageRows.forEach((row, idx) => {
3919
+ const tr = document.createElement("tr");
3920
+ if (idx % 2 === 1) tr.style.backgroundColor = "#f8fafc";
3921
+ const cells = row.split(delimiter);
3922
+ cells.forEach((cell) => {
3923
+ const td = document.createElement("td");
3924
+ td.textContent = cell.trim();
3925
+ td.style.border = "1px solid #d0d7de";
3926
+ td.style.padding = "6px 12px";
3927
+ td.style.whiteSpace = "nowrap";
3928
+ tr.appendChild(td);
3929
+ });
3930
+ table.appendChild(tr);
3931
+ });
3932
+ container.scrollTop = 0;
3933
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3934
+ };
3935
+ renderPage(1);
3213
3936
  container.appendChild(table);
3214
3937
  ctx.container.appendChild(container);
3215
3938
  let scale = 1;
3939
+ const applyScale = () => {
3940
+ container.style.transform = `scale(${scale})`;
3941
+ container.style.transformOrigin = "top left";
3942
+ };
3216
3943
  const cleanup = () => {
3217
3944
  container.remove();
3218
3945
  ctx.container.innerHTML = "";
@@ -3220,22 +3947,86 @@ var CsvPlugin = class {
3220
3947
  ctx.signal.addEventListener("abort", cleanup);
3221
3948
  return {
3222
3949
  destroy: cleanup,
3950
+ getPageCount: () => totalPages,
3951
+ getCurrentPage: () => currentPage,
3952
+ goToPage: (page) => renderPage(page),
3953
+ getThumbnails: () => {
3954
+ const thumbnails = [];
3955
+ for (let i = 0; i < totalPages; i++) {
3956
+ const pageIdx = i;
3957
+ const startRow = pageIdx * ROWS_PER_PAGE + 1;
3958
+ const endRow = Math.min(dataLines.length, (pageIdx + 1) * ROWS_PER_PAGE);
3959
+ const label = totalPages === 1 ? "Table" : `Page ${pageIdx + 1} (${startRow}-${endRow})`;
3960
+ thumbnails.push({
3961
+ index: pageIdx,
3962
+ label,
3963
+ render: async (canvas) => {
3964
+ canvas.width = 140;
3965
+ canvas.height = 100;
3966
+ const c = canvas.getContext("2d");
3967
+ if (!c) return;
3968
+ c.fillStyle = "#ffffff";
3969
+ c.fillRect(0, 0, canvas.width, canvas.height);
3970
+ c.fillStyle = "#2563eb";
3971
+ c.fillRect(0, 0, canvas.width, 16);
3972
+ c.fillStyle = "#ffffff";
3973
+ c.font = "bold 8px sans-serif";
3974
+ c.fillText(`\u{1F4C8} ${label.slice(0, 18)}`, 6, 11);
3975
+ const startY = 17;
3976
+ const rowH = 13;
3977
+ const colW = 28;
3978
+ const cols = Math.min(4, headerCells.length || 4);
3979
+ c.fillStyle = "#f1f5f9";
3980
+ c.fillRect(0, startY, canvas.width, rowH);
3981
+ c.strokeStyle = "#cbd5e1";
3982
+ c.lineWidth = 0.8;
3983
+ c.strokeRect(0, startY, canvas.width, rowH);
3984
+ c.fillStyle = "#64748b";
3985
+ c.font = "bold 7px sans-serif";
3986
+ for (let ci = 0; ci < cols; ci++) {
3987
+ const hName = headerCells[ci] || `Col ${ci + 1}`;
3988
+ c.fillText(hName.slice(0, 5), 8 + ci * colW, startY + 9);
3989
+ }
3990
+ const rowsToPreview = dataLines.slice(pageIdx * ROWS_PER_PAGE, pageIdx * ROWS_PER_PAGE + 5);
3991
+ for (let ri = 0; ri < rowsToPreview.length; ri++) {
3992
+ const y = startY + (ri + 1) * rowH;
3993
+ if (y > canvas.height - 2) break;
3994
+ c.strokeStyle = "#e2e8f0";
3995
+ c.strokeRect(0, y, canvas.width, rowH);
3996
+ const cVals = rowsToPreview[ri].split(delimiter);
3997
+ c.fillStyle = "#334155";
3998
+ c.font = "6px sans-serif";
3999
+ for (let ci = 0; ci < cols; ci++) {
4000
+ const val = cVals[ci] || "";
4001
+ c.fillText(val.trim().slice(0, 6), 8 + ci * colW, y + 9);
4002
+ }
4003
+ }
4004
+ c.strokeStyle = "#cbd5e1";
4005
+ c.lineWidth = 1;
4006
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
4007
+ }
4008
+ });
4009
+ }
4010
+ return thumbnails;
4011
+ },
3223
4012
  zoomIn: () => {
3224
4013
  scale += 0.1;
3225
- container.style.transform = `scale(${scale})`;
4014
+ applyScale();
3226
4015
  },
3227
4016
  zoomOut: () => {
3228
4017
  scale = Math.max(0.2, scale - 0.1);
3229
- container.style.transform = `scale(${scale})`;
4018
+ applyScale();
3230
4019
  },
3231
4020
  getZoom: () => scale,
3232
4021
  setZoom: (level) => {
3233
4022
  scale = level;
3234
- container.style.transform = `scale(${scale})`;
4023
+ applyScale();
3235
4024
  },
3236
4025
  fitToPage: () => {
3237
- scale = 1;
3238
- container.style.transform = `scale(1)`;
4026
+ const availW = Math.max(280, (ctx.container.clientWidth || window.innerWidth) - 48);
4027
+ const tW = table.offsetWidth || 600;
4028
+ scale = Math.max(0.35, Math.min(1, availW / tW));
4029
+ applyScale();
3239
4030
  },
3240
4031
  download: () => {
3241
4032
  const blob = new Blob([ctx.buffer], { type: "text/csv" });
@@ -3299,28 +4090,34 @@ var CodePlugin = class {
3299
4090
  getToolbarActions(instance) {
3300
4091
  const totalPages = instance.getPageCount?.() ?? 1;
3301
4092
  const actions = [];
3302
- if (totalPages > 1) {
3303
- actions.push({
3304
- id: "page-nav",
3305
- icon: "",
3306
- label: "Page Navigation",
3307
- type: "page-nav",
3308
- group: "navigation",
3309
- value: instance.getCurrentPage?.() ?? 1,
3310
- max: totalPages,
3311
- execute: (action, page) => {
3312
- const cur = instance.getCurrentPage?.() ?? 1;
3313
- const max = instance.getPageCount?.() ?? 1;
3314
- if (action === "prev") {
3315
- if (cur > 1) instance.goToPage?.(cur - 1);
3316
- } else if (action === "next") {
3317
- if (cur < max) instance.goToPage?.(cur + 1);
3318
- } else if (typeof page === "number") {
3319
- instance.goToPage?.(page);
3320
- }
4093
+ actions.push({
4094
+ id: "thumbnails",
4095
+ icon: "thumbnails",
4096
+ label: "Page Thumbnails",
4097
+ type: "button",
4098
+ group: "navigation",
4099
+ execute: () => instance.toggleThumbnails?.()
4100
+ });
4101
+ actions.push({
4102
+ id: "page-nav",
4103
+ icon: "",
4104
+ label: "Page Navigation",
4105
+ type: "page-nav",
4106
+ group: "navigation",
4107
+ value: instance.getCurrentPage?.() ?? 1,
4108
+ max: totalPages,
4109
+ execute: (action, page) => {
4110
+ const cur = instance.getCurrentPage?.() ?? 1;
4111
+ const max = instance.getPageCount?.() ?? 1;
4112
+ if (action === "prev") {
4113
+ if (cur > 1) instance.goToPage?.(cur - 1);
4114
+ } else if (action === "next") {
4115
+ if (cur < max) instance.goToPage?.(cur + 1);
4116
+ } else if (typeof page === "number") {
4117
+ instance.goToPage?.(page);
3321
4118
  }
3322
- });
3323
- }
4119
+ }
4120
+ });
3324
4121
  actions.push(
3325
4122
  {
3326
4123
  id: "zoom-out",
@@ -3464,16 +4261,19 @@ var CodePlugin = class {
3464
4261
  let fontSize = 13;
3465
4262
  let rotation = 0;
3466
4263
  let zoomLevel = 1;
4264
+ let isUserZoomed = false;
3467
4265
  const pre = document.createElement("pre");
3468
4266
  const code = document.createElement("code");
3469
4267
  if (isTxt) {
4268
+ container.style.overflowX = "hidden";
4269
+ container.style.overflowY = "auto";
3470
4270
  container.style.backgroundColor = "#f1f5f9";
3471
- container.style.padding = "32px";
4271
+ container.style.padding = "16px 8px";
3472
4272
  container.style.boxSizing = "border-box";
3473
4273
  container.style.display = "flex";
3474
4274
  container.style.flexDirection = "column";
3475
4275
  container.style.alignItems = "center";
3476
- pre.style.margin = "0";
4276
+ pre.style.margin = "0 auto";
3477
4277
  pre.style.fontFamily = "Consolas, 'Courier New', monospace";
3478
4278
  pre.style.fontSize = "13px";
3479
4279
  pre.style.color = "#1e293b";
@@ -3485,10 +4285,12 @@ var CodePlugin = class {
3485
4285
  pre.style.minHeight = "1056px";
3486
4286
  pre.style.padding = "72px 56px";
3487
4287
  pre.style.boxSizing = "border-box";
3488
- pre.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
4288
+ pre.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
3489
4289
  pre.style.borderRadius = "4px";
3490
4290
  pre.style.transformOrigin = "top center";
4291
+ pre.style.transition = "transform 0.15s ease";
3491
4292
  } else {
4293
+ container.style.overflow = "auto";
3492
4294
  container.style.backgroundColor = "#1e1e1e";
3493
4295
  container.style.color = "#d4d4d4";
3494
4296
  container.style.padding = "16px";
@@ -3520,42 +4322,9 @@ var CodePlugin = class {
3520
4322
  renderCodePage(rawPages[0] || (isTxt ? "" : fullText));
3521
4323
  pre.appendChild(code);
3522
4324
  container.appendChild(pre);
3523
- let indicator = null;
3524
- if (totalPages > 1) {
3525
- indicator = document.createElement("div");
3526
- indicator.className = "fp-code-page-indicator";
3527
- indicator.style.position = "sticky";
3528
- indicator.style.bottom = "16px";
3529
- if (isTxt) {
3530
- indicator.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
3531
- indicator.style.color = "#334155";
3532
- indicator.style.border = "1px solid #e2e8f0";
3533
- indicator.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)";
3534
- } else {
3535
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3536
- indicator.style.color = "#f8fafc";
3537
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3538
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3539
- indicator.style.backdropFilter = "blur(8px)";
3540
- }
3541
- indicator.style.fontSize = "12px";
3542
- indicator.style.fontWeight = "600";
3543
- indicator.style.padding = "5px 14px";
3544
- indicator.style.borderRadius = "20px";
3545
- indicator.style.zIndex = "10";
3546
- indicator.style.userSelect = "none";
3547
- indicator.style.pointerEvents = "none";
3548
- indicator.style.textAlign = "center";
3549
- indicator.style.width = "fit-content";
3550
- indicator.style.margin = "16px auto 0";
3551
- container.appendChild(indicator);
3552
- }
3553
4325
  const showPage = (pageNum) => {
3554
4326
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
3555
4327
  renderCodePage(rawPages[currentPage - 1] || (isTxt ? "" : fullText));
3556
- if (indicator) {
3557
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3558
- }
3559
4328
  container.scrollTop = 0;
3560
4329
  ctx.emit("page-change", { page: currentPage, total: totalPages });
3561
4330
  };
@@ -3563,36 +4332,90 @@ var CodePlugin = class {
3563
4332
  showPage(1);
3564
4333
  }
3565
4334
  ctx.container.appendChild(container);
3566
- const cleanup = () => {
3567
- indicator?.remove();
3568
- container.remove();
3569
- ctx.container.innerHTML = "";
4335
+ const calculateTxtFit = () => {
4336
+ const availW = Math.max(280, container.clientWidth - 32);
4337
+ return Math.max(0.4, Math.min(3, availW / 816));
3570
4338
  };
3571
- ctx.signal.addEventListener("abort", cleanup);
3572
4339
  const updateTransform = () => {
3573
4340
  if (isTxt) {
3574
4341
  pre.style.transform = `scale(${zoomLevel}) rotate(${rotation}deg)`;
4342
+ const scaledH = 1056 * zoomLevel;
4343
+ const extraH = Math.max(0, scaledH - 1056);
4344
+ pre.style.marginBottom = `${extraH + 32}px`;
3575
4345
  } else {
3576
4346
  pre.style.transform = `rotate(${rotation}deg)`;
3577
4347
  pre.style.fontSize = `${fontSize}px`;
3578
4348
  }
3579
4349
  };
4350
+ let resizeObserver = null;
4351
+ if (isTxt) {
4352
+ setTimeout(() => {
4353
+ zoomLevel = calculateTxtFit();
4354
+ updateTransform();
4355
+ }, 60);
4356
+ resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
4357
+ if (!isUserZoomed) {
4358
+ zoomLevel = calculateTxtFit();
4359
+ updateTransform();
4360
+ }
4361
+ }) : null;
4362
+ resizeObserver?.observe(container);
4363
+ }
4364
+ const cleanup = () => {
4365
+ resizeObserver?.disconnect();
4366
+ container.remove();
4367
+ ctx.container.innerHTML = "";
4368
+ };
4369
+ ctx.signal.addEventListener("abort", cleanup);
3580
4370
  return {
3581
4371
  destroy: cleanup,
3582
4372
  getPageCount: () => totalPages,
3583
4373
  getCurrentPage: () => currentPage,
3584
4374
  goToPage: (page) => showPage(page),
4375
+ getThumbnails: () => {
4376
+ return rawPages.map((pageText, idx) => ({
4377
+ index: idx,
4378
+ label: `Page ${idx + 1}`,
4379
+ render: async (canvas) => {
4380
+ canvas.width = 140;
4381
+ canvas.height = 180;
4382
+ const c = canvas.getContext("2d");
4383
+ if (!c) return;
4384
+ c.fillStyle = "#ffffff";
4385
+ c.fillRect(0, 0, canvas.width, canvas.height);
4386
+ c.strokeStyle = "#cbd5e1";
4387
+ c.lineWidth = 1;
4388
+ c.strokeRect(0.5, 0.5, canvas.width - 1, canvas.height - 1);
4389
+ c.fillStyle = "#64748b";
4390
+ const lines = (pageText || "").split("\n").slice(0, 24);
4391
+ const lineH = 6;
4392
+ const startY = 14;
4393
+ const startX = 12;
4394
+ const maxW = canvas.width - 24;
4395
+ c.font = "5px monospace";
4396
+ for (let li = 0; li < lines.length; li++) {
4397
+ const l = lines[li].trim();
4398
+ if (!l) continue;
4399
+ const y = startY + li * lineH;
4400
+ if (y > canvas.height - 12) break;
4401
+ c.fillText(l.slice(0, 30), startX, y, maxW);
4402
+ }
4403
+ }
4404
+ }));
4405
+ },
3585
4406
  zoomIn: () => {
4407
+ isUserZoomed = true;
3586
4408
  if (isTxt) {
3587
- zoomLevel = Math.min(3, zoomLevel + 0.1);
4409
+ zoomLevel = Math.min(3.5, zoomLevel + 0.15);
3588
4410
  } else {
3589
4411
  fontSize = Math.min(32, fontSize + 2);
3590
4412
  }
3591
4413
  updateTransform();
3592
4414
  },
3593
4415
  zoomOut: () => {
4416
+ isUserZoomed = true;
3594
4417
  if (isTxt) {
3595
- zoomLevel = Math.max(0.1, zoomLevel - 0.1);
4418
+ zoomLevel = Math.max(0.1, zoomLevel - 0.15);
3596
4419
  } else {
3597
4420
  fontSize = Math.max(8, fontSize - 2);
3598
4421
  }
@@ -3600,6 +4423,7 @@ var CodePlugin = class {
3600
4423
  },
3601
4424
  getZoom: () => isTxt ? zoomLevel : fontSize / 13,
3602
4425
  setZoom: (level) => {
4426
+ isUserZoomed = true;
3603
4427
  if (isTxt) {
3604
4428
  zoomLevel = level;
3605
4429
  } else {
@@ -3608,9 +4432,10 @@ var CodePlugin = class {
3608
4432
  updateTransform();
3609
4433
  },
3610
4434
  fitToPage: () => {
4435
+ isUserZoomed = false;
3611
4436
  fontSize = 13;
3612
4437
  rotation = 0;
3613
- zoomLevel = isTxt ? Math.max(0.65, Math.min(1.05, (container.clientWidth - 48) / 816)) : 1;
4438
+ zoomLevel = isTxt ? calculateTxtFit() : 1;
3614
4439
  updateTransform();
3615
4440
  },
3616
4441
  rotateCW: () => {
@@ -4491,28 +5316,26 @@ var RtfPlugin = class {
4491
5316
  getToolbarActions(instance) {
4492
5317
  const totalPages = instance.getPageCount?.() ?? 1;
4493
5318
  const actions = [];
4494
- if (totalPages > 1) {
4495
- actions.push({
4496
- id: "page-nav",
4497
- icon: "",
4498
- label: "Page Navigation",
4499
- type: "page-nav",
4500
- group: "navigation",
4501
- value: instance.getCurrentPage?.() ?? 1,
4502
- max: totalPages,
4503
- execute: (action, page) => {
4504
- const cur = instance.getCurrentPage?.() ?? 1;
4505
- const max = instance.getPageCount?.() ?? 1;
4506
- if (action === "prev") {
4507
- if (cur > 1) instance.goToPage?.(cur - 1);
4508
- } else if (action === "next") {
4509
- if (cur < max) instance.goToPage?.(cur + 1);
4510
- } else if (typeof page === "number") {
4511
- instance.goToPage?.(page);
4512
- }
5319
+ actions.push({
5320
+ id: "page-nav",
5321
+ icon: "",
5322
+ label: "Page Navigation",
5323
+ type: "page-nav",
5324
+ group: "navigation",
5325
+ value: instance.getCurrentPage?.() ?? 1,
5326
+ max: totalPages,
5327
+ execute: (action, page) => {
5328
+ const cur = instance.getCurrentPage?.() ?? 1;
5329
+ const max = instance.getPageCount?.() ?? 1;
5330
+ if (action === "prev") {
5331
+ if (cur > 1) instance.goToPage?.(cur - 1);
5332
+ } else if (action === "next") {
5333
+ if (cur < max) instance.goToPage?.(cur + 1);
5334
+ } else if (typeof page === "number") {
5335
+ instance.goToPage?.(page);
4513
5336
  }
4514
- });
4515
- }
5337
+ }
5338
+ });
4516
5339
  actions.push(
4517
5340
  {
4518
5341
  id: "zoom-out",
@@ -4584,43 +5407,57 @@ var RtfPlugin = class {
4584
5407
  async render(ctx) {
4585
5408
  const wrapper = document.createElement("div");
4586
5409
  wrapper.className = "fp-rtf-wrapper";
4587
- wrapper.style.padding = "32px";
4588
- wrapper.style.maxWidth = "850px";
5410
+ wrapper.style.padding = "0";
5411
+ wrapper.style.width = "816px";
4589
5412
  wrapper.style.margin = "0 auto";
4590
- wrapper.style.backgroundColor = "#fff";
4591
- wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
4592
- wrapper.style.borderRadius = "4px";
4593
- wrapper.style.minHeight = "100%";
5413
+ wrapper.style.boxSizing = "border-box";
5414
+ wrapper.style.display = "flex";
5415
+ wrapper.style.flexDirection = "column";
5416
+ wrapper.style.alignItems = "center";
5417
+ wrapper.style.backgroundColor = "transparent";
4594
5418
  wrapper.style.transformOrigin = "top center";
4595
- wrapper.style.transition = "transform 0.2s ease";
4596
- ctx.container.style.overflow = "auto";
4597
- ctx.container.style.padding = "24px";
5419
+ wrapper.style.transition = "transform 0.15s ease";
5420
+ ctx.container.style.overflowX = "hidden";
5421
+ ctx.container.style.overflowY = "auto";
5422
+ ctx.container.style.padding = "16px 8px";
4598
5423
  ctx.container.style.backgroundColor = "#f1f5f9";
4599
5424
  ctx.container.appendChild(wrapper);
4600
5425
  let scale = 1;
4601
5426
  let rotation = 0;
4602
5427
  let pageElements = [];
4603
- let fitMode = "width";
5428
+ let isUserZoomed = false;
5429
+ let fitMode = ctx?.options?.fitMode || "width";
4604
5430
  const calculateFitScale = (mode = fitMode) => {
4605
5431
  const activeEl = pageElements[currentPage - 1] || wrapper;
4606
- const elW = activeEl.offsetWidth || 850;
4607
- const singlePageH = Math.min(activeEl.offsetHeight || 1056, Math.round(elW * 1.32));
4608
- const availW = Math.max(280, ctx.container.clientWidth - 48);
4609
- const availH = Math.max(280, ctx.container.clientHeight - 80);
5432
+ const elW = 816;
5433
+ const singlePageH = activeEl?.offsetHeight || 1056;
5434
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
5435
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
4610
5436
  const sW = availW / elW;
4611
5437
  const sH = availH / singlePageH;
4612
5438
  if (mode === "page") {
4613
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
5439
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
4614
5440
  }
4615
- return Math.max(0.65, Math.min(1.05, sW));
5441
+ return Math.max(0.4, Math.min(3, sW));
4616
5442
  };
4617
5443
  const applyTransform = () => {
4618
5444
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
4619
5445
  wrapper.style.transformOrigin = "top center";
5446
+ const activeEl = pageElements[currentPage - 1];
5447
+ const baseH = activeEl?.offsetHeight || 1056;
5448
+ const scaledH = baseH * scale;
5449
+ const extraH = Math.max(0, scaledH - baseH);
5450
+ wrapper.style.marginBottom = `${extraH + 32}px`;
4620
5451
  };
5452
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
5453
+ if (!isUserZoomed) {
5454
+ scale = calculateFitScale(fitMode);
5455
+ applyTransform();
5456
+ }
5457
+ }) : null;
5458
+ resizeObserver?.observe(ctx.container);
4621
5459
  setTimeout(() => {
4622
- fitMode = "width";
4623
- scale = calculateFitScale("width");
5460
+ scale = calculateFitScale(fitMode);
4624
5461
  applyTransform();
4625
5462
  }, 60);
4626
5463
  try {
@@ -4630,20 +5467,34 @@ var RtfPlugin = class {
4630
5467
  const doc = new RTFJS.Document(ctx.buffer, {});
4631
5468
  const htmlElements = await doc.render();
4632
5469
  const contentNodes = [];
4633
- for (const item of htmlElements) {
4634
- if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4635
- contentNodes.push(...Array.from(item.children));
4636
- } else {
4637
- contentNodes.push(item);
5470
+ const extractBlocks = (nodes) => {
5471
+ for (const item of nodes) {
5472
+ if (!item || !(item instanceof HTMLElement)) continue;
5473
+ const tag = item.tagName.toLowerCase();
5474
+ const hasChildBlocks = Array.from(item.children).some((c) => {
5475
+ const ct = c.tagName.toLowerCase();
5476
+ return ["p", "div", "table", "h1", "h2", "h3", "h4", "h5", "h6", "section", "article", "ul", "ol"].includes(ct);
5477
+ });
5478
+ if (hasChildBlocks && !["table", "tr", "td", "th", "ul", "ol", "li"].includes(tag)) {
5479
+ extractBlocks(Array.from(item.children));
5480
+ } else {
5481
+ if (tag === "p" || tag === "div") {
5482
+ item.style.display = "block";
5483
+ item.style.marginBottom = "12px";
5484
+ item.style.lineHeight = "1.6";
5485
+ }
5486
+ contentNodes.push(item);
5487
+ }
4638
5488
  }
4639
- }
5489
+ };
5490
+ extractBlocks(htmlElements);
4640
5491
  wrapper.innerHTML = "";
4641
5492
  contentNodes.forEach((node) => wrapper.appendChild(node));
4642
5493
  const childHeights = contentNodes.map((c) => {
4643
5494
  const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4644
5495
  const offH = c.offsetHeight || 0;
4645
5496
  const textLen = c.textContent?.trim().length || 0;
4646
- const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
5497
+ const estH = Math.max(28, Math.ceil(textLen / 75) * 24 + 16);
4647
5498
  return Math.max(rectH, offH, estH);
4648
5499
  });
4649
5500
  wrapper.innerHTML = "";
@@ -4660,6 +5511,7 @@ var RtfPlugin = class {
4660
5511
  card.style.marginBottom = "24px";
4661
5512
  card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4662
5513
  card.style.lineHeight = "1.6";
5514
+ card.style.color = "#1e293b";
4663
5515
  return card;
4664
5516
  };
4665
5517
  let curCard = createRtfCard();
@@ -4695,9 +5547,8 @@ var RtfPlugin = class {
4695
5547
  pageCard.style.padding = "72px 56px";
4696
5548
  pageCard.style.width = "816px";
4697
5549
  pageCard.style.minHeight = "1056px";
4698
- pageCard.style.maxWidth = "100%";
4699
5550
  pageCard.style.boxSizing = "border-box";
4700
- pageCard.style.fontFamily = "serif";
5551
+ pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4701
5552
  pageCard.style.fontSize = "12pt";
4702
5553
  pageCard.style.lineHeight = "1.6";
4703
5554
  pageCard.style.color = "#1e293b";
@@ -4710,29 +5561,6 @@ var RtfPlugin = class {
4710
5561
  }
4711
5562
  const totalPages = Math.max(1, pageElements.length);
4712
5563
  let currentPage = 1;
4713
- let indicator = null;
4714
- if (totalPages > 1) {
4715
- indicator = document.createElement("div");
4716
- indicator.className = "fp-rtf-page-indicator";
4717
- indicator.style.position = "sticky";
4718
- indicator.style.bottom = "16px";
4719
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4720
- indicator.style.backdropFilter = "blur(8px)";
4721
- indicator.style.color = "#f8fafc";
4722
- indicator.style.fontSize = "12px";
4723
- indicator.style.fontWeight = "600";
4724
- indicator.style.padding = "5px 14px";
4725
- indicator.style.borderRadius = "20px";
4726
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4727
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4728
- indicator.style.zIndex = "10";
4729
- indicator.style.userSelect = "none";
4730
- indicator.style.pointerEvents = "none";
4731
- indicator.style.textAlign = "center";
4732
- indicator.style.width = "fit-content";
4733
- indicator.style.margin = "16px auto 0";
4734
- ctx.container.appendChild(indicator);
4735
- }
4736
5564
  const showPage = (pageNum) => {
4737
5565
  currentPage = Math.max(1, Math.min(totalPages, pageNum));
4738
5566
  if (totalPages > 1) {
@@ -4740,9 +5568,6 @@ var RtfPlugin = class {
4740
5568
  el.style.display = idx + 1 === currentPage ? "block" : "none";
4741
5569
  });
4742
5570
  }
4743
- if (indicator) {
4744
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4745
- }
4746
5571
  ctx.container.scrollTop = 0;
4747
5572
  ctx.emit("page-change", { page: currentPage, total: totalPages });
4748
5573
  };
@@ -4750,7 +5575,7 @@ var RtfPlugin = class {
4750
5575
  showPage(1);
4751
5576
  }
4752
5577
  const cleanup = () => {
4753
- indicator?.remove();
5578
+ resizeObserver?.disconnect();
4754
5579
  wrapper.remove();
4755
5580
  ctx.container.innerHTML = "";
4756
5581
  };
@@ -4761,21 +5586,25 @@ var RtfPlugin = class {
4761
5586
  getCurrentPage: () => currentPage,
4762
5587
  goToPage: (page) => showPage(page),
4763
5588
  zoomIn: () => {
5589
+ isUserZoomed = true;
4764
5590
  scale += 0.15;
4765
5591
  applyTransform();
4766
5592
  },
4767
5593
  zoomOut: () => {
5594
+ isUserZoomed = true;
4768
5595
  scale = Math.max(0.2, scale - 0.15);
4769
5596
  applyTransform();
4770
5597
  },
4771
5598
  getZoom: () => scale,
4772
5599
  setZoom: (level) => {
5600
+ isUserZoomed = true;
4773
5601
  scale = level;
4774
5602
  applyTransform();
4775
5603
  },
4776
5604
  fitToPage: () => {
4777
- fitMode = fitMode === "width" ? "page" : "width";
4778
- scale = calculateFitScale(fitMode);
5605
+ isUserZoomed = false;
5606
+ fitMode = "width";
5607
+ scale = calculateFitScale("width");
4779
5608
  rotation = 0;
4780
5609
  applyTransform();
4781
5610
  },
@@ -4963,40 +5792,36 @@ var OpenDocumentPlugin = class {
4963
5792
  const isPresentation = instance.isPresentation;
4964
5793
  const totalPages = instance.getPageCount?.() ?? 1;
4965
5794
  const actions = [];
4966
- if (isPresentation || totalPages > 1) {
4967
- if (isPresentation) {
4968
- actions.push({
4969
- id: "thumbnails",
4970
- icon: "thumbnails",
4971
- label: "Slide Thumbnails",
4972
- type: "button",
4973
- group: "navigation",
4974
- execute: () => instance.toggleThumbnails?.()
4975
- });
4976
- }
4977
- actions.push({
4978
- id: "page-nav",
4979
- icon: "",
4980
- label: isPresentation ? "Slide Navigation" : "Page Navigation",
4981
- type: "page-nav",
4982
- group: "navigation",
4983
- value: instance.getCurrentPage?.() ?? 1,
4984
- max: totalPages,
4985
- execute: (action, page) => {
4986
- const cur = instance.getCurrentPage?.() ?? 1;
4987
- const max = instance.getPageCount?.() ?? 1;
4988
- if (action === "prev") {
4989
- if (cur > 1) instance.goToPage?.(cur - 1);
4990
- } else if (action === "next") {
4991
- if (cur < max) instance.goToPage?.(cur + 1);
4992
- } else if (typeof page === "number") {
4993
- instance.goToPage?.(page);
4994
- } else if (typeof action === "number") {
4995
- instance.goToPage?.(action);
4996
- }
5795
+ actions.push({
5796
+ id: "thumbnails",
5797
+ icon: "thumbnails",
5798
+ label: isPresentation ? "Slide Thumbnails" : "Page Thumbnails",
5799
+ type: "button",
5800
+ group: "navigation",
5801
+ execute: () => instance.toggleThumbnails?.()
5802
+ });
5803
+ actions.push({
5804
+ id: "page-nav",
5805
+ icon: "",
5806
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
5807
+ type: "page-nav",
5808
+ group: "navigation",
5809
+ value: instance.getCurrentPage?.() ?? 1,
5810
+ max: totalPages,
5811
+ execute: (action, page) => {
5812
+ const cur = instance.getCurrentPage?.() ?? 1;
5813
+ const max = instance.getPageCount?.() ?? 1;
5814
+ if (action === "prev") {
5815
+ if (cur > 1) instance.goToPage?.(cur - 1);
5816
+ } else if (action === "next") {
5817
+ if (cur < max) instance.goToPage?.(cur + 1);
5818
+ } else if (typeof page === "number") {
5819
+ instance.goToPage?.(page);
5820
+ } else if (typeof action === "number") {
5821
+ instance.goToPage?.(action);
4997
5822
  }
4998
- });
4999
- }
5823
+ }
5824
+ });
5000
5825
  actions.push(
5001
5826
  {
5002
5827
  id: "zoom-out",
@@ -5087,49 +5912,65 @@ var OpenDocumentPlugin = class {
5087
5912
  container.className = "fp-odf-container";
5088
5913
  container.style.width = "100%";
5089
5914
  container.style.height = "100%";
5090
- container.style.overflow = "auto";
5091
- container.style.padding = "24px";
5915
+ container.style.overflowX = "hidden";
5916
+ container.style.overflowY = "auto";
5917
+ container.style.padding = "16px 8px";
5092
5918
  container.style.backgroundColor = "#f1f5f9";
5093
5919
  const wrapper = document.createElement("div");
5094
5920
  wrapper.className = "fp-odf-wrapper";
5095
5921
  wrapper.style.margin = "0 auto";
5922
+ wrapper.style.width = isPresentation ? "960px" : "816px";
5923
+ wrapper.style.boxSizing = "border-box";
5096
5924
  wrapper.style.backgroundColor = "#ffffff";
5097
5925
  wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
5098
5926
  wrapper.style.borderRadius = "4px";
5099
5927
  wrapper.style.transformOrigin = "top center";
5100
- wrapper.style.transition = "transform 0.2s ease";
5928
+ wrapper.style.transition = "transform 0.15s ease";
5101
5929
  container.appendChild(wrapper);
5102
5930
  ctx.container.appendChild(container);
5103
5931
  let scale = 1;
5104
5932
  let rotation = 0;
5105
- let fitMode = "width";
5933
+ let isUserZoomed = false;
5934
+ let fitMode = ctx?.options?.fitMode || "width";
5935
+ let currentPage = 1;
5936
+ let totalPages = 1;
5937
+ let slides = [];
5106
5938
  const calculateFitScale = (mode = fitMode) => {
5107
- const elW = wrapper.offsetWidth || 850;
5108
- const singlePageH = Math.min(wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
5109
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5110
- const availH = Math.max(280, ctx.container.clientHeight - 80);
5939
+ const activeSlide = slides[currentPage - 1];
5940
+ const elW = isPresentation ? 960 : 816;
5941
+ const singlePageH = Math.min(activeSlide?.offsetHeight || wrapper.offsetHeight || 1056, Math.round(elW * 1.32));
5942
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
5943
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5111
5944
  const sW = availW / elW;
5112
5945
  const sH = availH / (isPresentation ? 540 : singlePageH);
5113
5946
  if (isPresentation) {
5114
- return Math.min(1, Math.min(sW, sH));
5947
+ return Math.min(2.5, Math.min(sW, sH));
5115
5948
  }
5116
5949
  if (mode === "page") {
5117
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
5950
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5118
5951
  }
5119
- return Math.max(0.65, Math.min(1.05, sW));
5952
+ return Math.max(0.4, Math.min(3, sW));
5120
5953
  };
5121
5954
  const applyTransform = () => {
5122
5955
  wrapper.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
5123
5956
  wrapper.style.transformOrigin = "top center";
5957
+ const activeSlide = slides[currentPage - 1];
5958
+ const baseH = activeSlide?.offsetHeight || wrapper.offsetHeight || 1056;
5959
+ const scaledH = baseH * scale;
5960
+ const extraH = Math.max(0, scaledH - baseH);
5961
+ wrapper.style.marginBottom = `${extraH + 32}px`;
5124
5962
  };
5963
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
5964
+ if (!isUserZoomed) {
5965
+ scale = calculateFitScale(fitMode);
5966
+ applyTransform();
5967
+ }
5968
+ }) : null;
5969
+ resizeObserver?.observe(ctx.container);
5125
5970
  setTimeout(() => {
5126
- fitMode = "width";
5127
- scale = calculateFitScale("width");
5971
+ scale = calculateFitScale(fitMode);
5128
5972
  applyTransform();
5129
5973
  }, 60);
5130
- let currentPage = 1;
5131
- let totalPages = 1;
5132
- let slides = [];
5133
5974
  if (isPresentation) {
5134
5975
  wrapper.style.maxWidth = "960px";
5135
5976
  wrapper.style.aspectRatio = "16 / 9";
@@ -5230,24 +6071,9 @@ var OpenDocumentPlugin = class {
5230
6071
  wrapper.appendChild(page);
5231
6072
  slides.push(page);
5232
6073
  });
5233
- const pageIndicator = document.createElement("div");
5234
- pageIndicator.className = "fp-odt-page-indicator";
5235
- pageIndicator.style.position = "sticky";
5236
- pageIndicator.style.bottom = "16px";
5237
- pageIndicator.style.left = "50%";
5238
- pageIndicator.style.transform = "translateX(-50%)";
5239
- pageIndicator.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
5240
- pageIndicator.style.color = "#fff";
5241
- pageIndicator.style.padding = "6px 12px";
5242
- pageIndicator.style.borderRadius = "16px";
5243
- pageIndicator.style.fontSize = "12px";
5244
- pageIndicator.style.zIndex = "100";
5245
- pageIndicator.style.display = "inline-block";
5246
- pageIndicator.style.width = "fit-content";
5247
- pageIndicator.textContent = `Page 1 of ${totalPages}`;
5248
- container.appendChild(pageIndicator);
5249
6074
  }
5250
6075
  const cleanup = () => {
6076
+ resizeObserver?.disconnect();
5251
6077
  imageUrls.forEach((url) => URL.revokeObjectURL(url));
5252
6078
  container.remove();
5253
6079
  ctx.container.innerHTML = "";
@@ -5259,10 +6085,6 @@ var OpenDocumentPlugin = class {
5259
6085
  slides.forEach((s, idx) => {
5260
6086
  s.style.display = idx === page - 1 ? "block" : "none";
5261
6087
  });
5262
- const indicator = container.querySelector(".fp-odt-page-indicator");
5263
- if (indicator) {
5264
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5265
- }
5266
6088
  container.scrollTop = 0;
5267
6089
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5268
6090
  };
@@ -5270,21 +6092,25 @@ var OpenDocumentPlugin = class {
5270
6092
  destroy: cleanup,
5271
6093
  isPresentation,
5272
6094
  zoomIn: () => {
6095
+ isUserZoomed = true;
5273
6096
  scale += 0.15;
5274
6097
  applyTransform();
5275
6098
  },
5276
6099
  zoomOut: () => {
6100
+ isUserZoomed = true;
5277
6101
  scale = Math.max(0.2, scale - 0.15);
5278
6102
  applyTransform();
5279
6103
  },
5280
6104
  getZoom: () => scale,
5281
6105
  setZoom: (level) => {
6106
+ isUserZoomed = true;
5282
6107
  scale = level;
5283
6108
  applyTransform();
5284
6109
  },
5285
6110
  fitToPage: () => {
5286
- fitMode = fitMode === "width" ? "page" : "width";
5287
- scale = calculateFitScale(fitMode);
6111
+ isUserZoomed = false;
6112
+ fitMode = "width";
6113
+ scale = calculateFitScale("width");
5288
6114
  rotation = 0;
5289
6115
  applyTransform();
5290
6116
  },
@@ -5300,23 +6126,45 @@ var OpenDocumentPlugin = class {
5300
6126
  getPageCount: () => totalPages,
5301
6127
  getCurrentPage: () => currentPage,
5302
6128
  getThumbnails: async () => {
5303
- if (!isPresentation) return [];
5304
- return slides.map((_, idx) => ({
5305
- index: idx,
5306
- label: `Slide ${idx + 1}`,
5307
- render: async (canvas) => {
5308
- const ctx2d = canvas.getContext("2d");
5309
- if (!ctx2d) return;
5310
- canvas.width = 160;
5311
- canvas.height = 90;
5312
- ctx2d.fillStyle = "#f8fafc";
5313
- ctx2d.fillRect(0, 0, 160, 90);
5314
- ctx2d.fillStyle = "#334155";
5315
- ctx2d.font = "bold 12px sans-serif";
5316
- ctx2d.textAlign = "center";
5317
- ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
5318
- }
5319
- }));
6129
+ const count = totalPages || slides.length || 1;
6130
+ const items = [];
6131
+ for (let idx = 0; idx < count; idx++) {
6132
+ const itemIdx = idx;
6133
+ items.push({
6134
+ index: itemIdx,
6135
+ label: isPresentation ? `Slide ${itemIdx + 1}` : `Page ${itemIdx + 1}`,
6136
+ render: async (canvas) => {
6137
+ const ctx2d = canvas.getContext("2d");
6138
+ if (!ctx2d) return;
6139
+ if (isPresentation) {
6140
+ canvas.width = 160;
6141
+ canvas.height = 90;
6142
+ ctx2d.fillStyle = "#f8fafc";
6143
+ ctx2d.fillRect(0, 0, 160, 90);
6144
+ ctx2d.strokeStyle = "#cbd5e1";
6145
+ ctx2d.lineWidth = 1;
6146
+ ctx2d.strokeRect(0.5, 0.5, 159, 89);
6147
+ ctx2d.fillStyle = "#334155";
6148
+ ctx2d.font = "bold 11px sans-serif";
6149
+ ctx2d.textAlign = "center";
6150
+ ctx2d.fillText(`Slide ${itemIdx + 1}`, 80, 50);
6151
+ } else {
6152
+ canvas.width = 140;
6153
+ canvas.height = 180;
6154
+ ctx2d.fillStyle = "#ffffff";
6155
+ ctx2d.fillRect(0, 0, 140, 180);
6156
+ ctx2d.strokeStyle = "#cbd5e1";
6157
+ ctx2d.lineWidth = 1;
6158
+ ctx2d.strokeRect(0.5, 0.5, 139, 179);
6159
+ ctx2d.fillStyle = "#64748b";
6160
+ ctx2d.font = "bold 10px sans-serif";
6161
+ ctx2d.textAlign = "center";
6162
+ ctx2d.fillText(`Page ${itemIdx + 1}`, 70, 90);
6163
+ }
6164
+ }
6165
+ });
6166
+ }
6167
+ return items;
5320
6168
  },
5321
6169
  download: () => {
5322
6170
  const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
@@ -5421,23 +6269,24 @@ var OpenDocumentPlugin = class {
5421
6269
  case "h": {
5422
6270
  const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
5423
6271
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5424
- html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
6272
+ html += `<h${level} style="${style}; margin: 18px 0 8px; color: #1e3a8a; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${node.textContent || ""}</h${level}>`;
5425
6273
  break;
5426
6274
  }
5427
6275
  case "p": {
5428
6276
  const style = styles.get(node.getAttribute("text:style-name") || "") || "";
5429
6277
  const inner = this.renderSpans(node, styles, images);
5430
- html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
6278
+ const isInsideLi = node.parentElement?.localName === "list-item" || node.parentElement?.tagName?.toLowerCase() === "list-item";
6279
+ html += `<p style="${style}; line-height: 1.5; margin: ${isInsideLi ? "0" : "8px 0 12px"}; font-family: Calibri, 'Segoe UI', Arial, sans-serif;">${inner}</p>`;
5431
6280
  break;
5432
6281
  }
5433
6282
  case "list": {
5434
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
6283
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
5435
6284
  Array.from(node.children).forEach((c) => walk(c));
5436
6285
  html += "</ul>";
5437
6286
  break;
5438
6287
  }
5439
6288
  case "list-item": {
5440
- html += "<li>";
6289
+ html += '<li style="margin: 4px 0; font-size: 11pt; line-height: 1.5; color: #1e293b;">';
5441
6290
  Array.from(node.children).forEach((c) => walk(c));
5442
6291
  html += "</li>";
5443
6292
  break;
@@ -5563,28 +6412,26 @@ var DocPlugin = class {
5563
6412
  getToolbarActions(instance) {
5564
6413
  const totalPages = instance.getPageCount?.() ?? 1;
5565
6414
  const actions = [];
5566
- if (totalPages > 1) {
5567
- actions.push({
5568
- id: "page-nav",
5569
- icon: "",
5570
- label: "Page Navigation",
5571
- type: "page-nav",
5572
- group: "navigation",
5573
- value: instance.getCurrentPage?.() ?? 1,
5574
- max: totalPages,
5575
- execute: (action, page) => {
5576
- const cur = instance.getCurrentPage?.() ?? 1;
5577
- const max = instance.getPageCount?.() ?? 1;
5578
- if (action === "prev") {
5579
- if (cur > 1) instance.goToPage?.(cur - 1);
5580
- } else if (action === "next") {
5581
- if (cur < max) instance.goToPage?.(cur + 1);
5582
- } else if (typeof page === "number") {
5583
- instance.goToPage?.(page);
5584
- }
6415
+ actions.push({
6416
+ id: "page-nav",
6417
+ icon: "",
6418
+ label: "Page Navigation",
6419
+ type: "page-nav",
6420
+ group: "navigation",
6421
+ value: instance.getCurrentPage?.() ?? 1,
6422
+ max: totalPages,
6423
+ execute: (action, page) => {
6424
+ const cur = instance.getCurrentPage?.() ?? 1;
6425
+ const max = instance.getPageCount?.() ?? 1;
6426
+ if (action === "prev") {
6427
+ if (cur > 1) instance.goToPage?.(cur - 1);
6428
+ } else if (action === "next") {
6429
+ if (cur < max) instance.goToPage?.(cur + 1);
6430
+ } else if (typeof page === "number") {
6431
+ instance.goToPage?.(page);
5585
6432
  }
5586
- });
5587
- }
6433
+ }
6434
+ });
5588
6435
  actions.push(
5589
6436
  {
5590
6437
  id: "zoom-out",
@@ -5658,8 +6505,9 @@ var DocPlugin = class {
5658
6505
  container.className = "fp-doc-container";
5659
6506
  container.style.width = "100%";
5660
6507
  container.style.height = "100%";
5661
- container.style.overflow = "auto";
5662
- container.style.padding = "32px 16px";
6508
+ container.style.overflowX = "hidden";
6509
+ container.style.overflowY = "auto";
6510
+ container.style.padding = "16px 8px";
5663
6511
  container.style.backgroundColor = "#f1f5f9";
5664
6512
  container.style.display = "flex";
5665
6513
  container.style.justifyContent = "center";
@@ -5668,8 +6516,17 @@ var DocPlugin = class {
5668
6516
  let scale = 1;
5669
6517
  let extractedRawText = "";
5670
6518
  let isFallback = false;
6519
+ let chartSvg = "";
5671
6520
  try {
5672
6521
  const cfbf = new CfbfReader(ctx.buffer);
6522
+ try {
6523
+ const pkg = cfbf.readStream("package_stream");
6524
+ if (pkg && pkg.length > 100) {
6525
+ chartSvg = this.parseOdfChartToSvg(pkg);
6526
+ }
6527
+ } catch (chartErr) {
6528
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
6529
+ }
5673
6530
  const wordDocStream = cfbf.readStream("WordDocument");
5674
6531
  if (!wordDocStream || wordDocStream.length < 512) {
5675
6532
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5687,7 +6544,7 @@ var DocPlugin = class {
5687
6544
  extractedRawText = fallback;
5688
6545
  isFallback = true;
5689
6546
  }
5690
- const rawPages = this.splitIntoPages(extractedRawText);
6547
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5691
6548
  const totalPages = Math.max(1, rawPages.length);
5692
6549
  let currentPage = 1;
5693
6550
  const pageCards = [];
@@ -5695,16 +6552,15 @@ var DocPlugin = class {
5695
6552
  const pageCard = document.createElement("div");
5696
6553
  pageCard.className = "fp-doc-page-card";
5697
6554
  pageCard.style.width = "816px";
5698
- pageCard.style.height = "1056px";
6555
+ pageCard.style.minHeight = "1056px";
5699
6556
  pageCard.style.backgroundColor = "#ffffff";
5700
- pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
6557
+ pageCard.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
5701
6558
  pageCard.style.borderRadius = "4px";
5702
- pageCard.style.padding = "96px 72px";
6559
+ pageCard.style.padding = "72px 56px";
5703
6560
  pageCard.style.boxSizing = "border-box";
5704
- pageCard.style.overflow = "hidden";
5705
6561
  pageCard.style.display = i === 0 ? "block" : "none";
5706
6562
  pageCard.style.transformOrigin = "top center";
5707
- pageCard.style.transition = "transform 0.2s ease";
6563
+ pageCard.style.transition = "transform 0.15s ease";
5708
6564
  pageCard.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
5709
6565
  pageCard.style.color = "#1e293b";
5710
6566
  if (isFallback && i === 0) {
@@ -5721,34 +6577,15 @@ var DocPlugin = class {
5721
6577
  container.appendChild(pageCard);
5722
6578
  pageCards.push(pageCard);
5723
6579
  }
5724
- let indicator = null;
5725
- if (totalPages > 1) {
5726
- indicator = document.createElement("div");
5727
- indicator.className = "fp-doc-page-indicator";
5728
- indicator.style.position = "fixed";
5729
- indicator.style.bottom = "16px";
5730
- indicator.style.left = "50%";
5731
- indicator.style.transform = "translateX(-50%)";
5732
- indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
5733
- indicator.style.backdropFilter = "blur(8px)";
5734
- indicator.style.color = "#f8fafc";
5735
- indicator.style.fontSize = "12px";
5736
- indicator.style.fontWeight = "600";
5737
- indicator.style.padding = "5px 14px";
5738
- indicator.style.borderRadius = "20px";
5739
- indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
5740
- indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
5741
- indicator.style.zIndex = "10";
5742
- indicator.style.userSelect = "none";
5743
- indicator.style.pointerEvents = "none";
5744
- indicator.style.textAlign = "center";
5745
- container.appendChild(indicator);
5746
- }
5747
6580
  let rotation = 0;
5748
6581
  const applyTransform = () => {
5749
6582
  const activeCard = pageCards[currentPage - 1];
5750
6583
  if (activeCard) {
5751
6584
  activeCard.style.transform = `scale(${scale}) rotate(${rotation}deg)`;
6585
+ const baseH = activeCard.offsetHeight || 1056;
6586
+ const scaledH = baseH * scale;
6587
+ const extraH = Math.max(0, scaledH - baseH);
6588
+ activeCard.style.marginBottom = `${extraH + 32}px`;
5752
6589
  }
5753
6590
  };
5754
6591
  const showPage = (pageNum) => {
@@ -5761,35 +6598,39 @@ var DocPlugin = class {
5761
6598
  card.style.display = "none";
5762
6599
  }
5763
6600
  });
5764
- if (indicator) {
5765
- indicator.textContent = `Page ${currentPage} of ${totalPages}`;
5766
- }
5767
6601
  container.scrollTop = 0;
5768
6602
  ctx.emit("page-change", { page: currentPage, total: totalPages });
5769
6603
  };
5770
6604
  if (totalPages > 1) {
5771
6605
  showPage(1);
5772
6606
  }
5773
- let fitMode = "width";
6607
+ let fitMode = ctx?.options?.fitMode || "width";
6608
+ let isUserZoomed = false;
5774
6609
  const calculateFitScale = (mode = fitMode) => {
5775
6610
  const elW = 816;
5776
6611
  const elH = 1056;
5777
- const availW = Math.max(280, ctx.container.clientWidth - 48);
5778
- const availH = Math.max(280, ctx.container.clientHeight - 80);
6612
+ const availW = Math.max(280, ctx.container.clientWidth - 32);
6613
+ const availH = Math.max(280, ctx.container.clientHeight - 32);
5779
6614
  const sW = availW / elW;
5780
6615
  const sH = availH / elH;
5781
6616
  if (mode === "page") {
5782
- return Math.max(0.55, Math.min(1.15, Math.min(sW, sH)));
6617
+ return Math.max(0.35, Math.min(3, Math.min(sW, sH)));
5783
6618
  }
5784
- return Math.max(0.65, Math.min(1.05, sW));
6619
+ return Math.max(0.4, Math.min(3, sW));
5785
6620
  };
6621
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
6622
+ if (!isUserZoomed) {
6623
+ scale = calculateFitScale(fitMode);
6624
+ applyTransform();
6625
+ }
6626
+ }) : null;
6627
+ resizeObserver?.observe(ctx.container);
5786
6628
  setTimeout(() => {
5787
- fitMode = "width";
5788
- scale = calculateFitScale("width");
6629
+ scale = calculateFitScale(fitMode);
5789
6630
  applyTransform();
5790
6631
  }, 60);
5791
6632
  const cleanup = () => {
5792
- indicator?.remove();
6633
+ resizeObserver?.disconnect();
5793
6634
  container.remove();
5794
6635
  ctx.container.innerHTML = "";
5795
6636
  };
@@ -5800,21 +6641,25 @@ var DocPlugin = class {
5800
6641
  getCurrentPage: () => currentPage,
5801
6642
  goToPage: (page) => showPage(page),
5802
6643
  zoomIn: () => {
6644
+ isUserZoomed = true;
5803
6645
  scale += 0.15;
5804
6646
  applyTransform();
5805
6647
  },
5806
6648
  zoomOut: () => {
6649
+ isUserZoomed = true;
5807
6650
  scale = Math.max(0.2, scale - 0.15);
5808
6651
  applyTransform();
5809
6652
  },
5810
6653
  getZoom: () => scale,
5811
6654
  setZoom: (level) => {
6655
+ isUserZoomed = true;
5812
6656
  scale = level;
5813
6657
  applyTransform();
5814
6658
  },
5815
6659
  fitToPage: () => {
5816
- fitMode = fitMode === "width" ? "page" : "width";
5817
- scale = calculateFitScale(fitMode);
6660
+ isUserZoomed = false;
6661
+ fitMode = "width";
6662
+ scale = calculateFitScale("width");
5818
6663
  rotation = 0;
5819
6664
  applyTransform();
5820
6665
  },
@@ -5929,7 +6774,7 @@ var DocPlugin = class {
5929
6774
  for (const run of [...ansiRuns, ...utf16Runs]) {
5930
6775
  const trimmed = run.trim();
5931
6776
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5932
- 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)) {
6777
+ 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)) {
5933
6778
  seen.add(trimmed);
5934
6779
  candidateLines.push(trimmed);
5935
6780
  }
@@ -5940,9 +6785,114 @@ var DocPlugin = class {
5940
6785
  heuristicTextExtraction(buffer) {
5941
6786
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5942
6787
  }
5943
- cleanWordDocFields(text) {
6788
+ /**
6789
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6790
+ */
6791
+ parseOdfChartToSvg(zipBytes) {
6792
+ try {
6793
+ const unzipped = fflate.unzipSync(zipBytes);
6794
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6795
+ if (!contentXml) return "";
6796
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6797
+ if (rowsMatch.length < 2) return "";
6798
+ const headers = [];
6799
+ const firstRow = rowsMatch[0];
6800
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6801
+ for (const h2 of headerCells) {
6802
+ headers.push(h2.replace(/<\/?text:p>/g, "").trim());
6803
+ }
6804
+ const categories = [];
6805
+ const seriesValues = headers.map(() => []);
6806
+ for (let r = 1; r < rowsMatch.length; r++) {
6807
+ const rowStr = rowsMatch[r];
6808
+ if (!rowStr) continue;
6809
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6810
+ if (cells.length > 0 && cells[0]) {
6811
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6812
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6813
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6814
+ const cellStr = cells[c];
6815
+ if (!cellStr) continue;
6816
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6817
+ const series = seriesValues[c - 1];
6818
+ if (series) {
6819
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6820
+ }
6821
+ }
6822
+ }
6823
+ }
6824
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6825
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6826
+ let cIdx = 0;
6827
+ for (const cm of colorMatches) {
6828
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6829
+ cIdx++;
6830
+ }
6831
+ let maxVal = 10;
6832
+ for (const s of seriesValues) {
6833
+ for (const v of s) {
6834
+ if (v > maxVal) maxVal = v;
6835
+ }
6836
+ }
6837
+ maxVal = Math.ceil(maxVal * 1.15);
6838
+ const width = 560;
6839
+ const height = 280;
6840
+ const padLeft = 45;
6841
+ const padRight = 100;
6842
+ const padTop = 20;
6843
+ const padBottom = 40;
6844
+ const chartW = width - padLeft - padRight;
6845
+ const chartH = height - padTop - padBottom;
6846
+ 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);">`;
6847
+ for (let step = 0; step <= 4; step++) {
6848
+ const yVal = (maxVal / 4 * step).toFixed(1);
6849
+ const yPos = padTop + chartH - step / 4 * chartH;
6850
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6851
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6852
+ }
6853
+ const numCats = categories.length;
6854
+ const numSeries = headers.length;
6855
+ const groupW = chartW / numCats;
6856
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6857
+ const groupPad = (groupW - barW * numSeries) / 2;
6858
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6859
+ const groupX = padLeft + catIdx * groupW + groupPad;
6860
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6861
+ const val = seriesValues[sIdx][catIdx] || 0;
6862
+ const barH = val / maxVal * chartH;
6863
+ const barX = groupX + sIdx * barW;
6864
+ const barY = padTop + chartH - barH;
6865
+ const col = colors[sIdx % colors.length];
6866
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6867
+ }
6868
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6869
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6870
+ }
6871
+ let legendY = padTop + 20;
6872
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6873
+ const col = colors[sIdx % colors.length];
6874
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6875
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6876
+ legendY += 20;
6877
+ }
6878
+ svg += "</svg>";
6879
+ return svg;
6880
+ } catch (e) {
6881
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6882
+ return "";
6883
+ }
6884
+ }
6885
+ cleanWordDocFields(text, chartSvg = "") {
5944
6886
  if (!text) return "";
5945
6887
  let cleaned = text.replace(
6888
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6889
+ () => chartSvg ? `
6890
+
6891
+ ${chartSvg}
6892
+
6893
+ ` : ""
6894
+ );
6895
+ cleaned = cleaned.replace(
5946
6896
  /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
5947
6897
  (_match, url, label) => {
5948
6898
  const cleanUrl = url.trim();
@@ -5950,18 +6900,23 @@ var DocPlugin = class {
5950
6900
  return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
5951
6901
  }
5952
6902
  );
5953
- cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, "$1");
6903
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6904
+ if (/[\x00-\x1F]/.test(res)) return "";
6905
+ return res.trim();
6906
+ });
5954
6907
  cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
5955
6908
  cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6909
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6910
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
5956
6911
  return cleaned;
5957
6912
  }
5958
- splitIntoPages(text) {
6913
+ splitIntoPages(text, chartSvg = "") {
5959
6914
  if (!text) return [""];
5960
- const cleanedText = this.cleanWordDocFields(text);
6915
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
5961
6916
  const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5962
6917
  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);
5963
6918
  if (explicitParts.length === 0) explicitParts.push(normalized);
5964
- const maxLinesPerPage = 32;
6919
+ const maxLinesPerPage = 34;
5965
6920
  const charsPerLine = 80;
5966
6921
  const finalPages = [];
5967
6922
  for (const part of explicitParts) {
@@ -5969,8 +6924,8 @@ var DocPlugin = class {
5969
6924
  let currentLines = [];
5970
6925
  let count = 0;
5971
6926
  for (const line of lines) {
5972
- const plainLine = line.replace(/<[^>]+>/g, "");
5973
- const vLines = Math.max(1, Math.ceil((plainLine.length || 1) / charsPerLine));
6927
+ const isSvg = line.includes("<svg");
6928
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5974
6929
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5975
6930
  finalPages.push(currentLines.join("\n"));
5976
6931
  currentLines = [];
@@ -6010,9 +6965,44 @@ var DocPlugin = class {
6010
6965
  tableLines = [];
6011
6966
  }
6012
6967
  };
6968
+ const sanitizeOptions = {
6969
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6970
+ ADD_ATTR: [
6971
+ "href",
6972
+ "target",
6973
+ "rel",
6974
+ "style",
6975
+ "viewBox",
6976
+ "width",
6977
+ "height",
6978
+ "x",
6979
+ "y",
6980
+ "x1",
6981
+ "y1",
6982
+ "x2",
6983
+ "y2",
6984
+ "fill",
6985
+ "stroke",
6986
+ "stroke-width",
6987
+ "stroke-dasharray",
6988
+ "rx",
6989
+ "font-size",
6990
+ "text-anchor"
6991
+ ]
6992
+ };
6013
6993
  let i = 0;
6014
6994
  while (i < lines.length) {
6015
6995
  let line = lines[i];
6996
+ if (line.includes("<svg")) {
6997
+ if (inList) {
6998
+ html += "</ul>";
6999
+ inList = false;
7000
+ }
7001
+ flushTable();
7002
+ html += line;
7003
+ i++;
7004
+ continue;
7005
+ }
6016
7006
  let tabCount = (line.match(/\t/g) || []).length;
6017
7007
  if (tabCount > 0) {
6018
7008
  let j = i;
@@ -6046,26 +7036,31 @@ var DocPlugin = class {
6046
7036
  i++;
6047
7037
  continue;
6048
7038
  }
6049
- const sanitizeOptions = { ADD_TAGS: ["a"], ADD_ATTR: ["href", "target", "rel", "style"] };
6050
- if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
7039
+ if (/^\d{1,2}$/.test(line.trim())) {
7040
+ i++;
7041
+ continue;
7042
+ }
7043
+ const isShortLine = line.length < 60 && !line.endsWith(".") && !line.endsWith(",");
7044
+ 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));
7045
+ if (isTitleLike) {
6051
7046
  if (inList) {
6052
7047
  html += "</ul>";
6053
7048
  inList = false;
6054
7049
  }
6055
- 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>`;
7050
+ 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>`;
6056
7051
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
6057
7052
  if (!inList) {
6058
- html += '<ul style="margin: 8px 0; padding-left: 24px;">';
7053
+ html += '<ul style="margin: 8px 0 12px; padding-left: 24px;">';
6059
7054
  inList = true;
6060
7055
  }
6061
7056
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
6062
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
7057
+ html += `<li style="margin: 4px 0; line-height: 1.5; font-size: 11pt; color: #1e293b;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
6063
7058
  } else {
6064
7059
  if (inList) {
6065
7060
  html += "</ul>";
6066
7061
  inList = false;
6067
7062
  }
6068
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
7063
+ 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>`;
6069
7064
  }
6070
7065
  i++;
6071
7066
  }