@fxl-business/support-widget 0.1.0 → 0.2.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.
package/dist/index.d.mts CHANGED
@@ -12,6 +12,7 @@ type WidgetLabels = {
12
12
  descriptionPlaceholder: string;
13
13
  attachmentsLabel: string;
14
14
  attachmentsDropzone: string;
15
+ attachmentsPasteHint: string;
15
16
  send: string;
16
17
  sending: string;
17
18
  cancel: string;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ type WidgetLabels = {
12
12
  descriptionPlaceholder: string;
13
13
  attachmentsLabel: string;
14
14
  attachmentsDropzone: string;
15
+ attachmentsPasteHint: string;
15
16
  send: string;
16
17
  sending: string;
17
18
  cancel: string;
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
28
  // src/support-widget.tsx
29
- var import_react2 = require("react");
29
+ var import_react3 = require("react");
30
30
 
31
31
  // src/types.ts
32
32
  var defaultLabels = {
@@ -39,6 +39,7 @@ var defaultLabels = {
39
39
  descriptionPlaceholder: "Describe the issue or request in detail",
40
40
  attachmentsLabel: "Attachments (optional)",
41
41
  attachmentsDropzone: "Drag & drop or click to attach images/videos",
42
+ attachmentsPasteHint: "You can also paste images (Ctrl+V)",
42
43
  send: "Send",
43
44
  sending: "Sending...",
44
45
  cancel: "Cancel",
@@ -97,15 +98,46 @@ async function uploadFileToR2(presignedUrl, file) {
97
98
 
98
99
  // src/support-form.tsx
99
100
  var import_jsx_runtime = require("react/jsx-runtime");
101
+ var formStore = {
102
+ type: "bug",
103
+ title: "",
104
+ description: "",
105
+ files: []
106
+ };
107
+ function resetFormStore() {
108
+ formStore.type = "bug";
109
+ formStore.title = "";
110
+ formStore.description = "";
111
+ formStore.files = [];
112
+ }
100
113
  function SupportForm({ config, labels, onSuccess, onCancel }) {
101
- const [type, setType] = (0, import_react.useState)("bug");
102
- const [title, setTitle] = (0, import_react.useState)("");
103
- const [description, setDescription] = (0, import_react.useState)("");
104
- const [files, setFiles] = (0, import_react.useState)([]);
114
+ const [type, setTypeRaw] = (0, import_react.useState)(formStore.type);
115
+ const [title, setTitleRaw] = (0, import_react.useState)(formStore.title);
116
+ const [description, setDescriptionRaw] = (0, import_react.useState)(formStore.description);
117
+ const [files, setFilesRaw] = (0, import_react.useState)(formStore.files);
105
118
  const [isSubmitting, setIsSubmitting] = (0, import_react.useState)(false);
106
119
  const [error, setError] = (0, import_react.useState)(null);
107
120
  const [fileError, setFileError] = (0, import_react.useState)(null);
108
121
  const fileInputRef = (0, import_react.useRef)(null);
122
+ function setType(value) {
123
+ formStore.type = value;
124
+ setTypeRaw(value);
125
+ }
126
+ function setTitle(value) {
127
+ formStore.title = value;
128
+ setTitleRaw(value);
129
+ }
130
+ function setDescription(value) {
131
+ formStore.description = value;
132
+ setDescriptionRaw(value);
133
+ }
134
+ function setFiles(updater) {
135
+ setFilesRaw((prev) => {
136
+ const next = typeof updater === "function" ? updater(prev) : updater;
137
+ formStore.files = next;
138
+ return next;
139
+ });
140
+ }
109
141
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
110
142
  const ALLOWED_TYPES = /^(image|video)\//;
111
143
  function validateAndAddFiles(newFiles) {
@@ -171,6 +203,28 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
171
203
  const dropped = Array.from(e.dataTransfer.files);
172
204
  validateAndAddFiles(dropped);
173
205
  }
206
+ function handlePaste(e) {
207
+ const items = e.clipboardData?.items;
208
+ if (!items) return;
209
+ const imageFiles = [];
210
+ for (let i = 0; i < items.length; i++) {
211
+ const item = items[i];
212
+ if (item.type.startsWith("image/")) {
213
+ const file = item.getAsFile();
214
+ if (file) {
215
+ const ext = item.type.split("/")[1] || "png";
216
+ const named = new File([file], `pasted-image-${Date.now()}.${ext}`, {
217
+ type: file.type
218
+ });
219
+ imageFiles.push(named);
220
+ }
221
+ }
222
+ }
223
+ if (imageFiles.length > 0) {
224
+ e.preventDefault();
225
+ validateAndAddFiles(imageFiles);
226
+ }
227
+ }
174
228
  const isFormValid = title.trim().length > 0 && description.trim().length > 0;
175
229
  const inputStyle = {
176
230
  width: "100%",
@@ -249,6 +303,7 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
249
303
  {
250
304
  value: description,
251
305
  onChange: (e) => setDescription(e.target.value),
306
+ onPaste: handlePaste,
252
307
  placeholder: labels.descriptionPlaceholder,
253
308
  required: true,
254
309
  rows: 4,
@@ -302,6 +357,7 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
302
357
  ] }, `${f.name}-${i}`)) : labels.attachmentsDropzone
303
358
  }
304
359
  ),
360
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "#555", fontSize: 10, margin: "4px 0 0", fontStyle: "italic" }, children: labels.attachmentsPasteHint }),
305
361
  fileError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "#ff4d4d", fontSize: 11, margin: "4px 0 0" }, children: fileError }),
306
362
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
307
363
  "input",
@@ -363,27 +419,97 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
363
419
  ] });
364
420
  }
365
421
 
422
+ // src/use-drag.ts
423
+ var import_react2 = require("react");
424
+ var DRAG_THRESHOLD = 5;
425
+ function useDrag() {
426
+ const [position, setPosition] = (0, import_react2.useState)(null);
427
+ const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
428
+ const dragState = (0, import_react2.useRef)(null);
429
+ const handlePointerMove = (0, import_react2.useCallback)((e) => {
430
+ const state = dragState.current;
431
+ if (!state) return;
432
+ const dx = e.clientX - state.startX;
433
+ const dy = e.clientY - state.startY;
434
+ if (!state.moved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) {
435
+ return;
436
+ }
437
+ state.moved = true;
438
+ setIsDragging(true);
439
+ const newX = Math.max(0, Math.min(window.innerWidth - 44, state.startPosX + dx));
440
+ const newY = Math.max(0, Math.min(window.innerHeight - 44, state.startPosY + dy));
441
+ setPosition({ x: newX, y: newY });
442
+ }, []);
443
+ const handlePointerUp = (0, import_react2.useCallback)((_e) => {
444
+ const state = dragState.current;
445
+ if (!state) return;
446
+ if (state.element) {
447
+ state.element.releasePointerCapture(state.pointerId);
448
+ }
449
+ document.removeEventListener("pointermove", handlePointerMove);
450
+ document.removeEventListener("pointerup", handlePointerUp);
451
+ if (state.moved) {
452
+ requestAnimationFrame(() => setIsDragging(false));
453
+ }
454
+ dragState.current = null;
455
+ }, [handlePointerMove]);
456
+ const handlePointerDown = (0, import_react2.useCallback)((e) => {
457
+ if (e.button !== 0) return;
458
+ const element = e.currentTarget;
459
+ element.setPointerCapture(e.pointerId);
460
+ const rect = element.parentElement.getBoundingClientRect();
461
+ const startPosX = rect.left;
462
+ const startPosY = rect.top;
463
+ dragState.current = {
464
+ startX: e.clientX,
465
+ startY: e.clientY,
466
+ startPosX,
467
+ startPosY,
468
+ moved: false,
469
+ pointerId: e.pointerId,
470
+ element
471
+ };
472
+ document.addEventListener("pointermove", handlePointerMove);
473
+ document.addEventListener("pointerup", handlePointerUp);
474
+ }, [handlePointerMove, handlePointerUp]);
475
+ (0, import_react2.useEffect)(() => {
476
+ return () => {
477
+ document.removeEventListener("pointermove", handlePointerMove);
478
+ document.removeEventListener("pointerup", handlePointerUp);
479
+ };
480
+ }, [handlePointerMove, handlePointerUp]);
481
+ return { position, isDragging, handlePointerDown };
482
+ }
483
+
366
484
  // src/support-widget.tsx
367
485
  var import_jsx_runtime2 = require("react/jsx-runtime");
486
+ var persistedPanelState = "closed";
368
487
  function SupportWidget(props) {
369
488
  const labels = { ...defaultLabels, ...props.labels };
370
- const [panelState, setPanelState] = (0, import_react2.useState)("closed");
371
- const [openCount, setOpenCount] = (0, import_react2.useState)(0);
372
- const panelRef = (0, import_react2.useRef)(null);
373
- (0, import_react2.useEffect)(() => {
489
+ const [panelState, setPanelStateRaw] = (0, import_react3.useState)(persistedPanelState);
490
+ const panelRef = (0, import_react3.useRef)(null);
491
+ const { position, isDragging, handlePointerDown } = useDrag();
492
+ const setPanelState = (0, import_react3.useCallback)((next) => {
493
+ setPanelStateRaw((prev) => {
494
+ const value = typeof next === "function" ? next(prev) : next;
495
+ persistedPanelState = value;
496
+ return value;
497
+ });
498
+ }, []);
499
+ (0, import_react3.useEffect)(() => {
374
500
  function handleKeyDown(e) {
375
501
  if (e.key === "Escape") setPanelState("closed");
376
502
  }
377
503
  window.addEventListener("keydown", handleKeyDown);
378
504
  return () => window.removeEventListener("keydown", handleKeyDown);
379
505
  }, []);
380
- (0, import_react2.useEffect)(() => {
506
+ (0, import_react3.useEffect)(() => {
381
507
  if (panelState === "success") {
382
508
  const timer = setTimeout(() => setPanelState("closed"), 2e3);
383
509
  return () => clearTimeout(timer);
384
510
  }
385
511
  }, [panelState]);
386
- (0, import_react2.useEffect)(() => {
512
+ (0, import_react3.useEffect)(() => {
387
513
  function handleClickOutside(e) {
388
514
  if (panelRef.current && !panelRef.current.contains(e.target)) {
389
515
  setPanelState("closed");
@@ -401,8 +527,7 @@ function SupportWidget(props) {
401
527
  "data-fxl-widget": "",
402
528
  style: {
403
529
  position: "fixed",
404
- bottom: 24,
405
- right: 24,
530
+ ...position ? { left: position.x, top: position.y } : { bottom: 24, right: 24 },
406
531
  zIndex: 9999,
407
532
  fontFamily: "'Space Grotesk', system-ui, sans-serif"
408
533
  },
@@ -413,14 +538,22 @@ function SupportWidget(props) {
413
538
  className: "fxl-panel",
414
539
  style: {
415
540
  position: "absolute",
416
- bottom: 56,
417
- right: 0,
418
541
  width: 300,
419
542
  background: "#111111",
420
543
  border: "1px solid #2a2a2a",
421
544
  borderRadius: 8,
422
545
  padding: 16,
423
- boxShadow: "0 8px 32px rgba(0,0,0,0.6)"
546
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
547
+ ...(() => {
548
+ const btnRect = panelRef.current?.getBoundingClientRect();
549
+ if (!btnRect) return { bottom: 56, right: 0 };
550
+ const inBottomHalf = btnRect.top > window.innerHeight / 2;
551
+ const inRightHalf = btnRect.left > window.innerWidth / 2;
552
+ return {
553
+ ...inBottomHalf ? { bottom: 56 } : { top: 56 },
554
+ ...inRightHalf ? { right: 0 } : { left: 0 }
555
+ };
556
+ })()
424
557
  },
425
558
  children: panelState === "success" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center", padding: "16px 0" }, children: [
426
559
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 24, marginBottom: 8 }, children: "\u2713" }),
@@ -461,10 +594,12 @@ function SupportWidget(props) {
461
594
  {
462
595
  config: props,
463
596
  labels,
464
- onSuccess: () => setPanelState("success"),
597
+ onSuccess: () => {
598
+ resetFormStore();
599
+ setPanelState("success");
600
+ },
465
601
  onCancel: () => setPanelState("closed")
466
- },
467
- openCount
602
+ }
468
603
  )
469
604
  ] })
470
605
  }
@@ -472,33 +607,33 @@ function SupportWidget(props) {
472
607
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
473
608
  "button",
474
609
  {
610
+ onPointerDown: handlePointerDown,
475
611
  onClick: () => {
476
- setPanelState((s) => {
477
- if (s === "closed") {
478
- setOpenCount((c) => c + 1);
479
- return "open";
480
- }
481
- return "closed";
482
- });
612
+ if (isDragging) return;
613
+ setPanelState((s) => s === "closed" ? "open" : "closed");
483
614
  },
484
615
  "aria-label": labels.openWidget,
616
+ className: "fxl-drag-button",
485
617
  style: {
486
618
  width: 44,
487
619
  height: 44,
488
620
  borderRadius: "50%",
489
621
  background: "#b4e62e",
490
622
  border: "none",
491
- cursor: "pointer",
492
623
  display: "flex",
493
624
  alignItems: "center",
494
625
  justifyContent: "center",
495
626
  boxShadow: "0 2px 12px rgba(180,230,46,0.4)",
496
- transition: "transform 0.15s, box-shadow 0.15s"
627
+ transition: isDragging ? "none" : "transform 0.15s, box-shadow 0.15s",
628
+ touchAction: "none",
629
+ userSelect: "none"
497
630
  },
498
631
  onMouseEnter: (e) => {
499
- ;
500
- e.currentTarget.style.transform = "scale(1.1)";
501
- e.currentTarget.style.boxShadow = "0 4px 20px rgba(180,230,46,0.5)";
632
+ if (!isDragging) {
633
+ ;
634
+ e.currentTarget.style.transform = "scale(1.1)";
635
+ e.currentTarget.style.boxShadow = "0 4px 20px rgba(180,230,46,0.5)";
636
+ }
502
637
  },
503
638
  onMouseLeave: (e) => {
504
639
  ;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/support-widget.tsx
2
- import { useState as useState2, useEffect, useRef as useRef2 } from "react";
2
+ import { useState as useState3, useEffect as useEffect2, useRef as useRef3, useCallback as useCallback2 } from "react";
3
3
 
4
4
  // src/types.ts
5
5
  var defaultLabels = {
@@ -12,6 +12,7 @@ var defaultLabels = {
12
12
  descriptionPlaceholder: "Describe the issue or request in detail",
13
13
  attachmentsLabel: "Attachments (optional)",
14
14
  attachmentsDropzone: "Drag & drop or click to attach images/videos",
15
+ attachmentsPasteHint: "You can also paste images (Ctrl+V)",
15
16
  send: "Send",
16
17
  sending: "Sending...",
17
18
  cancel: "Cancel",
@@ -70,15 +71,46 @@ async function uploadFileToR2(presignedUrl, file) {
70
71
 
71
72
  // src/support-form.tsx
72
73
  import { jsx, jsxs } from "react/jsx-runtime";
74
+ var formStore = {
75
+ type: "bug",
76
+ title: "",
77
+ description: "",
78
+ files: []
79
+ };
80
+ function resetFormStore() {
81
+ formStore.type = "bug";
82
+ formStore.title = "";
83
+ formStore.description = "";
84
+ formStore.files = [];
85
+ }
73
86
  function SupportForm({ config, labels, onSuccess, onCancel }) {
74
- const [type, setType] = useState("bug");
75
- const [title, setTitle] = useState("");
76
- const [description, setDescription] = useState("");
77
- const [files, setFiles] = useState([]);
87
+ const [type, setTypeRaw] = useState(formStore.type);
88
+ const [title, setTitleRaw] = useState(formStore.title);
89
+ const [description, setDescriptionRaw] = useState(formStore.description);
90
+ const [files, setFilesRaw] = useState(formStore.files);
78
91
  const [isSubmitting, setIsSubmitting] = useState(false);
79
92
  const [error, setError] = useState(null);
80
93
  const [fileError, setFileError] = useState(null);
81
94
  const fileInputRef = useRef(null);
95
+ function setType(value) {
96
+ formStore.type = value;
97
+ setTypeRaw(value);
98
+ }
99
+ function setTitle(value) {
100
+ formStore.title = value;
101
+ setTitleRaw(value);
102
+ }
103
+ function setDescription(value) {
104
+ formStore.description = value;
105
+ setDescriptionRaw(value);
106
+ }
107
+ function setFiles(updater) {
108
+ setFilesRaw((prev) => {
109
+ const next = typeof updater === "function" ? updater(prev) : updater;
110
+ formStore.files = next;
111
+ return next;
112
+ });
113
+ }
82
114
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
83
115
  const ALLOWED_TYPES = /^(image|video)\//;
84
116
  function validateAndAddFiles(newFiles) {
@@ -144,6 +176,28 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
144
176
  const dropped = Array.from(e.dataTransfer.files);
145
177
  validateAndAddFiles(dropped);
146
178
  }
179
+ function handlePaste(e) {
180
+ const items = e.clipboardData?.items;
181
+ if (!items) return;
182
+ const imageFiles = [];
183
+ for (let i = 0; i < items.length; i++) {
184
+ const item = items[i];
185
+ if (item.type.startsWith("image/")) {
186
+ const file = item.getAsFile();
187
+ if (file) {
188
+ const ext = item.type.split("/")[1] || "png";
189
+ const named = new File([file], `pasted-image-${Date.now()}.${ext}`, {
190
+ type: file.type
191
+ });
192
+ imageFiles.push(named);
193
+ }
194
+ }
195
+ }
196
+ if (imageFiles.length > 0) {
197
+ e.preventDefault();
198
+ validateAndAddFiles(imageFiles);
199
+ }
200
+ }
147
201
  const isFormValid = title.trim().length > 0 && description.trim().length > 0;
148
202
  const inputStyle = {
149
203
  width: "100%",
@@ -222,6 +276,7 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
222
276
  {
223
277
  value: description,
224
278
  onChange: (e) => setDescription(e.target.value),
279
+ onPaste: handlePaste,
225
280
  placeholder: labels.descriptionPlaceholder,
226
281
  required: true,
227
282
  rows: 4,
@@ -275,6 +330,7 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
275
330
  ] }, `${f.name}-${i}`)) : labels.attachmentsDropzone
276
331
  }
277
332
  ),
333
+ /* @__PURE__ */ jsx("p", { style: { color: "#555", fontSize: 10, margin: "4px 0 0", fontStyle: "italic" }, children: labels.attachmentsPasteHint }),
278
334
  fileError && /* @__PURE__ */ jsx("p", { style: { color: "#ff4d4d", fontSize: 11, margin: "4px 0 0" }, children: fileError }),
279
335
  /* @__PURE__ */ jsx(
280
336
  "input",
@@ -336,27 +392,97 @@ function SupportForm({ config, labels, onSuccess, onCancel }) {
336
392
  ] });
337
393
  }
338
394
 
395
+ // src/use-drag.ts
396
+ import { useState as useState2, useRef as useRef2, useCallback, useEffect } from "react";
397
+ var DRAG_THRESHOLD = 5;
398
+ function useDrag() {
399
+ const [position, setPosition] = useState2(null);
400
+ const [isDragging, setIsDragging] = useState2(false);
401
+ const dragState = useRef2(null);
402
+ const handlePointerMove = useCallback((e) => {
403
+ const state = dragState.current;
404
+ if (!state) return;
405
+ const dx = e.clientX - state.startX;
406
+ const dy = e.clientY - state.startY;
407
+ if (!state.moved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) {
408
+ return;
409
+ }
410
+ state.moved = true;
411
+ setIsDragging(true);
412
+ const newX = Math.max(0, Math.min(window.innerWidth - 44, state.startPosX + dx));
413
+ const newY = Math.max(0, Math.min(window.innerHeight - 44, state.startPosY + dy));
414
+ setPosition({ x: newX, y: newY });
415
+ }, []);
416
+ const handlePointerUp = useCallback((_e) => {
417
+ const state = dragState.current;
418
+ if (!state) return;
419
+ if (state.element) {
420
+ state.element.releasePointerCapture(state.pointerId);
421
+ }
422
+ document.removeEventListener("pointermove", handlePointerMove);
423
+ document.removeEventListener("pointerup", handlePointerUp);
424
+ if (state.moved) {
425
+ requestAnimationFrame(() => setIsDragging(false));
426
+ }
427
+ dragState.current = null;
428
+ }, [handlePointerMove]);
429
+ const handlePointerDown = useCallback((e) => {
430
+ if (e.button !== 0) return;
431
+ const element = e.currentTarget;
432
+ element.setPointerCapture(e.pointerId);
433
+ const rect = element.parentElement.getBoundingClientRect();
434
+ const startPosX = rect.left;
435
+ const startPosY = rect.top;
436
+ dragState.current = {
437
+ startX: e.clientX,
438
+ startY: e.clientY,
439
+ startPosX,
440
+ startPosY,
441
+ moved: false,
442
+ pointerId: e.pointerId,
443
+ element
444
+ };
445
+ document.addEventListener("pointermove", handlePointerMove);
446
+ document.addEventListener("pointerup", handlePointerUp);
447
+ }, [handlePointerMove, handlePointerUp]);
448
+ useEffect(() => {
449
+ return () => {
450
+ document.removeEventListener("pointermove", handlePointerMove);
451
+ document.removeEventListener("pointerup", handlePointerUp);
452
+ };
453
+ }, [handlePointerMove, handlePointerUp]);
454
+ return { position, isDragging, handlePointerDown };
455
+ }
456
+
339
457
  // src/support-widget.tsx
340
458
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
459
+ var persistedPanelState = "closed";
341
460
  function SupportWidget(props) {
342
461
  const labels = { ...defaultLabels, ...props.labels };
343
- const [panelState, setPanelState] = useState2("closed");
344
- const [openCount, setOpenCount] = useState2(0);
345
- const panelRef = useRef2(null);
346
- useEffect(() => {
462
+ const [panelState, setPanelStateRaw] = useState3(persistedPanelState);
463
+ const panelRef = useRef3(null);
464
+ const { position, isDragging, handlePointerDown } = useDrag();
465
+ const setPanelState = useCallback2((next) => {
466
+ setPanelStateRaw((prev) => {
467
+ const value = typeof next === "function" ? next(prev) : next;
468
+ persistedPanelState = value;
469
+ return value;
470
+ });
471
+ }, []);
472
+ useEffect2(() => {
347
473
  function handleKeyDown(e) {
348
474
  if (e.key === "Escape") setPanelState("closed");
349
475
  }
350
476
  window.addEventListener("keydown", handleKeyDown);
351
477
  return () => window.removeEventListener("keydown", handleKeyDown);
352
478
  }, []);
353
- useEffect(() => {
479
+ useEffect2(() => {
354
480
  if (panelState === "success") {
355
481
  const timer = setTimeout(() => setPanelState("closed"), 2e3);
356
482
  return () => clearTimeout(timer);
357
483
  }
358
484
  }, [panelState]);
359
- useEffect(() => {
485
+ useEffect2(() => {
360
486
  function handleClickOutside(e) {
361
487
  if (panelRef.current && !panelRef.current.contains(e.target)) {
362
488
  setPanelState("closed");
@@ -374,8 +500,7 @@ function SupportWidget(props) {
374
500
  "data-fxl-widget": "",
375
501
  style: {
376
502
  position: "fixed",
377
- bottom: 24,
378
- right: 24,
503
+ ...position ? { left: position.x, top: position.y } : { bottom: 24, right: 24 },
379
504
  zIndex: 9999,
380
505
  fontFamily: "'Space Grotesk', system-ui, sans-serif"
381
506
  },
@@ -386,14 +511,22 @@ function SupportWidget(props) {
386
511
  className: "fxl-panel",
387
512
  style: {
388
513
  position: "absolute",
389
- bottom: 56,
390
- right: 0,
391
514
  width: 300,
392
515
  background: "#111111",
393
516
  border: "1px solid #2a2a2a",
394
517
  borderRadius: 8,
395
518
  padding: 16,
396
- boxShadow: "0 8px 32px rgba(0,0,0,0.6)"
519
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
520
+ ...(() => {
521
+ const btnRect = panelRef.current?.getBoundingClientRect();
522
+ if (!btnRect) return { bottom: 56, right: 0 };
523
+ const inBottomHalf = btnRect.top > window.innerHeight / 2;
524
+ const inRightHalf = btnRect.left > window.innerWidth / 2;
525
+ return {
526
+ ...inBottomHalf ? { bottom: 56 } : { top: 56 },
527
+ ...inRightHalf ? { right: 0 } : { left: 0 }
528
+ };
529
+ })()
397
530
  },
398
531
  children: panelState === "success" ? /* @__PURE__ */ jsxs2("div", { style: { textAlign: "center", padding: "16px 0" }, children: [
399
532
  /* @__PURE__ */ jsx2("div", { style: { fontSize: 24, marginBottom: 8 }, children: "\u2713" }),
@@ -434,10 +567,12 @@ function SupportWidget(props) {
434
567
  {
435
568
  config: props,
436
569
  labels,
437
- onSuccess: () => setPanelState("success"),
570
+ onSuccess: () => {
571
+ resetFormStore();
572
+ setPanelState("success");
573
+ },
438
574
  onCancel: () => setPanelState("closed")
439
- },
440
- openCount
575
+ }
441
576
  )
442
577
  ] })
443
578
  }
@@ -445,33 +580,33 @@ function SupportWidget(props) {
445
580
  /* @__PURE__ */ jsx2(
446
581
  "button",
447
582
  {
583
+ onPointerDown: handlePointerDown,
448
584
  onClick: () => {
449
- setPanelState((s) => {
450
- if (s === "closed") {
451
- setOpenCount((c) => c + 1);
452
- return "open";
453
- }
454
- return "closed";
455
- });
585
+ if (isDragging) return;
586
+ setPanelState((s) => s === "closed" ? "open" : "closed");
456
587
  },
457
588
  "aria-label": labels.openWidget,
589
+ className: "fxl-drag-button",
458
590
  style: {
459
591
  width: 44,
460
592
  height: 44,
461
593
  borderRadius: "50%",
462
594
  background: "#b4e62e",
463
595
  border: "none",
464
- cursor: "pointer",
465
596
  display: "flex",
466
597
  alignItems: "center",
467
598
  justifyContent: "center",
468
599
  boxShadow: "0 2px 12px rgba(180,230,46,0.4)",
469
- transition: "transform 0.15s, box-shadow 0.15s"
600
+ transition: isDragging ? "none" : "transform 0.15s, box-shadow 0.15s",
601
+ touchAction: "none",
602
+ userSelect: "none"
470
603
  },
471
604
  onMouseEnter: (e) => {
472
- ;
473
- e.currentTarget.style.transform = "scale(1.1)";
474
- e.currentTarget.style.boxShadow = "0 4px 20px rgba(180,230,46,0.5)";
605
+ if (!isDragging) {
606
+ ;
607
+ e.currentTarget.style.transform = "scale(1.1)";
608
+ e.currentTarget.style.boxShadow = "0 4px 20px rgba(180,230,46,0.5)";
609
+ }
475
610
  },
476
611
  onMouseLeave: (e) => {
477
612
  ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fxl-business/support-widget",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Embeddable support widget for FXL Support — bug reports and feature requests",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -16,11 +16,6 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
- "scripts": {
20
- "build": "tsup",
21
- "type-check": "tsc --noEmit",
22
- "prepublishOnly": "pnpm build"
23
- },
24
19
  "peerDependencies": {
25
20
  "react": "^18.0.0",
26
21
  "react-dom": "^18.0.0"
@@ -36,5 +31,9 @@
36
31
  },
37
32
  "publishConfig": {
38
33
  "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "type-check": "tsc --noEmit"
39
38
  }
40
- }
39
+ }