feedtack 0.0.2 → 0.1.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/{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 +8 -12
- package/dist/react/index.js +492 -226
- 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,34 +1,30 @@
|
|
|
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
|
}
|
|
12
|
+
interface FeedtackSentimentLabels {
|
|
13
|
+
satisfied?: React.ReactNode;
|
|
14
|
+
dissatisfied?: React.ReactNode;
|
|
15
|
+
}
|
|
17
16
|
interface FeedtackProviderProps {
|
|
18
17
|
children: React.ReactNode;
|
|
19
18
|
adapter: FeedtackAdapter;
|
|
20
19
|
currentUser: FeedtackUser;
|
|
21
|
-
/** Keyboard shortcut to toggle pin mode. Default: 'p' (Shift+P) */
|
|
22
20
|
hotkey?: string;
|
|
23
|
-
/** Only show the activation button for users whose role is in this list */
|
|
24
21
|
adminOnly?: boolean;
|
|
25
|
-
/** CSS token overrides for brand alignment */
|
|
26
22
|
theme?: FeedtackTheme;
|
|
27
|
-
/** Additional class names for individual feedtack elements */
|
|
28
23
|
classes?: FeedtackClasses;
|
|
24
|
+
sentimentLabels?: FeedtackSentimentLabels;
|
|
29
25
|
onError?: (err: Error) => void;
|
|
30
26
|
}
|
|
31
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, 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;
|
|
32
28
|
|
|
33
29
|
interface FeedtackContextValue {
|
|
34
30
|
activatePinMode: () => void;
|
|
@@ -39,4 +35,4 @@ interface FeedtackContextValue {
|
|
|
39
35
|
/** Hook for host app to programmatically control feedtack */
|
|
40
36
|
declare function useFeedtack(): FeedtackContextValue;
|
|
41
37
|
|
|
42
|
-
export { type FeedtackClasses, FeedtackProvider, type FeedtackProviderProps, useFeedtack };
|
|
38
|
+
export { type FeedtackClasses, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, useFeedtack };
|
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;
|
|
@@ -104,7 +242,7 @@ var FEEDTACK_STYLES = `
|
|
|
104
242
|
display: flex;
|
|
105
243
|
gap: 6px;
|
|
106
244
|
padding: 8px;
|
|
107
|
-
background: var(--ft-bg);
|
|
245
|
+
background: var(--ft-bg) !important;
|
|
108
246
|
border-radius: var(--ft-radius);
|
|
109
247
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
110
248
|
position: fixed;
|
|
@@ -130,7 +268,7 @@ var FEEDTACK_STYLES = `
|
|
|
130
268
|
.feedtack-form {
|
|
131
269
|
position: absolute;
|
|
132
270
|
z-index: 2147483642;
|
|
133
|
-
background: var(--ft-bg);
|
|
271
|
+
background: var(--ft-bg) !important;
|
|
134
272
|
border-radius: calc(var(--ft-radius) + 2px);
|
|
135
273
|
box-shadow: 0 4px 20px rgba(0,0,0,0.18);
|
|
136
274
|
padding: 16px;
|
|
@@ -224,7 +362,7 @@ var FEEDTACK_STYLES = `
|
|
|
224
362
|
.feedtack-thread {
|
|
225
363
|
position: absolute;
|
|
226
364
|
z-index: 2147483642;
|
|
227
|
-
background: var(--ft-bg);
|
|
365
|
+
background: var(--ft-bg) !important;
|
|
228
366
|
border-radius: calc(var(--ft-radius) + 2px);
|
|
229
367
|
box-shadow: 0 4px 20px rgba(0,0,0,0.18);
|
|
230
368
|
padding: 16px;
|
|
@@ -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 = {}, 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,126 @@ 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 [comment, setComment] = useState2("");
|
|
507
|
+
const [sentiment, setSentiment] = useState2(null);
|
|
508
|
+
const [commentError, setCommentError] = useState2(false);
|
|
509
|
+
const [submitting, setSubmitting] = useState2(false);
|
|
510
|
+
const [feedbackItems, setFeedbackItems] = useState2([]);
|
|
511
|
+
const [loading, setLoading] = useState2(true);
|
|
512
|
+
const [openThreadId, setOpenThreadId] = useState2(null);
|
|
513
|
+
const [replyBody, setReplyBody] = useState2("");
|
|
514
|
+
const resetForm = useCallback2(() => {
|
|
515
|
+
setComment("");
|
|
516
|
+
setSentiment(null);
|
|
517
|
+
setCommentError(false);
|
|
518
|
+
}, []);
|
|
519
|
+
const pinMode = usePinMode({
|
|
520
|
+
hotkey,
|
|
521
|
+
onDeactivate: () => {
|
|
522
|
+
resetForm();
|
|
523
|
+
setOpenThreadId(null);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
useEffect3(() => {
|
|
527
|
+
setLoading(true);
|
|
528
|
+
adapter.loadFeedback({ pathname: window.location.pathname }).then(setFeedbackItems).catch((err) => onError?.(err)).finally(() => setLoading(false));
|
|
529
|
+
}, [adapter, onError]);
|
|
530
|
+
const updateItem = (id, fn) => setFeedbackItems(
|
|
531
|
+
(prev) => prev.map((i) => i.payload.id === id ? fn(i) : i)
|
|
532
|
+
);
|
|
376
533
|
const handleSubmit = async () => {
|
|
377
534
|
if (!comment.trim()) {
|
|
378
535
|
setCommentError(true);
|
|
@@ -386,15 +543,18 @@ function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminO
|
|
|
386
543
|
submittedBy: currentUser,
|
|
387
544
|
comment: comment.trim(),
|
|
388
545
|
sentiment,
|
|
389
|
-
pins: pendingPins.map((p, i) => ({ ...p, index: i + 1 })),
|
|
546
|
+
pins: pinMode.pendingPins.map((p, i) => ({ ...p, index: i + 1 })),
|
|
390
547
|
page: getPageMeta(),
|
|
391
548
|
viewport: getViewportMeta(),
|
|
392
549
|
device: getDeviceMeta()
|
|
393
550
|
};
|
|
394
551
|
try {
|
|
395
552
|
await adapter.submit(payload);
|
|
396
|
-
setFeedbackItems((prev) => [
|
|
397
|
-
|
|
553
|
+
setFeedbackItems((prev) => [
|
|
554
|
+
...prev,
|
|
555
|
+
{ payload, replies: [], resolutions: [], archives: [] }
|
|
556
|
+
]);
|
|
557
|
+
pinMode.deactivate();
|
|
398
558
|
} catch (err) {
|
|
399
559
|
onError?.(err);
|
|
400
560
|
} finally {
|
|
@@ -403,173 +563,279 @@ function FeedtackProvider({ children, adapter, currentUser, hotkey = "p", adminO
|
|
|
403
563
|
};
|
|
404
564
|
const handleReply = async (feedbackId) => {
|
|
405
565
|
if (!replyBody.trim()) return;
|
|
566
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
567
|
+
const body = replyBody.trim();
|
|
406
568
|
try {
|
|
407
569
|
await adapter.reply(feedbackId, {
|
|
408
570
|
author: currentUser,
|
|
409
|
-
body
|
|
410
|
-
timestamp:
|
|
571
|
+
body,
|
|
572
|
+
timestamp: ts
|
|
411
573
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
574
|
+
updateItem(feedbackId, (item) => ({
|
|
575
|
+
...item,
|
|
576
|
+
replies: [
|
|
577
|
+
...item.replies,
|
|
578
|
+
{
|
|
579
|
+
id: generateId(),
|
|
580
|
+
feedbackId,
|
|
581
|
+
author: currentUser,
|
|
582
|
+
body,
|
|
583
|
+
timestamp: ts
|
|
584
|
+
}
|
|
585
|
+
]
|
|
586
|
+
}));
|
|
415
587
|
setReplyBody("");
|
|
416
588
|
} catch (err) {
|
|
417
589
|
onError?.(err);
|
|
418
590
|
}
|
|
419
591
|
};
|
|
420
592
|
const handleResolve = async (feedbackId) => {
|
|
593
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
421
594
|
try {
|
|
422
|
-
await adapter.resolve(feedbackId, {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
595
|
+
await adapter.resolve(feedbackId, {
|
|
596
|
+
resolvedBy: currentUser,
|
|
597
|
+
timestamp: ts
|
|
598
|
+
});
|
|
599
|
+
updateItem(feedbackId, (item) => ({
|
|
600
|
+
...item,
|
|
601
|
+
resolutions: [
|
|
602
|
+
...item.resolutions,
|
|
603
|
+
{ feedbackId, resolvedBy: currentUser, timestamp: ts }
|
|
604
|
+
]
|
|
605
|
+
}));
|
|
426
606
|
} catch (err) {
|
|
427
607
|
onError?.(err);
|
|
428
608
|
}
|
|
429
609
|
};
|
|
430
610
|
const handleArchive = async (feedbackId) => {
|
|
611
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
431
612
|
try {
|
|
432
613
|
await adapter.archive(feedbackId, currentUser.id);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
614
|
+
updateItem(feedbackId, (item) => ({
|
|
615
|
+
...item,
|
|
616
|
+
archives: [
|
|
617
|
+
...item.archives,
|
|
618
|
+
{ feedbackId, archivedBy: currentUser, timestamp: ts }
|
|
619
|
+
]
|
|
620
|
+
}));
|
|
436
621
|
setOpenThreadId(null);
|
|
437
622
|
} catch (err) {
|
|
438
623
|
onError?.(err);
|
|
439
624
|
}
|
|
440
625
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
626
|
+
return {
|
|
627
|
+
...pinMode,
|
|
628
|
+
isPinModeActive: pinMode.isActive,
|
|
629
|
+
activatePinMode: pinMode.activate,
|
|
630
|
+
deactivatePinMode: pinMode.deactivate,
|
|
631
|
+
comment,
|
|
632
|
+
setComment,
|
|
633
|
+
sentiment,
|
|
634
|
+
setSentiment,
|
|
635
|
+
commentError,
|
|
636
|
+
setCommentError,
|
|
637
|
+
submitting,
|
|
638
|
+
feedbackItems,
|
|
639
|
+
loading,
|
|
640
|
+
openThreadId,
|
|
641
|
+
setOpenThreadId,
|
|
642
|
+
replyBody,
|
|
643
|
+
setReplyBody,
|
|
644
|
+
handleSubmit,
|
|
645
|
+
handleReply,
|
|
646
|
+
handleResolve,
|
|
647
|
+
handleArchive,
|
|
648
|
+
isArchivedForUser: (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id),
|
|
649
|
+
hasUnread: (item) => item.replies.length > 0
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/react/FeedtackProvider.tsx
|
|
654
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
655
|
+
function FeedtackProvider({
|
|
656
|
+
children,
|
|
657
|
+
adapter,
|
|
658
|
+
currentUser,
|
|
659
|
+
hotkey = "p",
|
|
660
|
+
adminOnly = false,
|
|
661
|
+
theme,
|
|
662
|
+
classes = {},
|
|
663
|
+
sentimentLabels = {},
|
|
664
|
+
onError
|
|
665
|
+
}) {
|
|
666
|
+
const state = useFeedtackState({
|
|
667
|
+
adapter,
|
|
668
|
+
currentUser,
|
|
669
|
+
hotkey,
|
|
670
|
+
theme,
|
|
671
|
+
onError
|
|
672
|
+
});
|
|
673
|
+
const firstPin = state.pendingPins[0];
|
|
444
674
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|
|
445
675
|
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
|
|
676
|
+
const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
|
|
677
|
+
return /* @__PURE__ */ jsxs2(
|
|
678
|
+
FeedtackContext.Provider,
|
|
679
|
+
{
|
|
680
|
+
value: {
|
|
681
|
+
activatePinMode: state.activatePinMode,
|
|
682
|
+
deactivatePinMode: state.deactivatePinMode,
|
|
683
|
+
isPinModeActive: state.isPinModeActive
|
|
468
684
|
},
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
}
|
|
481
|
-
},
|
|
482
|
-
i
|
|
483
|
-
)),
|
|
484
|
-
showForm && /* @__PURE__ */ jsxs("div", { className: `feedtack-form${classes.form ? ` ${classes.form}` : ""}`, style: { position: "fixed", ...formPos }, children: [
|
|
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(
|
|
685
|
+
children: [
|
|
686
|
+
children,
|
|
687
|
+
showButton && /* @__PURE__ */ jsxs2(
|
|
501
688
|
"button",
|
|
502
689
|
{
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
690
|
+
type: "button",
|
|
691
|
+
className: cx(
|
|
692
|
+
"feedtack-btn",
|
|
693
|
+
state.isPinModeActive && "active",
|
|
694
|
+
classes.button
|
|
695
|
+
),
|
|
696
|
+
onClick: () => state.isPinModeActive ? state.deactivatePinMode() : state.activatePinMode(),
|
|
697
|
+
title: "Toggle feedback pin mode",
|
|
698
|
+
children: [
|
|
699
|
+
"Drop Pin [Shift+",
|
|
700
|
+
hotkey.toUpperCase(),
|
|
701
|
+
"]"
|
|
702
|
+
]
|
|
506
703
|
}
|
|
507
704
|
),
|
|
508
|
-
/* @__PURE__ */
|
|
705
|
+
state.isPinModeActive && /* @__PURE__ */ jsx2("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx2(
|
|
509
706
|
"button",
|
|
510
707
|
{
|
|
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"
|
|
708
|
+
type: "button",
|
|
709
|
+
className: cx(
|
|
710
|
+
"feedtack-color-swatch",
|
|
711
|
+
state.selectedColor === color && "selected"
|
|
712
|
+
),
|
|
713
|
+
style: { background: color },
|
|
714
|
+
onClick: () => state.setSelectedColor(color),
|
|
715
|
+
title: color
|
|
535
716
|
},
|
|
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",
|
|
717
|
+
color
|
|
718
|
+
)) }),
|
|
719
|
+
state.pendingPins.map((pin) => /* @__PURE__ */ jsx2(
|
|
720
|
+
"div",
|
|
556
721
|
{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
722
|
+
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
723
|
+
style: {
|
|
724
|
+
background: pin.color,
|
|
725
|
+
left: pin.x,
|
|
726
|
+
top: pin.y,
|
|
727
|
+
position: "absolute"
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
`${pin.x}-${pin.y}-${pin.color}`
|
|
731
|
+
)),
|
|
732
|
+
state.showForm && /* @__PURE__ */ jsxs2(
|
|
733
|
+
"div",
|
|
734
|
+
{
|
|
735
|
+
className: cx("feedtack-form", classes.form),
|
|
736
|
+
style: { position: "fixed", ...formPos },
|
|
737
|
+
children: [
|
|
738
|
+
/* @__PURE__ */ jsx2(
|
|
739
|
+
"textarea",
|
|
740
|
+
{
|
|
741
|
+
className: state.commentError ? "error" : "",
|
|
742
|
+
placeholder: "What's the issue? (required)",
|
|
743
|
+
value: state.comment,
|
|
744
|
+
onChange: (e) => {
|
|
745
|
+
state.setComment(e.target.value);
|
|
746
|
+
state.setCommentError(false);
|
|
747
|
+
},
|
|
748
|
+
ref: (el) => el?.focus()
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
state.commentError && /* @__PURE__ */ jsx2("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
752
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-sentiment", children: [
|
|
753
|
+
/* @__PURE__ */ jsx2(
|
|
754
|
+
"button",
|
|
755
|
+
{
|
|
756
|
+
type: "button",
|
|
757
|
+
className: state.sentiment === "satisfied" ? "selected" : "",
|
|
758
|
+
onClick: () => state.setSentiment(
|
|
759
|
+
state.sentiment === "satisfied" ? null : "satisfied"
|
|
760
|
+
),
|
|
761
|
+
children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
|
|
762
|
+
}
|
|
763
|
+
),
|
|
764
|
+
/* @__PURE__ */ jsx2(
|
|
765
|
+
"button",
|
|
766
|
+
{
|
|
767
|
+
type: "button",
|
|
768
|
+
className: state.sentiment === "dissatisfied" ? "selected" : "",
|
|
769
|
+
onClick: () => state.setSentiment(
|
|
770
|
+
state.sentiment === "dissatisfied" ? null : "dissatisfied"
|
|
771
|
+
),
|
|
772
|
+
children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
|
|
773
|
+
}
|
|
774
|
+
)
|
|
775
|
+
] }),
|
|
776
|
+
/* @__PURE__ */ jsxs2("div", { className: "feedtack-form-actions", children: [
|
|
777
|
+
/* @__PURE__ */ jsx2(
|
|
778
|
+
"button",
|
|
779
|
+
{
|
|
780
|
+
type: "button",
|
|
781
|
+
className: "feedtack-btn-cancel",
|
|
782
|
+
onClick: state.deactivatePinMode,
|
|
783
|
+
children: "Cancel"
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
/* @__PURE__ */ jsx2(
|
|
787
|
+
"button",
|
|
788
|
+
{
|
|
789
|
+
type: "button",
|
|
790
|
+
className: "feedtack-btn-submit",
|
|
791
|
+
onClick: state.handleSubmit,
|
|
792
|
+
disabled: state.submitting,
|
|
793
|
+
children: state.submitting ? "Sending\u2026" : "Submit"
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
] })
|
|
797
|
+
]
|
|
561
798
|
}
|
|
562
799
|
),
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
/* @__PURE__ */
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
800
|
+
!state.loading && state.feedbackItems.filter((item) => !state.isArchivedForUser(item)).map((item) => {
|
|
801
|
+
const pin = item.payload.pins[0];
|
|
802
|
+
return /* @__PURE__ */ jsx2(
|
|
803
|
+
"button",
|
|
804
|
+
{
|
|
805
|
+
type: "button",
|
|
806
|
+
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
807
|
+
style: {
|
|
808
|
+
background: pin.color,
|
|
809
|
+
left: pin.x,
|
|
810
|
+
top: pin.y,
|
|
811
|
+
position: "absolute",
|
|
812
|
+
cursor: "pointer"
|
|
813
|
+
},
|
|
814
|
+
onClick: () => state.setOpenThreadId(
|
|
815
|
+
state.openThreadId === item.payload.id ? null : item.payload.id
|
|
816
|
+
),
|
|
817
|
+
children: state.hasUnread(item) && /* @__PURE__ */ jsx2("div", { className: "feedtack-pin-badge" })
|
|
818
|
+
},
|
|
819
|
+
item.payload.id
|
|
820
|
+
);
|
|
821
|
+
}),
|
|
822
|
+
openItem && /* @__PURE__ */ jsx2(
|
|
823
|
+
ThreadPanel,
|
|
824
|
+
{
|
|
825
|
+
item: openItem,
|
|
826
|
+
replyBody: state.replyBody,
|
|
827
|
+
onReplyBodyChange: state.setReplyBody,
|
|
828
|
+
onReply: () => state.handleReply(openItem.payload.id),
|
|
829
|
+
onResolve: () => state.handleResolve(openItem.payload.id),
|
|
830
|
+
onArchive: () => state.handleArchive(openItem.payload.id),
|
|
831
|
+
onClose: () => state.setOpenThreadId(null),
|
|
832
|
+
className: classes.thread
|
|
833
|
+
}
|
|
834
|
+
),
|
|
835
|
+
state.loading && /* @__PURE__ */ jsx2("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
|
|
836
|
+
]
|
|
837
|
+
}
|
|
838
|
+
);
|
|
573
839
|
}
|
|
574
840
|
|
|
575
841
|
// 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.0
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
}
|