stimulus-pdf-viewer-rails 0.3.0 → 0.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43f0f370e8cb71ab25bae045345269c4567e3afaecb6a15f43dfe8dd26b4766c
4
- data.tar.gz: ce65b690119f2d649ef611f227e4da9bfddf4f27da9273221a9653211eb84445
3
+ metadata.gz: 6fe6b668cbffa8acaf9ade9ef15614aad4d7677e3dc505bebe4065ba8ccdb726
4
+ data.tar.gz: f8f51d4e310e36beb6dd51efc9dd3501daaa15067b2a853912d074427a04b9f7
5
5
  SHA512:
6
- metadata.gz: cd3c15bcfccb692d88527594f57cbcbe280b7c0ec79b84f15a8c36016e907b29431554098602042c51ed80a2cdd1601ced8782cf33ae9fd4fba216c29a764ffa
7
- data.tar.gz: c275cb1442a607111a11564d1ecf5326c69be6653ef497c49c45f602c0d4f44e38373b9d6d5308cc821ba92c1933bdc4f929185de0f524533a60b84d52bb7d6f
6
+ metadata.gz: ad37d63e9f0c26f373b293737818fd85121bf78945f8e97ab06371f75d2aa3ac6b8b4b9976a27cb5553b5cb3f738b51258a82541761bb78531116e34fa27e90f
7
+ data.tar.gz: 41b654cae115f12ef9a8f33c80d5d0645657cbccc7b70ee94651dd557b2ea416b38ace501b7a0e43cf9387dc8b16237c2f7e442915d338e1c03dd0ee3108ad45
@@ -2578,12 +2578,12 @@ class AnnotationEditToolbar {
2578
2578
  });
2579
2579
 
2580
2580
  // Handle keyboard shortcuts
2581
- document.addEventListener("keydown", (e) => {
2581
+ this._keydownHandler = (e) => {
2582
2582
  if (this.element.classList.contains("hidden")) return
2583
2583
 
2584
2584
  // Don't intercept if user is typing
2585
2585
  const activeEl = document.activeElement;
2586
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA")) return
2586
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
2587
2587
 
2588
2588
  if (e.key === "Escape") {
2589
2589
  if (this.colorDropdownOpen) {
@@ -2613,7 +2613,8 @@ class AnnotationEditToolbar {
2613
2613
  this.onComment(this.currentAnnotation);
2614
2614
  }
2615
2615
  }
2616
- });
2616
+ };
2617
+ document.addEventListener("keydown", this._keydownHandler);
2617
2618
  }
2618
2619
 
2619
2620
  _toggleColorDropdown() {
@@ -2722,6 +2723,347 @@ class AnnotationEditToolbar {
2722
2723
  }
2723
2724
 
2724
2725
  destroy() {
2726
+ if (this._keydownHandler) {
2727
+ document.removeEventListener("keydown", this._keydownHandler);
2728
+ }
2729
+ this.element.remove();
2730
+ }
2731
+ }
2732
+
2733
+ class AnnotationDetailPanel {
2734
+ constructor(options = {}) {
2735
+ this.container = options.container;
2736
+ this.onColorChange = options.onColorChange;
2737
+ this.onDelete = options.onDelete;
2738
+ this.onEdit = options.onEdit;
2739
+ this.onComment = options.onComment;
2740
+ this.onClose = options.onClose;
2741
+ this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
2742
+
2743
+ this.currentAnnotation = null;
2744
+ this.anchorElement = null;
2745
+ this.colorDropdownOpen = false;
2746
+
2747
+ this._createPanel();
2748
+ this._setupEventListeners();
2749
+ }
2750
+
2751
+ _createPanel() {
2752
+ this.element = document.createElement("div");
2753
+ this.element.className = "annotation-detail-panel hidden";
2754
+ this.element.innerHTML = `
2755
+ <div class="annotation-detail-header">
2756
+ <div class="annotation-detail-header-info">
2757
+ <span class="annotation-detail-type"></span>
2758
+ </div>
2759
+ <div class="annotation-detail-header-actions">
2760
+ <button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
2761
+ ${Icons.comment}
2762
+ </button>
2763
+ <button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
2764
+ <span class="color-swatch"></span>
2765
+ ${Icons.chevronDown}
2766
+ </button>
2767
+ <div class="color-dropdown hidden">
2768
+ ${this.colors.map(color => `
2769
+ <button class="color-option" data-color="${color}" aria-selected="false">
2770
+ <span class="color-swatch" style="background-color: ${color}"></span>
2771
+ </button>
2772
+ `).join("")}
2773
+ </div>
2774
+ <button class="toolbar-btn edit-btn hidden" title="Edit (E)">
2775
+ ${Icons.edit}
2776
+ </button>
2777
+ <div class="toolbar-divider"></div>
2778
+ <button class="toolbar-btn delete-btn" title="Delete (Delete)">
2779
+ ${Icons.delete}
2780
+ </button>
2781
+ <div class="toolbar-divider"></div>
2782
+ <button class="annotation-detail-close" title="Close (Escape)">
2783
+ ${Icons.close}
2784
+ </button>
2785
+ </div>
2786
+ </div>
2787
+ <div class="annotation-detail-body">
2788
+ <div class="annotation-detail-text hidden"></div>
2789
+ <div class="annotation-detail-content-slot"></div>
2790
+ </div>
2791
+ `;
2792
+
2793
+ this.commentBtn = this.element.querySelector(".comment-btn");
2794
+ this.editBtn = this.element.querySelector(".edit-btn");
2795
+ this.typeLabel = this.element.querySelector(".annotation-detail-type");
2796
+ this.textContent = this.element.querySelector(".annotation-detail-text");
2797
+ this.contentSlot = this.element.querySelector(".annotation-detail-content-slot");
2798
+ }
2799
+
2800
+ _setupEventListeners() {
2801
+ // Prevent pointer/click events inside the panel from triggering annotation tools
2802
+ // Tools listen on pointerdown on the pages container, so we must stop all phases
2803
+ this.element.addEventListener("pointerdown", (e) => {
2804
+ e.stopPropagation();
2805
+ });
2806
+ this.element.addEventListener("click", (e) => {
2807
+ e.stopPropagation();
2808
+ });
2809
+
2810
+ // Close button
2811
+ this.element.querySelector(".annotation-detail-close").addEventListener("click", (e) => {
2812
+ e.stopPropagation();
2813
+ this.onClose?.();
2814
+ });
2815
+
2816
+ // Color picker button
2817
+ const colorBtn = this.element.querySelector(".color-picker-btn");
2818
+ colorBtn.addEventListener("click", (e) => {
2819
+ e.stopPropagation();
2820
+ this._toggleColorDropdown();
2821
+ });
2822
+
2823
+ // Color options
2824
+ this.element.querySelectorAll(".color-option").forEach(option => {
2825
+ option.addEventListener("click", (e) => {
2826
+ e.stopPropagation();
2827
+ this._selectColor(option.dataset.color);
2828
+ });
2829
+ });
2830
+
2831
+ // Comment button
2832
+ this.commentBtn.addEventListener("click", (e) => {
2833
+ e.stopPropagation();
2834
+ if (this.currentAnnotation && this.onComment) {
2835
+ this.onComment(this.currentAnnotation);
2836
+ }
2837
+ });
2838
+
2839
+ // Edit button
2840
+ this.editBtn.addEventListener("click", (e) => {
2841
+ e.stopPropagation();
2842
+ if (this.currentAnnotation && this.onEdit) {
2843
+ this.onEdit(this.currentAnnotation);
2844
+ }
2845
+ });
2846
+
2847
+ // Delete button (hide is handled by _deselectAnnotation cascade)
2848
+ this.element.querySelector(".delete-btn").addEventListener("click", (e) => {
2849
+ e.stopPropagation();
2850
+ if (this.currentAnnotation && this.onDelete) {
2851
+ this.onDelete(this.currentAnnotation);
2852
+ }
2853
+ });
2854
+
2855
+ // Close color dropdown on outside click
2856
+ document.addEventListener("click", (e) => {
2857
+ if (this.colorDropdownOpen && !this.element.contains(e.target)) {
2858
+ this._closeColorDropdown();
2859
+ }
2860
+ });
2861
+
2862
+ // Keyboard shortcuts
2863
+ this._keydownHandler = (e) => {
2864
+ if (this.element.classList.contains("hidden")) return
2865
+
2866
+ const activeEl = document.activeElement;
2867
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
2868
+
2869
+ if (e.key === "Escape") {
2870
+ if (this.colorDropdownOpen) {
2871
+ this._closeColorDropdown();
2872
+ } else {
2873
+ this.onClose?.();
2874
+ }
2875
+ e.preventDefault();
2876
+ } else if (e.key === "Delete" || e.key === "Backspace") {
2877
+ e.preventDefault();
2878
+ if (this.currentAnnotation && this.onDelete) {
2879
+ this.onDelete(this.currentAnnotation);
2880
+ }
2881
+ } else if (e.key === "e" || e.key === "E") {
2882
+ if (this.currentAnnotation?.annotation_type === "note" && this.onEdit) {
2883
+ e.preventDefault();
2884
+ this.onEdit(this.currentAnnotation);
2885
+ }
2886
+ } else if (e.key === "c" || e.key === "C") {
2887
+ const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
2888
+ if (supportsComment && this.onComment) {
2889
+ e.preventDefault();
2890
+ this.onComment(this.currentAnnotation);
2891
+ }
2892
+ }
2893
+ };
2894
+ document.addEventListener("keydown", this._keydownHandler);
2895
+ }
2896
+
2897
+ _toggleColorDropdown() {
2898
+ if (this.colorDropdownOpen) {
2899
+ this._closeColorDropdown();
2900
+ } else {
2901
+ this._openColorDropdown();
2902
+ }
2903
+ }
2904
+
2905
+ _openColorDropdown() {
2906
+ const dropdown = this.element.querySelector(".color-dropdown");
2907
+ const btn = this.element.querySelector(".color-picker-btn");
2908
+ dropdown.classList.remove("hidden");
2909
+ btn.setAttribute("aria-expanded", "true");
2910
+ this.colorDropdownOpen = true;
2911
+ }
2912
+
2913
+ _closeColorDropdown() {
2914
+ const dropdown = this.element.querySelector(".color-dropdown");
2915
+ const btn = this.element.querySelector(".color-picker-btn");
2916
+ dropdown.classList.add("hidden");
2917
+ btn.setAttribute("aria-expanded", "false");
2918
+ this.colorDropdownOpen = false;
2919
+ }
2920
+
2921
+ _selectColor(color) {
2922
+ if (this.currentAnnotation && this.onColorChange) {
2923
+ this.onColorChange(this.currentAnnotation, color);
2924
+ }
2925
+ this._updateSelectedColor(color);
2926
+ this._closeColorDropdown();
2927
+ }
2928
+
2929
+ _updateSelectedColor(color) {
2930
+ const swatch = this.element.querySelector(".color-picker-btn .color-swatch");
2931
+ swatch.style.backgroundColor = color;
2932
+
2933
+ this.element.querySelectorAll(".color-option").forEach(option => {
2934
+ option.setAttribute("aria-selected", option.dataset.color === color ? "true" : "false");
2935
+ });
2936
+ }
2937
+
2938
+ _getTypeLabel(annotationType) {
2939
+ const labels = {
2940
+ highlight: "Highlight",
2941
+ line: "Underline",
2942
+ note: "Note",
2943
+ ink: "Drawing"
2944
+ };
2945
+ return labels[annotationType] || "Annotation"
2946
+ }
2947
+
2948
+ show(annotation, anchorElement, options = {}) {
2949
+ this.currentAnnotation = annotation;
2950
+ this.anchorElement = anchorElement;
2951
+
2952
+ // Update header info
2953
+ const page = annotation.page || 1;
2954
+ this.typeLabel.textContent = `${this._getTypeLabel(annotation.annotation_type)} \u2014 Page ${page}`;
2955
+
2956
+ // Update color swatch
2957
+ const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
2958
+ this._updateSelectedColor(color);
2959
+
2960
+ // Show/hide buttons based on annotation type
2961
+ const isNote = annotation.annotation_type === "note";
2962
+ const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
2963
+ this.commentBtn.classList.toggle("hidden", !supportsComment);
2964
+ this.editBtn.classList.toggle("hidden", !isNote);
2965
+
2966
+ if (supportsComment) {
2967
+ const hasComment = annotation.contents && annotation.contents.trim();
2968
+ this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
2969
+ }
2970
+
2971
+ // Show annotation text content if present
2972
+ if (annotation.contents) {
2973
+ this.textContent.textContent = annotation.contents;
2974
+ this.textContent.classList.remove("hidden");
2975
+ } else {
2976
+ this.textContent.textContent = "";
2977
+ this.textContent.classList.add("hidden");
2978
+ }
2979
+
2980
+ // Inject custom content if provided (HTML string or DOM element)
2981
+ if (options.content) {
2982
+ if (typeof options.content === "string") {
2983
+ this.contentSlot.innerHTML = options.content;
2984
+ } else {
2985
+ this.contentSlot.innerHTML = "";
2986
+ this.contentSlot.appendChild(options.content);
2987
+ }
2988
+ }
2989
+
2990
+ // Position and show the panel
2991
+ this._positionPanel(anchorElement);
2992
+ this.element.classList.remove("hidden");
2993
+ }
2994
+
2995
+ _positionPanel(anchorElement) {
2996
+ const pageContainer = anchorElement.closest(".pdf-page");
2997
+ if (!pageContainer) return
2998
+
2999
+ // Append to page container so it scrolls with the page
3000
+ pageContainer.appendChild(this.element);
3001
+
3002
+ // Get annotation position relative to the page
3003
+ const anchorRect = anchorElement.getBoundingClientRect();
3004
+ const pageRect = pageContainer.getBoundingClientRect();
3005
+
3006
+ const anchorTop = anchorRect.top - pageRect.top;
3007
+ const anchorLeft = anchorRect.left - pageRect.left;
3008
+ const anchorRight = anchorLeft + anchorRect.width;
3009
+ const pageWidth = pageRect.width;
3010
+
3011
+ const panelWidth = parseInt(
3012
+ getComputedStyle(this.element).getPropertyValue("--panel-width") || "320", 10
3013
+ );
3014
+
3015
+ // Try to position to the right of the annotation
3016
+ if (anchorRight + panelWidth + 12 <= pageWidth) {
3017
+ this.element.style.left = `${anchorRight + 8}px`;
3018
+ this.element.style.right = "auto";
3019
+ }
3020
+ // Fall back to the left side
3021
+ else if (anchorLeft - panelWidth - 12 >= 0) {
3022
+ this.element.style.left = `${anchorLeft - panelWidth - 8}px`;
3023
+ this.element.style.right = "auto";
3024
+ }
3025
+ // Fall back to below the annotation (centered)
3026
+ else {
3027
+ const centerX = anchorLeft + anchorRect.width / 2;
3028
+ this.element.style.left = `${Math.max(8, centerX - panelWidth / 2)}px`;
3029
+ this.element.style.right = "auto";
3030
+ }
3031
+
3032
+ // Vertical alignment: align top of panel with top of annotation
3033
+ this.element.style.top = `${anchorTop}px`;
3034
+ }
3035
+
3036
+ hide() {
3037
+ this._closeColorDropdown();
3038
+ this.element.classList.add("hidden");
3039
+ this.currentAnnotation = null;
3040
+ this.anchorElement = null;
3041
+
3042
+ // Clear text content
3043
+ this.textContent.textContent = "";
3044
+ this.textContent.classList.add("hidden");
3045
+
3046
+ // Clear injected content (but preserve the slot container)
3047
+ this.contentSlot.innerHTML = "";
3048
+
3049
+ // Remove from parent when hidden
3050
+ if (this.element.parentNode) {
3051
+ this.element.parentNode.removeChild(this.element);
3052
+ }
3053
+ }
3054
+
3055
+ isVisible() {
3056
+ return !this.element.classList.contains("hidden")
3057
+ }
3058
+
3059
+ getContentContainer() {
3060
+ return this.contentSlot
3061
+ }
3062
+
3063
+ destroy() {
3064
+ if (this._keydownHandler) {
3065
+ document.removeEventListener("keydown", this._keydownHandler);
3066
+ }
2725
3067
  this.element.remove();
2726
3068
  }
2727
3069
  }
@@ -6772,6 +7114,18 @@ class PdfViewer {
6772
7114
  onDeselect: this._deselectAnnotation.bind(this)
6773
7115
  });
6774
7116
 
7117
+ // Detail panel (opt-in: replaces edit toolbar when enabled)
7118
+ if (this.options.detailPanel && this.bodyContainer) {
7119
+ this.annotationDetailPanel = new AnnotationDetailPanel({
7120
+ container: this.bodyContainer,
7121
+ onColorChange: this._onAnnotationColorChange.bind(this),
7122
+ onDelete: this._onAnnotationDelete.bind(this),
7123
+ onEdit: this._onAnnotationEdit.bind(this),
7124
+ onComment: this._onAnnotationComment.bind(this),
7125
+ onClose: this._deselectAnnotation.bind(this)
7126
+ });
7127
+ }
7128
+
6775
7129
  this.undoBar = new UndoBar(this.undoBarContainer, {
6776
7130
  onUndo: this._onAnnotationUndo.bind(this)
6777
7131
  });
@@ -6911,8 +7265,8 @@ class PdfViewer {
6911
7265
 
6912
7266
  // Deselect annotation when clicking outside
6913
7267
  this.pagesContainer.addEventListener("click", (e) => {
6914
- // Don't deselect if clicking on an annotation or the edit toolbar
6915
- if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar")) {
7268
+ // Don't deselect if clicking on an annotation, edit toolbar, or detail panel
7269
+ if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar") || e.target.closest(".annotation-detail-panel")) {
6916
7270
  return
6917
7271
  }
6918
7272
  // Skip if an annotation was just created (click follows pointerup from text selection)
@@ -7127,9 +7481,9 @@ class PdfViewer {
7127
7481
  // Remember if this annotation was selected
7128
7482
  const wasSelected = this.selectedAnnotation && this.selectedAnnotation.id === annotation.id;
7129
7483
 
7130
- // Hide toolbar before re-render (it will be re-shown after)
7484
+ // Hide annotation UI before re-render (it will be re-shown after)
7131
7485
  if (wasSelected) {
7132
- this.annotationEditToolbar.hide();
7486
+ this._hideAnnotationUI();
7133
7487
  }
7134
7488
 
7135
7489
  this._renderAnnotationsForPage(annotation.page, this.viewer.getPageContainer(annotation.page));
@@ -7147,7 +7501,7 @@ class PdfViewer {
7147
7501
  this.selectedAnnotation = updatedAnnotation;
7148
7502
  this.selectedAnnotationElement = element;
7149
7503
  element.classList.add("selected");
7150
- this.annotationEditToolbar.show(updatedAnnotation, element);
7504
+ this._showAnnotationUI(updatedAnnotation, element);
7151
7505
  }
7152
7506
  }
7153
7507
  }
@@ -7794,7 +8148,7 @@ class PdfViewer {
7794
8148
  if (this.selectedAnnotationElement !== element) {
7795
8149
  this.selectedAnnotationElement = element;
7796
8150
  element.classList.add("selected");
7797
- this.annotationEditToolbar.show(annotation, element, pageHeight);
8151
+ this._showAnnotationUI(annotation, element, pageHeight);
7798
8152
  }
7799
8153
  return
7800
8154
  }
@@ -7807,8 +8161,8 @@ class PdfViewer {
7807
8161
  this.selectedAnnotationElement = element;
7808
8162
  element.classList.add("selected");
7809
8163
 
7810
- // Show the edit toolbar below the annotation (includes note content for notes)
7811
- this.annotationEditToolbar.show(annotation, element, pageHeight);
8164
+ // Show the appropriate annotation UI
8165
+ this._showAnnotationUI(annotation, element, pageHeight);
7812
8166
 
7813
8167
  this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-selected", {
7814
8168
  bubbles: true,
@@ -7816,6 +8170,33 @@ class PdfViewer {
7816
8170
  }));
7817
8171
  }
7818
8172
 
8173
+ _showAnnotationUI(annotation, element, pageHeight) {
8174
+ if (this.annotationDetailPanel) {
8175
+ this.annotationDetailPanel.show(annotation, element);
8176
+ this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-opened", {
8177
+ bubbles: true,
8178
+ detail: { annotation, element }
8179
+ }));
8180
+ } else {
8181
+ this.annotationEditToolbar.show(annotation, element, pageHeight);
8182
+ }
8183
+ }
8184
+
8185
+ _hideAnnotationUI() {
8186
+ if (this.annotationDetailPanel) {
8187
+ const annotation = this.annotationDetailPanel.currentAnnotation;
8188
+ this.annotationDetailPanel.hide();
8189
+ if (annotation) {
8190
+ this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-closed", {
8191
+ bubbles: true,
8192
+ detail: { annotation, annotationId: annotation.id }
8193
+ }));
8194
+ }
8195
+ } else {
8196
+ this.annotationEditToolbar.hide();
8197
+ }
8198
+ }
8199
+
7819
8200
  _deselectAnnotation() {
7820
8201
  const previousAnnotation = this.selectedAnnotation;
7821
8202
 
@@ -7825,8 +8206,8 @@ class PdfViewer {
7825
8206
  this.selectedAnnotation = null;
7826
8207
  this.selectedAnnotationElement = null;
7827
8208
 
7828
- // Hide the edit toolbar
7829
- this.annotationEditToolbar.hide();
8209
+ // Hide the annotation UI (detail panel or edit toolbar)
8210
+ this._hideAnnotationUI();
7830
8211
 
7831
8212
  if (previousAnnotation) {
7832
8213
  this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-deselected", {
@@ -8001,6 +8382,7 @@ class PdfViewer {
8001
8382
 
8002
8383
  this.viewer.destroy();
8003
8384
  this.annotationEditToolbar.destroy();
8385
+ this.annotationDetailPanel?.destroy();
8004
8386
  this.undoBar.destroy();
8005
8387
  this.thumbnailSidebar?.destroy();
8006
8388
  this.annotationSidebar?.destroy();
@@ -8027,7 +8409,8 @@ class pdf_viewer_controller extends Controller {
8027
8409
  trackingUrl: String,
8028
8410
  initialPage: Number,
8029
8411
  initialAnnotation: String,
8030
- autoHeight: { type: Boolean, default: true }
8412
+ autoHeight: { type: Boolean, default: true },
8413
+ detailPanel: { type: Boolean, default: false }
8031
8414
  }
8032
8415
 
8033
8416
  initialize() {
@@ -8048,7 +8431,8 @@ class pdf_viewer_controller extends Controller {
8048
8431
  userName: this.userNameValue,
8049
8432
  documentId: this.documentIdValue,
8050
8433
  initialPage: this.initialPageValue || 1,
8051
- initialAnnotation: this.initialAnnotationValue
8434
+ initialAnnotation: this.initialAnnotationValue,
8435
+ detailPanel: this.detailPanelValue
8052
8436
  });
8053
8437
 
8054
8438
  // Set up the toolbar
@@ -2598,12 +2598,12 @@
2598
2598
  });
2599
2599
 
2600
2600
  // Handle keyboard shortcuts
2601
- document.addEventListener("keydown", (e) => {
2601
+ this._keydownHandler = (e) => {
2602
2602
  if (this.element.classList.contains("hidden")) return
2603
2603
 
2604
2604
  // Don't intercept if user is typing
2605
2605
  const activeEl = document.activeElement;
2606
- if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA")) return
2606
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
2607
2607
 
2608
2608
  if (e.key === "Escape") {
2609
2609
  if (this.colorDropdownOpen) {
@@ -2633,7 +2633,8 @@
2633
2633
  this.onComment(this.currentAnnotation);
2634
2634
  }
2635
2635
  }
2636
- });
2636
+ };
2637
+ document.addEventListener("keydown", this._keydownHandler);
2637
2638
  }
2638
2639
 
2639
2640
  _toggleColorDropdown() {
@@ -2742,6 +2743,347 @@
2742
2743
  }
2743
2744
 
2744
2745
  destroy() {
2746
+ if (this._keydownHandler) {
2747
+ document.removeEventListener("keydown", this._keydownHandler);
2748
+ }
2749
+ this.element.remove();
2750
+ }
2751
+ }
2752
+
2753
+ class AnnotationDetailPanel {
2754
+ constructor(options = {}) {
2755
+ this.container = options.container;
2756
+ this.onColorChange = options.onColorChange;
2757
+ this.onDelete = options.onDelete;
2758
+ this.onEdit = options.onEdit;
2759
+ this.onComment = options.onComment;
2760
+ this.onClose = options.onClose;
2761
+ this.colors = options.colors || ColorPicker.COLORS.map(c => c.value);
2762
+
2763
+ this.currentAnnotation = null;
2764
+ this.anchorElement = null;
2765
+ this.colorDropdownOpen = false;
2766
+
2767
+ this._createPanel();
2768
+ this._setupEventListeners();
2769
+ }
2770
+
2771
+ _createPanel() {
2772
+ this.element = document.createElement("div");
2773
+ this.element.className = "annotation-detail-panel hidden";
2774
+ this.element.innerHTML = `
2775
+ <div class="annotation-detail-header">
2776
+ <div class="annotation-detail-header-info">
2777
+ <span class="annotation-detail-type"></span>
2778
+ </div>
2779
+ <div class="annotation-detail-header-actions">
2780
+ <button class="toolbar-btn comment-btn hidden" title="Add Comment (C)">
2781
+ ${Icons.comment}
2782
+ </button>
2783
+ <button class="color-picker-btn" title="Change color" aria-haspopup="true" aria-expanded="false">
2784
+ <span class="color-swatch"></span>
2785
+ ${Icons.chevronDown}
2786
+ </button>
2787
+ <div class="color-dropdown hidden">
2788
+ ${this.colors.map(color => `
2789
+ <button class="color-option" data-color="${color}" aria-selected="false">
2790
+ <span class="color-swatch" style="background-color: ${color}"></span>
2791
+ </button>
2792
+ `).join("")}
2793
+ </div>
2794
+ <button class="toolbar-btn edit-btn hidden" title="Edit (E)">
2795
+ ${Icons.edit}
2796
+ </button>
2797
+ <div class="toolbar-divider"></div>
2798
+ <button class="toolbar-btn delete-btn" title="Delete (Delete)">
2799
+ ${Icons.delete}
2800
+ </button>
2801
+ <div class="toolbar-divider"></div>
2802
+ <button class="annotation-detail-close" title="Close (Escape)">
2803
+ ${Icons.close}
2804
+ </button>
2805
+ </div>
2806
+ </div>
2807
+ <div class="annotation-detail-body">
2808
+ <div class="annotation-detail-text hidden"></div>
2809
+ <div class="annotation-detail-content-slot"></div>
2810
+ </div>
2811
+ `;
2812
+
2813
+ this.commentBtn = this.element.querySelector(".comment-btn");
2814
+ this.editBtn = this.element.querySelector(".edit-btn");
2815
+ this.typeLabel = this.element.querySelector(".annotation-detail-type");
2816
+ this.textContent = this.element.querySelector(".annotation-detail-text");
2817
+ this.contentSlot = this.element.querySelector(".annotation-detail-content-slot");
2818
+ }
2819
+
2820
+ _setupEventListeners() {
2821
+ // Prevent pointer/click events inside the panel from triggering annotation tools
2822
+ // Tools listen on pointerdown on the pages container, so we must stop all phases
2823
+ this.element.addEventListener("pointerdown", (e) => {
2824
+ e.stopPropagation();
2825
+ });
2826
+ this.element.addEventListener("click", (e) => {
2827
+ e.stopPropagation();
2828
+ });
2829
+
2830
+ // Close button
2831
+ this.element.querySelector(".annotation-detail-close").addEventListener("click", (e) => {
2832
+ e.stopPropagation();
2833
+ this.onClose?.();
2834
+ });
2835
+
2836
+ // Color picker button
2837
+ const colorBtn = this.element.querySelector(".color-picker-btn");
2838
+ colorBtn.addEventListener("click", (e) => {
2839
+ e.stopPropagation();
2840
+ this._toggleColorDropdown();
2841
+ });
2842
+
2843
+ // Color options
2844
+ this.element.querySelectorAll(".color-option").forEach(option => {
2845
+ option.addEventListener("click", (e) => {
2846
+ e.stopPropagation();
2847
+ this._selectColor(option.dataset.color);
2848
+ });
2849
+ });
2850
+
2851
+ // Comment button
2852
+ this.commentBtn.addEventListener("click", (e) => {
2853
+ e.stopPropagation();
2854
+ if (this.currentAnnotation && this.onComment) {
2855
+ this.onComment(this.currentAnnotation);
2856
+ }
2857
+ });
2858
+
2859
+ // Edit button
2860
+ this.editBtn.addEventListener("click", (e) => {
2861
+ e.stopPropagation();
2862
+ if (this.currentAnnotation && this.onEdit) {
2863
+ this.onEdit(this.currentAnnotation);
2864
+ }
2865
+ });
2866
+
2867
+ // Delete button (hide is handled by _deselectAnnotation cascade)
2868
+ this.element.querySelector(".delete-btn").addEventListener("click", (e) => {
2869
+ e.stopPropagation();
2870
+ if (this.currentAnnotation && this.onDelete) {
2871
+ this.onDelete(this.currentAnnotation);
2872
+ }
2873
+ });
2874
+
2875
+ // Close color dropdown on outside click
2876
+ document.addEventListener("click", (e) => {
2877
+ if (this.colorDropdownOpen && !this.element.contains(e.target)) {
2878
+ this._closeColorDropdown();
2879
+ }
2880
+ });
2881
+
2882
+ // Keyboard shortcuts
2883
+ this._keydownHandler = (e) => {
2884
+ if (this.element.classList.contains("hidden")) return
2885
+
2886
+ const activeEl = document.activeElement;
2887
+ if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA" || activeEl.isContentEditable)) return
2888
+
2889
+ if (e.key === "Escape") {
2890
+ if (this.colorDropdownOpen) {
2891
+ this._closeColorDropdown();
2892
+ } else {
2893
+ this.onClose?.();
2894
+ }
2895
+ e.preventDefault();
2896
+ } else if (e.key === "Delete" || e.key === "Backspace") {
2897
+ e.preventDefault();
2898
+ if (this.currentAnnotation && this.onDelete) {
2899
+ this.onDelete(this.currentAnnotation);
2900
+ }
2901
+ } else if (e.key === "e" || e.key === "E") {
2902
+ if (this.currentAnnotation?.annotation_type === "note" && this.onEdit) {
2903
+ e.preventDefault();
2904
+ this.onEdit(this.currentAnnotation);
2905
+ }
2906
+ } else if (e.key === "c" || e.key === "C") {
2907
+ const supportsComment = ["highlight", "line", "ink"].includes(this.currentAnnotation?.annotation_type);
2908
+ if (supportsComment && this.onComment) {
2909
+ e.preventDefault();
2910
+ this.onComment(this.currentAnnotation);
2911
+ }
2912
+ }
2913
+ };
2914
+ document.addEventListener("keydown", this._keydownHandler);
2915
+ }
2916
+
2917
+ _toggleColorDropdown() {
2918
+ if (this.colorDropdownOpen) {
2919
+ this._closeColorDropdown();
2920
+ } else {
2921
+ this._openColorDropdown();
2922
+ }
2923
+ }
2924
+
2925
+ _openColorDropdown() {
2926
+ const dropdown = this.element.querySelector(".color-dropdown");
2927
+ const btn = this.element.querySelector(".color-picker-btn");
2928
+ dropdown.classList.remove("hidden");
2929
+ btn.setAttribute("aria-expanded", "true");
2930
+ this.colorDropdownOpen = true;
2931
+ }
2932
+
2933
+ _closeColorDropdown() {
2934
+ const dropdown = this.element.querySelector(".color-dropdown");
2935
+ const btn = this.element.querySelector(".color-picker-btn");
2936
+ dropdown.classList.add("hidden");
2937
+ btn.setAttribute("aria-expanded", "false");
2938
+ this.colorDropdownOpen = false;
2939
+ }
2940
+
2941
+ _selectColor(color) {
2942
+ if (this.currentAnnotation && this.onColorChange) {
2943
+ this.onColorChange(this.currentAnnotation, color);
2944
+ }
2945
+ this._updateSelectedColor(color);
2946
+ this._closeColorDropdown();
2947
+ }
2948
+
2949
+ _updateSelectedColor(color) {
2950
+ const swatch = this.element.querySelector(".color-picker-btn .color-swatch");
2951
+ swatch.style.backgroundColor = color;
2952
+
2953
+ this.element.querySelectorAll(".color-option").forEach(option => {
2954
+ option.setAttribute("aria-selected", option.dataset.color === color ? "true" : "false");
2955
+ });
2956
+ }
2957
+
2958
+ _getTypeLabel(annotationType) {
2959
+ const labels = {
2960
+ highlight: "Highlight",
2961
+ line: "Underline",
2962
+ note: "Note",
2963
+ ink: "Drawing"
2964
+ };
2965
+ return labels[annotationType] || "Annotation"
2966
+ }
2967
+
2968
+ show(annotation, anchorElement, options = {}) {
2969
+ this.currentAnnotation = annotation;
2970
+ this.anchorElement = anchorElement;
2971
+
2972
+ // Update header info
2973
+ const page = annotation.page || 1;
2974
+ this.typeLabel.textContent = `${this._getTypeLabel(annotation.annotation_type)} \u2014 Page ${page}`;
2975
+
2976
+ // Update color swatch
2977
+ const color = annotation.color || ColorPicker.DEFAULT_HIGHLIGHT_COLOR;
2978
+ this._updateSelectedColor(color);
2979
+
2980
+ // Show/hide buttons based on annotation type
2981
+ const isNote = annotation.annotation_type === "note";
2982
+ const supportsComment = ["highlight", "line", "ink"].includes(annotation.annotation_type);
2983
+ this.commentBtn.classList.toggle("hidden", !supportsComment);
2984
+ this.editBtn.classList.toggle("hidden", !isNote);
2985
+
2986
+ if (supportsComment) {
2987
+ const hasComment = annotation.contents && annotation.contents.trim();
2988
+ this.commentBtn.title = hasComment ? "Edit Comment (C)" : "Add Comment (C)";
2989
+ }
2990
+
2991
+ // Show annotation text content if present
2992
+ if (annotation.contents) {
2993
+ this.textContent.textContent = annotation.contents;
2994
+ this.textContent.classList.remove("hidden");
2995
+ } else {
2996
+ this.textContent.textContent = "";
2997
+ this.textContent.classList.add("hidden");
2998
+ }
2999
+
3000
+ // Inject custom content if provided (HTML string or DOM element)
3001
+ if (options.content) {
3002
+ if (typeof options.content === "string") {
3003
+ this.contentSlot.innerHTML = options.content;
3004
+ } else {
3005
+ this.contentSlot.innerHTML = "";
3006
+ this.contentSlot.appendChild(options.content);
3007
+ }
3008
+ }
3009
+
3010
+ // Position and show the panel
3011
+ this._positionPanel(anchorElement);
3012
+ this.element.classList.remove("hidden");
3013
+ }
3014
+
3015
+ _positionPanel(anchorElement) {
3016
+ const pageContainer = anchorElement.closest(".pdf-page");
3017
+ if (!pageContainer) return
3018
+
3019
+ // Append to page container so it scrolls with the page
3020
+ pageContainer.appendChild(this.element);
3021
+
3022
+ // Get annotation position relative to the page
3023
+ const anchorRect = anchorElement.getBoundingClientRect();
3024
+ const pageRect = pageContainer.getBoundingClientRect();
3025
+
3026
+ const anchorTop = anchorRect.top - pageRect.top;
3027
+ const anchorLeft = anchorRect.left - pageRect.left;
3028
+ const anchorRight = anchorLeft + anchorRect.width;
3029
+ const pageWidth = pageRect.width;
3030
+
3031
+ const panelWidth = parseInt(
3032
+ getComputedStyle(this.element).getPropertyValue("--panel-width") || "320", 10
3033
+ );
3034
+
3035
+ // Try to position to the right of the annotation
3036
+ if (anchorRight + panelWidth + 12 <= pageWidth) {
3037
+ this.element.style.left = `${anchorRight + 8}px`;
3038
+ this.element.style.right = "auto";
3039
+ }
3040
+ // Fall back to the left side
3041
+ else if (anchorLeft - panelWidth - 12 >= 0) {
3042
+ this.element.style.left = `${anchorLeft - panelWidth - 8}px`;
3043
+ this.element.style.right = "auto";
3044
+ }
3045
+ // Fall back to below the annotation (centered)
3046
+ else {
3047
+ const centerX = anchorLeft + anchorRect.width / 2;
3048
+ this.element.style.left = `${Math.max(8, centerX - panelWidth / 2)}px`;
3049
+ this.element.style.right = "auto";
3050
+ }
3051
+
3052
+ // Vertical alignment: align top of panel with top of annotation
3053
+ this.element.style.top = `${anchorTop}px`;
3054
+ }
3055
+
3056
+ hide() {
3057
+ this._closeColorDropdown();
3058
+ this.element.classList.add("hidden");
3059
+ this.currentAnnotation = null;
3060
+ this.anchorElement = null;
3061
+
3062
+ // Clear text content
3063
+ this.textContent.textContent = "";
3064
+ this.textContent.classList.add("hidden");
3065
+
3066
+ // Clear injected content (but preserve the slot container)
3067
+ this.contentSlot.innerHTML = "";
3068
+
3069
+ // Remove from parent when hidden
3070
+ if (this.element.parentNode) {
3071
+ this.element.parentNode.removeChild(this.element);
3072
+ }
3073
+ }
3074
+
3075
+ isVisible() {
3076
+ return !this.element.classList.contains("hidden")
3077
+ }
3078
+
3079
+ getContentContainer() {
3080
+ return this.contentSlot
3081
+ }
3082
+
3083
+ destroy() {
3084
+ if (this._keydownHandler) {
3085
+ document.removeEventListener("keydown", this._keydownHandler);
3086
+ }
2745
3087
  this.element.remove();
2746
3088
  }
2747
3089
  }
@@ -6792,6 +7134,18 @@
6792
7134
  onDeselect: this._deselectAnnotation.bind(this)
6793
7135
  });
6794
7136
 
7137
+ // Detail panel (opt-in: replaces edit toolbar when enabled)
7138
+ if (this.options.detailPanel && this.bodyContainer) {
7139
+ this.annotationDetailPanel = new AnnotationDetailPanel({
7140
+ container: this.bodyContainer,
7141
+ onColorChange: this._onAnnotationColorChange.bind(this),
7142
+ onDelete: this._onAnnotationDelete.bind(this),
7143
+ onEdit: this._onAnnotationEdit.bind(this),
7144
+ onComment: this._onAnnotationComment.bind(this),
7145
+ onClose: this._deselectAnnotation.bind(this)
7146
+ });
7147
+ }
7148
+
6795
7149
  this.undoBar = new UndoBar(this.undoBarContainer, {
6796
7150
  onUndo: this._onAnnotationUndo.bind(this)
6797
7151
  });
@@ -6931,8 +7285,8 @@
6931
7285
 
6932
7286
  // Deselect annotation when clicking outside
6933
7287
  this.pagesContainer.addEventListener("click", (e) => {
6934
- // Don't deselect if clicking on an annotation or the edit toolbar
6935
- if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar")) {
7288
+ // Don't deselect if clicking on an annotation, edit toolbar, or detail panel
7289
+ if (e.target.closest(".annotation") || e.target.closest(".annotation-edit-toolbar") || e.target.closest(".annotation-detail-panel")) {
6936
7290
  return
6937
7291
  }
6938
7292
  // Skip if an annotation was just created (click follows pointerup from text selection)
@@ -7147,9 +7501,9 @@
7147
7501
  // Remember if this annotation was selected
7148
7502
  const wasSelected = this.selectedAnnotation && this.selectedAnnotation.id === annotation.id;
7149
7503
 
7150
- // Hide toolbar before re-render (it will be re-shown after)
7504
+ // Hide annotation UI before re-render (it will be re-shown after)
7151
7505
  if (wasSelected) {
7152
- this.annotationEditToolbar.hide();
7506
+ this._hideAnnotationUI();
7153
7507
  }
7154
7508
 
7155
7509
  this._renderAnnotationsForPage(annotation.page, this.viewer.getPageContainer(annotation.page));
@@ -7167,7 +7521,7 @@
7167
7521
  this.selectedAnnotation = updatedAnnotation;
7168
7522
  this.selectedAnnotationElement = element;
7169
7523
  element.classList.add("selected");
7170
- this.annotationEditToolbar.show(updatedAnnotation, element);
7524
+ this._showAnnotationUI(updatedAnnotation, element);
7171
7525
  }
7172
7526
  }
7173
7527
  }
@@ -7814,7 +8168,7 @@
7814
8168
  if (this.selectedAnnotationElement !== element) {
7815
8169
  this.selectedAnnotationElement = element;
7816
8170
  element.classList.add("selected");
7817
- this.annotationEditToolbar.show(annotation, element, pageHeight);
8171
+ this._showAnnotationUI(annotation, element, pageHeight);
7818
8172
  }
7819
8173
  return
7820
8174
  }
@@ -7827,8 +8181,8 @@
7827
8181
  this.selectedAnnotationElement = element;
7828
8182
  element.classList.add("selected");
7829
8183
 
7830
- // Show the edit toolbar below the annotation (includes note content for notes)
7831
- this.annotationEditToolbar.show(annotation, element, pageHeight);
8184
+ // Show the appropriate annotation UI
8185
+ this._showAnnotationUI(annotation, element, pageHeight);
7832
8186
 
7833
8187
  this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-selected", {
7834
8188
  bubbles: true,
@@ -7836,6 +8190,33 @@
7836
8190
  }));
7837
8191
  }
7838
8192
 
8193
+ _showAnnotationUI(annotation, element, pageHeight) {
8194
+ if (this.annotationDetailPanel) {
8195
+ this.annotationDetailPanel.show(annotation, element);
8196
+ this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-opened", {
8197
+ bubbles: true,
8198
+ detail: { annotation, element }
8199
+ }));
8200
+ } else {
8201
+ this.annotationEditToolbar.show(annotation, element, pageHeight);
8202
+ }
8203
+ }
8204
+
8205
+ _hideAnnotationUI() {
8206
+ if (this.annotationDetailPanel) {
8207
+ const annotation = this.annotationDetailPanel.currentAnnotation;
8208
+ this.annotationDetailPanel.hide();
8209
+ if (annotation) {
8210
+ this.container.dispatchEvent(new CustomEvent("pdf-viewer:detail-panel-closed", {
8211
+ bubbles: true,
8212
+ detail: { annotation, annotationId: annotation.id }
8213
+ }));
8214
+ }
8215
+ } else {
8216
+ this.annotationEditToolbar.hide();
8217
+ }
8218
+ }
8219
+
7839
8220
  _deselectAnnotation() {
7840
8221
  const previousAnnotation = this.selectedAnnotation;
7841
8222
 
@@ -7845,8 +8226,8 @@
7845
8226
  this.selectedAnnotation = null;
7846
8227
  this.selectedAnnotationElement = null;
7847
8228
 
7848
- // Hide the edit toolbar
7849
- this.annotationEditToolbar.hide();
8229
+ // Hide the annotation UI (detail panel or edit toolbar)
8230
+ this._hideAnnotationUI();
7850
8231
 
7851
8232
  if (previousAnnotation) {
7852
8233
  this.container.dispatchEvent(new CustomEvent("pdf-viewer:annotation-deselected", {
@@ -8021,6 +8402,7 @@
8021
8402
 
8022
8403
  this.viewer.destroy();
8023
8404
  this.annotationEditToolbar.destroy();
8405
+ this.annotationDetailPanel?.destroy();
8024
8406
  this.undoBar.destroy();
8025
8407
  this.thumbnailSidebar?.destroy();
8026
8408
  this.annotationSidebar?.destroy();
@@ -8047,7 +8429,8 @@
8047
8429
  trackingUrl: String,
8048
8430
  initialPage: Number,
8049
8431
  initialAnnotation: String,
8050
- autoHeight: { type: Boolean, default: true }
8432
+ autoHeight: { type: Boolean, default: true },
8433
+ detailPanel: { type: Boolean, default: false }
8051
8434
  }
8052
8435
 
8053
8436
  initialize() {
@@ -8068,7 +8451,8 @@
8068
8451
  userName: this.userNameValue,
8069
8452
  documentId: this.documentIdValue,
8070
8453
  initialPage: this.initialPageValue || 1,
8071
- initialAnnotation: this.initialAnnotationValue
8454
+ initialAnnotation: this.initialAnnotationValue,
8455
+ detailPanel: this.detailPanelValue
8072
8456
  });
8073
8457
 
8074
8458
  // Set up the toolbar
@@ -877,6 +877,214 @@
877
877
  }
878
878
  }
879
879
 
880
+ // Annotation Detail Panel (anchored card for annotation threads)
881
+ .annotation-detail-panel {
882
+ --panel-bg: #ffffff;
883
+ --panel-border: #c8c8d0;
884
+ --panel-shadow: 0 4px 12px rgba(58, 57, 68, 0.15);
885
+ --panel-header-bg: #f0f0f4;
886
+ --panel-fg: #2e2e56;
887
+ --panel-fg-secondary: #5b5b66;
888
+ --panel-hover-bg: #e0e0e6;
889
+ --panel-focus-color: #0060df;
890
+ --panel-width: 320px;
891
+
892
+ position: absolute;
893
+ z-index: 1000;
894
+ width: var(--panel-width);
895
+ display: flex;
896
+ flex-direction: column;
897
+ background: var(--panel-bg);
898
+ border: 1px solid var(--panel-border);
899
+ border-radius: 8px;
900
+ box-shadow: var(--panel-shadow);
901
+ pointer-events: auto;
902
+ cursor: default;
903
+
904
+ .annotation-detail-header {
905
+ display: flex;
906
+ align-items: center;
907
+ justify-content: space-between;
908
+ gap: 4px;
909
+ padding: 6px 4px 6px 12px;
910
+ background: var(--panel-header-bg);
911
+ border-bottom: 1px solid var(--panel-border);
912
+ min-height: 36px;
913
+ }
914
+
915
+ .annotation-detail-header-info {
916
+ flex: 1;
917
+ min-width: 0;
918
+ }
919
+
920
+ .annotation-detail-type {
921
+ font-size: 12px;
922
+ font-weight: 500;
923
+ color: var(--panel-fg-secondary);
924
+ white-space: nowrap;
925
+ overflow: hidden;
926
+ text-overflow: ellipsis;
927
+ }
928
+
929
+ .annotation-detail-header-actions {
930
+ position: relative;
931
+ display: flex;
932
+ align-items: center;
933
+ gap: 0;
934
+ flex-shrink: 0;
935
+ }
936
+
937
+ .toolbar-btn {
938
+ display: flex;
939
+ align-items: center;
940
+ justify-content: center;
941
+ width: 28px;
942
+ height: 28px;
943
+ padding: 0;
944
+ border: none;
945
+ border-radius: 4px;
946
+ background: transparent;
947
+ color: var(--panel-fg);
948
+ cursor: pointer;
949
+
950
+ &:hover {
951
+ background: var(--panel-hover-bg);
952
+ }
953
+
954
+ &:focus-visible {
955
+ outline: 2px solid var(--panel-focus-color);
956
+ outline-offset: -2px;
957
+ }
958
+
959
+ svg {
960
+ width: 16px;
961
+ height: 16px;
962
+ }
963
+ }
964
+
965
+ .toolbar-divider {
966
+ width: 0;
967
+ height: 20px;
968
+ border-left: 1px solid var(--panel-border);
969
+ margin: 0 2px;
970
+ }
971
+
972
+ .color-picker-btn {
973
+ display: flex;
974
+ align-items: center;
975
+ justify-content: center;
976
+ gap: 3px;
977
+ padding: 0 6px 0 8px;
978
+ height: 28px;
979
+ border: none;
980
+ border-radius: 4px;
981
+ background: transparent;
982
+ cursor: pointer;
983
+
984
+ &:hover {
985
+ background: var(--panel-hover-bg);
986
+ }
987
+
988
+ .color-swatch {
989
+ width: 14px;
990
+ height: 14px;
991
+ flex-shrink: 0;
992
+ border-radius: 50%;
993
+ border: 1px solid rgba(0, 0, 0, 0.2);
994
+ }
995
+ }
996
+
997
+ .color-dropdown {
998
+ position: absolute;
999
+ top: calc(100% + 4px);
1000
+ right: 0;
1001
+ display: flex;
1002
+ flex-direction: row;
1003
+ gap: 8px;
1004
+ padding: 8px;
1005
+ background: var(--panel-header-bg);
1006
+ border: 1px solid var(--panel-border);
1007
+ border-radius: 6px;
1008
+ box-shadow: var(--panel-shadow);
1009
+ white-space: nowrap;
1010
+ z-index: 1;
1011
+
1012
+ .color-option {
1013
+ width: auto;
1014
+ height: auto;
1015
+ padding: 0;
1016
+ border: none;
1017
+ background: transparent;
1018
+ cursor: pointer;
1019
+ display: flex;
1020
+ justify-content: center;
1021
+
1022
+ .color-swatch {
1023
+ width: 16px;
1024
+ height: 16px;
1025
+ border-radius: 50%;
1026
+ border: 1px solid rgba(0, 0, 0, 0.15);
1027
+ outline-offset: 2px;
1028
+ }
1029
+
1030
+ &:hover .color-swatch,
1031
+ &:focus .color-swatch {
1032
+ outline: 2px solid var(--panel-focus-color);
1033
+ }
1034
+
1035
+ &[aria-selected="true"] .color-swatch {
1036
+ outline: 2px solid var(--panel-focus-color);
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ .annotation-detail-close {
1042
+ display: flex;
1043
+ align-items: center;
1044
+ justify-content: center;
1045
+ width: 28px;
1046
+ height: 28px;
1047
+ padding: 0;
1048
+ border: none;
1049
+ border-radius: 4px;
1050
+ background: transparent;
1051
+ color: var(--panel-fg-secondary);
1052
+ cursor: pointer;
1053
+
1054
+ &:hover {
1055
+ background: var(--panel-hover-bg);
1056
+ color: var(--panel-fg);
1057
+ }
1058
+
1059
+ svg {
1060
+ width: 14px;
1061
+ height: 14px;
1062
+ }
1063
+ }
1064
+
1065
+ .annotation-detail-body {
1066
+ max-height: 400px;
1067
+ overflow-y: auto;
1068
+ }
1069
+
1070
+ .annotation-detail-text {
1071
+ padding: 10px 12px;
1072
+ font-size: 14px;
1073
+ line-height: 1.5;
1074
+ color: var(--panel-fg);
1075
+ white-space: pre-wrap;
1076
+ word-break: break-word;
1077
+ border-bottom: 1px solid var(--panel-border);
1078
+ }
1079
+
1080
+ .annotation-detail-content-slot {
1081
+ // Host app injects content here (e.g. Turbo Frames)
1082
+ &:empty {
1083
+ display: none;
1084
+ }
1085
+ }
1086
+ }
1087
+
880
1088
  // Underline annotations
881
1089
  .annotation-underline {
882
1090
  // Container styles - dimensions set by JS
@@ -2447,6 +2655,31 @@
2447
2655
  }
2448
2656
  }
2449
2657
 
2658
+ // Detail panel
2659
+ .annotation-detail-panel {
2660
+ border: 1px solid CanvasText;
2661
+ background: Canvas;
2662
+ color: CanvasText;
2663
+
2664
+ .annotation-detail-header {
2665
+ border-bottom: 1px solid CanvasText;
2666
+ }
2667
+
2668
+ .toolbar-btn,
2669
+ .annotation-detail-close,
2670
+ .color-picker-btn {
2671
+ border: 1px solid ButtonText;
2672
+
2673
+ &:hover {
2674
+ border-color: Highlight;
2675
+ }
2676
+ }
2677
+
2678
+ .color-dropdown .color-option .color-swatch {
2679
+ forced-color-adjust: none;
2680
+ }
2681
+ }
2682
+
2450
2683
  // Popup and dialogs
2451
2684
  .annotation-popup {
2452
2685
  border: 1px solid CanvasText;
@@ -1,7 +1,7 @@
1
1
  module StimulusPdfViewer
2
2
  module Rails
3
- VERSION = "0.3.0"
3
+ VERSION = "0.3.1"
4
4
  # This should match the npm package version
5
- STIMULUS_PDF_VIEWER_VERSION = "0.3.0"
5
+ STIMULUS_PDF_VIEWER_VERSION = "0.3.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimulus-pdf-viewer-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Baker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-02 00:00:00.000000000 Z
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties