feedtack 0.3.1 → 0.4.0
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.ts +4 -1
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +154 -41
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -50,7 +50,10 @@ declare class WebhookAdapter implements FeedtackAdapter {
|
|
|
50
50
|
declare function getViewportMeta(): FeedtackViewportMeta;
|
|
51
51
|
declare function getPageMeta(): FeedtackPageMeta;
|
|
52
52
|
declare function getDeviceMeta(): FeedtackDeviceMeta;
|
|
53
|
-
declare function getPinCoords(event:
|
|
53
|
+
declare function getPinCoords(event: {
|
|
54
|
+
clientX: number;
|
|
55
|
+
clientY: number;
|
|
56
|
+
}): {
|
|
54
57
|
x: number;
|
|
55
58
|
y: number;
|
|
56
59
|
xPct: number;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme
|
|
3
|
+
import { e as FeedbackItem, F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme } from '../theme-C-uctIoI.js';
|
|
4
|
+
|
|
5
|
+
interface FeedtackFlushEvent {
|
|
6
|
+
pathname: string;
|
|
7
|
+
items: FeedbackItem[];
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
interface FeedtackClasses {
|
|
6
11
|
button?: string;
|
|
@@ -26,8 +31,14 @@ interface FeedtackProviderProps {
|
|
|
26
31
|
disabled?: boolean;
|
|
27
32
|
/** Render custom content inside a submitted pin marker. Receives the feedback item. */
|
|
28
33
|
renderPinIcon?: (item: FeedbackItem) => React.ReactNode;
|
|
34
|
+
/** Called with batched feedback when user leaves a page or goes idle */
|
|
35
|
+
onFlush?: (event: FeedtackFlushEvent) => void;
|
|
36
|
+
/** Idle timeout in ms before flushing (default 5 min) */
|
|
37
|
+
flushIdleMs?: number;
|
|
38
|
+
/** User roles that trigger re-scope on reply (default: any non-'agent' role) */
|
|
39
|
+
rescopeRoles?: string[];
|
|
29
40
|
}
|
|
30
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
41
|
+
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, onFlush, flushIdleMs, rescopeRoles, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
31
42
|
|
|
32
43
|
interface FeedtackContextValue {
|
|
33
44
|
activatePinMode: () => void;
|
|
@@ -38,4 +49,4 @@ interface FeedtackContextValue {
|
|
|
38
49
|
/** Hook for host app to programmatically control feedtack */
|
|
39
50
|
declare function useFeedtack(): FeedtackContextValue;
|
|
40
51
|
|
|
41
|
-
export { type FeedtackClasses, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
|
|
52
|
+
export { type FeedtackClasses, type FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
|
package/dist/react/index.js
CHANGED
|
@@ -191,6 +191,10 @@ function ThreadPanel({
|
|
|
191
191
|
padding: 6,
|
|
192
192
|
borderRadius: 6,
|
|
193
193
|
border: "1px solid var(--ft-border)",
|
|
194
|
+
background: "var(--ft-surface)",
|
|
195
|
+
color: "var(--ft-text)",
|
|
196
|
+
minHeight: 60,
|
|
197
|
+
resize: "vertical",
|
|
194
198
|
marginTop: 4
|
|
195
199
|
}
|
|
196
200
|
}
|
|
@@ -243,14 +247,14 @@ function ThreadPanel({
|
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
// src/react/useFeedtackState.ts
|
|
246
|
-
import { useCallback as
|
|
250
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState2 } from "react";
|
|
247
251
|
|
|
248
252
|
// src/react/useFeedtackDom.ts
|
|
249
253
|
import { useEffect, useRef } from "react";
|
|
250
254
|
|
|
251
255
|
// src/ui/styles.ts
|
|
252
256
|
var FEEDTACK_DEFAULT_TOKENS = `
|
|
253
|
-
#feedtack-root {
|
|
257
|
+
#feedtack-root, .feedtack-form, .feedtack-thread {
|
|
254
258
|
--ft-primary: #2563eb;
|
|
255
259
|
--ft-primary-hover: #1d4ed8;
|
|
256
260
|
--ft-bg: #ffffff;
|
|
@@ -532,8 +536,68 @@ function useFeedtackDom(theme, disabled) {
|
|
|
532
536
|
return rootRef;
|
|
533
537
|
}
|
|
534
538
|
|
|
539
|
+
// src/react/useFeedtackFlush.ts
|
|
540
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
541
|
+
var DEFAULT_IDLE_MS = 5 * 60 * 1e3;
|
|
542
|
+
function useFeedtackFlush({
|
|
543
|
+
pathname,
|
|
544
|
+
feedbackItems,
|
|
545
|
+
onFlush,
|
|
546
|
+
flushIdleMs = DEFAULT_IDLE_MS,
|
|
547
|
+
disabled
|
|
548
|
+
}) {
|
|
549
|
+
const flushedRef = useRef2(/* @__PURE__ */ new Set());
|
|
550
|
+
const prevPathnameRef = useRef2(pathname);
|
|
551
|
+
const idleTimerRef = useRef2(null);
|
|
552
|
+
const flush = useCallback(
|
|
553
|
+
(path, items) => {
|
|
554
|
+
if (!onFlush || flushedRef.current.has(path)) return;
|
|
555
|
+
const pageItems = items.filter((i) => i.payload.page.pathname === path);
|
|
556
|
+
if (pageItems.length === 0) return;
|
|
557
|
+
flushedRef.current.add(path);
|
|
558
|
+
onFlush({ pathname: path, items: pageItems });
|
|
559
|
+
},
|
|
560
|
+
[onFlush]
|
|
561
|
+
);
|
|
562
|
+
useEffect2(() => {
|
|
563
|
+
if (disabled || !onFlush) return;
|
|
564
|
+
const prev = prevPathnameRef.current;
|
|
565
|
+
prevPathnameRef.current = pathname;
|
|
566
|
+
if (prev !== pathname) {
|
|
567
|
+
flush(prev, feedbackItems);
|
|
568
|
+
}
|
|
569
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
570
|
+
useEffect2(() => {
|
|
571
|
+
if (disabled || !onFlush || flushIdleMs <= 0) return;
|
|
572
|
+
const resetTimer = () => {
|
|
573
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
574
|
+
idleTimerRef.current = setTimeout(() => {
|
|
575
|
+
flush(pathname, feedbackItems);
|
|
576
|
+
}, flushIdleMs);
|
|
577
|
+
};
|
|
578
|
+
const events = ["mousemove", "keydown", "scroll", "touchstart"];
|
|
579
|
+
for (const e of events)
|
|
580
|
+
window.addEventListener(e, resetTimer, { passive: true });
|
|
581
|
+
resetTimer();
|
|
582
|
+
return () => {
|
|
583
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
584
|
+
for (const e of events) window.removeEventListener(e, resetTimer);
|
|
585
|
+
};
|
|
586
|
+
}, [pathname, feedbackItems, flush, onFlush, flushIdleMs, disabled]);
|
|
587
|
+
useEffect2(() => {
|
|
588
|
+
if (disabled || !onFlush) return;
|
|
589
|
+
const handleUnload = () => flush(pathname, feedbackItems);
|
|
590
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
591
|
+
return () => window.removeEventListener("beforeunload", handleUnload);
|
|
592
|
+
}, [pathname, feedbackItems, flush, onFlush, disabled]);
|
|
593
|
+
const clearFlushed = useCallback((path) => {
|
|
594
|
+
flushedRef.current.delete(path);
|
|
595
|
+
}, []);
|
|
596
|
+
return { clearFlushed };
|
|
597
|
+
}
|
|
598
|
+
|
|
535
599
|
// src/react/usePinMode.ts
|
|
536
|
-
import { useCallback, useEffect as
|
|
600
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState } from "react";
|
|
537
601
|
function usePinMode({
|
|
538
602
|
hotkey,
|
|
539
603
|
onDeactivate,
|
|
@@ -544,14 +608,14 @@ function usePinMode({
|
|
|
544
608
|
const [pendingPins, setPendingPins] = useState([]);
|
|
545
609
|
const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
|
|
546
610
|
const [showForm, setShowForm] = useState(false);
|
|
547
|
-
const activate =
|
|
548
|
-
const deactivate =
|
|
611
|
+
const activate = useCallback2(() => setIsActive(true), []);
|
|
612
|
+
const deactivate = useCallback2(() => {
|
|
549
613
|
setIsActive(false);
|
|
550
614
|
setPendingPins([]);
|
|
551
615
|
setShowForm(false);
|
|
552
616
|
onDeactivate?.();
|
|
553
617
|
}, [onDeactivate]);
|
|
554
|
-
|
|
618
|
+
useEffect3(() => {
|
|
555
619
|
if (isActive) {
|
|
556
620
|
document.documentElement.classList.add("feedtack-crosshair");
|
|
557
621
|
} else {
|
|
@@ -559,7 +623,7 @@ function usePinMode({
|
|
|
559
623
|
}
|
|
560
624
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
561
625
|
}, [isActive]);
|
|
562
|
-
|
|
626
|
+
useEffect3(() => {
|
|
563
627
|
if (disabled) return;
|
|
564
628
|
const handler = (e) => {
|
|
565
629
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
@@ -580,31 +644,52 @@ function usePinMode({
|
|
|
580
644
|
window.addEventListener("keydown", handler);
|
|
581
645
|
return () => window.removeEventListener("keydown", handler);
|
|
582
646
|
}, [hotkey, deactivate, isActive, disabled, isModalOpen, showForm]);
|
|
583
|
-
const
|
|
584
|
-
(
|
|
585
|
-
if (!isActive) return;
|
|
586
|
-
const target = e.target;
|
|
647
|
+
const placePin = useCallback2(
|
|
648
|
+
(coords, target) => {
|
|
587
649
|
if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
|
|
588
650
|
return;
|
|
589
|
-
e.preventDefault();
|
|
590
|
-
e.stopPropagation();
|
|
591
651
|
setPendingPins((prev) => [
|
|
592
652
|
...prev,
|
|
593
653
|
{
|
|
594
654
|
color: selectedColor,
|
|
595
|
-
...getPinCoords(
|
|
655
|
+
...getPinCoords(coords),
|
|
596
656
|
target: getTargetMeta(target)
|
|
597
657
|
}
|
|
598
658
|
]);
|
|
599
659
|
setShowForm(true);
|
|
600
660
|
},
|
|
601
|
-
[
|
|
661
|
+
[selectedColor]
|
|
602
662
|
);
|
|
603
|
-
|
|
663
|
+
const handlePageClick = useCallback2(
|
|
664
|
+
(e) => {
|
|
665
|
+
if (!isActive) return;
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
e.stopPropagation();
|
|
668
|
+
placePin(e, e.target);
|
|
669
|
+
},
|
|
670
|
+
[isActive, placePin]
|
|
671
|
+
);
|
|
672
|
+
const handleTouchEnd = useCallback2(
|
|
673
|
+
(e) => {
|
|
674
|
+
if (!isActive) return;
|
|
675
|
+
const touch = e.changedTouches[0];
|
|
676
|
+
if (!touch) return;
|
|
677
|
+
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
678
|
+
if (!target) return;
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
placePin(touch, target);
|
|
681
|
+
},
|
|
682
|
+
[isActive, placePin]
|
|
683
|
+
);
|
|
684
|
+
useEffect3(() => {
|
|
604
685
|
if (disabled) return;
|
|
605
686
|
document.addEventListener("click", handlePageClick, true);
|
|
606
|
-
|
|
607
|
-
|
|
687
|
+
document.addEventListener("touchend", handleTouchEnd, true);
|
|
688
|
+
return () => {
|
|
689
|
+
document.removeEventListener("click", handlePageClick, true);
|
|
690
|
+
document.removeEventListener("touchend", handleTouchEnd, true);
|
|
691
|
+
};
|
|
692
|
+
}, [handlePageClick, handleTouchEnd, disabled]);
|
|
608
693
|
return {
|
|
609
694
|
isActive,
|
|
610
695
|
activate,
|
|
@@ -623,11 +708,16 @@ function useFeedtackState({
|
|
|
623
708
|
hotkey,
|
|
624
709
|
theme,
|
|
625
710
|
onError,
|
|
626
|
-
disabled
|
|
711
|
+
disabled,
|
|
712
|
+
onFlush,
|
|
713
|
+
flushIdleMs,
|
|
714
|
+
rescopeRoles
|
|
627
715
|
}) {
|
|
628
716
|
useFeedtackDom(theme, disabled);
|
|
629
|
-
const [pathname, setPathname] = useState2(
|
|
630
|
-
|
|
717
|
+
const [pathname, setPathname] = useState2(
|
|
718
|
+
() => typeof window === "undefined" ? "/" : window.location.pathname
|
|
719
|
+
);
|
|
720
|
+
useEffect4(() => {
|
|
631
721
|
const update = () => setPathname(window.location.pathname);
|
|
632
722
|
const origPush = history.pushState.bind(history);
|
|
633
723
|
const origReplace = history.replaceState.bind(history);
|
|
@@ -654,7 +744,7 @@ function useFeedtackState({
|
|
|
654
744
|
const [loading, setLoading] = useState2(true);
|
|
655
745
|
const [openThreadId, setOpenThreadId] = useState2(null);
|
|
656
746
|
const [replyBody, setReplyBody] = useState2("");
|
|
657
|
-
const resetForm =
|
|
747
|
+
const resetForm = useCallback3(() => {
|
|
658
748
|
setComment("");
|
|
659
749
|
setSentiment(null);
|
|
660
750
|
setCommentError(false);
|
|
@@ -668,7 +758,14 @@ function useFeedtackState({
|
|
|
668
758
|
setOpenThreadId(null);
|
|
669
759
|
}
|
|
670
760
|
});
|
|
671
|
-
|
|
761
|
+
const { clearFlushed } = useFeedtackFlush({
|
|
762
|
+
pathname,
|
|
763
|
+
feedbackItems,
|
|
764
|
+
onFlush,
|
|
765
|
+
flushIdleMs,
|
|
766
|
+
disabled
|
|
767
|
+
});
|
|
768
|
+
useEffect4(() => {
|
|
672
769
|
setLoading(true);
|
|
673
770
|
adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
674
771
|
}, [adapter, onError, pathname]);
|
|
@@ -716,19 +813,26 @@ function useFeedtackState({
|
|
|
716
813
|
body,
|
|
717
814
|
timestamp: ts
|
|
718
815
|
});
|
|
719
|
-
updateItem(feedbackId, (item) =>
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
816
|
+
updateItem(feedbackId, (item) => {
|
|
817
|
+
const updated = {
|
|
818
|
+
...item,
|
|
819
|
+
replies: [
|
|
820
|
+
...item.replies,
|
|
821
|
+
{
|
|
822
|
+
id: generateId(),
|
|
823
|
+
feedbackId,
|
|
824
|
+
author: currentUser,
|
|
825
|
+
body,
|
|
826
|
+
timestamp: ts
|
|
827
|
+
}
|
|
828
|
+
]
|
|
829
|
+
};
|
|
830
|
+
const triggerRescope = rescopeRoles ? rescopeRoles.includes(currentUser.role) : currentUser.role !== "agent";
|
|
831
|
+
if (triggerRescope && updated.resolutions.length === 0 && onFlush) {
|
|
832
|
+
clearFlushed(pathname);
|
|
833
|
+
}
|
|
834
|
+
return updated;
|
|
835
|
+
});
|
|
732
836
|
setReplyBody("");
|
|
733
837
|
} catch (err) {
|
|
734
838
|
onError?.(err);
|
|
@@ -768,6 +872,9 @@ function useFeedtackState({
|
|
|
768
872
|
onError?.(err);
|
|
769
873
|
}
|
|
770
874
|
};
|
|
875
|
+
const isArchivedForUser = (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id);
|
|
876
|
+
const hasUnread = (item) => item.replies.length > 0;
|
|
877
|
+
const hasValidPins = (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0;
|
|
771
878
|
return {
|
|
772
879
|
...pinMode,
|
|
773
880
|
isPinModeActive: pinMode.isActive,
|
|
@@ -791,9 +898,9 @@ function useFeedtackState({
|
|
|
791
898
|
handleReply,
|
|
792
899
|
handleResolve,
|
|
793
900
|
handleArchive,
|
|
794
|
-
isArchivedForUser
|
|
795
|
-
hasUnread
|
|
796
|
-
hasValidPins
|
|
901
|
+
isArchivedForUser,
|
|
902
|
+
hasUnread,
|
|
903
|
+
hasValidPins
|
|
797
904
|
};
|
|
798
905
|
}
|
|
799
906
|
|
|
@@ -810,7 +917,10 @@ function FeedtackProvider({
|
|
|
810
917
|
sentimentLabels = {},
|
|
811
918
|
onError,
|
|
812
919
|
disabled = false,
|
|
813
|
-
renderPinIcon
|
|
920
|
+
renderPinIcon,
|
|
921
|
+
onFlush,
|
|
922
|
+
flushIdleMs,
|
|
923
|
+
rescopeRoles
|
|
814
924
|
}) {
|
|
815
925
|
const state = useFeedtackState({
|
|
816
926
|
adapter,
|
|
@@ -818,7 +928,10 @@ function FeedtackProvider({
|
|
|
818
928
|
hotkey,
|
|
819
929
|
theme,
|
|
820
930
|
onError,
|
|
821
|
-
disabled
|
|
931
|
+
disabled,
|
|
932
|
+
onFlush,
|
|
933
|
+
flushIdleMs,
|
|
934
|
+
rescopeRoles
|
|
822
935
|
});
|
|
823
936
|
const firstPin = state.pendingPins[0];
|
|
824
937
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|