feedtack 0.0.3 → 0.1.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/{chunk-NOFOQJHM.js → chunk-XVWG3PLK.js} +74 -55
- package/dist/index.d.ts +25 -8
- package/dist/index.js +66 -1
- package/dist/react/index.d.ts +2 -12
- package/dist/react/index.js +510 -223
- package/dist/{theme-CHGvGcG5.d.ts → theme-C_JZCVVA.d.ts} +5 -1
- package/package.json +18 -3
|
@@ -1,21 +1,58 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
// src/capture/meta.ts
|
|
2
|
+
function getViewportMeta() {
|
|
3
|
+
return {
|
|
4
|
+
width: window.innerWidth,
|
|
5
|
+
height: window.innerHeight,
|
|
6
|
+
scrollX: window.scrollX,
|
|
7
|
+
scrollY: window.scrollY,
|
|
8
|
+
devicePixelRatio: window.devicePixelRatio
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function getPageMeta() {
|
|
12
|
+
return {
|
|
13
|
+
url: window.location.href,
|
|
14
|
+
pathname: window.location.pathname,
|
|
15
|
+
title: document.title
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getDeviceMeta() {
|
|
19
|
+
return {
|
|
20
|
+
userAgent: navigator.userAgent,
|
|
21
|
+
platform: navigator.platform,
|
|
22
|
+
touchEnabled: navigator.maxTouchPoints > 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function getPinCoords(event) {
|
|
26
|
+
const x = event.clientX + window.scrollX;
|
|
27
|
+
const y = event.clientY + window.scrollY;
|
|
28
|
+
const docWidth = document.documentElement.scrollWidth;
|
|
29
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
30
|
+
return {
|
|
31
|
+
x,
|
|
32
|
+
y,
|
|
33
|
+
xPct: Number((x / docWidth * 100).toFixed(2)),
|
|
34
|
+
yPct: Number((y / docHeight * 100).toFixed(2))
|
|
35
|
+
};
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
// src/capture/target.ts
|
|
39
|
+
function getElementPath(element) {
|
|
40
|
+
const parts = [];
|
|
41
|
+
let current = element;
|
|
42
|
+
while (current && current !== document.body) {
|
|
43
|
+
const tag = current.tagName.toLowerCase();
|
|
44
|
+
const classes = Array.from(current.classList).join(".");
|
|
45
|
+
let part = classes ? `${tag}.${classes}` : tag;
|
|
46
|
+
if (current !== element && current.hasAttribute("data-testid")) {
|
|
47
|
+
part = `[data-testid="${current.getAttribute("data-testid")}"]`;
|
|
48
|
+
parts.unshift(part);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
parts.unshift(part);
|
|
52
|
+
current = current.parentElement;
|
|
53
|
+
}
|
|
54
|
+
return parts.join(" > ");
|
|
55
|
+
}
|
|
19
56
|
function getCSSSelector(element) {
|
|
20
57
|
const parts = [];
|
|
21
58
|
let current = element;
|
|
@@ -59,6 +96,8 @@ function getTargetMeta(element) {
|
|
|
59
96
|
return {
|
|
60
97
|
selector,
|
|
61
98
|
best_effort,
|
|
99
|
+
testId: testId ?? null,
|
|
100
|
+
elementPath: testId ? null : getElementPath(element),
|
|
62
101
|
tagName: element.tagName,
|
|
63
102
|
textContent: (element.textContent ?? "").trim().slice(0, 200),
|
|
64
103
|
attributes: attrs,
|
|
@@ -71,50 +110,30 @@ function getTargetMeta(element) {
|
|
|
71
110
|
};
|
|
72
111
|
}
|
|
73
112
|
|
|
74
|
-
// src/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
function getDeviceMeta() {
|
|
92
|
-
return {
|
|
93
|
-
userAgent: navigator.userAgent,
|
|
94
|
-
platform: navigator.platform,
|
|
95
|
-
touchEnabled: navigator.maxTouchPoints > 0
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function getPinCoords(event) {
|
|
99
|
-
const x = event.clientX + window.scrollX;
|
|
100
|
-
const y = event.clientY + window.scrollY;
|
|
101
|
-
const docWidth = document.documentElement.scrollWidth;
|
|
102
|
-
const docHeight = document.documentElement.scrollHeight;
|
|
103
|
-
return {
|
|
104
|
-
x,
|
|
105
|
-
y,
|
|
106
|
-
xPct: Number((x / docWidth * 100).toFixed(2)),
|
|
107
|
-
yPct: Number((y / docHeight * 100).toFixed(2))
|
|
108
|
-
};
|
|
113
|
+
// src/types/payload.ts
|
|
114
|
+
var SCHEMA_VERSION = "1.0.0";
|
|
115
|
+
|
|
116
|
+
// src/types/theme.ts
|
|
117
|
+
function themeToCSS(theme) {
|
|
118
|
+
const map = {};
|
|
119
|
+
if (theme.primary) map["--ft-primary"] = theme.primary;
|
|
120
|
+
if (theme.background) map["--ft-bg"] = theme.background;
|
|
121
|
+
if (theme.surface) map["--ft-surface"] = theme.surface;
|
|
122
|
+
if (theme.text) map["--ft-text"] = theme.text;
|
|
123
|
+
if (theme.textMuted) map["--ft-text-muted"] = theme.textMuted;
|
|
124
|
+
if (theme.border) map["--ft-border"] = theme.border;
|
|
125
|
+
if (theme.radius) map["--ft-radius"] = theme.radius;
|
|
126
|
+
if (theme.badge) map["--ft-badge"] = theme.badge;
|
|
127
|
+
return map;
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
export {
|
|
112
|
-
SCHEMA_VERSION,
|
|
113
|
-
themeToCSS,
|
|
114
|
-
getCSSSelector,
|
|
115
|
-
getTargetMeta,
|
|
116
131
|
getViewportMeta,
|
|
117
132
|
getPageMeta,
|
|
118
133
|
getDeviceMeta,
|
|
119
|
-
getPinCoords
|
|
134
|
+
getPinCoords,
|
|
135
|
+
getCSSSelector,
|
|
136
|
+
getTargetMeta,
|
|
137
|
+
SCHEMA_VERSION,
|
|
138
|
+
themeToCSS
|
|
120
139
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as
|
|
2
|
-
export { j as FeedtackArchive, k as FeedtackBoundingRect, l as FeedtackPin, m as FeedtackSentiment, n as FeedtackTheme, o as FeedtackUser, S as SCHEMA_VERSION, t as themeToCSS } from './theme-
|
|
1
|
+
import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget } from './theme-C_JZCVVA.js';
|
|
2
|
+
export { j as FeedtackArchive, k as FeedtackBoundingRect, l as FeedtackPin, m as FeedtackSentiment, n as FeedtackTheme, o as FeedtackUser, S as SCHEMA_VERSION, t as themeToCSS } from './theme-C_JZCVVA.js';
|
|
3
3
|
|
|
4
4
|
/** Development adapter — logs all operations to the browser console */
|
|
5
5
|
declare class ConsoleAdapter implements FeedtackAdapter {
|
|
@@ -10,6 +10,23 @@ declare class ConsoleAdapter implements FeedtackAdapter {
|
|
|
10
10
|
loadFeedback(_filter?: FeedtackFilter): Promise<FeedbackItem[]>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface LocalStorageAdapterConfig {
|
|
14
|
+
/** localStorage key. Default: 'feedtack' */
|
|
15
|
+
key?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Zero-infrastructure adapter — persists feedback to localStorage */
|
|
18
|
+
declare class LocalStorageAdapter implements FeedtackAdapter {
|
|
19
|
+
private key;
|
|
20
|
+
constructor(config?: LocalStorageAdapterConfig);
|
|
21
|
+
private read;
|
|
22
|
+
private write;
|
|
23
|
+
submit(payload: FeedtackPayload): Promise<void>;
|
|
24
|
+
reply(feedbackId: string, reply: Omit<FeedtackReply, 'id' | 'feedbackId'>): Promise<void>;
|
|
25
|
+
resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
|
|
26
|
+
archive(feedbackId: string, userId: string): Promise<void>;
|
|
27
|
+
loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
interface WebhookAdapterConfig {
|
|
14
31
|
/** URL to POST new feedback payloads to */
|
|
15
32
|
submitUrl: string;
|
|
@@ -30,11 +47,6 @@ declare class WebhookAdapter implements FeedtackAdapter {
|
|
|
30
47
|
loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
/** Build shortest unique CSS selector for an element */
|
|
34
|
-
declare function getCSSSelector(element: Element): string;
|
|
35
|
-
/** Capture DOM target metadata at the clicked element */
|
|
36
|
-
declare function getTargetMeta(element: Element): FeedtackPinTarget;
|
|
37
|
-
|
|
38
50
|
declare function getViewportMeta(): FeedtackViewportMeta;
|
|
39
51
|
declare function getPageMeta(): FeedtackPageMeta;
|
|
40
52
|
declare function getDeviceMeta(): FeedtackDeviceMeta;
|
|
@@ -45,4 +57,9 @@ declare function getPinCoords(event: MouseEvent): {
|
|
|
45
57
|
yPct: number;
|
|
46
58
|
};
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
/** Build shortest unique CSS selector for an element */
|
|
61
|
+
declare function getCSSSelector(element: Element): string;
|
|
62
|
+
/** Capture DOM target metadata at the clicked element */
|
|
63
|
+
declare function getTargetMeta(element: Element): FeedtackPinTarget;
|
|
64
|
+
|
|
65
|
+
export { ConsoleAdapter, FeedbackItem, FeedtackAdapter, FeedtackDeviceMeta, FeedtackFilter, FeedtackPageMeta, FeedtackPayload, FeedtackPinTarget, FeedtackReply, FeedtackResolution, FeedtackViewportMeta, LocalStorageAdapter, type LocalStorageAdapterConfig, WebhookAdapter, type WebhookAdapterConfig, getCSSSelector, getDeviceMeta, getPageMeta, getPinCoords, getTargetMeta, getViewportMeta };
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getTargetMeta,
|
|
8
8
|
getViewportMeta,
|
|
9
9
|
themeToCSS
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-XVWG3PLK.js";
|
|
11
11
|
|
|
12
12
|
// src/adapters/ConsoleAdapter.ts
|
|
13
13
|
var ConsoleAdapter = class {
|
|
@@ -29,6 +29,70 @@ var ConsoleAdapter = class {
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
// src/adapters/LocalStorageAdapter.ts
|
|
33
|
+
var LocalStorageAdapter = class {
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
this.key = config.key ?? "feedtack";
|
|
36
|
+
}
|
|
37
|
+
read() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = localStorage.getItem(this.key);
|
|
40
|
+
return raw ? JSON.parse(raw) : [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
write(items) {
|
|
46
|
+
localStorage.setItem(this.key, JSON.stringify(items));
|
|
47
|
+
}
|
|
48
|
+
async submit(payload) {
|
|
49
|
+
const items = this.read();
|
|
50
|
+
items.push({ payload, replies: [], resolutions: [], archives: [] });
|
|
51
|
+
this.write(items);
|
|
52
|
+
}
|
|
53
|
+
async reply(feedbackId, reply) {
|
|
54
|
+
const items = this.read();
|
|
55
|
+
const item = items.find((i) => i.payload.id === feedbackId);
|
|
56
|
+
if (!item) return;
|
|
57
|
+
item.replies.push({
|
|
58
|
+
id: `r_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
|
|
59
|
+
feedbackId,
|
|
60
|
+
...reply
|
|
61
|
+
});
|
|
62
|
+
this.write(items);
|
|
63
|
+
}
|
|
64
|
+
async resolve(feedbackId, resolution) {
|
|
65
|
+
const items = this.read();
|
|
66
|
+
const item = items.find((i) => i.payload.id === feedbackId);
|
|
67
|
+
if (!item) return;
|
|
68
|
+
item.resolutions.push({ feedbackId, ...resolution });
|
|
69
|
+
this.write(items);
|
|
70
|
+
}
|
|
71
|
+
async archive(feedbackId, userId) {
|
|
72
|
+
const items = this.read();
|
|
73
|
+
const item = items.find((i) => i.payload.id === feedbackId);
|
|
74
|
+
if (!item) return;
|
|
75
|
+
item.archives.push({
|
|
76
|
+
feedbackId,
|
|
77
|
+
archivedBy: { id: userId, name: "", role: "" },
|
|
78
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
79
|
+
});
|
|
80
|
+
this.write(items);
|
|
81
|
+
}
|
|
82
|
+
async loadFeedback(filter) {
|
|
83
|
+
const items = this.read();
|
|
84
|
+
if (!filter) return items;
|
|
85
|
+
return items.filter((item) => {
|
|
86
|
+
if (filter.pathname && item.payload.page.pathname !== filter.pathname)
|
|
87
|
+
return false;
|
|
88
|
+
if (filter.url && item.payload.page.url !== filter.url) return false;
|
|
89
|
+
if (filter.userId && item.payload.submittedBy.id !== filter.userId)
|
|
90
|
+
return false;
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
32
96
|
// src/adapters/WebhookAdapter.ts
|
|
33
97
|
var WebhookAdapter = class {
|
|
34
98
|
constructor(config) {
|
|
@@ -70,6 +134,7 @@ var WebhookAdapter = class {
|
|
|
70
134
|
};
|
|
71
135
|
export {
|
|
72
136
|
ConsoleAdapter,
|
|
137
|
+
LocalStorageAdapter,
|
|
73
138
|
SCHEMA_VERSION,
|
|
74
139
|
WebhookAdapter,
|
|
75
140
|
getCSSSelector,
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
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 } from '../theme-
|
|
3
|
+
import { F as FeedtackAdapter, o as FeedtackUser, n as FeedtackTheme } from '../theme-C_JZCVVA.js';
|
|
4
4
|
|
|
5
5
|
interface FeedtackClasses {
|
|
6
|
-
/** Class added to the activation button */
|
|
7
6
|
button?: string;
|
|
8
|
-
/** Class added to the comment form panel */
|
|
9
7
|
form?: string;
|
|
10
|
-
/** Class added to the thread/reply panel */
|
|
11
8
|
thread?: string;
|
|
12
|
-
/** Class added to the color picker row */
|
|
13
9
|
colorPicker?: string;
|
|
14
|
-
/** Class added to each pin marker */
|
|
15
10
|
pinMarker?: string;
|
|
16
11
|
}
|
|
17
12
|
interface FeedtackSentimentLabels {
|
|
@@ -22,19 +17,14 @@ interface FeedtackProviderProps {
|
|
|
22
17
|
children: React.ReactNode;
|
|
23
18
|
adapter: FeedtackAdapter;
|
|
24
19
|
currentUser: FeedtackUser;
|
|
25
|
-
/** Keyboard shortcut to toggle pin mode. Default: 'p' (Shift+P) */
|
|
26
20
|
hotkey?: string;
|
|
27
|
-
/** Only show the activation button for users whose role is in this list */
|
|
28
21
|
adminOnly?: boolean;
|
|
29
|
-
/** CSS token overrides for brand alignment */
|
|
30
22
|
theme?: FeedtackTheme;
|
|
31
|
-
/** Additional class names for individual feedtack elements */
|
|
32
23
|
classes?: FeedtackClasses;
|
|
33
|
-
/** Custom labels/content for sentiment toggle buttons */
|
|
34
24
|
sentimentLabels?: FeedtackSentimentLabels;
|
|
35
25
|
onError?: (err: Error) => void;
|
|
36
26
|
}
|
|
37
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
27
|
+
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
38
28
|
|
|
39
29
|
interface FeedtackContextValue {
|
|
40
30
|
activatePinMode: () => void;
|
package/dist/react/index.js
CHANGED
|
@@ -6,20 +6,157 @@ import {
|
|
|
6
6
|
getTargetMeta,
|
|
7
7
|
getViewportMeta,
|
|
8
8
|
themeToCSS
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-XVWG3PLK.js";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
11
|
+
// src/ui/colors.ts
|
|
12
|
+
var PIN_PALETTE = [
|
|
13
|
+
"#ef4444",
|
|
14
|
+
// red
|
|
15
|
+
"#3b82f6",
|
|
16
|
+
// blue
|
|
17
|
+
"#22c55e",
|
|
18
|
+
// green
|
|
19
|
+
"#f59e0b",
|
|
20
|
+
// amber
|
|
21
|
+
"#a855f7",
|
|
22
|
+
// purple
|
|
23
|
+
"#ec4899"
|
|
24
|
+
// pink
|
|
25
|
+
];
|
|
13
26
|
|
|
14
27
|
// src/react/context.ts
|
|
15
28
|
import { createContext, useContext } from "react";
|
|
16
29
|
var FeedtackContext = createContext(null);
|
|
17
30
|
function useFeedtackContext() {
|
|
18
31
|
const ctx = useContext(FeedtackContext);
|
|
19
|
-
if (!ctx)
|
|
32
|
+
if (!ctx)
|
|
33
|
+
throw new Error("useFeedtack must be used inside <FeedtackProvider>");
|
|
20
34
|
return ctx;
|
|
21
35
|
}
|
|
22
36
|
|
|
37
|
+
// src/react/utils.ts
|
|
38
|
+
function generateId() {
|
|
39
|
+
return `ft_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
40
|
+
}
|
|
41
|
+
function getAnchoredPosition(x, y) {
|
|
42
|
+
const FORM_HEIGHT = 220;
|
|
43
|
+
const EDGE = 300;
|
|
44
|
+
const vw = window.innerWidth;
|
|
45
|
+
const vh = window.innerHeight;
|
|
46
|
+
const clientX = x - window.scrollX;
|
|
47
|
+
const clientY = y - window.scrollY;
|
|
48
|
+
const left = clientX > vw - EDGE ? void 0 : clientX + 16;
|
|
49
|
+
const right = clientX > vw - EDGE ? vw - clientX + 16 : void 0;
|
|
50
|
+
const top = clientY > vh - EDGE ? void 0 : clientY + 16;
|
|
51
|
+
const bottom = clientY > vh - EDGE ? vh - clientY + FORM_HEIGHT : void 0;
|
|
52
|
+
return { left, right, top, bottom };
|
|
53
|
+
}
|
|
54
|
+
function cx(...parts) {
|
|
55
|
+
return parts.filter(Boolean).join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/react/ThreadPanel.tsx
|
|
59
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
60
|
+
function ThreadPanel({
|
|
61
|
+
item,
|
|
62
|
+
replyBody,
|
|
63
|
+
onReplyBodyChange,
|
|
64
|
+
onReply,
|
|
65
|
+
onResolve,
|
|
66
|
+
onArchive,
|
|
67
|
+
onClose,
|
|
68
|
+
className
|
|
69
|
+
}) {
|
|
70
|
+
const pin = item.payload.pins[0];
|
|
71
|
+
const pos = getAnchoredPosition(pin.x, pin.y);
|
|
72
|
+
return /* @__PURE__ */ jsxs(
|
|
73
|
+
"div",
|
|
74
|
+
{
|
|
75
|
+
className: cx("feedtack-thread", className),
|
|
76
|
+
style: { position: "fixed", ...pos },
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
|
|
79
|
+
/* @__PURE__ */ jsx("p", { style: { fontSize: 13 }, children: item.payload.comment }),
|
|
80
|
+
item.replies.map((r) => /* @__PURE__ */ jsxs(
|
|
81
|
+
"div",
|
|
82
|
+
{
|
|
83
|
+
style: { borderTop: "1px solid #f3f4f6", paddingTop: 8 },
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
|
|
86
|
+
/* @__PURE__ */ jsx("p", { style: { fontSize: 12 }, children: r.body })
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
r.id
|
|
90
|
+
)),
|
|
91
|
+
/* @__PURE__ */ jsx(
|
|
92
|
+
"textarea",
|
|
93
|
+
{
|
|
94
|
+
placeholder: "Reply\u2026",
|
|
95
|
+
value: replyBody,
|
|
96
|
+
onChange: (e) => onReplyBodyChange(e.target.value),
|
|
97
|
+
style: {
|
|
98
|
+
width: "100%",
|
|
99
|
+
fontSize: 12,
|
|
100
|
+
padding: 6,
|
|
101
|
+
borderRadius: 6,
|
|
102
|
+
border: "1px solid #e5e7eb",
|
|
103
|
+
marginTop: 4
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
|
|
108
|
+
/* @__PURE__ */ jsx(
|
|
109
|
+
"button",
|
|
110
|
+
{
|
|
111
|
+
type: "button",
|
|
112
|
+
className: "feedtack-btn-submit",
|
|
113
|
+
style: { fontSize: 12, padding: "4px 10px" },
|
|
114
|
+
onClick: onReply,
|
|
115
|
+
children: "Reply"
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/* @__PURE__ */ jsx(
|
|
119
|
+
"button",
|
|
120
|
+
{
|
|
121
|
+
type: "button",
|
|
122
|
+
className: "feedtack-btn-cancel",
|
|
123
|
+
style: { fontSize: 12 },
|
|
124
|
+
onClick: onResolve,
|
|
125
|
+
children: "Mark Resolved"
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
type: "button",
|
|
132
|
+
className: "feedtack-btn-cancel",
|
|
133
|
+
style: { fontSize: 12 },
|
|
134
|
+
onClick: onArchive,
|
|
135
|
+
children: "Archive"
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
"button",
|
|
140
|
+
{
|
|
141
|
+
type: "button",
|
|
142
|
+
className: "feedtack-btn-cancel",
|
|
143
|
+
style: { fontSize: 12 },
|
|
144
|
+
onClick: onClose,
|
|
145
|
+
children: "Close"
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
] })
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/react/useFeedtackState.ts
|
|
155
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
|
|
156
|
+
|
|
157
|
+
// src/react/useFeedtackDom.ts
|
|
158
|
+
import { useEffect, useRef } from "react";
|
|
159
|
+
|
|
23
160
|
// src/ui/styles.ts
|
|
24
161
|
var FEEDTACK_DEFAULT_TOKENS = `
|
|
25
162
|
#feedtack-root {
|
|
@@ -82,7 +219,8 @@ var FEEDTACK_STYLES = `
|
|
|
82
219
|
width: 24px;
|
|
83
220
|
height: 24px;
|
|
84
221
|
border-radius: 50% 50% 50% 0;
|
|
85
|
-
transform:
|
|
222
|
+
transform: translate(-50%, -100%) rotate(-45deg);
|
|
223
|
+
transform-origin: bottom center;
|
|
86
224
|
border: 2px solid rgba(255,255,255,0.8);
|
|
87
225
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
88
226
|
cursor: pointer;
|
|
@@ -246,54 +384,8 @@ var FEEDTACK_STYLES = `
|
|
|
246
384
|
}
|
|
247
385
|
`;
|
|
248
386
|
|
|
249
|
-
// src/
|
|
250
|
-
|
|
251
|
-
"#ef4444",
|
|
252
|
-
// red
|
|
253
|
-
"#3b82f6",
|
|
254
|
-
// blue
|
|
255
|
-
"#22c55e",
|
|
256
|
-
// green
|
|
257
|
-
"#f59e0b",
|
|
258
|
-
// amber
|
|
259
|
-
"#a855f7",
|
|
260
|
-
// purple
|
|
261
|
-
"#ec4899"
|
|
262
|
-
// pink
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
// src/react/FeedtackProvider.tsx
|
|
266
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
267
|
-
function generateId() {
|
|
268
|
-
return `ft_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
269
|
-
}
|
|
270
|
-
function getAnchoredPosition(x, y) {
|
|
271
|
-
const FORM_WIDTH = 290;
|
|
272
|
-
const FORM_HEIGHT = 220;
|
|
273
|
-
const EDGE = 300;
|
|
274
|
-
const vw = window.innerWidth;
|
|
275
|
-
const vh = window.innerHeight;
|
|
276
|
-
const clientX = x - window.scrollX;
|
|
277
|
-
const clientY = y - window.scrollY;
|
|
278
|
-
const left = clientX > vw - EDGE ? void 0 : clientX + 16;
|
|
279
|
-
const right = clientX > vw - EDGE ? vw - clientX + 16 : void 0;
|
|
280
|
-
const top = clientY > vh - EDGE ? void 0 : clientY + 16;
|
|
281
|
-
const bottom = clientY > vh - EDGE ? vh - clientY + FORM_HEIGHT : void 0;
|
|
282
|
-
return { left, right, top, bottom };
|
|
283
|
-
}
|
|
284
|
-
function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminOnly = false, theme, classes = {}, sentimentLabels = {}, onError }) {
|
|
285
|
-
const [isPinModeActive, setIsPinModeActive] = useState(false);
|
|
286
|
-
const [pendingPins, setPendingPins] = useState([]);
|
|
287
|
-
const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
|
|
288
|
-
const [showForm, setShowForm] = useState(false);
|
|
289
|
-
const [comment, setComment] = useState("");
|
|
290
|
-
const [sentiment, setSentiment] = useState(null);
|
|
291
|
-
const [commentError, setCommentError] = useState(false);
|
|
292
|
-
const [submitting, setSubmitting] = useState(false);
|
|
293
|
-
const [feedbackItems, setFeedbackItems] = useState([]);
|
|
294
|
-
const [loading, setLoading] = useState(true);
|
|
295
|
-
const [openThreadId, setOpenThreadId] = useState(null);
|
|
296
|
-
const [replyBody, setReplyBody] = useState("");
|
|
387
|
+
// src/react/useFeedtackDom.ts
|
|
388
|
+
function useFeedtackDom(theme) {
|
|
297
389
|
const rootRef = useRef(null);
|
|
298
390
|
useEffect(() => {
|
|
299
391
|
if (document.getElementById("feedtack-styles")) return;
|
|
@@ -318,61 +410,146 @@ function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminO
|
|
|
318
410
|
const root = document.getElementById("feedtack-root");
|
|
319
411
|
if (!root || !theme) return;
|
|
320
412
|
const tokens = themeToCSS(theme);
|
|
321
|
-
|
|
413
|
+
for (const [k, v] of Object.entries(tokens)) {
|
|
414
|
+
root.style.setProperty(k, v);
|
|
415
|
+
}
|
|
322
416
|
}, [theme]);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
417
|
+
return rootRef;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/react/usePinMode.ts
|
|
421
|
+
import { useCallback, useEffect as useEffect2, useState } from "react";
|
|
422
|
+
function usePinMode({ hotkey, onDeactivate }) {
|
|
423
|
+
const [isActive, setIsActive] = useState(false);
|
|
424
|
+
const [pendingPins, setPendingPins] = useState([]);
|
|
425
|
+
const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
|
|
426
|
+
const [showForm, setShowForm] = useState(false);
|
|
427
|
+
const activate = useCallback(() => setIsActive(true), []);
|
|
428
|
+
const deactivate = useCallback(() => {
|
|
429
|
+
setIsActive(false);
|
|
330
430
|
setPendingPins([]);
|
|
331
431
|
setShowForm(false);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
useEffect(() => {
|
|
337
|
-
if (isPinModeActive) {
|
|
432
|
+
onDeactivate?.();
|
|
433
|
+
}, [onDeactivate]);
|
|
434
|
+
useEffect2(() => {
|
|
435
|
+
if (isActive) {
|
|
338
436
|
document.documentElement.classList.add("feedtack-crosshair");
|
|
339
437
|
} else {
|
|
340
438
|
document.documentElement.classList.remove("feedtack-crosshair");
|
|
341
439
|
}
|
|
342
440
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
343
|
-
}, [
|
|
344
|
-
|
|
441
|
+
}, [isActive]);
|
|
442
|
+
useEffect2(() => {
|
|
345
443
|
const handler = (e) => {
|
|
346
444
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
347
|
-
|
|
445
|
+
setIsActive((prev) => !prev);
|
|
348
446
|
}
|
|
349
447
|
if (e.key === "Escape") {
|
|
350
|
-
|
|
351
|
-
|
|
448
|
+
deactivate();
|
|
449
|
+
}
|
|
450
|
+
if (isActive && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
setSelectedColor((prev) => {
|
|
453
|
+
const idx = PIN_PALETTE.indexOf(prev);
|
|
454
|
+
const dir = e.key === "ArrowRight" ? 1 : -1;
|
|
455
|
+
return PIN_PALETTE[(idx + dir + PIN_PALETTE.length) % PIN_PALETTE.length];
|
|
456
|
+
});
|
|
352
457
|
}
|
|
353
458
|
};
|
|
354
459
|
window.addEventListener("keydown", handler);
|
|
355
460
|
return () => window.removeEventListener("keydown", handler);
|
|
356
|
-
}, [hotkey,
|
|
357
|
-
const handlePageClick = useCallback(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
461
|
+
}, [hotkey, deactivate, isActive]);
|
|
462
|
+
const handlePageClick = useCallback(
|
|
463
|
+
(e) => {
|
|
464
|
+
if (!isActive) return;
|
|
465
|
+
const target = e.target;
|
|
466
|
+
if (target.closest("#feedtack-root, .feedtack-form, .feedtack-color-picker"))
|
|
467
|
+
return;
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
e.stopPropagation();
|
|
470
|
+
setPendingPins((prev) => [
|
|
471
|
+
...prev,
|
|
472
|
+
{
|
|
473
|
+
color: selectedColor,
|
|
474
|
+
...getPinCoords(e),
|
|
475
|
+
target: getTargetMeta(target)
|
|
476
|
+
}
|
|
477
|
+
]);
|
|
478
|
+
setShowForm(true);
|
|
479
|
+
},
|
|
480
|
+
[isActive, selectedColor]
|
|
481
|
+
);
|
|
482
|
+
useEffect2(() => {
|
|
373
483
|
document.addEventListener("click", handlePageClick, true);
|
|
374
484
|
return () => document.removeEventListener("click", handlePageClick, true);
|
|
375
485
|
}, [handlePageClick]);
|
|
486
|
+
return {
|
|
487
|
+
isActive,
|
|
488
|
+
activate,
|
|
489
|
+
deactivate,
|
|
490
|
+
pendingPins,
|
|
491
|
+
selectedColor,
|
|
492
|
+
setSelectedColor,
|
|
493
|
+
showForm
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/react/useFeedtackState.ts
|
|
498
|
+
function useFeedtackState({
|
|
499
|
+
adapter,
|
|
500
|
+
currentUser,
|
|
501
|
+
hotkey,
|
|
502
|
+
theme,
|
|
503
|
+
onError
|
|
504
|
+
}) {
|
|
505
|
+
useFeedtackDom(theme);
|
|
506
|
+
const [pathname, setPathname] = useState2(() => window.location.pathname);
|
|
507
|
+
useEffect3(() => {
|
|
508
|
+
const update = () => setPathname(window.location.pathname);
|
|
509
|
+
const origPush = history.pushState.bind(history);
|
|
510
|
+
const origReplace = history.replaceState.bind(history);
|
|
511
|
+
history.pushState = (...args) => {
|
|
512
|
+
origPush(...args);
|
|
513
|
+
update();
|
|
514
|
+
};
|
|
515
|
+
history.replaceState = (...args) => {
|
|
516
|
+
origReplace(...args);
|
|
517
|
+
update();
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener("popstate", update);
|
|
520
|
+
return () => {
|
|
521
|
+
window.removeEventListener("popstate", update);
|
|
522
|
+
history.pushState = origPush;
|
|
523
|
+
history.replaceState = origReplace;
|
|
524
|
+
};
|
|
525
|
+
}, []);
|
|
526
|
+
const [comment, setComment] = useState2("");
|
|
527
|
+
const [sentiment, setSentiment] = useState2(null);
|
|
528
|
+
const [commentError, setCommentError] = useState2(false);
|
|
529
|
+
const [submitting, setSubmitting] = useState2(false);
|
|
530
|
+
const [feedbackItems, setFeedbackItems] = useState2([]);
|
|
531
|
+
const [loading, setLoading] = useState2(true);
|
|
532
|
+
const [openThreadId, setOpenThreadId] = useState2(null);
|
|
533
|
+
const [replyBody, setReplyBody] = useState2("");
|
|
534
|
+
const resetForm = useCallback2(() => {
|
|
535
|
+
setComment("");
|
|
536
|
+
setSentiment(null);
|
|
537
|
+
setCommentError(false);
|
|
538
|
+
}, []);
|
|
539
|
+
const pinMode = usePinMode({
|
|
540
|
+
hotkey,
|
|
541
|
+
onDeactivate: () => {
|
|
542
|
+
resetForm();
|
|
543
|
+
setOpenThreadId(null);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
useEffect3(() => {
|
|
547
|
+
setLoading(true);
|
|
548
|
+
adapter.loadFeedback({ pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
549
|
+
}, [adapter, onError, pathname]);
|
|
550
|
+
const updateItem = (id, fn) => setFeedbackItems(
|
|
551
|
+
(prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
|
|
552
|
+
);
|
|
376
553
|
const handleSubmit = async () => {
|
|
377
554
|
if (!comment.trim()) {
|
|
378
555
|
setCommentError(true);
|
|
@@ -386,15 +563,18 @@ function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminO
|
|
|
386
563
|
submittedBy: currentUser,
|
|
387
564
|
comment: comment.trim(),
|
|
388
565
|
sentiment,
|
|
389
|
-
pins: pendingPins.map((p, i) => ({ ...p, index: i + 1 })),
|
|
566
|
+
pins: pinMode.pendingPins.map((p, i) => ({ ...p, index: i + 1 })),
|
|
390
567
|
page: getPageMeta(),
|
|
391
568
|
viewport: getViewportMeta(),
|
|
392
569
|
device: getDeviceMeta()
|
|
393
570
|
};
|
|
394
571
|
try {
|
|
395
572
|
await adapter.submit(payload);
|
|
396
|
-
setFeedbackItems((prev) => [
|
|
397
|
-
|
|
573
|
+
setFeedbackItems((prev) => [
|
|
574
|
+
...prev,
|
|
575
|
+
{ payload, replies: [], resolutions: [], archives: [] }
|
|
576
|
+
]);
|
|
577
|
+
pinMode.deactivate();
|
|
398
578
|
} catch (err) {
|
|
399
579
|
onError?.(err);
|
|
400
580
|
} finally {
|
|
@@ -403,173 +583,280 @@ function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminO
|
|
|
403
583
|
};
|
|
404
584
|
const handleReply = async (feedbackId) => {
|
|
405
585
|
if (!replyBody.trim()) return;
|
|
586
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
|
+
const body = replyBody.trim();
|
|
406
588
|
try {
|
|
407
589
|
await adapter.reply(feedbackId, {
|
|
408
590
|
author: currentUser,
|
|
409
|
-
body
|
|
410
|
-
timestamp:
|
|
591
|
+
body,
|
|
592
|
+
timestamp: ts
|
|
411
593
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
594
|
+
updateItem(feedbackId, (item) => ({
|
|
595
|
+
...item,
|
|
596
|
+
replies: [
|
|
597
|
+
...item.replies,
|
|
598
|
+
{
|
|
599
|
+
id: generateId(),
|
|
600
|
+
feedbackId,
|
|
601
|
+
author: currentUser,
|
|
602
|
+
body,
|
|
603
|
+
timestamp: ts
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
}));
|
|
415
607
|
setReplyBody("");
|
|
416
608
|
} catch (err) {
|
|
417
609
|
onError?.(err);
|
|
418
610
|
}
|
|
419
611
|
};
|
|
420
612
|
const handleResolve = async (feedbackId) => {
|
|
613
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
421
614
|
try {
|
|
422
|
-
await adapter.resolve(feedbackId, {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
615
|
+
await adapter.resolve(feedbackId, {
|
|
616
|
+
resolvedBy: currentUser,
|
|
617
|
+
timestamp: ts
|
|
618
|
+
});
|
|
619
|
+
updateItem(feedbackId, (item) => ({
|
|
620
|
+
...item,
|
|
621
|
+
resolutions: [
|
|
622
|
+
...item.resolutions,
|
|
623
|
+
{ feedbackId, resolvedBy: currentUser, timestamp: ts }
|
|
624
|
+
]
|
|
625
|
+
}));
|
|
426
626
|
} catch (err) {
|
|
427
627
|
onError?.(err);
|
|
428
628
|
}
|
|
429
629
|
};
|
|
430
630
|
const handleArchive = async (feedbackId) => {
|
|
631
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
431
632
|
try {
|
|
432
633
|
await adapter.archive(feedbackId, currentUser.id);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
634
|
+
updateItem(feedbackId, (item) => ({
|
|
635
|
+
...item,
|
|
636
|
+
archives: [
|
|
637
|
+
...item.archives,
|
|
638
|
+
{ feedbackId, archivedBy: currentUser, timestamp: ts }
|
|
639
|
+
]
|
|
640
|
+
}));
|
|
436
641
|
setOpenThreadId(null);
|
|
437
642
|
} catch (err) {
|
|
438
643
|
onError?.(err);
|
|
439
644
|
}
|
|
440
645
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
646
|
+
return {
|
|
647
|
+
...pinMode,
|
|
648
|
+
isPinModeActive: pinMode.isActive,
|
|
649
|
+
activatePinMode: pinMode.activate,
|
|
650
|
+
deactivatePinMode: pinMode.deactivate,
|
|
651
|
+
comment,
|
|
652
|
+
setComment,
|
|
653
|
+
sentiment,
|
|
654
|
+
setSentiment,
|
|
655
|
+
commentError,
|
|
656
|
+
setCommentError,
|
|
657
|
+
submitting,
|
|
658
|
+
pathname,
|
|
659
|
+
feedbackItems,
|
|
660
|
+
loading,
|
|
661
|
+
openThreadId,
|
|
662
|
+
setOpenThreadId,
|
|
663
|
+
replyBody,
|
|
664
|
+
setReplyBody,
|
|
665
|
+
handleSubmit,
|
|
666
|
+
handleReply,
|
|
667
|
+
handleResolve,
|
|
668
|
+
handleArchive,
|
|
669
|
+
isArchivedForUser: (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id),
|
|
670
|
+
hasUnread: (item) => item.replies.length > 0
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/react/FeedtackProvider.tsx
|
|
675
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
676
|
+
function FeedtackProvider({
|
|
677
|
+
children,
|
|
678
|
+
adapter,
|
|
679
|
+
currentUser,
|
|
680
|
+
hotkey = "p",
|
|
681
|
+
adminOnly = false,
|
|
682
|
+
theme,
|
|
683
|
+
classes = {},
|
|
684
|
+
sentimentLabels = {},
|
|
685
|
+
onError
|
|
686
|
+
}) {
|
|
687
|
+
const state = useFeedtackState({
|
|
688
|
+
adapter,
|
|
689
|
+
currentUser,
|
|
690
|
+
hotkey,
|
|
691
|
+
theme,
|
|
692
|
+
onError
|
|
693
|
+
});
|
|
694
|
+
const firstPin = state.pendingPins[0];
|
|
444
695
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|
|
445
696
|
const showButton = !adminOnly || currentUser.role === "admin";
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
children: [
|
|
455
|
-
"Drop Pin [Shift+",
|
|
456
|
-
hotkey.toUpperCase(),
|
|
457
|
-
"]"
|
|
458
|
-
]
|
|
459
|
-
}
|
|
460
|
-
),
|
|
461
|
-
isPinModeActive && /* @__PURE__ */ jsx("div", { className: `feedtack-color-picker${classes.colorPicker ? ` ${classes.colorPicker}` : ""}`, children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx(
|
|
462
|
-
"button",
|
|
463
|
-
{
|
|
464
|
-
className: `feedtack-color-swatch${selectedColor === color ? " selected" : ""}`,
|
|
465
|
-
style: { background: color },
|
|
466
|
-
onClick: () => setSelectedColor(color),
|
|
467
|
-
title: color
|
|
468
|
-
},
|
|
469
|
-
color
|
|
470
|
-
)) }),
|
|
471
|
-
pendingPins.map((pin, i) => /* @__PURE__ */ jsx(
|
|
472
|
-
"div",
|
|
473
|
-
{
|
|
474
|
-
className: `feedtack-pin-marker${classes.pinMarker ? ` ${classes.pinMarker}` : ""}`,
|
|
475
|
-
style: {
|
|
476
|
-
background: pin.color,
|
|
477
|
-
left: pin.x,
|
|
478
|
-
top: pin.y,
|
|
479
|
-
position: "absolute"
|
|
480
|
-
}
|
|
697
|
+
const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
|
|
698
|
+
return /* @__PURE__ */ jsxs2(
|
|
699
|
+
FeedtackContext.Provider,
|
|
700
|
+
{
|
|
701
|
+
value: {
|
|
702
|
+
activatePinMode: state.activatePinMode,
|
|
703
|
+
deactivatePinMode: state.deactivatePinMode,
|
|
704
|
+
isPinModeActive: state.isPinModeActive
|
|
481
705
|
},
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
/* @__PURE__ */ jsx(
|
|
486
|
-
"textarea",
|
|
487
|
-
{
|
|
488
|
-
className: commentError ? "error" : "",
|
|
489
|
-
placeholder: "What's the issue? (required)",
|
|
490
|
-
value: comment,
|
|
491
|
-
onChange: (e) => {
|
|
492
|
-
setComment(e.target.value);
|
|
493
|
-
setCommentError(false);
|
|
494
|
-
},
|
|
495
|
-
autoFocus: true
|
|
496
|
-
}
|
|
497
|
-
),
|
|
498
|
-
commentError && /* @__PURE__ */ jsx("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
499
|
-
/* @__PURE__ */ jsxs("div", { className: "feedtack-sentiment", children: [
|
|
500
|
-
/* @__PURE__ */ jsx(
|
|
706
|
+
children: [
|
|
707
|
+
children,
|
|
708
|
+
showButton && /* @__PURE__ */ jsxs2(
|
|
501
709
|
"button",
|
|
502
710
|
{
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
711
|
+
type: "button",
|
|
712
|
+
className: cx(
|
|
713
|
+
"feedtack-btn",
|
|
714
|
+
state.isPinModeActive && "active",
|
|
715
|
+
classes.button
|
|
716
|
+
),
|
|
717
|
+
onClick: () => state.isPinModeActive ? state.deactivatePinMode() : state.activatePinMode(),
|
|
718
|
+
title: "Toggle feedback pin mode",
|
|
719
|
+
children: [
|
|
720
|
+
"Drop Pin [Shift+",
|
|
721
|
+
hotkey.toUpperCase(),
|
|
722
|
+
"]"
|
|
723
|
+
]
|
|
506
724
|
}
|
|
507
725
|
),
|
|
508
|
-
/* @__PURE__ */
|
|
726
|
+
state.isPinModeActive && /* @__PURE__ */ jsx2("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx2(
|
|
509
727
|
"button",
|
|
510
728
|
{
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
/* @__PURE__ */ jsx("button", { className: "feedtack-btn-submit", onClick: handleSubmit, disabled: submitting, children: submitting ? "Sending\u2026" : "Submit" })
|
|
520
|
-
] })
|
|
521
|
-
] }),
|
|
522
|
-
!loading && feedbackItems.filter((item) => !isArchivedForUser(item)).map((item) => {
|
|
523
|
-
const firstItemPin = item.payload.pins[0];
|
|
524
|
-
const unread = hasUnread(item);
|
|
525
|
-
return /* @__PURE__ */ jsx(
|
|
526
|
-
"div",
|
|
527
|
-
{
|
|
528
|
-
className: `feedtack-pin-marker${classes.pinMarker ? ` ${classes.pinMarker}` : ""}`,
|
|
529
|
-
style: {
|
|
530
|
-
background: firstItemPin.color,
|
|
531
|
-
left: firstItemPin.x,
|
|
532
|
-
top: firstItemPin.y,
|
|
533
|
-
position: "absolute",
|
|
534
|
-
cursor: "pointer"
|
|
729
|
+
type: "button",
|
|
730
|
+
className: cx(
|
|
731
|
+
"feedtack-color-swatch",
|
|
732
|
+
state.selectedColor === color && "selected"
|
|
733
|
+
),
|
|
734
|
+
style: { background: color },
|
|
735
|
+
onClick: () => state.setSelectedColor(color),
|
|
736
|
+
title: color
|
|
535
737
|
},
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
);
|
|
541
|
-
}),
|
|
542
|
-
openThreadId && (() => {
|
|
543
|
-
const item = feedbackItems.find((i) => i.payload.id === openThreadId);
|
|
544
|
-
if (!item) return null;
|
|
545
|
-
const pin = item.payload.pins[0];
|
|
546
|
-
const pos = getAnchoredPosition(pin.x, pin.y);
|
|
547
|
-
return /* @__PURE__ */ jsxs("div", { className: `feedtack-thread${classes.thread ? ` ${classes.thread}` : ""}`, style: { position: "fixed", ...pos }, children: [
|
|
548
|
-
/* @__PURE__ */ jsx("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
|
|
549
|
-
/* @__PURE__ */ jsx("p", { style: { fontSize: 13 }, children: item.payload.comment }),
|
|
550
|
-
item.replies.map((r) => /* @__PURE__ */ jsxs("div", { style: { borderTop: "1px solid #f3f4f6", paddingTop: 8 }, children: [
|
|
551
|
-
/* @__PURE__ */ jsx("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
|
|
552
|
-
/* @__PURE__ */ jsx("p", { style: { fontSize: 12 }, children: r.body })
|
|
553
|
-
] }, r.id)),
|
|
554
|
-
/* @__PURE__ */ jsx(
|
|
555
|
-
"textarea",
|
|
738
|
+
color
|
|
739
|
+
)) }),
|
|
740
|
+
state.pendingPins.map((pin) => /* @__PURE__ */ jsx2(
|
|
741
|
+
"div",
|
|
556
742
|
{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
743
|
+
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
744
|
+
style: {
|
|
745
|
+
background: pin.color,
|
|
746
|
+
left: pin.x,
|
|
747
|
+
top: pin.y,
|
|
748
|
+
position: "absolute"
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
`${pin.x}-${pin.y}-${pin.color}`
|
|
752
|
+
)),
|
|
753
|
+
state.showForm && /* @__PURE__ */ jsxs2(
|
|
754
|
+
"div",
|
|
755
|
+
{
|
|
756
|
+
className: cx("feedtack-form", classes.form),
|
|
757
|
+
style: { position: "fixed", ...formPos },
|
|
758
|
+
children: [
|
|
759
|
+
/* @__PURE__ */ jsx2(
|
|
760
|
+
"textarea",
|
|
761
|
+
{
|
|
762
|
+
className: state.commentError ? "error" : "",
|
|
763
|
+
placeholder: "What's the issue? (required)",
|
|
764
|
+
value: state.comment,
|
|
765
|
+
onChange: (e) => {
|
|
766
|
+
state.setComment(e.target.value);
|
|
767
|
+
state.setCommentError(false);
|
|
768
|
+
},
|
|
769
|
+
ref: (el) => el?.focus()
|
|
770
|
+
}
|
|
771
|
+
),
|
|
772
|
+
state.commentError && /* @__PURE__ */ jsx2("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
773
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-sentiment", children: [
|
|
774
|
+
/* @__PURE__ */ jsx2(
|
|
775
|
+
"button",
|
|
776
|
+
{
|
|
777
|
+
type: "button",
|
|
778
|
+
className: state.sentiment === "satisfied" ? "selected" : "",
|
|
779
|
+
onClick: () => state.setSentiment(
|
|
780
|
+
state.sentiment === "satisfied" ? null : "satisfied"
|
|
781
|
+
),
|
|
782
|
+
children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
/* @__PURE__ */ jsx2(
|
|
786
|
+
"button",
|
|
787
|
+
{
|
|
788
|
+
type: "button",
|
|
789
|
+
className: state.sentiment === "dissatisfied" ? "selected" : "",
|
|
790
|
+
onClick: () => state.setSentiment(
|
|
791
|
+
state.sentiment === "dissatisfied" ? null : "dissatisfied"
|
|
792
|
+
),
|
|
793
|
+
children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
] }),
|
|
797
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-form-actions", children: [
|
|
798
|
+
/* @__PURE__ */ jsx2(
|
|
799
|
+
"button",
|
|
800
|
+
{
|
|
801
|
+
type: "button",
|
|
802
|
+
className: "feedtack-btn-cancel",
|
|
803
|
+
onClick: state.deactivatePinMode,
|
|
804
|
+
children: "Cancel"
|
|
805
|
+
}
|
|
806
|
+
),
|
|
807
|
+
/* @__PURE__ */ jsx2(
|
|
808
|
+
"button",
|
|
809
|
+
{
|
|
810
|
+
type: "button",
|
|
811
|
+
className: "feedtack-btn-submit",
|
|
812
|
+
onClick: state.handleSubmit,
|
|
813
|
+
disabled: state.submitting,
|
|
814
|
+
children: state.submitting ? "Sending\u2026" : "Submit"
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
] })
|
|
818
|
+
]
|
|
561
819
|
}
|
|
562
820
|
),
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
/* @__PURE__ */
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
821
|
+
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).map((item) => {
|
|
822
|
+
const pin = item.payload.pins[0];
|
|
823
|
+
return /* @__PURE__ */ jsx2(
|
|
824
|
+
"button",
|
|
825
|
+
{
|
|
826
|
+
type: "button",
|
|
827
|
+
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
828
|
+
style: {
|
|
829
|
+
background: pin.color,
|
|
830
|
+
left: pin.x,
|
|
831
|
+
top: pin.y,
|
|
832
|
+
position: "absolute",
|
|
833
|
+
cursor: "pointer"
|
|
834
|
+
},
|
|
835
|
+
onClick: () => state.setOpenThreadId(
|
|
836
|
+
state.openThreadId === item.payload.id ? null : item.payload.id
|
|
837
|
+
),
|
|
838
|
+
children: state.hasUnread(item) && /* @__PURE__ */ jsx2("div", { className: "feedtack-pin-badge" })
|
|
839
|
+
},
|
|
840
|
+
item.payload.id
|
|
841
|
+
);
|
|
842
|
+
}),
|
|
843
|
+
openItem && /* @__PURE__ */ jsx2(
|
|
844
|
+
ThreadPanel,
|
|
845
|
+
{
|
|
846
|
+
item: openItem,
|
|
847
|
+
replyBody: state.replyBody,
|
|
848
|
+
onReplyBodyChange: state.setReplyBody,
|
|
849
|
+
onReply: () => state.handleReply(openItem.payload.id),
|
|
850
|
+
onResolve: () => state.handleResolve(openItem.payload.id),
|
|
851
|
+
onArchive: () => state.handleArchive(openItem.payload.id),
|
|
852
|
+
onClose: () => state.setOpenThreadId(null),
|
|
853
|
+
className: classes.thread
|
|
854
|
+
}
|
|
855
|
+
),
|
|
856
|
+
state.loading && /* @__PURE__ */ jsx2("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
);
|
|
573
860
|
}
|
|
574
861
|
|
|
575
862
|
// src/react/useFeedtack.ts
|
|
@@ -22,6 +22,10 @@ interface FeedtackPinTarget {
|
|
|
22
22
|
selector: string;
|
|
23
23
|
/** True when no stable selector was found — downstream consumers should not rely on selector for automated targeting */
|
|
24
24
|
best_effort: boolean;
|
|
25
|
+
/** data-testid attribute value if present, null otherwise — always shipped for downstream consumers */
|
|
26
|
+
testId: string | null;
|
|
27
|
+
/** Readable DOM ancestry: "div.hero > div.card > button.btn.btn-primary". Walks up to body or nearest data-testid ancestor. Null when element itself has a data-testid (testId is sufficient). */
|
|
28
|
+
elementPath: string | null;
|
|
25
29
|
tagName: string;
|
|
26
30
|
/** Trimmed text content of the element, max 200 chars */
|
|
27
31
|
textContent: string;
|
|
@@ -143,4 +147,4 @@ interface FeedtackTheme {
|
|
|
143
147
|
/** Maps FeedtackTheme fields to CSS custom properties on #feedtack-root */
|
|
144
148
|
declare function themeToCSS(theme: FeedtackTheme): Record<string, string>;
|
|
145
149
|
|
|
146
|
-
export { type FeedtackAdapter as F, SCHEMA_VERSION as S, type FeedtackPayload as a, type FeedtackReply as b, type FeedtackResolution as c, type FeedtackFilter as d, type FeedbackItem as e, type
|
|
150
|
+
export { type FeedtackAdapter as F, SCHEMA_VERSION as S, type FeedtackPayload as a, type FeedtackReply as b, type FeedtackResolution as c, type FeedtackFilter as d, type FeedbackItem as e, type FeedtackDeviceMeta as f, type FeedtackPageMeta as g, type FeedtackViewportMeta as h, type FeedtackPinTarget as i, type FeedtackArchive as j, type FeedtackBoundingRect as k, type FeedtackPin as l, type FeedtackSentiment as m, type FeedtackTheme as n, type FeedtackUser as o, themeToCSS as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedtack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Click anywhere. Drop a pin. Get a payload a developer can act on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,14 +28,21 @@
|
|
|
28
28
|
"dev": "tsup --watch",
|
|
29
29
|
"test": "vitest run",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "
|
|
32
|
-
"
|
|
31
|
+
"lint": "biome check src/",
|
|
32
|
+
"lint:fix": "biome check --fix src/",
|
|
33
|
+
"release": "release-it",
|
|
34
|
+
"prepublishOnly": "pnpm test && pnpm build",
|
|
35
|
+
"prepare": "husky"
|
|
33
36
|
},
|
|
34
37
|
"peerDependencies": {
|
|
35
38
|
"react": ">=18.0.0",
|
|
36
39
|
"react-dom": ">=18.0.0"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
42
|
+
"@biomejs/biome": "^2.4.11",
|
|
43
|
+
"@commitlint/cli": "^20.5.0",
|
|
44
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
45
|
+
"@release-it/conventional-changelog": "^10.0.6",
|
|
39
46
|
"@testing-library/jest-dom": "^6.4.0",
|
|
40
47
|
"@testing-library/react": "^16.0.0",
|
|
41
48
|
"@types/node": "^22.0.0",
|
|
@@ -43,10 +50,18 @@
|
|
|
43
50
|
"@types/react-dom": "^18.3.0",
|
|
44
51
|
"@vitejs/plugin-react": "^4.3.0",
|
|
45
52
|
"eslint": "^9.0.0",
|
|
53
|
+
"husky": "^9.1.7",
|
|
46
54
|
"jsdom": "^25.0.0",
|
|
55
|
+
"lint-staged": "^16.4.0",
|
|
56
|
+
"release-it": "^19.2.4",
|
|
47
57
|
"tsup": "^8.5.1",
|
|
48
58
|
"typescript": "^5.5.0",
|
|
49
59
|
"typescript-eslint": "^8.0.0",
|
|
50
60
|
"vitest": "^2.0.0"
|
|
61
|
+
},
|
|
62
|
+
"lint-staged": {
|
|
63
|
+
"*.{ts,tsx}": [
|
|
64
|
+
"biome check --fix --no-errors-on-unmatched"
|
|
65
|
+
]
|
|
51
66
|
}
|
|
52
67
|
}
|