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