feedtack 0.2.0 → 0.3.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-NCW2V5JL.js +237 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +4 -2
- package/dist/react/index.js +63 -12
- package/dist/{theme-C_JZCVVA.d.ts → theme-C-uctIoI.d.ts} +25 -8
- package/package.json +2 -1
- 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, e as FeedbackItem } from '../theme-C-uctIoI.js';
|
|
4
4
|
|
|
5
5
|
interface FeedtackClasses {
|
|
6
6
|
button?: string;
|
|
@@ -24,8 +24,10 @@ interface FeedtackProviderProps {
|
|
|
24
24
|
sentimentLabels?: FeedtackSentimentLabels;
|
|
25
25
|
onError?: (err: Error) => void;
|
|
26
26
|
disabled?: boolean;
|
|
27
|
+
/** Render custom content inside a submitted pin marker. Receives the feedback item. */
|
|
28
|
+
renderPinIcon?: (item: FeedbackItem) => React.ReactNode;
|
|
27
29
|
}
|
|
28
|
-
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
30
|
+
declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
|
|
29
31
|
|
|
30
32
|
interface FeedtackContextValue {
|
|
31
33
|
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 = [
|
|
@@ -66,17 +66,27 @@ function CommentForm({
|
|
|
66
66
|
className: cx("feedtack-form", classes.form),
|
|
67
67
|
style: { position: "fixed", ...formPos },
|
|
68
68
|
children: [
|
|
69
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "feedtack-comment", className: "feedtack-sr-only", children: "Feedback comment" }),
|
|
69
70
|
/* @__PURE__ */ jsx(
|
|
70
71
|
"textarea",
|
|
71
72
|
{
|
|
73
|
+
id: "feedtack-comment",
|
|
72
74
|
className: commentError ? "error" : "",
|
|
73
75
|
placeholder: "What's the issue? (required)",
|
|
74
76
|
value: comment,
|
|
75
77
|
onChange: (e) => onCommentChange(e.target.value),
|
|
76
|
-
|
|
78
|
+
onKeyDown: (e) => {
|
|
79
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
onSubmit();
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
ref: (el) => el?.focus(),
|
|
85
|
+
"aria-describedby": commentError ? "feedtack-comment-error" : void 0,
|
|
86
|
+
"aria-invalid": commentError || void 0
|
|
77
87
|
}
|
|
78
88
|
),
|
|
79
|
-
commentError && /* @__PURE__ */ jsx("span", { className: "feedtack-error-msg", children: "Comment is required" }),
|
|
89
|
+
commentError && /* @__PURE__ */ jsx("span", { id: "feedtack-comment-error", className: "feedtack-error-msg", children: "Comment is required" }),
|
|
80
90
|
/* @__PURE__ */ jsxs("div", { className: "feedtack-sentiment", children: [
|
|
81
91
|
/* @__PURE__ */ jsx(
|
|
82
92
|
"button",
|
|
@@ -147,7 +157,8 @@ function ThreadPanel({
|
|
|
147
157
|
onClose,
|
|
148
158
|
className
|
|
149
159
|
}) {
|
|
150
|
-
const pin = item.payload
|
|
160
|
+
const pin = item.payload?.pins?.[0];
|
|
161
|
+
if (!pin) return null;
|
|
151
162
|
const pos = getAnchoredPosition(pin.x, pin.y);
|
|
152
163
|
return /* @__PURE__ */ jsxs2(
|
|
153
164
|
"div",
|
|
@@ -307,6 +318,22 @@ var FEEDTACK_STYLES = `
|
|
|
307
318
|
pointer-events: all;
|
|
308
319
|
}
|
|
309
320
|
|
|
321
|
+
.feedtack-pin-resolved { opacity: 0.6; }
|
|
322
|
+
|
|
323
|
+
.feedtack-pin-icon {
|
|
324
|
+
position: absolute;
|
|
325
|
+
inset: 0;
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
transform: rotate(45deg);
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
font-weight: 700;
|
|
332
|
+
color: #fff;
|
|
333
|
+
line-height: 1;
|
|
334
|
+
pointer-events: none;
|
|
335
|
+
}
|
|
336
|
+
|
|
310
337
|
.feedtack-pin-badge {
|
|
311
338
|
position: absolute;
|
|
312
339
|
top: -4px;
|
|
@@ -454,6 +481,11 @@ var FEEDTACK_STYLES = `
|
|
|
454
481
|
gap: 10px;
|
|
455
482
|
}
|
|
456
483
|
|
|
484
|
+
.feedtack-sr-only {
|
|
485
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
486
|
+
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
457
489
|
.feedtack-loading {
|
|
458
490
|
position: fixed;
|
|
459
491
|
bottom: 70px;
|
|
@@ -596,11 +628,11 @@ function useFeedtackState({
|
|
|
596
628
|
const origReplace = history.replaceState.bind(history);
|
|
597
629
|
history.pushState = (...args) => {
|
|
598
630
|
origPush(...args);
|
|
599
|
-
update
|
|
631
|
+
queueMicrotask(update);
|
|
600
632
|
};
|
|
601
633
|
history.replaceState = (...args) => {
|
|
602
634
|
origReplace(...args);
|
|
603
|
-
update
|
|
635
|
+
queueMicrotask(update);
|
|
604
636
|
};
|
|
605
637
|
window.addEventListener("popstate", update);
|
|
606
638
|
return () => {
|
|
@@ -754,7 +786,8 @@ function useFeedtackState({
|
|
|
754
786
|
handleResolve,
|
|
755
787
|
handleArchive,
|
|
756
788
|
isArchivedForUser: (item) => item.archives.some((a) => a.archivedBy.id === currentUser.id),
|
|
757
|
-
hasUnread: (item) => item.replies.length > 0
|
|
789
|
+
hasUnread: (item) => item.replies.length > 0,
|
|
790
|
+
hasValidPins: (item) => Array.isArray(item.payload?.pins) && item.payload.pins.length > 0
|
|
758
791
|
};
|
|
759
792
|
}
|
|
760
793
|
|
|
@@ -770,7 +803,8 @@ function FeedtackProvider({
|
|
|
770
803
|
classes = {},
|
|
771
804
|
sentimentLabels = {},
|
|
772
805
|
onError,
|
|
773
|
-
disabled = false
|
|
806
|
+
disabled = false,
|
|
807
|
+
renderPinIcon
|
|
774
808
|
}) {
|
|
775
809
|
const state = useFeedtackState({
|
|
776
810
|
adapter,
|
|
@@ -807,6 +841,8 @@ function FeedtackProvider({
|
|
|
807
841
|
),
|
|
808
842
|
onClick: () => state.isPinModeActive ? state.deactivatePinMode() : state.activatePinMode(),
|
|
809
843
|
title: "Toggle feedback pin mode",
|
|
844
|
+
"aria-label": "Toggle feedback pin mode",
|
|
845
|
+
"aria-pressed": state.isPinModeActive,
|
|
810
846
|
children: [
|
|
811
847
|
"Drop Pin [Shift+",
|
|
812
848
|
hotkey.toUpperCase(),
|
|
@@ -860,13 +896,17 @@ function FeedtackProvider({
|
|
|
860
896
|
onCancel: state.deactivatePinMode
|
|
861
897
|
}
|
|
862
898
|
),
|
|
863
|
-
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).map((item) => {
|
|
899
|
+
!state.loading && state.feedbackItems.filter((item) => item.payload.page.pathname === state.pathname).filter((item) => !state.isArchivedForUser(item)).filter((item) => state.hasValidPins(item)).map((item) => {
|
|
864
900
|
const pin = item.payload.pins[0];
|
|
865
|
-
return /* @__PURE__ */
|
|
901
|
+
return /* @__PURE__ */ jsxs3(
|
|
866
902
|
"button",
|
|
867
903
|
{
|
|
868
904
|
type: "button",
|
|
869
|
-
className: cx(
|
|
905
|
+
className: cx(
|
|
906
|
+
"feedtack-pin-marker",
|
|
907
|
+
item.resolutions.length > 0 && "feedtack-pin-resolved",
|
|
908
|
+
classes.pinMarker
|
|
909
|
+
),
|
|
870
910
|
style: {
|
|
871
911
|
background: pin.color,
|
|
872
912
|
left: pin.x,
|
|
@@ -877,7 +917,18 @@ function FeedtackProvider({
|
|
|
877
917
|
onClick: () => state.setOpenThreadId(
|
|
878
918
|
state.openThreadId === item.payload.id ? null : item.payload.id
|
|
879
919
|
),
|
|
880
|
-
children:
|
|
920
|
+
children: [
|
|
921
|
+
renderPinIcon ? /* @__PURE__ */ jsx3("span", { className: "feedtack-pin-icon", children: renderPinIcon(item) }) : item.resolutions.length > 0 && /* @__PURE__ */ jsx3(
|
|
922
|
+
"span",
|
|
923
|
+
{
|
|
924
|
+
className: "feedtack-pin-icon",
|
|
925
|
+
role: "img",
|
|
926
|
+
"aria-label": "Resolved",
|
|
927
|
+
children: "\u2713"
|
|
928
|
+
}
|
|
929
|
+
),
|
|
930
|
+
state.hasUnread(item) && /* @__PURE__ */ jsx3("div", { className: "feedtack-pin-badge" })
|
|
931
|
+
]
|
|
881
932
|
},
|
|
882
933
|
item.payload.id
|
|
883
934
|
);
|
|
@@ -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.3.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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"lint": "biome check src/",
|
|
32
32
|
"lint:fix": "biome check --fix src/",
|
|
33
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
|
-
};
|