feedtack 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-NCW2V5JL.js +237 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +161 -117
- package/dist/{theme-C_JZCVVA.d.ts → theme-C-uctIoI.d.ts} +25 -8
- package/package.json +3 -2
- package/dist/chunk-XVWG3PLK.js +0 -139
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/capture/fiber.ts
|
|
39
|
+
var fiberKey;
|
|
40
|
+
function getFiberKey(element) {
|
|
41
|
+
if (fiberKey !== void 0) return fiberKey;
|
|
42
|
+
const key = Object.keys(element).find((k) => k.startsWith("__reactFiber$"));
|
|
43
|
+
fiberKey = key ?? null;
|
|
44
|
+
return fiberKey;
|
|
45
|
+
}
|
|
46
|
+
function getComponentName(element) {
|
|
47
|
+
try {
|
|
48
|
+
const key = getFiberKey(element);
|
|
49
|
+
if (!key) return null;
|
|
50
|
+
let fiber = element[key];
|
|
51
|
+
while (fiber) {
|
|
52
|
+
const type = fiber.type;
|
|
53
|
+
if (type && typeof type !== "string") {
|
|
54
|
+
const name = type.displayName ?? type.name;
|
|
55
|
+
if (name && name !== "Anonymous") return name;
|
|
56
|
+
}
|
|
57
|
+
fiber = fiber.return;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/capture/target.ts
|
|
66
|
+
var INTERACTIVE_SELECTOR = "button,a,input,select,textarea,label";
|
|
67
|
+
function resolveTarget(element) {
|
|
68
|
+
const promoted = element.closest(INTERACTIVE_SELECTOR);
|
|
69
|
+
return promoted ?? element;
|
|
70
|
+
}
|
|
71
|
+
function attr(el, name) {
|
|
72
|
+
return el.getAttribute(name);
|
|
73
|
+
}
|
|
74
|
+
function nthChild(el) {
|
|
75
|
+
let n = 1;
|
|
76
|
+
let sib = el.previousElementSibling;
|
|
77
|
+
while (sib) {
|
|
78
|
+
n++;
|
|
79
|
+
sib = sib.previousElementSibling;
|
|
80
|
+
}
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
83
|
+
function nthOfType(el) {
|
|
84
|
+
const tag = el.tagName;
|
|
85
|
+
let n = 1;
|
|
86
|
+
let sib = el.previousElementSibling;
|
|
87
|
+
while (sib) {
|
|
88
|
+
if (sib.tagName === tag) n++;
|
|
89
|
+
sib = sib.previousElementSibling;
|
|
90
|
+
}
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
93
|
+
function serializeNode(el) {
|
|
94
|
+
const id = attr(el, "id");
|
|
95
|
+
const dataTestId = attr(el, "data-testid") ?? attr(el, "data-test-id");
|
|
96
|
+
const dataFeedtackComponent = attr(el, "data-feedtack-component");
|
|
97
|
+
const hasStableId = !!(id || dataTestId);
|
|
98
|
+
return {
|
|
99
|
+
tag: el.tagName.toLowerCase(),
|
|
100
|
+
id,
|
|
101
|
+
ariaLabel: attr(el, "aria-label"),
|
|
102
|
+
role: attr(el, "role"),
|
|
103
|
+
type: attr(el, "type"),
|
|
104
|
+
name: attr(el, "name"),
|
|
105
|
+
title: attr(el, "title"),
|
|
106
|
+
alt: attr(el, "alt"),
|
|
107
|
+
dataTestId,
|
|
108
|
+
dataFeedtackComponent,
|
|
109
|
+
nthChild: hasStableId ? null : nthChild(el),
|
|
110
|
+
nthOfType: hasStableId ? null : nthOfType(el),
|
|
111
|
+
componentName: dataFeedtackComponent ?? getComponentName(el)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function getAncestorChain(element) {
|
|
115
|
+
const chain = [];
|
|
116
|
+
let current = element.parentElement;
|
|
117
|
+
while (current && current !== document.body && chain.length < 5) {
|
|
118
|
+
chain.push(serializeNode(current));
|
|
119
|
+
current = current.parentElement;
|
|
120
|
+
}
|
|
121
|
+
return chain;
|
|
122
|
+
}
|
|
123
|
+
function getCSSSelector(element) {
|
|
124
|
+
const parts = [];
|
|
125
|
+
let current = element;
|
|
126
|
+
while (current && current !== document.body) {
|
|
127
|
+
const id = current.getAttribute("id");
|
|
128
|
+
const testId = current.getAttribute("data-testid") ?? current.getAttribute("data-test-id");
|
|
129
|
+
const feedtackComponent = current.getAttribute("data-feedtack-component");
|
|
130
|
+
if (id) {
|
|
131
|
+
parts.unshift(`#${id}`);
|
|
132
|
+
break;
|
|
133
|
+
} else if (testId) {
|
|
134
|
+
parts.unshift(`[data-testid="${testId}"]`);
|
|
135
|
+
break;
|
|
136
|
+
} else if (feedtackComponent) {
|
|
137
|
+
parts.unshift(`[data-feedtack-component="${feedtackComponent}"]`);
|
|
138
|
+
break;
|
|
139
|
+
} else {
|
|
140
|
+
const tag = current.tagName.toLowerCase();
|
|
141
|
+
const parent = current.parentElement;
|
|
142
|
+
if (parent) {
|
|
143
|
+
const siblings = Array.from(parent.children).filter(
|
|
144
|
+
(c) => c.tagName === current.tagName
|
|
145
|
+
);
|
|
146
|
+
const index = siblings.indexOf(current) + 1;
|
|
147
|
+
parts.unshift(
|
|
148
|
+
siblings.length > 1 ? `${tag}:nth-of-type(${index})` : tag
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
parts.unshift(tag);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
current = current.parentElement;
|
|
155
|
+
}
|
|
156
|
+
return parts.join(" > ");
|
|
157
|
+
}
|
|
158
|
+
function deriveElementPath(target, ancestors) {
|
|
159
|
+
const dataTestId = target.getAttribute("data-testid") ?? target.getAttribute("data-test-id");
|
|
160
|
+
if (dataTestId) return null;
|
|
161
|
+
const targetPart = (() => {
|
|
162
|
+
const tag = target.tagName.toLowerCase();
|
|
163
|
+
const classes = Array.from(target.classList).join(".");
|
|
164
|
+
return classes ? `${tag}.${classes}` : tag;
|
|
165
|
+
})();
|
|
166
|
+
const ancestorParts = ancestors.map((a) => {
|
|
167
|
+
if (a.dataTestId) return `[data-testid="${a.dataTestId}"]`;
|
|
168
|
+
const classes = "";
|
|
169
|
+
return classes ? `${a.tag}.${classes}` : a.tag;
|
|
170
|
+
});
|
|
171
|
+
return [targetPart, ...ancestorParts].join(" > ");
|
|
172
|
+
}
|
|
173
|
+
function getTargetMeta(element) {
|
|
174
|
+
const resolved = resolveTarget(element);
|
|
175
|
+
const id = resolved.getAttribute("id");
|
|
176
|
+
const dataTestId = resolved.getAttribute("data-testid") ?? resolved.getAttribute("data-test-id");
|
|
177
|
+
const feedtackComponent = resolved.getAttribute("data-feedtack-component");
|
|
178
|
+
let selector;
|
|
179
|
+
let best_effort;
|
|
180
|
+
if (id) {
|
|
181
|
+
selector = `#${id}`;
|
|
182
|
+
best_effort = false;
|
|
183
|
+
} else if (dataTestId) {
|
|
184
|
+
selector = `[data-testid="${dataTestId}"]`;
|
|
185
|
+
best_effort = false;
|
|
186
|
+
} else if (feedtackComponent) {
|
|
187
|
+
selector = `[data-feedtack-component="${feedtackComponent}"]`;
|
|
188
|
+
best_effort = false;
|
|
189
|
+
} else {
|
|
190
|
+
selector = getCSSSelector(resolved);
|
|
191
|
+
best_effort = true;
|
|
192
|
+
}
|
|
193
|
+
const ancestors = getAncestorChain(resolved);
|
|
194
|
+
const rect = resolved.getBoundingClientRect();
|
|
195
|
+
return {
|
|
196
|
+
selector,
|
|
197
|
+
best_effort,
|
|
198
|
+
dataTestId,
|
|
199
|
+
elementPath: deriveElementPath(resolved, ancestors),
|
|
200
|
+
tagName: resolved.tagName,
|
|
201
|
+
ancestors,
|
|
202
|
+
boundingRect: {
|
|
203
|
+
x: rect.x,
|
|
204
|
+
y: rect.y,
|
|
205
|
+
width: rect.width,
|
|
206
|
+
height: rect.height
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/types/payload.ts
|
|
212
|
+
var SCHEMA_VERSION = "1.0.0";
|
|
213
|
+
|
|
214
|
+
// src/types/theme.ts
|
|
215
|
+
function themeToCSS(theme) {
|
|
216
|
+
const map = {};
|
|
217
|
+
if (theme.primary) map["--ft-primary"] = theme.primary;
|
|
218
|
+
if (theme.background) map["--ft-bg"] = theme.background;
|
|
219
|
+
if (theme.surface) map["--ft-surface"] = theme.surface;
|
|
220
|
+
if (theme.text) map["--ft-text"] = theme.text;
|
|
221
|
+
if (theme.textMuted) map["--ft-text-muted"] = theme.textMuted;
|
|
222
|
+
if (theme.border) map["--ft-border"] = theme.border;
|
|
223
|
+
if (theme.radius) map["--ft-radius"] = theme.radius;
|
|
224
|
+
if (theme.badge) map["--ft-badge"] = theme.badge;
|
|
225
|
+
return map;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
getViewportMeta,
|
|
230
|
+
getPageMeta,
|
|
231
|
+
getDeviceMeta,
|
|
232
|
+
getPinCoords,
|
|
233
|
+
getCSSSelector,
|
|
234
|
+
getTargetMeta,
|
|
235
|
+
SCHEMA_VERSION,
|
|
236
|
+
themeToCSS
|
|
237
|
+
};
|
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 FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget } from './theme-
|
|
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-uctIoI.js';
|
|
2
|
+
export { A as AncestorNode, 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-uctIoI.js';
|
|
3
3
|
|
|
4
4
|
/** Development adapter — logs all operations to the browser console */
|
|
5
5
|
declare class ConsoleAdapter implements FeedtackAdapter {
|
package/dist/index.js
CHANGED
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-uctIoI.js';
|
|
4
4
|
|
|
5
5
|
interface FeedtackClasses {
|
|
6
6
|
button?: string;
|
|
@@ -23,8 +23,9 @@ interface FeedtackProviderProps {
|
|
|
23
23
|
classes?: FeedtackClasses;
|
|
24
24
|
sentimentLabels?: FeedtackSentimentLabels;
|
|
25
25
|
onError?: (err: Error) => void;
|
|
26
|
+
disabled?: boolean;
|
|
26
27
|
}
|
|
27
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
|
+
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
28
29
|
|
|
29
30
|
interface FeedtackContextValue {
|
|
30
31
|
activatePinMode: () => void;
|
package/dist/react/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getTargetMeta,
|
|
7
7
|
getViewportMeta,
|
|
8
8
|
themeToCSS
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-NCW2V5JL.js";
|
|
10
10
|
|
|
11
11
|
// src/ui/colors.ts
|
|
12
12
|
var PIN_PALETTE = [
|
|
@@ -24,16 +24,6 @@ var PIN_PALETTE = [
|
|
|
24
24
|
// pink
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
-
// src/react/context.ts
|
|
28
|
-
import { createContext, useContext } from "react";
|
|
29
|
-
var FeedtackContext = createContext(null);
|
|
30
|
-
function useFeedtackContext() {
|
|
31
|
-
const ctx = useContext(FeedtackContext);
|
|
32
|
-
if (!ctx)
|
|
33
|
-
throw new Error("useFeedtack must be used inside <FeedtackProvider>");
|
|
34
|
-
return ctx;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
// src/react/utils.ts
|
|
38
28
|
function generateId() {
|
|
39
29
|
return `ft_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
@@ -55,8 +45,98 @@ function cx(...parts) {
|
|
|
55
45
|
return parts.filter(Boolean).join(" ");
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
// src/react/
|
|
48
|
+
// src/react/CommentForm.tsx
|
|
59
49
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
50
|
+
function CommentForm({
|
|
51
|
+
comment,
|
|
52
|
+
commentError,
|
|
53
|
+
sentiment,
|
|
54
|
+
submitting,
|
|
55
|
+
formPos,
|
|
56
|
+
classes,
|
|
57
|
+
sentimentLabels,
|
|
58
|
+
onCommentChange,
|
|
59
|
+
onSentimentChange,
|
|
60
|
+
onSubmit,
|
|
61
|
+
onCancel
|
|
62
|
+
}) {
|
|
63
|
+
return /* @__PURE__ */ jsxs(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
className: cx("feedtack-form", classes.form),
|
|
67
|
+
style: { position: "fixed", ...formPos },
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(
|
|
70
|
+
"textarea",
|
|
71
|
+
{
|
|
72
|
+
className: commentError ? "error" : "",
|
|
73
|
+
placeholder: "What's the issue? (required)",
|
|
74
|
+
value: comment,
|
|
75
|
+
onChange: (e) => onCommentChange(e.target.value),
|
|
76
|
+
ref: (el) => el?.focus()
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
commentError && /* @__PURE__ */ jsx("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
80
|
+
/* @__PURE__ */ jsxs("div", { className: "feedtack-sentiment", children: [
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"button",
|
|
83
|
+
{
|
|
84
|
+
type: "button",
|
|
85
|
+
className: sentiment === "satisfied" ? "selected" : "",
|
|
86
|
+
onClick: () => onSentimentChange(sentiment === "satisfied" ? null : "satisfied"),
|
|
87
|
+
children: sentimentLabels.satisfied ?? "\u{1F60A} Satisfied"
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
/* @__PURE__ */ jsx(
|
|
91
|
+
"button",
|
|
92
|
+
{
|
|
93
|
+
type: "button",
|
|
94
|
+
className: sentiment === "dissatisfied" ? "selected" : "",
|
|
95
|
+
onClick: () => onSentimentChange(
|
|
96
|
+
sentiment === "dissatisfied" ? null : "dissatisfied"
|
|
97
|
+
),
|
|
98
|
+
children: sentimentLabels.dissatisfied ?? "\u{1F61E} Dissatisfied"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] }),
|
|
102
|
+
/* @__PURE__ */ jsxs("div", { className: "feedtack-form-actions", children: [
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
"button",
|
|
105
|
+
{
|
|
106
|
+
type: "button",
|
|
107
|
+
className: "feedtack-btn-cancel",
|
|
108
|
+
onClick: onCancel,
|
|
109
|
+
children: "Cancel"
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
className: "feedtack-btn-submit",
|
|
117
|
+
onClick: onSubmit,
|
|
118
|
+
disabled: submitting,
|
|
119
|
+
children: submitting ? "Sending\u2026" : "Submit"
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
] })
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/react/context.ts
|
|
129
|
+
import { createContext, useContext } from "react";
|
|
130
|
+
var FeedtackContext = createContext(null);
|
|
131
|
+
function useFeedtackContext() {
|
|
132
|
+
const ctx = useContext(FeedtackContext);
|
|
133
|
+
if (!ctx)
|
|
134
|
+
throw new Error("useFeedtack must be used inside <FeedtackProvider>");
|
|
135
|
+
return ctx;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/react/ThreadPanel.tsx
|
|
139
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
60
140
|
function ThreadPanel({
|
|
61
141
|
item,
|
|
62
142
|
replyBody,
|
|
@@ -67,28 +147,29 @@ function ThreadPanel({
|
|
|
67
147
|
onClose,
|
|
68
148
|
className
|
|
69
149
|
}) {
|
|
70
|
-
const pin = item.payload
|
|
150
|
+
const pin = item.payload?.pins?.[0];
|
|
151
|
+
if (!pin) return null;
|
|
71
152
|
const pos = getAnchoredPosition(pin.x, pin.y);
|
|
72
|
-
return /* @__PURE__ */
|
|
153
|
+
return /* @__PURE__ */ jsxs2(
|
|
73
154
|
"div",
|
|
74
155
|
{
|
|
75
156
|
className: cx("feedtack-thread", className),
|
|
76
157
|
style: { position: "fixed", ...pos },
|
|
77
158
|
children: [
|
|
78
|
-
/* @__PURE__ */
|
|
79
|
-
/* @__PURE__ */
|
|
80
|
-
item.replies.map((r) => /* @__PURE__ */
|
|
159
|
+
/* @__PURE__ */ jsx2("strong", { style: { fontSize: 13 }, children: item.payload.submittedBy.name }),
|
|
160
|
+
/* @__PURE__ */ jsx2("p", { style: { fontSize: 13 }, children: item.payload.comment }),
|
|
161
|
+
item.replies.map((r) => /* @__PURE__ */ jsxs2(
|
|
81
162
|
"div",
|
|
82
163
|
{
|
|
83
164
|
style: { borderTop: "1px solid #f3f4f6", paddingTop: 8 },
|
|
84
165
|
children: [
|
|
85
|
-
/* @__PURE__ */
|
|
86
|
-
/* @__PURE__ */
|
|
166
|
+
/* @__PURE__ */ jsx2("span", { style: { fontSize: 12, fontWeight: 600 }, children: r.author.name }),
|
|
167
|
+
/* @__PURE__ */ jsx2("p", { style: { fontSize: 12 }, children: r.body })
|
|
87
168
|
]
|
|
88
169
|
},
|
|
89
170
|
r.id
|
|
90
171
|
)),
|
|
91
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ jsx2(
|
|
92
173
|
"textarea",
|
|
93
174
|
{
|
|
94
175
|
placeholder: "Reply\u2026",
|
|
@@ -104,8 +185,8 @@ function ThreadPanel({
|
|
|
104
185
|
}
|
|
105
186
|
}
|
|
106
187
|
),
|
|
107
|
-
/* @__PURE__ */
|
|
108
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
|
|
189
|
+
/* @__PURE__ */ jsx2(
|
|
109
190
|
"button",
|
|
110
191
|
{
|
|
111
192
|
type: "button",
|
|
@@ -115,7 +196,7 @@ function ThreadPanel({
|
|
|
115
196
|
children: "Reply"
|
|
116
197
|
}
|
|
117
198
|
),
|
|
118
|
-
/* @__PURE__ */
|
|
199
|
+
/* @__PURE__ */ jsx2(
|
|
119
200
|
"button",
|
|
120
201
|
{
|
|
121
202
|
type: "button",
|
|
@@ -125,7 +206,7 @@ function ThreadPanel({
|
|
|
125
206
|
children: "Mark Resolved"
|
|
126
207
|
}
|
|
127
208
|
),
|
|
128
|
-
/* @__PURE__ */
|
|
209
|
+
/* @__PURE__ */ jsx2(
|
|
129
210
|
"button",
|
|
130
211
|
{
|
|
131
212
|
type: "button",
|
|
@@ -135,7 +216,7 @@ function ThreadPanel({
|
|
|
135
216
|
children: "Archive"
|
|
136
217
|
}
|
|
137
218
|
),
|
|
138
|
-
/* @__PURE__ */
|
|
219
|
+
/* @__PURE__ */ jsx2(
|
|
139
220
|
"button",
|
|
140
221
|
{
|
|
141
222
|
type: "button",
|
|
@@ -385,9 +466,10 @@ var FEEDTACK_STYLES = `
|
|
|
385
466
|
`;
|
|
386
467
|
|
|
387
468
|
// src/react/useFeedtackDom.ts
|
|
388
|
-
function useFeedtackDom(theme) {
|
|
469
|
+
function useFeedtackDom(theme, disabled) {
|
|
389
470
|
const rootRef = useRef(null);
|
|
390
471
|
useEffect(() => {
|
|
472
|
+
if (disabled) return;
|
|
391
473
|
if (document.getElementById("feedtack-styles")) return;
|
|
392
474
|
const style = document.createElement("style");
|
|
393
475
|
style.id = "feedtack-styles";
|
|
@@ -396,8 +478,9 @@ function useFeedtackDom(theme) {
|
|
|
396
478
|
return () => {
|
|
397
479
|
style.remove();
|
|
398
480
|
};
|
|
399
|
-
}, []);
|
|
481
|
+
}, [disabled]);
|
|
400
482
|
useEffect(() => {
|
|
483
|
+
if (disabled) return;
|
|
401
484
|
const root = document.createElement("div");
|
|
402
485
|
root.id = "feedtack-root";
|
|
403
486
|
document.body.appendChild(root);
|
|
@@ -405,21 +488,22 @@ function useFeedtackDom(theme) {
|
|
|
405
488
|
return () => {
|
|
406
489
|
root.remove();
|
|
407
490
|
};
|
|
408
|
-
}, []);
|
|
491
|
+
}, [disabled]);
|
|
409
492
|
useEffect(() => {
|
|
493
|
+
if (disabled) return;
|
|
410
494
|
const root = document.getElementById("feedtack-root");
|
|
411
495
|
if (!root || !theme) return;
|
|
412
496
|
const tokens = themeToCSS(theme);
|
|
413
497
|
for (const [k, v] of Object.entries(tokens)) {
|
|
414
498
|
root.style.setProperty(k, v);
|
|
415
499
|
}
|
|
416
|
-
}, [theme]);
|
|
500
|
+
}, [theme, disabled]);
|
|
417
501
|
return rootRef;
|
|
418
502
|
}
|
|
419
503
|
|
|
420
504
|
// src/react/usePinMode.ts
|
|
421
505
|
import { useCallback, useEffect as useEffect2, useState } from "react";
|
|
422
|
-
function usePinMode({ hotkey, onDeactivate }) {
|
|
506
|
+
function usePinMode({ hotkey, onDeactivate, disabled }) {
|
|
423
507
|
const [isActive, setIsActive] = useState(false);
|
|
424
508
|
const [pendingPins, setPendingPins] = useState([]);
|
|
425
509
|
const [selectedColor, setSelectedColor] = useState(PIN_PALETTE[0]);
|
|
@@ -440,6 +524,7 @@ function usePinMode({ hotkey, onDeactivate }) {
|
|
|
440
524
|
return () => document.documentElement.classList.remove("feedtack-crosshair");
|
|
441
525
|
}, [isActive]);
|
|
442
526
|
useEffect2(() => {
|
|
527
|
+
if (disabled) return;
|
|
443
528
|
const handler = (e) => {
|
|
444
529
|
if (e.key === hotkey.toUpperCase() && e.shiftKey) {
|
|
445
530
|
setIsActive((prev) => !prev);
|
|
@@ -458,7 +543,7 @@ function usePinMode({ hotkey, onDeactivate }) {
|
|
|
458
543
|
};
|
|
459
544
|
window.addEventListener("keydown", handler);
|
|
460
545
|
return () => window.removeEventListener("keydown", handler);
|
|
461
|
-
}, [hotkey, deactivate, isActive]);
|
|
546
|
+
}, [hotkey, deactivate, isActive, disabled]);
|
|
462
547
|
const handlePageClick = useCallback(
|
|
463
548
|
(e) => {
|
|
464
549
|
if (!isActive) return;
|
|
@@ -480,9 +565,10 @@ function usePinMode({ hotkey, onDeactivate }) {
|
|
|
480
565
|
[isActive, selectedColor]
|
|
481
566
|
);
|
|
482
567
|
useEffect2(() => {
|
|
568
|
+
if (disabled) return;
|
|
483
569
|
document.addEventListener("click", handlePageClick, true);
|
|
484
570
|
return () => document.removeEventListener("click", handlePageClick, true);
|
|
485
|
-
}, [handlePageClick]);
|
|
571
|
+
}, [handlePageClick, disabled]);
|
|
486
572
|
return {
|
|
487
573
|
isActive,
|
|
488
574
|
activate,
|
|
@@ -500,9 +586,10 @@ function useFeedtackState({
|
|
|
500
586
|
currentUser,
|
|
501
587
|
hotkey,
|
|
502
588
|
theme,
|
|
503
|
-
onError
|
|
589
|
+
onError,
|
|
590
|
+
disabled
|
|
504
591
|
}) {
|
|
505
|
-
useFeedtackDom(theme);
|
|
592
|
+
useFeedtackDom(theme, disabled);
|
|
506
593
|
const [pathname, setPathname] = useState2(() => window.location.pathname);
|
|
507
594
|
useEffect3(() => {
|
|
508
595
|
const update = () => setPathname(window.location.pathname);
|
|
@@ -510,11 +597,11 @@ function useFeedtackState({
|
|
|
510
597
|
const origReplace = history.replaceState.bind(history);
|
|
511
598
|
history.pushState = (...args) => {
|
|
512
599
|
origPush(...args);
|
|
513
|
-
update
|
|
600
|
+
queueMicrotask(update);
|
|
514
601
|
};
|
|
515
602
|
history.replaceState = (...args) => {
|
|
516
603
|
origReplace(...args);
|
|
517
|
-
update
|
|
604
|
+
queueMicrotask(update);
|
|
518
605
|
};
|
|
519
606
|
window.addEventListener("popstate", update);
|
|
520
607
|
return () => {
|
|
@@ -538,6 +625,7 @@ function useFeedtackState({
|
|
|
538
625
|
}, []);
|
|
539
626
|
const pinMode = usePinMode({
|
|
540
627
|
hotkey,
|
|
628
|
+
disabled,
|
|
541
629
|
onDeactivate: () => {
|
|
542
630
|
resetForm();
|
|
543
631
|
setOpenThreadId(null);
|
|
@@ -667,12 +755,13 @@ function useFeedtackState({
|
|
|
667
755
|
handleResolve,
|
|
668
756
|
handleArchive,
|
|
669
757
|
isArchivedForUser: (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id),
|
|
670
|
-
hasUnread: (item) => item.replies.length > 0
|
|
758
|
+
hasUnread: (item) => item.replies.length > 0,
|
|
759
|
+
hasValidPins: (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0
|
|
671
760
|
};
|
|
672
761
|
}
|
|
673
762
|
|
|
674
763
|
// src/react/FeedtackProvider.tsx
|
|
675
|
-
import { jsx as
|
|
764
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
676
765
|
function FeedtackProvider({
|
|
677
766
|
children,
|
|
678
767
|
adapter,
|
|
@@ -682,30 +771,34 @@ function FeedtackProvider({
|
|
|
682
771
|
theme,
|
|
683
772
|
classes = {},
|
|
684
773
|
sentimentLabels = {},
|
|
685
|
-
onError
|
|
774
|
+
onError,
|
|
775
|
+
disabled = false
|
|
686
776
|
}) {
|
|
687
777
|
const state = useFeedtackState({
|
|
688
778
|
adapter,
|
|
689
779
|
currentUser,
|
|
690
780
|
hotkey,
|
|
691
781
|
theme,
|
|
692
|
-
onError
|
|
782
|
+
onError,
|
|
783
|
+
disabled
|
|
693
784
|
});
|
|
694
785
|
const firstPin = state.pendingPins[0];
|
|
695
786
|
const formPos = firstPin ? getAnchoredPosition(firstPin.x, firstPin.y) : {};
|
|
696
787
|
const showButton = !adminOnly || currentUser.role === "admin";
|
|
697
788
|
const openItem = state.openThreadId ? state.feedbackItems.find((i) => i.payload.id === state.openThreadId) : null;
|
|
698
|
-
return /* @__PURE__ */
|
|
789
|
+
return /* @__PURE__ */ jsxs3(
|
|
699
790
|
FeedtackContext.Provider,
|
|
700
791
|
{
|
|
701
792
|
value: {
|
|
702
|
-
activatePinMode:
|
|
703
|
-
|
|
704
|
-
|
|
793
|
+
activatePinMode: disabled ? () => {
|
|
794
|
+
} : state.activatePinMode,
|
|
795
|
+
deactivatePinMode: disabled ? () => {
|
|
796
|
+
} : state.deactivatePinMode,
|
|
797
|
+
isPinModeActive: disabled ? false : state.isPinModeActive
|
|
705
798
|
},
|
|
706
799
|
children: [
|
|
707
800
|
children,
|
|
708
|
-
showButton && /* @__PURE__ */
|
|
801
|
+
!disabled && showButton && /* @__PURE__ */ jsxs3(
|
|
709
802
|
"button",
|
|
710
803
|
{
|
|
711
804
|
type: "button",
|
|
@@ -723,7 +816,7 @@ function FeedtackProvider({
|
|
|
723
816
|
]
|
|
724
817
|
}
|
|
725
818
|
),
|
|
726
|
-
state.isPinModeActive && /* @__PURE__ */
|
|
819
|
+
state.isPinModeActive && /* @__PURE__ */ jsx3("div", { className: cx("feedtack-color-picker", classes.colorPicker), children: PIN_PALETTE.map((color) => /* @__PURE__ */ jsx3(
|
|
727
820
|
"button",
|
|
728
821
|
{
|
|
729
822
|
type: "button",
|
|
@@ -737,7 +830,7 @@ function FeedtackProvider({
|
|
|
737
830
|
},
|
|
738
831
|
color
|
|
739
832
|
)) }),
|
|
740
|
-
state.pendingPins.map((pin) => /* @__PURE__ */
|
|
833
|
+
state.pendingPins.map((pin) => /* @__PURE__ */ jsx3(
|
|
741
834
|
"div",
|
|
742
835
|
{
|
|
743
836
|
className: cx("feedtack-pin-marker", classes.pinMarker),
|
|
@@ -750,77 +843,28 @@ function FeedtackProvider({
|
|
|
750
843
|
},
|
|
751
844
|
`${pin.x}-${pin.y}-${pin.color}`
|
|
752
845
|
)),
|
|
753
|
-
state.showForm && /* @__PURE__ */
|
|
754
|
-
|
|
846
|
+
state.showForm && /* @__PURE__ */ jsx3(
|
|
847
|
+
CommentForm,
|
|
755
848
|
{
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
]
|
|
849
|
+
comment: state.comment,
|
|
850
|
+
commentError: state.commentError,
|
|
851
|
+
sentiment: state.sentiment,
|
|
852
|
+
submitting: state.submitting,
|
|
853
|
+
formPos,
|
|
854
|
+
classes,
|
|
855
|
+
sentimentLabels,
|
|
856
|
+
onCommentChange: (v) => {
|
|
857
|
+
state.setComment(v);
|
|
858
|
+
state.setCommentError(false);
|
|
859
|
+
},
|
|
860
|
+
onSentimentChange: state.setSentiment,
|
|
861
|
+
onSubmit: state.handleSubmit,
|
|
862
|
+
onCancel: state.deactivatePinMode
|
|
819
863
|
}
|
|
820
864
|
),
|
|
821
|
-
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).map((item) => {
|
|
865
|
+
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).filter((item) => state.hasValidPins(item)).map((item) => {
|
|
822
866
|
const pin = item.payload.pins[0];
|
|
823
|
-
return /* @__PURE__ */
|
|
867
|
+
return /* @__PURE__ */ jsx3(
|
|
824
868
|
"button",
|
|
825
869
|
{
|
|
826
870
|
type: "button",
|
|
@@ -835,12 +879,12 @@ function FeedtackProvider({
|
|
|
835
879
|
onClick: () => state.setOpenThreadId(
|
|
836
880
|
state.openThreadId === item.payload.id ? null : item.payload.id
|
|
837
881
|
),
|
|
838
|
-
children: state.hasUnread(item) && /* @__PURE__ */
|
|
882
|
+
children: state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
|
|
839
883
|
},
|
|
840
884
|
item.payload.id
|
|
841
885
|
);
|
|
842
886
|
}),
|
|
843
|
-
openItem && /* @__PURE__ */
|
|
887
|
+
openItem && /* @__PURE__ */ jsx3(
|
|
844
888
|
ThreadPanel,
|
|
845
889
|
{
|
|
846
890
|
item: openItem,
|
|
@@ -853,7 +897,7 @@ function FeedtackProvider({
|
|
|
853
897
|
className: classes.thread
|
|
854
898
|
}
|
|
855
899
|
),
|
|
856
|
-
state.loading && /* @__PURE__ */
|
|
900
|
+
state.loading && /* @__PURE__ */ jsx3("div", { className: "feedtack-loading", children: "Loading feedback\u2026" })
|
|
857
901
|
]
|
|
858
902
|
}
|
|
859
903
|
);
|
|
@@ -17,19 +17,36 @@ interface FeedtackBoundingRect {
|
|
|
17
17
|
width: number;
|
|
18
18
|
height: number;
|
|
19
19
|
}
|
|
20
|
+
interface AncestorNode {
|
|
21
|
+
tag: string;
|
|
22
|
+
id: string | null;
|
|
23
|
+
ariaLabel: string | null;
|
|
24
|
+
role: string | null;
|
|
25
|
+
type: string | null;
|
|
26
|
+
name: string | null;
|
|
27
|
+
title: string | null;
|
|
28
|
+
alt: string | null;
|
|
29
|
+
dataTestId: string | null;
|
|
30
|
+
dataFeedtackComponent: string | null;
|
|
31
|
+
/** 1-indexed position among all sibling elements. Null when node has a stable id or dataTestId. */
|
|
32
|
+
nthChild: number | null;
|
|
33
|
+
/** 1-indexed position among same-tag siblings. Null when node has a stable id or dataTestId. */
|
|
34
|
+
nthOfType: number | null;
|
|
35
|
+
/** React component display name from fiber traversal, or data-feedtack-component value */
|
|
36
|
+
componentName: string | null;
|
|
37
|
+
}
|
|
20
38
|
interface FeedtackPinTarget {
|
|
21
|
-
/** CSS selector path to the
|
|
39
|
+
/** CSS selector path to the resolved interactive target */
|
|
22
40
|
selector: string;
|
|
23
41
|
/** True when no stable selector was found — downstream consumers should not rely on selector for automated targeting */
|
|
24
42
|
best_effort: boolean;
|
|
25
|
-
/** data-testid attribute value if present, null otherwise
|
|
26
|
-
|
|
27
|
-
/** Readable DOM ancestry
|
|
43
|
+
/** data-testid attribute value if present, null otherwise */
|
|
44
|
+
dataTestId: string | null;
|
|
45
|
+
/** Readable DOM ancestry path retained for backward compatibility */
|
|
28
46
|
elementPath: string | null;
|
|
29
47
|
tagName: string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
attributes: Record<string, string>;
|
|
48
|
+
/** Ancestor chain up to 5 levels from the resolved target, for LLM element location */
|
|
49
|
+
ancestors: AncestorNode[];
|
|
33
50
|
boundingRect: FeedtackBoundingRect;
|
|
34
51
|
}
|
|
35
52
|
interface FeedtackPin {
|
|
@@ -147,4 +164,4 @@ interface FeedtackTheme {
|
|
|
147
164
|
/** Maps FeedtackTheme fields to CSS custom properties on #feedtack-root */
|
|
148
165
|
declare function themeToCSS(theme: FeedtackTheme): Record<string, string>;
|
|
149
166
|
|
|
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 };
|
|
167
|
+
export { type AncestorNode as A, 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.2.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",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"test:watch": "vitest",
|
|
31
31
|
"lint": "biome check src/",
|
|
32
32
|
"lint:fix": "biome check --fix src/",
|
|
33
|
-
"release": "release-it",
|
|
33
|
+
"release": "release-it --ci",
|
|
34
|
+
"openspec:archive": "openspec archive -y",
|
|
34
35
|
"prepublishOnly": "pnpm test && pnpm build",
|
|
35
36
|
"prepare": "husky"
|
|
36
37
|
},
|
package/dist/chunk-XVWG3PLK.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
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
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
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
|
-
}
|
|
56
|
-
function getCSSSelector(element) {
|
|
57
|
-
const parts = [];
|
|
58
|
-
let current = element;
|
|
59
|
-
while (current && current !== document.body) {
|
|
60
|
-
let selector = current.tagName.toLowerCase();
|
|
61
|
-
const parent = current.parentElement;
|
|
62
|
-
if (parent) {
|
|
63
|
-
const siblings = Array.from(parent.children).filter(
|
|
64
|
-
(c) => c.tagName === current.tagName
|
|
65
|
-
);
|
|
66
|
-
if (siblings.length > 1) {
|
|
67
|
-
const index = siblings.indexOf(current) + 1;
|
|
68
|
-
selector += `:nth-of-type(${index})`;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
parts.unshift(selector);
|
|
72
|
-
current = current.parentElement;
|
|
73
|
-
}
|
|
74
|
-
return parts.join(" > ");
|
|
75
|
-
}
|
|
76
|
-
function getTargetMeta(element) {
|
|
77
|
-
const id = element.getAttribute("id");
|
|
78
|
-
const testId = element.getAttribute("data-testid");
|
|
79
|
-
let selector;
|
|
80
|
-
let best_effort;
|
|
81
|
-
if (id) {
|
|
82
|
-
selector = `#${id}`;
|
|
83
|
-
best_effort = false;
|
|
84
|
-
} else if (testId) {
|
|
85
|
-
selector = `[data-testid="${testId}"]`;
|
|
86
|
-
best_effort = false;
|
|
87
|
-
} else {
|
|
88
|
-
selector = getCSSSelector(element);
|
|
89
|
-
best_effort = true;
|
|
90
|
-
}
|
|
91
|
-
const rect = element.getBoundingClientRect();
|
|
92
|
-
const attrs = {};
|
|
93
|
-
for (const attr of Array.from(element.attributes)) {
|
|
94
|
-
attrs[attr.name] = attr.value;
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
selector,
|
|
98
|
-
best_effort,
|
|
99
|
-
testId: testId ?? null,
|
|
100
|
-
elementPath: testId ? null : getElementPath(element),
|
|
101
|
-
tagName: element.tagName,
|
|
102
|
-
textContent: (element.textContent ?? "").trim().slice(0, 200),
|
|
103
|
-
attributes: attrs,
|
|
104
|
-
boundingRect: {
|
|
105
|
-
x: rect.x,
|
|
106
|
-
y: rect.y,
|
|
107
|
-
width: rect.width,
|
|
108
|
-
height: rect.height
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
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;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export {
|
|
131
|
-
getViewportMeta,
|
|
132
|
-
getPageMeta,
|
|
133
|
-
getDeviceMeta,
|
|
134
|
-
getPinCoords,
|
|
135
|
-
getCSSSelector,
|
|
136
|
-
getTargetMeta,
|
|
137
|
-
SCHEMA_VERSION,
|
|
138
|
-
themeToCSS
|
|
139
|
-
};
|