pushfeedback 0.1.84 → 0.1.85

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.
@@ -2,6 +2,9 @@ import { proxyCustomElement, HTMLElement, createEvent, h } from '@stencil/core/i
2
2
 
3
3
  const canvasEditorCss = ":host{display:block}.canvas-editor-wrapper{position:relative}.canvas-editor-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0, 0, 0, 0.8);display:flex;align-items:center;justify-content:center;z-index:9999}.canvas-editor-modal{width:95vw;height:95vh;max-width:none;max-height:none;background:var(--feedback-canvas-editor-bg-color, #ffffff);border-radius:12px;border:1px solid rgba(0, 0, 0, 0.08);display:flex;flex-direction:column;overflow:hidden;box-shadow:0 0 0 1px rgba(0, 0, 0, 0.02),\n 0 8px 16px rgba(0, 0, 0, 0.08),\n 0 16px 32px rgba(0, 0, 0, 0.08),\n 0 32px 64px rgba(0, 0, 0, 0.06)}.canvas-editor-header{background:var(--feedback-canvas-editor-header-bg-color, #fafafa);border-bottom:1px solid rgba(0, 0, 0, 0.06);padding:16px 20px;display:flex;flex-direction:column;gap:16px}.canvas-editor-title h3{margin:0;font-size:18px;font-weight:600;color:var(--feedback-canvas-editor-tool-text-color, #1a1a1a);letter-spacing:-0.02em}.canvas-editor-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:16px}.toolbar-section{display:flex;align-items:center;gap:8px}.tool-group{display:flex;align-items:center;gap:4px}.tool-btn{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--feedback-canvas-editor-tool-bg-color, #ffffff);border:1.5px solid rgba(0, 0, 0, 0.08);border-radius:8px;cursor:pointer;transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);padding:0;box-shadow:0 1px 2px rgba(0, 0, 0, 0.04)}.tool-btn svg{width:18px;height:18px;color:var(--feedback-canvas-editor-tool-text-color, #333)}.tool-btn:hover:not(:disabled){background:var(--feedback-canvas-editor-tool-bg-hover, #f5f5f5);border-color:rgba(0, 0, 0, 0.12);transform:translateY(-1px);box-shadow:0 2px 4px rgba(0, 0, 0, 0.08)}.tool-btn.active,.tool-btn.active:hover{background:var(--feedback-canvas-editor-tool-bg-active, #0070f4);color:var(--feedback-canvas-editor-tool-text-active, #ffffff);border-color:var(--feedback-canvas-editor-tool-bg-active, #0070f4);box-shadow:0 2px 8px rgba(0, 112, 244, 0.3)}.tool-btn.active svg{color:var(--feedback-canvas-editor-tool-text-active, #ffffff)}.tool-btn:disabled{opacity:0.4;cursor:not-allowed;color:var(--feedback-canvas-editor-tool-text-color, #333)}.toolbar-divider{width:1px;height:24px;background:var(--feedback-canvas-editor-divider-color, #e0e0e0);margin:0 4px}.undo-btn,.delete-btn{background:var(--feedback-canvas-editor-tool-bg-color, #ffffff) !important;border:1px solid var(--feedback-canvas-editor-border-color, #e0e0e0) !important}.undo-btn:hover:not(:disabled),.delete-btn:hover:not(:disabled){background:var(--feedback-canvas-editor-tool-bg-hover, #f0f0f0) !important}.undo-btn:disabled,.delete-btn:disabled{opacity:0.3;cursor:not-allowed}.delete-btn:hover:not(:disabled){background:#fee !important;border-color:#fcc !important}.delete-btn:hover:not(:disabled) svg{color:#c53030}.color-palette{display:flex;align-items:center;gap:6px}.color-slot-wrapper{position:relative;display:flex;align-items:center}.color-btn{width:32px;height:32px;border-radius:8px;border:2px solid transparent;cursor:pointer;transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);display:flex;align-items:center;justify-content:center;background:var(--feedback-canvas-editor-tool-bg-color, #ffffff);border:2px solid rgba(0, 0, 0, 0.08);box-shadow:0 1px 2px rgba(0, 0, 0, 0.04)}.color-btn:hover{transform:translateY(-2px) scale(1.05);box-shadow:0 4px 8px rgba(0, 0, 0, 0.12)}.color-btn.active{border-color:var(--feedback-primary-color, #0070f4);box-shadow:0 0 0 2px rgba(0, 112, 244, 0.2)}.color-btn.editing{border-color:var(--feedback-primary-color, #0070f4)}.color-picker-dropdown{position:absolute;top:100%;left:0;z-index:1000;margin-top:4px;background:var(--feedback-canvas-editor-tool-bg-color, #ffffff);border:1px solid var(--feedback-canvas-editor-border-color, #e0e0e0);border-radius:6px;padding:8px;box-shadow:0 4px 12px rgba(0, 0, 0, 0.1)}.color-picker-dropdown input[type=\"color\"]{width:40px;height:40px;border:none;border-radius:4px;cursor:pointer}.size-control{display:flex;align-items:center;gap:8px;background:var(--feedback-canvas-editor-tool-bg-color, #ffffff);border:1px solid var(--feedback-canvas-editor-border-color, #e0e0e0);border-radius:6px;padding:6px 12px}.size-slider{width:80px;height:14px;-webkit-appearance:none;appearance:none;background:transparent;border-radius:2px;outline:none;cursor:pointer;border:none;position:relative}.size-slider::before{content:'';position:absolute;top:50%;left:0;right:0;height:4px;background:rgba(0, 0, 0, 0.15);border-radius:2px;transform:translateY(-50%);pointer-events:none}.size-slider::-webkit-slider-track{width:100%;height:4px;background:transparent;border-radius:2px;border:none}.size-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;background:var(--feedback-primary-color, #0070f4);border-radius:50%;cursor:pointer;position:relative;z-index:1}.size-slider::-moz-range-track{width:100%;height:4px;background:transparent;border-radius:2px;border:none}.size-slider::-moz-range-thumb{width:14px;height:14px;background:var(--feedback-primary-color, #0070f4);border-radius:50%;border:none;cursor:pointer}.size-slider::-ms-track{width:100%;height:4px;background:transparent;border-radius:2px;border:none;color:transparent}.size-slider::-ms-thumb{width:14px;height:14px;background:var(--feedback-primary-color, #0070f4);border-radius:50%;border:none;cursor:pointer}.size-value{font-weight:500;color:var(--feedback-canvas-editor-tool-text-color, #333);font-size:12px;min-width:30px}.selected-annotation-controls{border-left:2px solid var(--feedback-canvas-editor-divider-color, #e0e0e0);padding-left:12px;margin-left:8px;min-width:200px}.text-controls{display:flex;flex-direction:row;align-items:center;gap:12px}.font-size-control,.border-width-control{display:flex;align-items:center;gap:6px}.font-size-control label,.border-width-control label{font-size:12px;color:var(--feedback-canvas-editor-tool-text-color, #333);font-weight:500;min-width:35px}.action-btn{display:flex;align-items:center;gap:8px;padding:0 20px;border:1.5px solid rgba(0, 0, 0, 0.08);border-radius:8px;cursor:pointer;font-size:14px;font-weight:500;transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);min-width:80px;justify-content:center;height:40px;box-shadow:0 1px 2px rgba(0, 0, 0, 0.04)}.action-btn.secondary{background:var(--feedback-canvas-editor-tool-bg-color, #ffffff);color:var(--feedback-canvas-editor-tool-text-color, #333);border-color:rgba(0, 0, 0, 0.08)}.action-btn.secondary:hover{background:var(--feedback-canvas-editor-tool-bg-hover, #f5f5f5);border-color:rgba(0, 0, 0, 0.12);transform:translateY(-1px);box-shadow:0 4px 8px rgba(0, 0, 0, 0.08)}.action-btn.primary{background:var(--feedback-primary-color, #0070f4);color:#ffffff;border-color:var(--feedback-primary-color, #0070f4);box-shadow:0 2px 4px rgba(0, 112, 244, 0.2)}.action-btn.primary:hover{background:#0056cc;border-color:#0056cc;transform:translateY(-1px);box-shadow:0 6px 12px rgba(0, 112, 244, 0.3)}.action-btn:active{transform:translateY(0)}.action-btn.small{height:28px;padding:4px 8px;font-size:12px;min-width:65px}.shape-controls{display:flex;flex-direction:column;gap:8px}.canvas-editor-content{flex:1;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--feedback-canvas-editor-content-bg, #f5f5f5);overflow:hidden;min-height:0;min-width:0;position:relative}.canvas-editor-content::-webkit-scrollbar{width:8px;height:8px}.canvas-editor-content::-webkit-scrollbar-track{background:transparent}.canvas-editor-content::-webkit-scrollbar-thumb{background:rgba(0, 0, 0, 0.2);border-radius:4px}.canvas-editor-content::-webkit-scrollbar-thumb:hover{background:rgba(0, 0, 0, 0.3)}.annotation-canvas{max-width:calc(100% - 48px);max-height:calc(100% - 48px);width:auto !important;height:auto !important;cursor:crosshair;border-radius:8px;box-shadow:0 0 0 1px rgba(0, 0, 0, 0.04),\n 0 4px 6px rgba(0, 0, 0, 0.06),\n 0 8px 16px rgba(0, 0, 0, 0.08);background:#ffffff;transition:box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);object-fit:contain;display:block;margin:auto}.annotation-canvas:hover{box-shadow:0 0 0 1px rgba(0, 0, 0, 0.06),\n 0 6px 10px rgba(0, 0, 0, 0.08),\n 0 12px 24px rgba(0, 0, 0, 0.1)}@media screen and (min-width: 1920px){.canvas-editor-modal{width:90vw;height:90vh}.canvas-editor-content{padding:32px}}@media screen and (min-width: 1200px) and (max-width: 1919px){.canvas-editor-modal{width:92vw;height:92vh}.canvas-editor-content{padding:28px}}@media screen and (min-width: 769px) and (max-width: 1199px){.canvas-editor-modal{width:95vw;height:95vh}}@media screen and (max-width: 768px){.canvas-editor-modal{width:100vw;height:100vh;border-radius:0}.canvas-editor-toolbar{flex-direction:column;align-items:stretch;gap:8px}.toolbar-section{justify-content:center}.selected-annotation-controls{border-left:none;border-top:2px solid var(--feedback-canvas-editor-divider-color, #e0e0e0);padding-left:0;padding-top:8px;margin-left:0;margin-top:8px;min-width:auto}.canvas-editor-content{padding:16px}}";
4
4
 
5
+ // Upper bound on waiting for the capture stream to produce video metadata,
6
+ // after which the stream is stopped so it cannot leak
7
+ const CAPTURE_TIMEOUT_MS = 10000;
5
8
  const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
6
9
  constructor() {
7
10
  super();
@@ -10,6 +13,50 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
10
13
  this.screenshotReady = createEvent(this, "screenshotReady", 7);
11
14
  this.screenshotCancelled = createEvent(this, "screenshotCancelled", 7);
12
15
  this.screenshotFailed = createEvent(this, "screenshotFailed", 7);
16
+ this.handleKeyDown = (event) => {
17
+ if (!this.showCanvasEditor || !this.escapeClose || event.key !== 'Escape') {
18
+ return;
19
+ }
20
+ // While the browser is capturing the screen, the picker owns the keyboard
21
+ if (this.takingScreenshot) {
22
+ return;
23
+ }
24
+ event.preventDefault();
25
+ // The colour picker is a popup on top of the editor, so Escape dismisses
26
+ // that first and leaves the editor (and any annotations) in place
27
+ if (this.showColorPicker) {
28
+ this.showColorPicker = false;
29
+ this.editingColorIndex = -1;
30
+ return;
31
+ }
32
+ // Deselect before closing, the way drawing tools do. Closing throws away
33
+ // every annotation, so someone who has just selected one gets a cheap
34
+ // Escape that only drops the selection rather than their work
35
+ if (this.selectedAnnotation) {
36
+ this.selectedAnnotation = null;
37
+ this.redrawAnnotations();
38
+ return;
39
+ }
40
+ // Same as the Cancel button: discards annotations and returns to the form
41
+ this.closeCanvasEditor();
42
+ };
43
+ // Tracks whether the press that started this click landed on the backdrop.
44
+ // Dragging an annotation and releasing past the edge of the editor fires a
45
+ // click on the backdrop too, and that must not be read as "clicked outside".
46
+ this.pressStartedOnOverlay = false;
47
+ this.handleOverlayMouseDown = (event) => {
48
+ this.pressStartedOnOverlay = event.target === event.currentTarget;
49
+ };
50
+ this.handleOverlayClick = (event) => {
51
+ const startedOutside = this.pressStartedOnOverlay;
52
+ this.pressStartedOnOverlay = false;
53
+ if (!this.clickOutsideClose || !startedOutside || event.target !== event.currentTarget) {
54
+ return;
55
+ }
56
+ // Closes the editor only. The feedback form reopens behind it, so a stray
57
+ // click never dismisses the whole widget
58
+ this.closeCanvasEditor();
59
+ };
13
60
  this.handleWindowResize = () => {
14
61
  // Debounce resize events
15
62
  if (this.resizeTimeout) {
@@ -689,8 +736,16 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
689
736
  if (!this.isDrawing || !this.currentAnnotation)
690
737
  return;
691
738
  this.isDrawing = false;
692
- this.annotations = [...this.annotations, this.currentAnnotation];
739
+ const annotation = this.currentAnnotation;
693
740
  this.currentAnnotation = null;
741
+ // A click with no drag never reaches mousemove, so the shape has no end
742
+ // point or size. Committing it added an invisible annotation that could
743
+ // not be selected, dragged or deleted, and it still consumed an undo step.
744
+ if (this.isDegenerateAnnotation(annotation)) {
745
+ this.redrawAnnotations();
746
+ return;
747
+ }
748
+ this.annotations = [...this.annotations, annotation];
694
749
  this.redrawAnnotations();
695
750
  };
696
751
  // Convert screen coordinates to canvas coordinates
@@ -732,11 +787,28 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
732
787
  }
733
788
  case 'line':
734
789
  case 'arrow': {
735
- // Distance from point to line
736
- const A = annotation.endY - annotation.startY;
737
- const B = annotation.startX - annotation.endX;
738
- const C = annotation.endX * annotation.startY - annotation.startX * annotation.endY;
739
- const distance = Math.abs(A * x + B * y + C) / Math.sqrt(A * A + B * B);
790
+ // Distance from the point to the line *segment*, not to the infinite
791
+ // line through it. The previous formula measured the infinite line, so
792
+ // a click far past either end still counted as a hit, and a zero-length
793
+ // segment divided by zero and gave NaN, leaving that annotation
794
+ // impossible to select, drag or delete.
795
+ // A click with no drag never reaches mousemove, so endX/endY are
796
+ // undefined rather than equal to the start. Treat that as a segment
797
+ // collapsed to its start point instead of letting NaN propagate.
798
+ const endX = Number.isFinite(annotation.endX) ? annotation.endX : annotation.startX;
799
+ const endY = Number.isFinite(annotation.endY) ? annotation.endY : annotation.startY;
800
+ const dx = endX - annotation.startX;
801
+ const dy = endY - annotation.startY;
802
+ const lengthSquared = dx * dx + dy * dy;
803
+ // Zero-length segment: t stays 0 and we measure to the single point
804
+ let t = 0;
805
+ if (lengthSquared > 0) {
806
+ t = ((x - annotation.startX) * dx + (y - annotation.startY) * dy) / lengthSquared;
807
+ t = Math.max(0, Math.min(1, t));
808
+ }
809
+ const closestX = annotation.startX + t * dx;
810
+ const closestY = annotation.startY + t * dy;
811
+ const distance = Math.hypot(x - closestX, y - closestY);
740
812
  return distance <= tolerance;
741
813
  }
742
814
  case 'text': {
@@ -761,6 +833,8 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
761
833
  this.screenshotTakingText = 'Taking screenshot...';
762
834
  this.screenshotAttachedText = 'Screenshot attached';
763
835
  this.screenshotButtonText = 'Add a screenshot';
836
+ this.escapeClose = true;
837
+ this.clickOutsideClose = true;
764
838
  this.autoStartScreenshot = false;
765
839
  this.existingScreenshot = '';
766
840
  this.editTextButtonText = 'Edit Text';
@@ -816,10 +890,12 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
816
890
  // Add window resize listener for canvas adaptation
817
891
  this.handleWindowResize = this.handleWindowResize.bind(this);
818
892
  window.addEventListener('resize', this.handleWindowResize);
893
+ document.addEventListener('keydown', this.handleKeyDown);
819
894
  }
820
895
  disconnectedCallback() {
821
896
  // Clean up resize listener
822
897
  window.removeEventListener('resize', this.handleWindowResize);
898
+ document.removeEventListener('keydown', this.handleKeyDown);
823
899
  }
824
900
  async captureViewportScreenshot() {
825
901
  try {
@@ -843,6 +919,30 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
843
919
  video.autoplay = true;
844
920
  video.muted = true;
845
921
  return new Promise((resolve, reject) => {
922
+ // If neither onloadedmetadata nor onerror ever fires, the promise would
923
+ // never settle: the capture stream would stay live with the browser's
924
+ // recording indicator on, and the widget would sit in "Taking
925
+ // screenshot..." forever with no way back short of a page reload.
926
+ let settled = false;
927
+ const stopStream = () => {
928
+ stream.getTracks().forEach((track) => track.stop());
929
+ };
930
+ const finish = (fn) => {
931
+ if (settled) {
932
+ return;
933
+ }
934
+ settled = true;
935
+ clearTimeout(watchdog);
936
+ video.onloadedmetadata = null;
937
+ video.onerror = null;
938
+ fn();
939
+ };
940
+ const watchdog = setTimeout(() => {
941
+ finish(() => {
942
+ stopStream();
943
+ reject(new Error('Screenshot capture timed out'));
944
+ });
945
+ }, CAPTURE_TIMEOUT_MS);
846
946
  video.onloadedmetadata = () => {
847
947
  video.play();
848
948
  // Wait a moment for video to stabilize
@@ -854,22 +954,26 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
854
954
  canvas.height = video.videoHeight;
855
955
  const ctx = canvas.getContext('2d');
856
956
  ctx.drawImage(video, 0, 0);
857
- // Stop the stream
858
- stream.getTracks().forEach((track) => track.stop());
859
957
  // Convert to data URL
860
958
  const dataUrl = canvas.toDataURL('image/png');
861
- console.log('Screenshot captured successfully using Screen Capture API');
862
- resolve(dataUrl);
959
+ finish(() => {
960
+ stopStream();
961
+ resolve(dataUrl);
962
+ });
863
963
  }
864
964
  catch (error) {
865
- stream.getTracks().forEach((track) => track.stop());
866
- reject(error);
965
+ finish(() => {
966
+ stopStream();
967
+ reject(error);
968
+ });
867
969
  }
868
970
  }, 100);
869
971
  };
870
972
  video.onerror = () => {
871
- stream.getTracks().forEach((track) => track.stop());
872
- reject(new Error('Failed to load video for screenshot capture'));
973
+ finish(() => {
974
+ stopStream();
975
+ reject(new Error('Failed to load video for screenshot capture'));
976
+ });
873
977
  };
874
978
  });
875
979
  }
@@ -878,9 +982,27 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
878
982
  throw error;
879
983
  }
880
984
  }
985
+ // Whether a freshly drawn shape is too small to be worth keeping
986
+ isDegenerateAnnotation(annotation) {
987
+ const MIN_SIZE = 2; // canvas pixels
988
+ if (annotation.type === 'line' || annotation.type === 'arrow') {
989
+ if (!Number.isFinite(annotation.endX) || !Number.isFinite(annotation.endY)) {
990
+ return true;
991
+ }
992
+ return (Math.hypot(annotation.endX - annotation.startX, annotation.endY - annotation.startY) <
993
+ MIN_SIZE);
994
+ }
995
+ if (annotation.type === 'rectangle') {
996
+ if (!Number.isFinite(annotation.width) || !Number.isFinite(annotation.height)) {
997
+ return true;
998
+ }
999
+ return Math.abs(annotation.width) < MIN_SIZE && Math.abs(annotation.height) < MIN_SIZE;
1000
+ }
1001
+ return false;
1002
+ }
881
1003
  render() {
882
1004
  var _a, _b, _c, _d, _e, _f;
883
- return (h("div", { class: "canvas-editor-wrapper" }, this.showCanvasEditor && (h("div", { class: "canvas-editor-overlay", part: "overlay" }, h("div", { class: "canvas-editor-modal", part: "modal" }, h("div", { class: "canvas-editor-header", part: "header" }, h("div", { class: "canvas-editor-title", part: "title" }, h("h3", null, this.canvasEditorTitle)), h("div", { class: "canvas-editor-toolbar", part: "toolbar" }, h("div", { class: "toolbar-section" }, h("div", { class: "tool-group" }, h("button", { class: `tool-btn ${this.canvasDrawingTool === 'rectangle' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'rectangle'), title: "Rectangle" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-square" }, h("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'line' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'line'), title: "Line" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-minus" }, h("path", { d: "M5 12h14" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'arrow' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'arrow'), title: "Arrow" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-move-up-right" }, h("path", { d: "M13 5H19V11" }), h("path", { d: "M19 5L5 19" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'text' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'text'), title: "Text" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-type" }, h("path", { d: "M12 4v16" }), h("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }), h("path", { d: "M9 20h6" }))), h("div", { class: "toolbar-divider" }), h("button", { class: "tool-btn undo-btn", part: "tool-button", onClick: this.undoLastAnnotation, disabled: this.annotations.length === 0, title: "Undo" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-undo" }, h("path", { d: "M3 7v6h6" }), h("path", { d: "M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" }))), this.selectedAnnotation && (h("button", { class: "tool-btn delete-btn", part: "tool-button", onClick: this.deleteSelectedAnnotation, title: "Delete" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-trash-2" }, h("path", { d: "M3 6h18" }), h("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), h("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }), h("line", { x1: "10", x2: "10", y1: "11", y2: "17" }), h("line", { x1: "14", x2: "14", y1: "11", y2: "17" })))))), h("div", { class: "toolbar-section" }, h("div", { class: "color-palette" }, this.defaultColors.map((color, index) => (h("div", { class: "color-slot-wrapper" }, h("button", { class: `color-btn ${this.canvasDrawingColor === color ? 'active' : ''} ${this.editingColorIndex === index ? 'editing' : ''}`, style: { backgroundColor: color }, onClick: () => this.handleColorSlotClick(index), title: `Color ${index + 1} - Click to customize` }, this.editingColorIndex === index && (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "white", "stroke-width": "2" }, h("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" })))), this.editingColorIndex === index && this.showColorPicker && (h("div", { class: "color-picker-dropdown" }, h("input", { type: "color", value: color, onInput: (e) => this.handleColorPickerInput(e), onClick: (e) => this.handleColorPickerClick(e) })))))))), (this.selectedAnnotation || this.canvasDrawingTool) && (h("div", { class: "toolbar-section selected-annotation-controls" }, (((_a = this.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === 'text' ||
1005
+ return (h("div", { class: "canvas-editor-wrapper" }, this.showCanvasEditor && (h("div", { class: "canvas-editor-overlay", part: "overlay", onMouseDown: this.handleOverlayMouseDown, onClick: this.handleOverlayClick }, h("div", { class: "canvas-editor-modal", part: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "canvas-editor-title" }, h("div", { class: "canvas-editor-header", part: "header" }, h("div", { class: "canvas-editor-title", part: "title" }, h("h3", { id: "canvas-editor-title" }, this.canvasEditorTitle)), h("div", { class: "canvas-editor-toolbar", part: "toolbar" }, h("div", { class: "toolbar-section" }, h("div", { class: "tool-group" }, h("button", { class: `tool-btn ${this.canvasDrawingTool === 'rectangle' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'rectangle'), title: "Rectangle" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-square" }, h("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'line' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'line'), title: "Line" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-minus" }, h("path", { d: "M5 12h14" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'arrow' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'arrow'), title: "Arrow" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-move-up-right" }, h("path", { d: "M13 5H19V11" }), h("path", { d: "M19 5L5 19" }))), h("button", { class: `tool-btn ${this.canvasDrawingTool === 'text' ? 'active' : ''}`, part: "tool-button", onClick: () => (this.canvasDrawingTool = 'text'), title: "Text" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-type" }, h("path", { d: "M12 4v16" }), h("path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }), h("path", { d: "M9 20h6" }))), h("div", { class: "toolbar-divider" }), h("button", { class: "tool-btn undo-btn", part: "tool-button", onClick: this.undoLastAnnotation, disabled: this.annotations.length === 0, title: "Undo" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-undo" }, h("path", { d: "M3 7v6h6" }), h("path", { d: "M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" }))), this.selectedAnnotation && (h("button", { class: "tool-btn delete-btn", part: "tool-button", onClick: this.deleteSelectedAnnotation, title: "Delete" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "lucide lucide-trash-2" }, h("path", { d: "M3 6h18" }), h("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), h("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }), h("line", { x1: "10", x2: "10", y1: "11", y2: "17" }), h("line", { x1: "14", x2: "14", y1: "11", y2: "17" })))))), h("div", { class: "toolbar-section" }, h("div", { class: "color-palette" }, this.defaultColors.map((color, index) => (h("div", { class: "color-slot-wrapper" }, h("button", { class: `color-btn ${this.canvasDrawingColor === color ? 'active' : ''} ${this.editingColorIndex === index ? 'editing' : ''}`, style: { backgroundColor: color }, onClick: () => this.handleColorSlotClick(index), title: `Color ${index + 1} - Click to customize` }, this.editingColorIndex === index && (h("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "white", "stroke-width": "2" }, h("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" })))), this.editingColorIndex === index && this.showColorPicker && (h("div", { class: "color-picker-dropdown" }, h("input", { type: "color", value: color, onInput: (e) => this.handleColorPickerInput(e), onClick: (e) => this.handleColorPickerClick(e) })))))))), (this.selectedAnnotation || this.canvasDrawingTool) && (h("div", { class: "toolbar-section selected-annotation-controls" }, (((_a = this.selectedAnnotation) === null || _a === void 0 ? void 0 : _a.type) === 'text' ||
884
1006
  (!this.selectedAnnotation && this.canvasDrawingTool === 'text')) && (h("div", { class: "text-controls" }, h("div", { class: "font-size-control" }, h("label", null, this.sizeLabelText), h("input", { type: "range", min: "8", max: "72", value: ((_b = this.selectedAnnotation) === null || _b === void 0 ? void 0 : _b.fontSize) || this.canvasTextSize, onInput: (e) => {
885
1007
  const newSize = parseInt(e.target.value);
886
1008
  if (this.selectedAnnotation) {
@@ -909,6 +1031,8 @@ const CanvasEditor = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement
909
1031
  "screenshotTakingText": [1, "screenshot-taking-text"],
910
1032
  "screenshotAttachedText": [1, "screenshot-attached-text"],
911
1033
  "screenshotButtonText": [1, "screenshot-button-text"],
1034
+ "escapeClose": [4, "escape-close"],
1035
+ "clickOutsideClose": [4, "click-outside-close"],
912
1036
  "autoStartScreenshot": [4, "auto-start-screenshot"],
913
1037
  "existingScreenshot": [1, "existing-screenshot"],
914
1038
  "editTextButtonText": [1, "edit-text-button-text"],
@@ -2,7 +2,7 @@ import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/
2
2
  import { d as defineCustomElement$3 } from './canvas-editor2.js';
3
3
  import { d as defineCustomElement$2 } from './feedback-modal2.js';
4
4
 
5
- const feedbackButtonCss = ".feedback-button-content{cursor:pointer;max-width:fit-content;letter-spacing:var(--feedback-button-letter-spacing);z-index:var(--feedback-button-z-index);font-family:var(--feedback-font-family)}.feedback-button-content--custom-font{font-family:inherit}.feedback-button-content--light{align-items:center;background-color:var(--feedback-button-light-bg-color);border-radius:var(--feedback-button-border-radius);box-shadow:rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;box-sizing:border-box;color:var(--feedback-button-light-text-color);display:flex;font-size:var(--feedback-button-text-font-size);font-weight:var(--feedback-button-text-font-weight);padding:8px 15px}.feedback-button-content--dark{align-items:center;background-color:var(--feedback-button-dark-bg-color);border-radius:var(--feedback-button-border-radius);box-shadow:rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;box-sizing:border-box;color:var(--feedback-button-dark-text-color);display:flex;font-weight:var(--feedback-button-text-font-weight);font-size:var(--feedback-button-text-font-size);padding:8px 15px}.icon-edit{stroke:var(--feedback-button-light-icon-color)}.feedback-button-content--dark .icon-edit{stroke:var(--feedback-button-dark-icon-color)}.feedback-button-content--bottom-right{bottom:10px;position:fixed;right:10px}.feedback-button-content--center-right{position:fixed;transform:rotate(-90deg) translateY(-50%);top:50%}.feedback-button-content--center-right.feedback-button-content--dark,.feedback-button-content--center-right.feedback-button-content--light{border-radius:4px;border-bottom-left-radius:0px;border-bottom-right-radius:0px}.feedback-button-content-icon{height:16px;margin-right:5px;width:16px}.feedback-button-content--center-right .feedback-button-content-icon{rotate:90deg}@media screen and (max-width: 767px){.feedback-button-content--hide-mobile{display:none}}";
5
+ const feedbackButtonCss = ".feedback-button-content{cursor:pointer;max-width:fit-content;letter-spacing:var(--feedback-button-letter-spacing);z-index:var(--feedback-button-z-index);font-family:var(--feedback-font-family);background:none;border:none;margin:0;padding:0;color:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;text-align:inherit}.feedback-button-content--custom-font{font-family:inherit}.feedback-button-content--light{align-items:center;background-color:var(--feedback-button-light-bg-color);border-radius:var(--feedback-button-border-radius);box-shadow:rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;box-sizing:border-box;color:var(--feedback-button-light-text-color);display:flex;font-size:var(--feedback-button-text-font-size);font-weight:var(--feedback-button-text-font-weight);padding:8px 15px}.feedback-button-content--dark{align-items:center;background-color:var(--feedback-button-dark-bg-color);border-radius:var(--feedback-button-border-radius);box-shadow:rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 2px 6px 2px;box-sizing:border-box;color:var(--feedback-button-dark-text-color);display:flex;font-weight:var(--feedback-button-text-font-weight);font-size:var(--feedback-button-text-font-size);padding:8px 15px}.icon-edit{stroke:var(--feedback-button-light-icon-color)}.feedback-button-content--dark .icon-edit{stroke:var(--feedback-button-dark-icon-color)}.feedback-button-content--bottom-right{bottom:10px;position:fixed;right:10px}.feedback-button-content--bottom-left{bottom:10px;left:10px;position:fixed}.feedback-button-content--top-right{position:fixed;right:10px;top:10px}.feedback-button-content--top-left{left:10px;position:fixed;top:10px}.feedback-button-content--center-right{position:fixed;transform:rotate(-90deg) translateY(-50%);top:50%}.feedback-button-content--center-left{position:fixed;transform:rotate(90deg) translateY(-50%);top:50%}.feedback-button-content--center-right.feedback-button-content--dark,.feedback-button-content--center-right.feedback-button-content--light,.feedback-button-content--center-left.feedback-button-content--dark,.feedback-button-content--center-left.feedback-button-content--light{border-radius:4px;border-bottom-left-radius:0px;border-bottom-right-radius:0px}.feedback-button-content-icon{height:16px;margin-right:5px;width:16px}.feedback-button-content--center-right .feedback-button-content-icon{rotate:90deg}.feedback-button-content--center-left .feedback-button-content-icon{rotate:-90deg}@media screen and (max-width: 767px){.feedback-button-content--hide-mobile{display:none}}";
6
6
 
7
7
  const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
8
8
  constructor() {
@@ -18,6 +18,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
18
18
  this.sessionId = '';
19
19
  this.metadata = '';
20
20
  this.submit = false;
21
+ this.apiKey = '';
21
22
  this.clickOutsideClose = true;
22
23
  this.customFont = false;
23
24
  this.emailAddress = '';
@@ -34,6 +35,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
34
35
  this.ratingMode = 'thumbs';
35
36
  this.emailPlaceholder = 'Email address (optional)';
36
37
  this.errorMessage = 'Please try again later.';
38
+ this.errorMessage401 = 'This project requires a valid API key to submit feedback. Check the api-key attribute.';
37
39
  this.errorMessage403 = 'The request URL does not match the one defined in PushFeedback for this project.';
38
40
  this.errorMessage404 = 'We could not find the provided project id in PushFeedback.';
39
41
  this.footerText = '';
@@ -79,13 +81,19 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
79
81
  }
80
82
  }
81
83
  componentDidLoad() {
82
- if (this.buttonPosition === 'center-right') {
84
+ if (this.buttonPosition === 'center-right' || this.buttonPosition === 'center-left') {
83
85
  const buttonContent = this.el.shadowRoot.querySelector('.feedback-button-content');
84
86
  let adjustement = 0;
85
87
  if (this.isSafariBrowser()) {
86
88
  adjustement = 5;
87
89
  }
88
- buttonContent.style.right = `${((buttonContent.offsetWidth + adjustement) / 2) * -1}px`;
90
+ const offset = `${((buttonContent.offsetWidth + adjustement) / 2) * -1}px`;
91
+ if (this.buttonPosition === 'center-right') {
92
+ buttonContent.style.right = offset;
93
+ }
94
+ else {
95
+ buttonContent.style.left = offset;
96
+ }
89
97
  }
90
98
  if (!this.customFont) {
91
99
  this.loadInterFont();
@@ -94,6 +102,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
94
102
  connectedCallback() {
95
103
  this.feedbackModal = document.createElement('feedback-modal');
96
104
  const props = [
105
+ 'apiKey',
97
106
  'clickOutsideClose',
98
107
  'customFont',
99
108
  'emailAddress',
@@ -124,6 +133,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
124
133
  'screenshotErrorUnexpected',
125
134
  'emailPlaceholder',
126
135
  'errorMessage',
136
+ 'errorMessage401',
127
137
  'errorMessage403',
128
138
  'errorMessage404',
129
139
  'footerText',
@@ -188,12 +198,16 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
188
198
  }
189
199
  }
190
200
  const body = Object.assign({ url: window.location.href, project: this.project, rating: this.rating || -1, ratingMode: this.ratingMode, message: '', metadata: this.metadata, session: localStorage.getItem('pushfeedback_sessionid') || '' }, (recaptchaToken && { recaptchaToken }));
191
- const res = await fetch('https://app.pushfeedback.com/api/feedback/', {
201
+ const headers = {
202
+ 'Content-Type': 'application/json',
203
+ };
204
+ if (this.apiKey) {
205
+ headers['Authorization'] = `Api-Key ${this.apiKey}`;
206
+ }
207
+ const res = await fetch('https://app.pushfeedback.com/api/v1/feedback/', {
192
208
  method: 'POST',
193
209
  body: JSON.stringify(body),
194
- headers: {
195
- 'Content-Type': 'application/json',
196
- },
210
+ headers,
197
211
  });
198
212
  if (res.status === 201) {
199
213
  const feedback_with_id = Object.assign(Object.assign({}, body), { id: await res.json() });
@@ -224,7 +238,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
224
238
  }
225
239
  }
226
240
  render() {
227
- return (h(Host, null, h("a", { class: `feedback-button-content feedback-button-content--${this.buttonStyle} feedback-button-content--${this.buttonPosition} ${this.customFont ? 'feedback-button-content--custom-font' : ''} ${this.hideMobile ? 'feedback-button-content--hide-mobile' : ''}`, part: "button", onClick: () => this.showModal() }, !this.hideIcon && this.buttonStyle != 'default' && (h("span", { class: "feedback-button-content-icon", part: "icon" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "icon-edit" }, h("path", { d: "M12 20h9" }), h("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })))), h("slot", null))));
241
+ return (h(Host, null, h("button", { type: "button", class: `feedback-button-content feedback-button-content--${this.buttonStyle} feedback-button-content--${this.buttonPosition} ${this.customFont ? 'feedback-button-content--custom-font' : ''} ${this.hideMobile ? 'feedback-button-content--hide-mobile' : ''}`, part: "button", onClick: () => this.showModal() }, !this.hideIcon && this.buttonStyle != 'default' && (h("span", { class: "feedback-button-content-icon", part: "icon" }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "icon-edit" }, h("path", { d: "M12 20h9" }), h("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" })))), h("slot", null))));
228
242
  }
229
243
  get el() { return this; }
230
244
  static get style() { return feedbackButtonCss; }
@@ -236,6 +250,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
236
250
  "sessionId": [1537, "session-id"],
237
251
  "metadata": [1],
238
252
  "submit": [4],
253
+ "apiKey": [1, "api-key"],
239
254
  "clickOutsideClose": [4, "click-outside-close"],
240
255
  "customFont": [4, "custom-font"],
241
256
  "emailAddress": [1, "email-address"],
@@ -252,6 +267,7 @@ const FeedbackButton$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
252
267
  "ratingMode": [1, "rating-mode"],
253
268
  "emailPlaceholder": [1, "email-placeholder"],
254
269
  "errorMessage": [1, "error-message"],
270
+ "errorMessage401": [1, "error-message-4-0-1"],
255
271
  "errorMessage403": [1, "error-message-4-0-3"],
256
272
  "errorMessage404": [1, "error-message-4-0-4"],
257
273
  "footerText": [1, "footer-text"],