@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 +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +166 -31
- package/dist/index.mjs +166 -31
- package/package.json +6 -7
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
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
|
|
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,
|
|
102
|
-
const [title,
|
|
103
|
-
const [description,
|
|
104
|
-
const [files,
|
|
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,
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
(0,
|
|
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,
|
|
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,
|
|
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: () =>
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
|
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,
|
|
75
|
-
const [title,
|
|
76
|
-
const [description,
|
|
77
|
-
const [files,
|
|
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,
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
|
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
|
+
}
|