@mahmulp/feedback-sdk 0.0.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/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/index.cjs +1714 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +252 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.global.js +397 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1701 -0
- package/dist/index.js.map +1 -0
- package/dist/mock.cjs +98 -0
- package/dist/mock.cjs.map +1 -0
- package/dist/mock.d.cts +111 -0
- package/dist/mock.d.ts +111 -0
- package/dist/mock.js +96 -0
- package/dist/mock.js.map +1 -0
- package/dist/svelte.d.ts +192 -0
- package/dist/svelte.js +1753 -0
- package/dist/svelte.js.map +1 -0
- package/package.json +89 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1701 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/coordinates.ts
|
|
6
|
+
function computeCoordinates(target, clientX, clientY) {
|
|
7
|
+
const rect = target.getBoundingClientRect();
|
|
8
|
+
const width = rect.width || 1;
|
|
9
|
+
const height = rect.height || 1;
|
|
10
|
+
const xPercent = clamp01((clientX - rect.left) / width);
|
|
11
|
+
const yPercent = clamp01((clientY - rect.top) / height);
|
|
12
|
+
const xPx = Math.round(clientX + window.scrollX);
|
|
13
|
+
const yPx = Math.round(clientY + window.scrollY);
|
|
14
|
+
return { xPercent, yPercent, xPx, yPx };
|
|
15
|
+
}
|
|
16
|
+
function getViewportInfo() {
|
|
17
|
+
return {
|
|
18
|
+
width: window.innerWidth,
|
|
19
|
+
height: window.innerHeight,
|
|
20
|
+
devicePixelRatio: window.devicePixelRatio || 1
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function projectCoordinates(target, coords) {
|
|
24
|
+
if (target) {
|
|
25
|
+
const rect = target.getBoundingClientRect();
|
|
26
|
+
return {
|
|
27
|
+
x: rect.left + window.scrollX + rect.width * coords.xPercent,
|
|
28
|
+
y: rect.top + window.scrollY + rect.height * coords.yPercent,
|
|
29
|
+
orphaned: false
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { x: coords.xPx, y: coords.yPx, orphaned: true };
|
|
33
|
+
}
|
|
34
|
+
function clamp01(n) {
|
|
35
|
+
if (Number.isNaN(n)) return 0;
|
|
36
|
+
if (n < 0) return 0;
|
|
37
|
+
if (n > 1) return 1;
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/identity.ts
|
|
42
|
+
var STORAGE_KEY_PREFIX = "mahmulp-fb-author:";
|
|
43
|
+
function storageKey(projectId) {
|
|
44
|
+
return `${STORAGE_KEY_PREFIX}${projectId}`;
|
|
45
|
+
}
|
|
46
|
+
function getStorage() {
|
|
47
|
+
try {
|
|
48
|
+
if (typeof localStorage === "undefined") return null;
|
|
49
|
+
return localStorage;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function loadAuthor(projectId) {
|
|
55
|
+
const storage = getStorage();
|
|
56
|
+
if (!storage) return null;
|
|
57
|
+
try {
|
|
58
|
+
const raw = storage.getItem(storageKey(projectId));
|
|
59
|
+
if (!raw) return null;
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (typeof parsed === "object" && parsed !== null && "name" in parsed && typeof parsed.name === "string") {
|
|
62
|
+
const name = parsed.name.trim();
|
|
63
|
+
if (name.length === 0) return null;
|
|
64
|
+
const emailValue = parsed.email;
|
|
65
|
+
const email = typeof emailValue === "string" ? emailValue.trim() : "";
|
|
66
|
+
return email.length > 0 ? { name, email } : { name };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function saveAuthor(projectId, author) {
|
|
74
|
+
const storage = getStorage();
|
|
75
|
+
if (!storage) return;
|
|
76
|
+
try {
|
|
77
|
+
storage.setItem(storageKey(projectId), JSON.stringify(author));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/selector.ts
|
|
83
|
+
var MAX_DEPTH = 5;
|
|
84
|
+
function isStableId(id) {
|
|
85
|
+
if (id.length === 0 || id.length > 64) return false;
|
|
86
|
+
if (/[0-9a-f]{8,}/i.test(id)) return false;
|
|
87
|
+
if (/[\s"'<>`]/.test(id)) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function isStableClass(cls) {
|
|
91
|
+
if (cls.length === 0 || cls.length > 48) return false;
|
|
92
|
+
if (/[0-9a-f]{6,}/i.test(cls)) return false;
|
|
93
|
+
if (/[:[\]]/.test(cls)) return false;
|
|
94
|
+
if (/^(sm|md|lg|xl|2xl|hover|focus|active|dark)-/.test(cls)) return false;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
function nearestFeedbackId(el) {
|
|
98
|
+
let cur = el;
|
|
99
|
+
while (cur && cur !== document.documentElement) {
|
|
100
|
+
const id = cur.getAttribute("data-feedback-id");
|
|
101
|
+
if (id && id.length > 0) return { id, node: cur };
|
|
102
|
+
cur = cur.parentElement;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function isSlugSafe(value) {
|
|
107
|
+
if (value.length === 0 || value.length > 96) return false;
|
|
108
|
+
return /^[A-Za-z0-9_.:\-]+$/.test(value);
|
|
109
|
+
}
|
|
110
|
+
function pickStableClass(el) {
|
|
111
|
+
const list = Array.from(el.classList);
|
|
112
|
+
for (const cls of list) {
|
|
113
|
+
if (isStableClass(cls)) return cls;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function nthOfType(el) {
|
|
118
|
+
const parent = el.parentElement;
|
|
119
|
+
if (!parent) return 1;
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (const sibling of Array.from(parent.children)) {
|
|
122
|
+
if (sibling.tagName === el.tagName) {
|
|
123
|
+
n++;
|
|
124
|
+
if (sibling === el) return n;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return n;
|
|
128
|
+
}
|
|
129
|
+
function structuralStep(el) {
|
|
130
|
+
const tag = el.tagName.toLowerCase();
|
|
131
|
+
const parent = el.parentElement;
|
|
132
|
+
if (!parent) return tag;
|
|
133
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
|
|
134
|
+
if (siblings.length === 1) return tag;
|
|
135
|
+
return `${tag}:nth-of-type(${nthOfType(el)})`;
|
|
136
|
+
}
|
|
137
|
+
function buildStructuralSelector(el) {
|
|
138
|
+
const parts = [];
|
|
139
|
+
let cur = el;
|
|
140
|
+
let depth = 0;
|
|
141
|
+
while (cur && cur !== document.body && cur !== document.documentElement && depth < MAX_DEPTH) {
|
|
142
|
+
parts.unshift(structuralStep(cur));
|
|
143
|
+
cur = cur.parentElement;
|
|
144
|
+
depth++;
|
|
145
|
+
}
|
|
146
|
+
return parts.join(" > ");
|
|
147
|
+
}
|
|
148
|
+
function resolveSelector(target) {
|
|
149
|
+
if (!(target instanceof Element)) {
|
|
150
|
+
throw new TypeError("resolveSelector: target must be an Element");
|
|
151
|
+
}
|
|
152
|
+
const fid = nearestFeedbackId(target);
|
|
153
|
+
if (fid && isSlugSafe(fid.id)) {
|
|
154
|
+
return `[data-feedback-id="${fid.id}"]`;
|
|
155
|
+
}
|
|
156
|
+
const id = target.id;
|
|
157
|
+
if (id && isStableId(id)) {
|
|
158
|
+
return `#${CSS.escape(id)}`;
|
|
159
|
+
}
|
|
160
|
+
const structural = buildStructuralSelector(target);
|
|
161
|
+
if (structural.length > 0) return structural;
|
|
162
|
+
const tag = target.tagName.toLowerCase();
|
|
163
|
+
const cls = pickStableClass(target);
|
|
164
|
+
return cls ? `${tag}.${CSS.escape(cls)}` : tag;
|
|
165
|
+
}
|
|
166
|
+
function findElement(selector, root = document) {
|
|
167
|
+
if (typeof selector !== "string" || selector.length === 0) return null;
|
|
168
|
+
try {
|
|
169
|
+
return root.querySelector(selector);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/launcher.ts
|
|
176
|
+
var LauncherManager = class {
|
|
177
|
+
constructor(parent, callbacks) {
|
|
178
|
+
__publicField(this, "launcherEl");
|
|
179
|
+
__publicField(this, "revealEl");
|
|
180
|
+
__publicField(this, "toggleBtn");
|
|
181
|
+
__publicField(this, "pinBtn");
|
|
182
|
+
__publicField(this, "state", {
|
|
183
|
+
enabled: false,
|
|
184
|
+
pinsVisible: true,
|
|
185
|
+
launcherVisible: true
|
|
186
|
+
});
|
|
187
|
+
this.launcherEl = document.createElement("div");
|
|
188
|
+
this.launcherEl.className = "launcher";
|
|
189
|
+
this.launcherEl.setAttribute("role", "toolbar");
|
|
190
|
+
this.launcherEl.setAttribute("aria-label", "Feedback controls");
|
|
191
|
+
this.toggleBtn = document.createElement("button");
|
|
192
|
+
this.toggleBtn.type = "button";
|
|
193
|
+
this.toggleBtn.className = "launcher-btn primary";
|
|
194
|
+
this.toggleBtn.setAttribute("aria-pressed", "false");
|
|
195
|
+
this.toggleBtn.addEventListener("click", () => callbacks.onToggleFeedback());
|
|
196
|
+
const divider = document.createElement("span");
|
|
197
|
+
divider.className = "launcher-divider";
|
|
198
|
+
divider.setAttribute("aria-hidden", "true");
|
|
199
|
+
this.pinBtn = document.createElement("button");
|
|
200
|
+
this.pinBtn.type = "button";
|
|
201
|
+
this.pinBtn.className = "launcher-btn";
|
|
202
|
+
this.pinBtn.setAttribute("aria-pressed", "true");
|
|
203
|
+
this.pinBtn.addEventListener("click", () => callbacks.onTogglePins());
|
|
204
|
+
const closeBtn = document.createElement("button");
|
|
205
|
+
closeBtn.type = "button";
|
|
206
|
+
closeBtn.className = "launcher-btn muted";
|
|
207
|
+
closeBtn.setAttribute("aria-label", "Hide feedback launcher");
|
|
208
|
+
closeBtn.title = "Hide launcher (Ctrl/\u2318+Shift+F to reopen)";
|
|
209
|
+
closeBtn.innerHTML = ICON_X;
|
|
210
|
+
closeBtn.addEventListener("click", () => callbacks.onHideLauncher());
|
|
211
|
+
this.launcherEl.append(this.toggleBtn, divider, this.pinBtn, closeBtn);
|
|
212
|
+
this.revealEl = document.createElement("button");
|
|
213
|
+
this.revealEl.type = "button";
|
|
214
|
+
this.revealEl.className = "launcher-reveal";
|
|
215
|
+
this.revealEl.setAttribute("aria-label", "Show feedback launcher");
|
|
216
|
+
this.revealEl.title = "Show feedback launcher";
|
|
217
|
+
this.revealEl.innerHTML = ICON_BUBBLE;
|
|
218
|
+
this.revealEl.addEventListener("click", () => callbacks.onHideLauncher());
|
|
219
|
+
parent.append(this.launcherEl, this.revealEl);
|
|
220
|
+
this.render();
|
|
221
|
+
}
|
|
222
|
+
setEnabled(enabled) {
|
|
223
|
+
this.state.enabled = enabled;
|
|
224
|
+
this.render();
|
|
225
|
+
}
|
|
226
|
+
setPinsVisible(pinsVisible) {
|
|
227
|
+
this.state.pinsVisible = pinsVisible;
|
|
228
|
+
this.render();
|
|
229
|
+
}
|
|
230
|
+
setLauncherVisible(launcherVisible) {
|
|
231
|
+
this.state.launcherVisible = launcherVisible;
|
|
232
|
+
this.render();
|
|
233
|
+
}
|
|
234
|
+
/** True when an event originated from the launcher's own UI. */
|
|
235
|
+
ownsNode(node) {
|
|
236
|
+
if (!node) return false;
|
|
237
|
+
return this.launcherEl.contains(node) || this.revealEl.contains(node);
|
|
238
|
+
}
|
|
239
|
+
destroy() {
|
|
240
|
+
this.launcherEl.remove();
|
|
241
|
+
this.revealEl.remove();
|
|
242
|
+
}
|
|
243
|
+
render() {
|
|
244
|
+
const { enabled, pinsVisible, launcherVisible } = this.state;
|
|
245
|
+
this.launcherEl.classList.toggle("hidden", !launcherVisible);
|
|
246
|
+
this.revealEl.classList.toggle("visible", !launcherVisible);
|
|
247
|
+
this.toggleBtn.classList.toggle("active", enabled);
|
|
248
|
+
this.toggleBtn.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
249
|
+
this.toggleBtn.title = enabled ? "Stop feedback mode (Esc)" : "Start feedback mode (Ctrl/\u2318+Shift+F)";
|
|
250
|
+
this.toggleBtn.innerHTML = enabled ? `${ICON_DOT} <span>Stop</span>` : `${ICON_PIN} <span>Feedback</span>`;
|
|
251
|
+
this.pinBtn.classList.toggle("muted", !pinsVisible);
|
|
252
|
+
this.pinBtn.setAttribute("aria-pressed", pinsVisible ? "true" : "false");
|
|
253
|
+
this.pinBtn.setAttribute("aria-label", pinsVisible ? "Hide pins" : "Show pins");
|
|
254
|
+
this.pinBtn.title = pinsVisible ? "Hide pins" : "Show pins";
|
|
255
|
+
this.pinBtn.innerHTML = pinsVisible ? ICON_EYE : ICON_EYE_OFF;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var ICON_PIN = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
259
|
+
<path d="M12 21s7-7.5 7-12a7 7 0 1 0-14 0c0 4.5 7 12 7 12Z" />
|
|
260
|
+
<circle cx="12" cy="9" r="2.5" />
|
|
261
|
+
</svg>`;
|
|
262
|
+
var ICON_DOT = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
263
|
+
<circle cx="12" cy="12" r="6" />
|
|
264
|
+
</svg>`;
|
|
265
|
+
var ICON_EYE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
266
|
+
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
|
|
267
|
+
<circle cx="12" cy="12" r="3" />
|
|
268
|
+
</svg>`;
|
|
269
|
+
var ICON_EYE_OFF = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
270
|
+
<path d="M3 3l18 18" />
|
|
271
|
+
<path d="M10.6 10.6a3 3 0 0 0 4.2 4.2" />
|
|
272
|
+
<path d="M9.9 5.1A10.4 10.4 0 0 1 12 5c6.5 0 10 7 10 7a17.3 17.3 0 0 1-3.6 4.5" />
|
|
273
|
+
<path d="M6.6 6.6A17.4 17.4 0 0 0 2 12s3.5 7 10 7c1.6 0 3-.3 4.2-.8" />
|
|
274
|
+
</svg>`;
|
|
275
|
+
var ICON_X = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
276
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
277
|
+
</svg>`;
|
|
278
|
+
var ICON_BUBBLE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
279
|
+
<path d="M21 12a8 8 0 1 1-3.4-6.5" />
|
|
280
|
+
<path d="M21 4v5h-5" />
|
|
281
|
+
</svg>`;
|
|
282
|
+
|
|
283
|
+
// src/popover.ts
|
|
284
|
+
var POPOVER_OFFSET = 18;
|
|
285
|
+
var VIEWPORT_PADDING = 8;
|
|
286
|
+
var PopoverManager = class {
|
|
287
|
+
constructor(layer) {
|
|
288
|
+
__publicField(this, "layer");
|
|
289
|
+
__publicField(this, "current", null);
|
|
290
|
+
this.layer = layer;
|
|
291
|
+
}
|
|
292
|
+
isOpen() {
|
|
293
|
+
return this.current !== null;
|
|
294
|
+
}
|
|
295
|
+
/** True when the event originated inside the currently-open popover element. */
|
|
296
|
+
ownsEvent(event) {
|
|
297
|
+
if (!this.current) return false;
|
|
298
|
+
const path = event.composedPath();
|
|
299
|
+
return path.includes(this.current.el);
|
|
300
|
+
}
|
|
301
|
+
hide() {
|
|
302
|
+
if (!this.current) return;
|
|
303
|
+
this.current.el.remove();
|
|
304
|
+
this.current = null;
|
|
305
|
+
}
|
|
306
|
+
showComposer(anchor, cb) {
|
|
307
|
+
this.hide();
|
|
308
|
+
const el = this.buildComposer(cb);
|
|
309
|
+
this.layer.appendChild(el);
|
|
310
|
+
this.current = { type: "composer", el, pageX: anchor.pageX, pageY: anchor.pageY };
|
|
311
|
+
this.repositionInternal();
|
|
312
|
+
queueMicrotask(() => {
|
|
313
|
+
const focusTarget = el.querySelector("textarea, input");
|
|
314
|
+
focusTarget?.focus();
|
|
315
|
+
});
|
|
316
|
+
return el;
|
|
317
|
+
}
|
|
318
|
+
showThread(feedback, anchor, cb) {
|
|
319
|
+
this.hide();
|
|
320
|
+
const el = this.buildThread(feedback, cb);
|
|
321
|
+
this.layer.appendChild(el);
|
|
322
|
+
this.current = { type: "thread", el, pageX: anchor.pageX, pageY: anchor.pageY };
|
|
323
|
+
this.repositionInternal();
|
|
324
|
+
return el;
|
|
325
|
+
}
|
|
326
|
+
/** Re-project to viewport coordinates after a scroll/resize. */
|
|
327
|
+
reposition(anchor) {
|
|
328
|
+
if (!this.current) return;
|
|
329
|
+
if (anchor) {
|
|
330
|
+
this.current.pageX = anchor.pageX;
|
|
331
|
+
this.current.pageY = anchor.pageY;
|
|
332
|
+
}
|
|
333
|
+
this.repositionInternal();
|
|
334
|
+
}
|
|
335
|
+
repositionInternal() {
|
|
336
|
+
if (!this.current) return;
|
|
337
|
+
const viewportX = this.current.pageX - window.scrollX;
|
|
338
|
+
const viewportY = this.current.pageY - window.scrollY;
|
|
339
|
+
this.applyPosition(this.current.el, viewportX, viewportY);
|
|
340
|
+
}
|
|
341
|
+
applyPosition(el, anchorX, anchorY) {
|
|
342
|
+
const vw = window.innerWidth;
|
|
343
|
+
const vh = window.innerHeight;
|
|
344
|
+
const rect = el.getBoundingClientRect();
|
|
345
|
+
const w = rect.width || 320;
|
|
346
|
+
const h = rect.height || 200;
|
|
347
|
+
let x = anchorX + POPOVER_OFFSET;
|
|
348
|
+
if (x + w > vw - VIEWPORT_PADDING) x = anchorX - POPOVER_OFFSET - w;
|
|
349
|
+
if (x < VIEWPORT_PADDING) x = VIEWPORT_PADDING;
|
|
350
|
+
let y = anchorY;
|
|
351
|
+
if (y + h > vh - VIEWPORT_PADDING) y = vh - h - VIEWPORT_PADDING;
|
|
352
|
+
if (y < VIEWPORT_PADDING) y = VIEWPORT_PADDING;
|
|
353
|
+
el.style.left = `${x}px`;
|
|
354
|
+
el.style.top = `${y}px`;
|
|
355
|
+
}
|
|
356
|
+
// ---------- Composer ----------
|
|
357
|
+
buildComposer(cb) {
|
|
358
|
+
const root = this.makeShell("New comment", () => cb.onCancel());
|
|
359
|
+
const body = root.querySelector(".popover-body");
|
|
360
|
+
const identity = this.buildIdentityFields(cb.initialAuthor);
|
|
361
|
+
body.appendChild(identity.el);
|
|
362
|
+
const textarea = document.createElement("textarea");
|
|
363
|
+
textarea.className = "popover-textarea";
|
|
364
|
+
textarea.placeholder = "Describe what you see...";
|
|
365
|
+
textarea.rows = 4;
|
|
366
|
+
body.appendChild(textarea);
|
|
367
|
+
const actions = document.createElement("div");
|
|
368
|
+
actions.className = "popover-actions";
|
|
369
|
+
const cancel = document.createElement("button");
|
|
370
|
+
cancel.type = "button";
|
|
371
|
+
cancel.className = "btn btn-ghost";
|
|
372
|
+
cancel.textContent = "Cancel";
|
|
373
|
+
cancel.addEventListener("click", () => cb.onCancel());
|
|
374
|
+
const submit = document.createElement("button");
|
|
375
|
+
submit.type = "button";
|
|
376
|
+
submit.className = "btn btn-primary popover-submit";
|
|
377
|
+
submit.textContent = "Send";
|
|
378
|
+
submit.addEventListener("click", async () => {
|
|
379
|
+
const text = textarea.value.trim();
|
|
380
|
+
if (text.length === 0) {
|
|
381
|
+
textarea.focus();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const author = identity.read();
|
|
385
|
+
if (!author) return;
|
|
386
|
+
submit.disabled = true;
|
|
387
|
+
try {
|
|
388
|
+
await cb.onSubmit(text, author);
|
|
389
|
+
} finally {
|
|
390
|
+
submit.disabled = false;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
actions.appendChild(cancel);
|
|
394
|
+
actions.appendChild(submit);
|
|
395
|
+
body.appendChild(actions);
|
|
396
|
+
return root;
|
|
397
|
+
}
|
|
398
|
+
// ---------- Thread ----------
|
|
399
|
+
buildThread(feedback, cb) {
|
|
400
|
+
const titleText = `Status: ${feedback.status}`;
|
|
401
|
+
const root = this.makeShell(titleText, () => cb.onClose());
|
|
402
|
+
root.classList.add("popover-thread");
|
|
403
|
+
root.dataset.status = feedback.status;
|
|
404
|
+
const body = root.querySelector(".popover-body");
|
|
405
|
+
const toolbar = document.createElement("div");
|
|
406
|
+
toolbar.className = "popover-toolbar";
|
|
407
|
+
const transitions = nextStatuses(feedback.status);
|
|
408
|
+
for (const status of transitions) {
|
|
409
|
+
const btn = document.createElement("button");
|
|
410
|
+
btn.type = "button";
|
|
411
|
+
btn.className = "btn btn-toolbar";
|
|
412
|
+
btn.dataset.status = status;
|
|
413
|
+
btn.textContent = labelForStatusTransition(status);
|
|
414
|
+
btn.addEventListener("click", () => {
|
|
415
|
+
void cb.onStatus(status);
|
|
416
|
+
});
|
|
417
|
+
toolbar.appendChild(btn);
|
|
418
|
+
}
|
|
419
|
+
body.appendChild(toolbar);
|
|
420
|
+
const list = document.createElement("ul");
|
|
421
|
+
list.className = "popover-thread-list";
|
|
422
|
+
if (feedback.thread.length === 0) {
|
|
423
|
+
const empty = document.createElement("li");
|
|
424
|
+
empty.className = "popover-empty";
|
|
425
|
+
empty.textContent = "No comments yet.";
|
|
426
|
+
list.appendChild(empty);
|
|
427
|
+
} else {
|
|
428
|
+
for (const c of feedback.thread) {
|
|
429
|
+
const item = document.createElement("li");
|
|
430
|
+
item.className = "popover-comment";
|
|
431
|
+
const meta = document.createElement("div");
|
|
432
|
+
meta.className = "popover-comment-meta";
|
|
433
|
+
meta.textContent = c.author.name;
|
|
434
|
+
const text = document.createElement("div");
|
|
435
|
+
text.className = "popover-comment-body";
|
|
436
|
+
text.textContent = c.body;
|
|
437
|
+
item.appendChild(meta);
|
|
438
|
+
item.appendChild(text);
|
|
439
|
+
list.appendChild(item);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
body.appendChild(list);
|
|
443
|
+
const identity = this.buildIdentityFields(cb.initialAuthor);
|
|
444
|
+
body.appendChild(identity.el);
|
|
445
|
+
const reply = document.createElement("textarea");
|
|
446
|
+
reply.className = "popover-textarea";
|
|
447
|
+
reply.placeholder = "Reply...";
|
|
448
|
+
reply.rows = 3;
|
|
449
|
+
body.appendChild(reply);
|
|
450
|
+
const actions = document.createElement("div");
|
|
451
|
+
actions.className = "popover-actions";
|
|
452
|
+
const submit = document.createElement("button");
|
|
453
|
+
submit.type = "button";
|
|
454
|
+
submit.className = "btn btn-primary popover-submit";
|
|
455
|
+
submit.textContent = "Reply";
|
|
456
|
+
submit.addEventListener("click", async () => {
|
|
457
|
+
const text = reply.value.trim();
|
|
458
|
+
if (text.length === 0) {
|
|
459
|
+
reply.focus();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const author = identity.read();
|
|
463
|
+
if (!author) return;
|
|
464
|
+
submit.disabled = true;
|
|
465
|
+
try {
|
|
466
|
+
await cb.onReply(text, author);
|
|
467
|
+
} finally {
|
|
468
|
+
submit.disabled = false;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
actions.appendChild(submit);
|
|
472
|
+
body.appendChild(actions);
|
|
473
|
+
return root;
|
|
474
|
+
}
|
|
475
|
+
// ---------- Shared bits ----------
|
|
476
|
+
makeShell(title, onClose) {
|
|
477
|
+
const root = document.createElement("div");
|
|
478
|
+
root.className = "popover";
|
|
479
|
+
root.setAttribute("role", "dialog");
|
|
480
|
+
root.setAttribute("aria-label", title);
|
|
481
|
+
const header = document.createElement("div");
|
|
482
|
+
header.className = "popover-header";
|
|
483
|
+
const titleEl = document.createElement("span");
|
|
484
|
+
titleEl.className = "popover-title";
|
|
485
|
+
titleEl.textContent = title;
|
|
486
|
+
header.appendChild(titleEl);
|
|
487
|
+
const closeBtn = document.createElement("button");
|
|
488
|
+
closeBtn.type = "button";
|
|
489
|
+
closeBtn.className = "popover-close";
|
|
490
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
491
|
+
closeBtn.textContent = "\xD7";
|
|
492
|
+
closeBtn.addEventListener("click", () => onClose());
|
|
493
|
+
header.appendChild(closeBtn);
|
|
494
|
+
root.appendChild(header);
|
|
495
|
+
const body = document.createElement("div");
|
|
496
|
+
body.className = "popover-body";
|
|
497
|
+
root.appendChild(body);
|
|
498
|
+
return root;
|
|
499
|
+
}
|
|
500
|
+
buildIdentityFields(initial) {
|
|
501
|
+
const wrap = document.createElement("div");
|
|
502
|
+
wrap.className = "popover-identity";
|
|
503
|
+
const nameInput = document.createElement("input");
|
|
504
|
+
nameInput.type = "text";
|
|
505
|
+
nameInput.placeholder = "Your name";
|
|
506
|
+
nameInput.className = "popover-input popover-name";
|
|
507
|
+
nameInput.required = true;
|
|
508
|
+
nameInput.autocomplete = "name";
|
|
509
|
+
const emailInput = document.createElement("input");
|
|
510
|
+
emailInput.type = "email";
|
|
511
|
+
emailInput.placeholder = "Email (optional)";
|
|
512
|
+
emailInput.className = "popover-input popover-email";
|
|
513
|
+
emailInput.autocomplete = "email";
|
|
514
|
+
if (initial) {
|
|
515
|
+
nameInput.value = initial.name;
|
|
516
|
+
if (initial.email) emailInput.value = initial.email;
|
|
517
|
+
}
|
|
518
|
+
wrap.appendChild(nameInput);
|
|
519
|
+
wrap.appendChild(emailInput);
|
|
520
|
+
return {
|
|
521
|
+
el: wrap,
|
|
522
|
+
read() {
|
|
523
|
+
const name = nameInput.value.trim();
|
|
524
|
+
if (name.length === 0) {
|
|
525
|
+
nameInput.focus();
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const email = emailInput.value.trim();
|
|
529
|
+
return email.length > 0 ? { name, email } : { name };
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
function nextStatuses(current) {
|
|
535
|
+
switch (current) {
|
|
536
|
+
case "open":
|
|
537
|
+
return ["resolved", "archived"];
|
|
538
|
+
case "resolved":
|
|
539
|
+
return ["open", "archived"];
|
|
540
|
+
case "archived":
|
|
541
|
+
return ["open"];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function labelForStatusTransition(target) {
|
|
545
|
+
switch (target) {
|
|
546
|
+
case "open":
|
|
547
|
+
return "Reopen";
|
|
548
|
+
case "resolved":
|
|
549
|
+
return "Resolve";
|
|
550
|
+
case "archived":
|
|
551
|
+
return "Archive";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/overlay.ts
|
|
556
|
+
var HOST_ATTR = "data-mahmulp-feedback-host";
|
|
557
|
+
var OVERLAY_STYLES = `
|
|
558
|
+
:host { all: initial; }
|
|
559
|
+
|
|
560
|
+
.layer {
|
|
561
|
+
position: fixed;
|
|
562
|
+
inset: 0;
|
|
563
|
+
pointer-events: none;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.highlight {
|
|
567
|
+
position: fixed;
|
|
568
|
+
border: 2px solid #1F5132;
|
|
569
|
+
background: rgba(31, 81, 50, 0.08);
|
|
570
|
+
border-radius: 4px;
|
|
571
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.6) inset;
|
|
572
|
+
transition: top 60ms linear, left 60ms linear, width 60ms linear, height 60ms linear;
|
|
573
|
+
display: none;
|
|
574
|
+
pointer-events: none;
|
|
575
|
+
z-index: 1;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.hud {
|
|
579
|
+
position: fixed;
|
|
580
|
+
padding: 6px 10px;
|
|
581
|
+
background: #1F5132;
|
|
582
|
+
color: #F5FFF8;
|
|
583
|
+
font: 500 12px/1.2 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
584
|
+
border-radius: 4px;
|
|
585
|
+
pointer-events: none;
|
|
586
|
+
display: none;
|
|
587
|
+
z-index: 2;
|
|
588
|
+
max-width: 320px;
|
|
589
|
+
overflow: hidden;
|
|
590
|
+
text-overflow: ellipsis;
|
|
591
|
+
white-space: nowrap;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.pin {
|
|
595
|
+
position: fixed;
|
|
596
|
+
width: 28px;
|
|
597
|
+
height: 28px;
|
|
598
|
+
margin-left: -14px;
|
|
599
|
+
margin-top: -28px;
|
|
600
|
+
background: #1F5132;
|
|
601
|
+
color: #F5FFF8;
|
|
602
|
+
border: 2px solid #F5FFF8;
|
|
603
|
+
border-radius: 50% 50% 50% 0;
|
|
604
|
+
transform: rotate(-45deg);
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
justify-content: center;
|
|
608
|
+
font: 600 11px/1 ui-sans-serif, system-ui, sans-serif;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
pointer-events: auto;
|
|
611
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
|
612
|
+
transition: transform 80ms ease;
|
|
613
|
+
z-index: 3;
|
|
614
|
+
}
|
|
615
|
+
.pin:hover { transform: rotate(-45deg) scale(1.08); }
|
|
616
|
+
.pin > span { transform: rotate(45deg); }
|
|
617
|
+
.pin.orphaned { opacity: 0.55; filter: grayscale(0.4); }
|
|
618
|
+
.pin.resolved { background: #2F7A4D; }
|
|
619
|
+
.pin.archived { background: #6B7280; }
|
|
620
|
+
.pin.dragging { cursor: grabbing; transform: rotate(-45deg) scale(1.18); }
|
|
621
|
+
|
|
622
|
+
.pinLayer.hidden { display: none; }
|
|
623
|
+
|
|
624
|
+
/* ---------- Floating launcher ---------- */
|
|
625
|
+
.launcher {
|
|
626
|
+
position: fixed;
|
|
627
|
+
right: 20px;
|
|
628
|
+
bottom: 20px;
|
|
629
|
+
display: flex;
|
|
630
|
+
align-items: center;
|
|
631
|
+
gap: 6px;
|
|
632
|
+
padding: 6px;
|
|
633
|
+
background: rgba(255, 255, 255, 0.96);
|
|
634
|
+
color: #1F5132;
|
|
635
|
+
border: 1px solid rgba(31, 81, 50, 0.18);
|
|
636
|
+
border-radius: 999px;
|
|
637
|
+
box-shadow: 0 12px 24px -10px rgba(15, 35, 24, 0.32),
|
|
638
|
+
0 4px 8px -2px rgba(15, 35, 24, 0.16);
|
|
639
|
+
pointer-events: auto;
|
|
640
|
+
z-index: 5;
|
|
641
|
+
font: 600 12px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
642
|
+
user-select: none;
|
|
643
|
+
transition: opacity 120ms ease, transform 120ms ease;
|
|
644
|
+
}
|
|
645
|
+
.launcher.hidden {
|
|
646
|
+
opacity: 0;
|
|
647
|
+
transform: translateY(8px);
|
|
648
|
+
pointer-events: none;
|
|
649
|
+
}
|
|
650
|
+
.launcher-btn {
|
|
651
|
+
appearance: none;
|
|
652
|
+
width: 36px;
|
|
653
|
+
height: 36px;
|
|
654
|
+
border-radius: 999px;
|
|
655
|
+
border: 0;
|
|
656
|
+
background: transparent;
|
|
657
|
+
color: inherit;
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
display: inline-flex;
|
|
660
|
+
align-items: center;
|
|
661
|
+
justify-content: center;
|
|
662
|
+
transition: background 120ms ease;
|
|
663
|
+
}
|
|
664
|
+
.launcher-btn:hover { background: rgba(31, 81, 50, 0.08); }
|
|
665
|
+
.launcher-btn:focus-visible {
|
|
666
|
+
outline: 2px solid #1F5132;
|
|
667
|
+
outline-offset: 2px;
|
|
668
|
+
}
|
|
669
|
+
.launcher-btn svg { width: 18px; height: 18px; pointer-events: none; }
|
|
670
|
+
.launcher-btn.primary {
|
|
671
|
+
background: #1F5132;
|
|
672
|
+
color: #F5FFF8;
|
|
673
|
+
width: auto;
|
|
674
|
+
padding: 0 14px 0 12px;
|
|
675
|
+
border-radius: 999px;
|
|
676
|
+
gap: 6px;
|
|
677
|
+
}
|
|
678
|
+
.launcher-btn.primary:hover { background: #265E3B; }
|
|
679
|
+
.launcher-btn.primary.active {
|
|
680
|
+
background: #B91C1C;
|
|
681
|
+
color: #FFE8E8;
|
|
682
|
+
}
|
|
683
|
+
.launcher-btn.primary.active:hover { background: #A11616; }
|
|
684
|
+
.launcher-divider {
|
|
685
|
+
width: 1px;
|
|
686
|
+
height: 22px;
|
|
687
|
+
background: rgba(31, 81, 50, 0.14);
|
|
688
|
+
}
|
|
689
|
+
.launcher-btn.muted { color: #6B7280; }
|
|
690
|
+
|
|
691
|
+
/* ---------- Re-show launcher pill ---------- */
|
|
692
|
+
.launcher-reveal {
|
|
693
|
+
position: fixed;
|
|
694
|
+
right: 20px;
|
|
695
|
+
bottom: 20px;
|
|
696
|
+
width: 36px;
|
|
697
|
+
height: 36px;
|
|
698
|
+
border-radius: 999px;
|
|
699
|
+
background: #1F5132;
|
|
700
|
+
color: #F5FFF8;
|
|
701
|
+
border: 0;
|
|
702
|
+
display: none;
|
|
703
|
+
align-items: center;
|
|
704
|
+
justify-content: center;
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
pointer-events: auto;
|
|
707
|
+
box-shadow: 0 8px 16px -8px rgba(15, 35, 24, 0.4);
|
|
708
|
+
z-index: 5;
|
|
709
|
+
}
|
|
710
|
+
.launcher-reveal.visible { display: inline-flex; }
|
|
711
|
+
.launcher-reveal:hover { background: #265E3B; }
|
|
712
|
+
.launcher-reveal svg { width: 18px; height: 18px; }
|
|
713
|
+
|
|
714
|
+
@media (prefers-color-scheme: dark) {
|
|
715
|
+
.launcher {
|
|
716
|
+
background: #0F1F17;
|
|
717
|
+
color: #B6D8C5;
|
|
718
|
+
border-color: rgba(120, 200, 160, 0.22);
|
|
719
|
+
}
|
|
720
|
+
.launcher-btn.primary { color: #0F1F17; background: #B6D8C5; }
|
|
721
|
+
.launcher-btn.primary:hover { background: #C7E2D2; }
|
|
722
|
+
.launcher-divider { background: rgba(120, 200, 160, 0.22); }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.popover-layer {
|
|
726
|
+
position: fixed;
|
|
727
|
+
inset: 0;
|
|
728
|
+
pointer-events: none;
|
|
729
|
+
z-index: 4;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.popover {
|
|
733
|
+
position: fixed;
|
|
734
|
+
width: 320px;
|
|
735
|
+
max-height: min(70vh, 520px);
|
|
736
|
+
display: flex;
|
|
737
|
+
flex-direction: column;
|
|
738
|
+
background: #FFFFFF;
|
|
739
|
+
color: #111827;
|
|
740
|
+
border: 1px solid rgba(31, 81, 50, 0.18);
|
|
741
|
+
border-radius: 10px;
|
|
742
|
+
box-shadow: 0 18px 36px -12px rgba(15, 35, 24, 0.32),
|
|
743
|
+
0 4px 8px -2px rgba(15, 35, 24, 0.16);
|
|
744
|
+
font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
745
|
+
pointer-events: auto;
|
|
746
|
+
overflow: hidden;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.popover-header {
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
justify-content: space-between;
|
|
753
|
+
padding: 10px 12px;
|
|
754
|
+
background: #1F5132;
|
|
755
|
+
color: #F5FFF8;
|
|
756
|
+
}
|
|
757
|
+
.popover-title {
|
|
758
|
+
font-weight: 600;
|
|
759
|
+
font-size: 13px;
|
|
760
|
+
letter-spacing: 0.01em;
|
|
761
|
+
text-transform: capitalize;
|
|
762
|
+
}
|
|
763
|
+
.popover-close {
|
|
764
|
+
background: transparent;
|
|
765
|
+
color: inherit;
|
|
766
|
+
border: 0;
|
|
767
|
+
font-size: 18px;
|
|
768
|
+
line-height: 1;
|
|
769
|
+
padding: 2px 6px;
|
|
770
|
+
cursor: pointer;
|
|
771
|
+
border-radius: 4px;
|
|
772
|
+
}
|
|
773
|
+
.popover-close:hover { background: rgba(255,255,255,0.15); }
|
|
774
|
+
|
|
775
|
+
.popover-body {
|
|
776
|
+
padding: 12px;
|
|
777
|
+
display: flex;
|
|
778
|
+
flex-direction: column;
|
|
779
|
+
gap: 10px;
|
|
780
|
+
overflow-y: auto;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.popover-toolbar {
|
|
784
|
+
display: flex;
|
|
785
|
+
gap: 6px;
|
|
786
|
+
flex-wrap: wrap;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.popover-thread-list {
|
|
790
|
+
list-style: none;
|
|
791
|
+
margin: 0;
|
|
792
|
+
padding: 0;
|
|
793
|
+
display: flex;
|
|
794
|
+
flex-direction: column;
|
|
795
|
+
gap: 8px;
|
|
796
|
+
max-height: 200px;
|
|
797
|
+
overflow-y: auto;
|
|
798
|
+
}
|
|
799
|
+
.popover-comment {
|
|
800
|
+
border: 1px solid #E5E7EB;
|
|
801
|
+
border-radius: 8px;
|
|
802
|
+
padding: 8px 10px;
|
|
803
|
+
background: #F9FAFB;
|
|
804
|
+
}
|
|
805
|
+
.popover-comment-meta {
|
|
806
|
+
font-size: 11px;
|
|
807
|
+
font-weight: 600;
|
|
808
|
+
color: #374151;
|
|
809
|
+
margin-bottom: 2px;
|
|
810
|
+
}
|
|
811
|
+
.popover-comment-body { white-space: pre-wrap; word-break: break-word; }
|
|
812
|
+
.popover-empty {
|
|
813
|
+
font-size: 12px;
|
|
814
|
+
color: #6B7280;
|
|
815
|
+
font-style: italic;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.popover-identity {
|
|
819
|
+
display: grid;
|
|
820
|
+
grid-template-columns: 1fr 1fr;
|
|
821
|
+
gap: 6px;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.popover-input,
|
|
825
|
+
.popover-textarea {
|
|
826
|
+
width: 100%;
|
|
827
|
+
box-sizing: border-box;
|
|
828
|
+
padding: 8px 10px;
|
|
829
|
+
border: 1px solid #D1D5DB;
|
|
830
|
+
border-radius: 6px;
|
|
831
|
+
font: inherit;
|
|
832
|
+
color: inherit;
|
|
833
|
+
background: #FFFFFF;
|
|
834
|
+
resize: vertical;
|
|
835
|
+
}
|
|
836
|
+
.popover-input:focus,
|
|
837
|
+
.popover-textarea:focus {
|
|
838
|
+
outline: none;
|
|
839
|
+
border-color: #1F5132;
|
|
840
|
+
box-shadow: 0 0 0 3px rgba(31, 81, 50, 0.15);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.popover-actions {
|
|
844
|
+
display: flex;
|
|
845
|
+
justify-content: flex-end;
|
|
846
|
+
gap: 8px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.btn {
|
|
850
|
+
display: inline-flex;
|
|
851
|
+
align-items: center;
|
|
852
|
+
justify-content: center;
|
|
853
|
+
padding: 7px 14px;
|
|
854
|
+
border-radius: 6px;
|
|
855
|
+
border: 1px solid transparent;
|
|
856
|
+
font: 600 13px/1 inherit;
|
|
857
|
+
cursor: pointer;
|
|
858
|
+
}
|
|
859
|
+
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }
|
|
860
|
+
.btn-primary {
|
|
861
|
+
background: #1F5132;
|
|
862
|
+
color: #F5FFF8;
|
|
863
|
+
}
|
|
864
|
+
.btn-primary:hover { background: #295F3D; }
|
|
865
|
+
.btn-ghost {
|
|
866
|
+
background: transparent;
|
|
867
|
+
color: #1F5132;
|
|
868
|
+
border-color: rgba(31, 81, 50, 0.3);
|
|
869
|
+
}
|
|
870
|
+
.btn-ghost:hover { background: rgba(31, 81, 50, 0.05); }
|
|
871
|
+
.btn-toolbar {
|
|
872
|
+
background: rgba(31, 81, 50, 0.08);
|
|
873
|
+
color: #1F5132;
|
|
874
|
+
border-color: rgba(31, 81, 50, 0.18);
|
|
875
|
+
font-weight: 500;
|
|
876
|
+
font-size: 12px;
|
|
877
|
+
padding: 5px 10px;
|
|
878
|
+
}
|
|
879
|
+
.btn-toolbar:hover { background: rgba(31, 81, 50, 0.16); }
|
|
880
|
+
|
|
881
|
+
@media (prefers-color-scheme: dark) {
|
|
882
|
+
.popover {
|
|
883
|
+
background: #0F1F17;
|
|
884
|
+
color: #E5F2EB;
|
|
885
|
+
border-color: rgba(120, 200, 160, 0.22);
|
|
886
|
+
}
|
|
887
|
+
.popover-comment {
|
|
888
|
+
background: #102A20;
|
|
889
|
+
border-color: rgba(120, 200, 160, 0.18);
|
|
890
|
+
}
|
|
891
|
+
.popover-comment-meta { color: #B6D8C5; }
|
|
892
|
+
.popover-empty { color: #94B3A4; }
|
|
893
|
+
.popover-input,
|
|
894
|
+
.popover-textarea {
|
|
895
|
+
background: #102A20;
|
|
896
|
+
color: inherit;
|
|
897
|
+
border-color: rgba(120, 200, 160, 0.22);
|
|
898
|
+
}
|
|
899
|
+
.btn-ghost { color: #B6D8C5; border-color: rgba(120, 200, 160, 0.3); }
|
|
900
|
+
.btn-ghost:hover { background: rgba(120, 200, 160, 0.08); }
|
|
901
|
+
.btn-toolbar {
|
|
902
|
+
background: rgba(120, 200, 160, 0.12);
|
|
903
|
+
color: #B6D8C5;
|
|
904
|
+
border-color: rgba(120, 200, 160, 0.22);
|
|
905
|
+
}
|
|
906
|
+
.btn-toolbar:hover { background: rgba(120, 200, 160, 0.2); }
|
|
907
|
+
}
|
|
908
|
+
`;
|
|
909
|
+
var Overlay = class {
|
|
910
|
+
constructor() {
|
|
911
|
+
__publicField(this, "host");
|
|
912
|
+
__publicField(this, "root");
|
|
913
|
+
__publicField(this, "highlight");
|
|
914
|
+
__publicField(this, "hud");
|
|
915
|
+
__publicField(this, "pinLayer");
|
|
916
|
+
__publicField(this, "popoverLayer");
|
|
917
|
+
__publicField(this, "popover");
|
|
918
|
+
__publicField(this, "launcher", null);
|
|
919
|
+
__publicField(this, "rafHandle", null);
|
|
920
|
+
__publicField(this, "pendingFeedback", []);
|
|
921
|
+
__publicField(this, "onPinClick", null);
|
|
922
|
+
__publicField(this, "onPinDragEnd", null);
|
|
923
|
+
__publicField(this, "draggingPin", null);
|
|
924
|
+
this.host = document.createElement("div");
|
|
925
|
+
this.host.setAttribute(HOST_ATTR, "");
|
|
926
|
+
this.host.style.cssText = [
|
|
927
|
+
"position: fixed",
|
|
928
|
+
"inset: 0",
|
|
929
|
+
"width: 0",
|
|
930
|
+
"height: 0",
|
|
931
|
+
"pointer-events: none",
|
|
932
|
+
"z-index: 2147483647"
|
|
933
|
+
].join(";");
|
|
934
|
+
document.body.appendChild(this.host);
|
|
935
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
936
|
+
const style = document.createElement("style");
|
|
937
|
+
style.textContent = OVERLAY_STYLES;
|
|
938
|
+
this.root.appendChild(style);
|
|
939
|
+
this.pinLayer = makeLayer("pinLayer");
|
|
940
|
+
this.highlight = makeLayer("highlight", "div");
|
|
941
|
+
this.hud = makeLayer("hud", "div");
|
|
942
|
+
this.popoverLayer = makeLayer("popover-layer");
|
|
943
|
+
this.root.append(this.pinLayer, this.highlight, this.hud, this.popoverLayer);
|
|
944
|
+
this.popover = new PopoverManager(this.popoverLayer);
|
|
945
|
+
}
|
|
946
|
+
/** Called by the controller when the user enters/leaves feedback mode. */
|
|
947
|
+
setEnabledStyles(enabled) {
|
|
948
|
+
if (!enabled) {
|
|
949
|
+
this.hideHighlight();
|
|
950
|
+
this.hideHud();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
showHighlight(target) {
|
|
954
|
+
const rect = target.getBoundingClientRect();
|
|
955
|
+
this.highlight.style.display = "block";
|
|
956
|
+
this.highlight.style.left = `${rect.left}px`;
|
|
957
|
+
this.highlight.style.top = `${rect.top}px`;
|
|
958
|
+
this.highlight.style.width = `${rect.width}px`;
|
|
959
|
+
this.highlight.style.height = `${rect.height}px`;
|
|
960
|
+
}
|
|
961
|
+
hideHighlight() {
|
|
962
|
+
this.highlight.style.display = "none";
|
|
963
|
+
}
|
|
964
|
+
showHud(target) {
|
|
965
|
+
const tag = target.tagName.toLowerCase();
|
|
966
|
+
const cls = (target.getAttribute("class") || "").trim();
|
|
967
|
+
const fid = target.getAttribute("data-feedback-id");
|
|
968
|
+
const label = fid ? `${tag} [data-feedback-id="${fid}"]` : cls ? `${tag}.${cls.split(/\s+/).slice(0, 2).join(".")}` : tag;
|
|
969
|
+
const rect = target.getBoundingClientRect();
|
|
970
|
+
this.hud.textContent = label;
|
|
971
|
+
this.hud.style.display = "block";
|
|
972
|
+
const hudHeight = 28;
|
|
973
|
+
const top = rect.top - hudHeight - 6 > 0 ? rect.top - hudHeight - 6 : Math.min(rect.bottom + 6, window.innerHeight - hudHeight);
|
|
974
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - 320));
|
|
975
|
+
this.hud.style.top = `${top}px`;
|
|
976
|
+
this.hud.style.left = `${left}px`;
|
|
977
|
+
}
|
|
978
|
+
hideHud() {
|
|
979
|
+
this.hud.style.display = "none";
|
|
980
|
+
}
|
|
981
|
+
/** True if the event originated from the SDK's own UI (so we ignore it). */
|
|
982
|
+
ownsEvent(event) {
|
|
983
|
+
const path = event.composedPath();
|
|
984
|
+
if (path.includes(this.host)) return true;
|
|
985
|
+
if (path.some((n) => n === this.root)) return true;
|
|
986
|
+
if (this.launcher) {
|
|
987
|
+
for (const node of path) {
|
|
988
|
+
if (this.launcher.ownsNode(node)) return true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
/** True if the event originated from the popover specifically. */
|
|
994
|
+
popoverOwnsEvent(event) {
|
|
995
|
+
return this.popover.ownsEvent(event);
|
|
996
|
+
}
|
|
997
|
+
/** Render the given feedback list as pins. */
|
|
998
|
+
renderPins(feedbacks, onClick, onDragEnd = null) {
|
|
999
|
+
this.onPinClick = onClick;
|
|
1000
|
+
this.onPinDragEnd = onDragEnd;
|
|
1001
|
+
this.pendingFeedback = feedbacks;
|
|
1002
|
+
this.scheduleRepositionPins();
|
|
1003
|
+
}
|
|
1004
|
+
/** Re-position pins after layout changes. Called on resize/scroll. */
|
|
1005
|
+
reposition() {
|
|
1006
|
+
this.scheduleRepositionPins();
|
|
1007
|
+
this.popover.reposition();
|
|
1008
|
+
}
|
|
1009
|
+
popoverManager() {
|
|
1010
|
+
return this.popover;
|
|
1011
|
+
}
|
|
1012
|
+
/** Wire the floating launcher to controller callbacks. Idempotent. */
|
|
1013
|
+
installLauncher(callbacks) {
|
|
1014
|
+
if (this.launcher) return this.launcher;
|
|
1015
|
+
this.launcher = new LauncherManager(this.root, callbacks);
|
|
1016
|
+
return this.launcher;
|
|
1017
|
+
}
|
|
1018
|
+
/** Show or hide the pin layer entirely (e.g. via the eye toggle). */
|
|
1019
|
+
setPinsVisible(visible) {
|
|
1020
|
+
this.pinLayer.classList.toggle("hidden", !visible);
|
|
1021
|
+
}
|
|
1022
|
+
scheduleRepositionPins() {
|
|
1023
|
+
if (this.rafHandle !== null) return;
|
|
1024
|
+
this.rafHandle = requestAnimationFrame(() => {
|
|
1025
|
+
this.rafHandle = null;
|
|
1026
|
+
this.layoutPins();
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
layoutPins() {
|
|
1030
|
+
const next = document.createDocumentFragment();
|
|
1031
|
+
for (const fb of this.pendingFeedback) {
|
|
1032
|
+
const target = findElement(fb.selector);
|
|
1033
|
+
const projected = projectCoordinates(target, fb.coordinates);
|
|
1034
|
+
const pin = document.createElement("button");
|
|
1035
|
+
pin.type = "button";
|
|
1036
|
+
const classes = ["pin"];
|
|
1037
|
+
if (projected.orphaned) classes.push("orphaned");
|
|
1038
|
+
if (fb.status === "resolved") classes.push("resolved");
|
|
1039
|
+
if (fb.status === "archived") classes.push("archived");
|
|
1040
|
+
pin.className = classes.join(" ");
|
|
1041
|
+
pin.setAttribute("data-feedback-id", fb.id);
|
|
1042
|
+
pin.setAttribute("aria-label", `Feedback ${fb.id}`);
|
|
1043
|
+
pin.style.left = `${projected.x - window.scrollX}px`;
|
|
1044
|
+
pin.style.top = `${projected.y - window.scrollY}px`;
|
|
1045
|
+
const label = document.createElement("span");
|
|
1046
|
+
label.textContent = String(fb.thread.length || 1);
|
|
1047
|
+
pin.appendChild(label);
|
|
1048
|
+
pin.addEventListener("click", (e) => {
|
|
1049
|
+
e.stopPropagation();
|
|
1050
|
+
if (this.draggingPin === fb.id) return;
|
|
1051
|
+
this.onPinClick?.(fb);
|
|
1052
|
+
});
|
|
1053
|
+
this.attachDragHandlers(pin, fb);
|
|
1054
|
+
next.appendChild(pin);
|
|
1055
|
+
}
|
|
1056
|
+
this.pinLayer.replaceChildren(next);
|
|
1057
|
+
}
|
|
1058
|
+
destroy() {
|
|
1059
|
+
if (this.rafHandle !== null) {
|
|
1060
|
+
cancelAnimationFrame(this.rafHandle);
|
|
1061
|
+
this.rafHandle = null;
|
|
1062
|
+
}
|
|
1063
|
+
this.popover.hide();
|
|
1064
|
+
this.launcher?.destroy();
|
|
1065
|
+
this.launcher = null;
|
|
1066
|
+
this.host.remove();
|
|
1067
|
+
}
|
|
1068
|
+
attachDragHandlers(pin, fb) {
|
|
1069
|
+
let startClientX = 0;
|
|
1070
|
+
let startClientY = 0;
|
|
1071
|
+
let isDragging = false;
|
|
1072
|
+
let pointerId = null;
|
|
1073
|
+
let originLeft = 0;
|
|
1074
|
+
let originTop = 0;
|
|
1075
|
+
const DRAG_THRESHOLD = 4;
|
|
1076
|
+
const onPointerMove = (e) => {
|
|
1077
|
+
if (pointerId === null || e.pointerId !== pointerId) return;
|
|
1078
|
+
const dx = e.clientX - startClientX;
|
|
1079
|
+
const dy = e.clientY - startClientY;
|
|
1080
|
+
if (!isDragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
1081
|
+
if (!isDragging) {
|
|
1082
|
+
isDragging = true;
|
|
1083
|
+
this.draggingPin = fb.id;
|
|
1084
|
+
pin.classList.add("dragging");
|
|
1085
|
+
}
|
|
1086
|
+
const nextLeft = Math.max(0, Math.min(window.innerWidth, originLeft + dx));
|
|
1087
|
+
const nextTop = Math.max(0, Math.min(window.innerHeight, originTop + dy));
|
|
1088
|
+
pin.style.left = `${nextLeft}px`;
|
|
1089
|
+
pin.style.top = `${nextTop}px`;
|
|
1090
|
+
};
|
|
1091
|
+
const finish = (e) => {
|
|
1092
|
+
if (pointerId === null || e.pointerId !== pointerId) return;
|
|
1093
|
+
pin.removeEventListener("pointermove", onPointerMove);
|
|
1094
|
+
pin.removeEventListener("pointerup", finish);
|
|
1095
|
+
pin.removeEventListener("pointercancel", finish);
|
|
1096
|
+
try {
|
|
1097
|
+
pin.releasePointerCapture(pointerId);
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
pointerId = null;
|
|
1101
|
+
pin.classList.remove("dragging");
|
|
1102
|
+
if (!isDragging) return;
|
|
1103
|
+
const target = findElement(fb.selector);
|
|
1104
|
+
const rect = pin.getBoundingClientRect();
|
|
1105
|
+
const tipX = rect.left + rect.width / 2;
|
|
1106
|
+
const tipY = rect.top + rect.height;
|
|
1107
|
+
const next = {
|
|
1108
|
+
xPx: Math.round(tipX + window.scrollX),
|
|
1109
|
+
yPx: Math.round(tipY + window.scrollY),
|
|
1110
|
+
xPercent: 0,
|
|
1111
|
+
yPercent: 0
|
|
1112
|
+
};
|
|
1113
|
+
if (target) {
|
|
1114
|
+
const t = target.getBoundingClientRect();
|
|
1115
|
+
next.xPercent = clamp012((tipX - t.left) / (t.width || 1));
|
|
1116
|
+
next.yPercent = clamp012((tipY - t.top) / (t.height || 1));
|
|
1117
|
+
} else {
|
|
1118
|
+
next.xPercent = clamp012(tipX / (window.innerWidth || 1));
|
|
1119
|
+
next.yPercent = clamp012(tipY / (window.innerHeight || 1));
|
|
1120
|
+
}
|
|
1121
|
+
this.onPinDragEnd?.(fb.id, next);
|
|
1122
|
+
const releasedId = fb.id;
|
|
1123
|
+
requestAnimationFrame(() => {
|
|
1124
|
+
if (this.draggingPin === releasedId) this.draggingPin = null;
|
|
1125
|
+
});
|
|
1126
|
+
isDragging = false;
|
|
1127
|
+
};
|
|
1128
|
+
pin.addEventListener("pointerdown", (e) => {
|
|
1129
|
+
if (e.button !== 0) return;
|
|
1130
|
+
pointerId = e.pointerId;
|
|
1131
|
+
startClientX = e.clientX;
|
|
1132
|
+
startClientY = e.clientY;
|
|
1133
|
+
const rect = pin.getBoundingClientRect();
|
|
1134
|
+
originLeft = rect.left;
|
|
1135
|
+
originTop = rect.top;
|
|
1136
|
+
try {
|
|
1137
|
+
pin.setPointerCapture(e.pointerId);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
pin.addEventListener("pointermove", onPointerMove);
|
|
1141
|
+
pin.addEventListener("pointerup", finish);
|
|
1142
|
+
pin.addEventListener("pointercancel", finish);
|
|
1143
|
+
e.preventDefault();
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
function clamp012(n) {
|
|
1148
|
+
if (Number.isNaN(n)) return 0;
|
|
1149
|
+
if (n < 0) return 0;
|
|
1150
|
+
if (n > 1) return 1;
|
|
1151
|
+
return n;
|
|
1152
|
+
}
|
|
1153
|
+
function makeLayer(className, tag = "div") {
|
|
1154
|
+
const el = document.createElement(tag);
|
|
1155
|
+
el.className = className;
|
|
1156
|
+
if (className === "pinLayer" || className === "popover-layer") {
|
|
1157
|
+
el.classList.add("layer");
|
|
1158
|
+
}
|
|
1159
|
+
return el;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/screenshot.ts
|
|
1163
|
+
var HOST_ATTR2 = "data-mahmulp-feedback-host";
|
|
1164
|
+
var DEFAULT_OPTIONS = {
|
|
1165
|
+
mimeType: "image/png",
|
|
1166
|
+
quality: 0.92,
|
|
1167
|
+
scale: 1
|
|
1168
|
+
};
|
|
1169
|
+
async function captureViewport(options = {}) {
|
|
1170
|
+
if (typeof window === "undefined" || typeof document === "undefined") return null;
|
|
1171
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
1172
|
+
const html2canvas = await loadHtml2Canvas();
|
|
1173
|
+
if (!html2canvas) return null;
|
|
1174
|
+
const host = document.querySelector(`[${HOST_ATTR2}]`);
|
|
1175
|
+
const previousVisibility = host?.style.visibility ?? "";
|
|
1176
|
+
if (host) host.style.visibility = "hidden";
|
|
1177
|
+
try {
|
|
1178
|
+
const canvas = await html2canvas(document.documentElement, {
|
|
1179
|
+
backgroundColor: null,
|
|
1180
|
+
scale: opts.scale,
|
|
1181
|
+
width: window.innerWidth,
|
|
1182
|
+
height: window.innerHeight,
|
|
1183
|
+
x: window.scrollX,
|
|
1184
|
+
y: window.scrollY,
|
|
1185
|
+
windowWidth: window.innerWidth,
|
|
1186
|
+
windowHeight: window.innerHeight,
|
|
1187
|
+
logging: false,
|
|
1188
|
+
useCORS: true,
|
|
1189
|
+
allowTaint: false
|
|
1190
|
+
});
|
|
1191
|
+
return await canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
1192
|
+
} catch {
|
|
1193
|
+
return null;
|
|
1194
|
+
} finally {
|
|
1195
|
+
if (host) host.style.visibility = previousVisibility;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
var html2canvasPromise = null;
|
|
1199
|
+
async function loadHtml2Canvas() {
|
|
1200
|
+
if (html2canvasPromise) return html2canvasPromise;
|
|
1201
|
+
html2canvasPromise = (async () => {
|
|
1202
|
+
try {
|
|
1203
|
+
const mod = await import('html2canvas');
|
|
1204
|
+
return mod.default ?? mod ?? null;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
})();
|
|
1209
|
+
return html2canvasPromise;
|
|
1210
|
+
}
|
|
1211
|
+
function canvasToBlob(canvas, mimeType, quality) {
|
|
1212
|
+
return new Promise((resolve) => {
|
|
1213
|
+
if (typeof canvas.toBlob === "function") {
|
|
1214
|
+
canvas.toBlob(resolve, mimeType, quality);
|
|
1215
|
+
} else {
|
|
1216
|
+
try {
|
|
1217
|
+
const dataUrl = canvas.toDataURL(mimeType, quality);
|
|
1218
|
+
const [meta, b64] = dataUrl.split(",");
|
|
1219
|
+
const bin = atob(b64 ?? "");
|
|
1220
|
+
const arr = new Uint8Array(bin.length);
|
|
1221
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
1222
|
+
const detectedMime = meta?.match(/data:([^;]+);/)?.[1] ?? mimeType;
|
|
1223
|
+
resolve(new Blob([arr], { type: detectedMime }));
|
|
1224
|
+
} catch {
|
|
1225
|
+
resolve(null);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/transport.ts
|
|
1232
|
+
function createHttpTransport(options) {
|
|
1233
|
+
if (!options.apiKey) {
|
|
1234
|
+
throw new Error("createHttpTransport: `apiKey` is required");
|
|
1235
|
+
}
|
|
1236
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1237
|
+
const base = options.apiUrl.replace(/\/$/, "");
|
|
1238
|
+
function jsonHeaders() {
|
|
1239
|
+
return {
|
|
1240
|
+
"content-type": "application/json",
|
|
1241
|
+
accept: "application/json",
|
|
1242
|
+
"x-feedback-key": options.apiKey,
|
|
1243
|
+
...options.headers ?? {}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function authHeaders() {
|
|
1247
|
+
return { "x-feedback-key": options.apiKey, ...options.headers ?? {} };
|
|
1248
|
+
}
|
|
1249
|
+
async function asJson(res) {
|
|
1250
|
+
if (!res.ok) {
|
|
1251
|
+
const text = await res.text().catch(() => "");
|
|
1252
|
+
throw new Error(`feedback-sdk: ${res.status} ${res.statusText} ${text}`);
|
|
1253
|
+
}
|
|
1254
|
+
return await res.json();
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
async list(query) {
|
|
1258
|
+
const params = new URLSearchParams();
|
|
1259
|
+
if (query.pageUrl) params.set("pageUrl", query.pageUrl);
|
|
1260
|
+
if (query.status) params.set("status", query.status);
|
|
1261
|
+
const qs = params.toString();
|
|
1262
|
+
const res = await fetchImpl(`${base}/v1/feedback${qs ? `?${qs}` : ""}`, {
|
|
1263
|
+
method: "GET",
|
|
1264
|
+
headers: jsonHeaders()
|
|
1265
|
+
});
|
|
1266
|
+
return asJson(res);
|
|
1267
|
+
},
|
|
1268
|
+
async create(input) {
|
|
1269
|
+
const { projectId: _ignored, ...payload } = input;
|
|
1270
|
+
const res = await fetchImpl(`${base}/v1/feedback`, {
|
|
1271
|
+
method: "POST",
|
|
1272
|
+
headers: jsonHeaders(),
|
|
1273
|
+
body: JSON.stringify(payload)
|
|
1274
|
+
});
|
|
1275
|
+
return asJson(res);
|
|
1276
|
+
},
|
|
1277
|
+
async reply(feedbackId, comment) {
|
|
1278
|
+
const res = await fetchImpl(`${base}/v1/feedback/${encodeURIComponent(feedbackId)}/comments`, {
|
|
1279
|
+
method: "POST",
|
|
1280
|
+
headers: jsonHeaders(),
|
|
1281
|
+
body: JSON.stringify(comment)
|
|
1282
|
+
});
|
|
1283
|
+
return asJson(res);
|
|
1284
|
+
},
|
|
1285
|
+
async setStatus(feedbackId, status) {
|
|
1286
|
+
const res = await fetchImpl(`${base}/v1/feedback/${encodeURIComponent(feedbackId)}`, {
|
|
1287
|
+
method: "PATCH",
|
|
1288
|
+
headers: jsonHeaders(),
|
|
1289
|
+
body: JSON.stringify({ status })
|
|
1290
|
+
});
|
|
1291
|
+
return asJson(res);
|
|
1292
|
+
},
|
|
1293
|
+
async move(feedbackId, coordinates) {
|
|
1294
|
+
const res = await fetchImpl(
|
|
1295
|
+
`${base}/v1/feedback/${encodeURIComponent(feedbackId)}/coordinates`,
|
|
1296
|
+
{
|
|
1297
|
+
method: "PATCH",
|
|
1298
|
+
headers: jsonHeaders(),
|
|
1299
|
+
body: JSON.stringify({ coordinates })
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
return asJson(res);
|
|
1303
|
+
},
|
|
1304
|
+
async uploadScreenshot(feedbackId, blob) {
|
|
1305
|
+
const form = new FormData();
|
|
1306
|
+
form.append("file", blob, `${feedbackId}.png`);
|
|
1307
|
+
const res = await fetchImpl(
|
|
1308
|
+
`${base}/v1/feedback/${encodeURIComponent(feedbackId)}/screenshot`,
|
|
1309
|
+
{
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
headers: authHeaders(),
|
|
1312
|
+
body: form
|
|
1313
|
+
}
|
|
1314
|
+
);
|
|
1315
|
+
return asJson(res);
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/controller.ts
|
|
1321
|
+
function initFeedback(options) {
|
|
1322
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1323
|
+
return ssrNoopController();
|
|
1324
|
+
}
|
|
1325
|
+
const transport = options.transport ?? (() => {
|
|
1326
|
+
if (!options.apiUrl) {
|
|
1327
|
+
throw new Error("initFeedback: provide either `transport` or `apiUrl`");
|
|
1328
|
+
}
|
|
1329
|
+
if (!options.apiKey) {
|
|
1330
|
+
throw new Error("initFeedback: `apiKey` is required when using the default HTTP transport");
|
|
1331
|
+
}
|
|
1332
|
+
return createHttpTransport({ apiUrl: options.apiUrl, apiKey: options.apiKey });
|
|
1333
|
+
})();
|
|
1334
|
+
const namespace = options.cacheNamespace ?? (options.apiKey ? options.apiKey.slice(0, 12) : "default");
|
|
1335
|
+
const overlay = new Overlay();
|
|
1336
|
+
const state = {
|
|
1337
|
+
enabled: !!options.enabled,
|
|
1338
|
+
hoverTarget: null,
|
|
1339
|
+
candidate: null,
|
|
1340
|
+
feedbacks: [],
|
|
1341
|
+
destroyed: false
|
|
1342
|
+
};
|
|
1343
|
+
let pendingPin = null;
|
|
1344
|
+
let activeThreadId = null;
|
|
1345
|
+
const getPageUrl = options.getPageUrl ?? (() => window.location.pathname + window.location.search);
|
|
1346
|
+
const modKey = options.selectParentModifier ?? "Alt";
|
|
1347
|
+
const getAuthor = options.getAuthor ?? (() => loadAuthor(namespace));
|
|
1348
|
+
const setAuthor = options.setAuthor ?? ((author) => {
|
|
1349
|
+
saveAuthor(namespace, author);
|
|
1350
|
+
});
|
|
1351
|
+
const wantsScreenshots = options.captureScreenshots !== false;
|
|
1352
|
+
const captureFn = options.captureScreenshot ?? (() => captureViewport());
|
|
1353
|
+
function isModifierActive(e) {
|
|
1354
|
+
switch (modKey) {
|
|
1355
|
+
case "Alt":
|
|
1356
|
+
return e.altKey;
|
|
1357
|
+
case "Shift":
|
|
1358
|
+
return e.shiftKey;
|
|
1359
|
+
case "Meta":
|
|
1360
|
+
return e.metaKey;
|
|
1361
|
+
case "Control":
|
|
1362
|
+
return e.ctrlKey;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function reportError(err) {
|
|
1366
|
+
if (options.onError) {
|
|
1367
|
+
try {
|
|
1368
|
+
options.onError(err);
|
|
1369
|
+
} catch {
|
|
1370
|
+
}
|
|
1371
|
+
} else if (typeof console !== "undefined") {
|
|
1372
|
+
console.error("[feedback-sdk]", err);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function onMouseMove(e) {
|
|
1376
|
+
if (!state.enabled) return;
|
|
1377
|
+
if (overlay.ownsEvent(e)) return;
|
|
1378
|
+
if (pendingPin || activeThreadId) return;
|
|
1379
|
+
const target = elementAtFiltered(e.clientX, e.clientY, overlay);
|
|
1380
|
+
if (!target) {
|
|
1381
|
+
state.hoverTarget = null;
|
|
1382
|
+
overlay.hideHighlight();
|
|
1383
|
+
overlay.hideHud();
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
state.hoverTarget = target;
|
|
1387
|
+
state.candidate = target;
|
|
1388
|
+
overlay.showHighlight(target);
|
|
1389
|
+
overlay.showHud(target);
|
|
1390
|
+
}
|
|
1391
|
+
function onClick(e) {
|
|
1392
|
+
if (overlay.popoverOwnsEvent(e)) return;
|
|
1393
|
+
if (!state.enabled) return;
|
|
1394
|
+
if (overlay.ownsEvent(e)) return;
|
|
1395
|
+
if (isModifierActive(e)) {
|
|
1396
|
+
const next = state.candidate?.parentElement ?? null;
|
|
1397
|
+
if (next && next !== document.body && next !== document.documentElement) {
|
|
1398
|
+
state.candidate = next;
|
|
1399
|
+
overlay.showHighlight(next);
|
|
1400
|
+
overlay.showHud(next);
|
|
1401
|
+
}
|
|
1402
|
+
e.preventDefault();
|
|
1403
|
+
e.stopPropagation();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const target = state.candidate ?? state.hoverTarget;
|
|
1407
|
+
if (!target) return;
|
|
1408
|
+
e.preventDefault();
|
|
1409
|
+
e.stopPropagation();
|
|
1410
|
+
openComposerAt(target, e.clientX, e.clientY);
|
|
1411
|
+
}
|
|
1412
|
+
function onKeyDown(e) {
|
|
1413
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === "F" || e.key === "f")) {
|
|
1414
|
+
e.preventDefault();
|
|
1415
|
+
e.stopPropagation();
|
|
1416
|
+
if (!launcherState.launcherVisible) {
|
|
1417
|
+
launcherState.launcherVisible = true;
|
|
1418
|
+
syncLauncher();
|
|
1419
|
+
}
|
|
1420
|
+
publicSetEnabled(!state.enabled);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (e.key === "Escape") {
|
|
1424
|
+
if (pendingPin || activeThreadId) {
|
|
1425
|
+
cancelComposer();
|
|
1426
|
+
closeThread();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (state.enabled) {
|
|
1430
|
+
state.candidate = null;
|
|
1431
|
+
state.hoverTarget = null;
|
|
1432
|
+
overlay.hideHighlight();
|
|
1433
|
+
overlay.hideHud();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function onWindowReposition() {
|
|
1438
|
+
overlay.reposition();
|
|
1439
|
+
}
|
|
1440
|
+
function openComposerAt(target, clientX, clientY) {
|
|
1441
|
+
const selector = resolveSelector(target);
|
|
1442
|
+
const coordinates = computeCoordinates(target, clientX, clientY);
|
|
1443
|
+
pendingPin = {
|
|
1444
|
+
selector,
|
|
1445
|
+
coordinates,
|
|
1446
|
+
pageX: clientX + window.scrollX,
|
|
1447
|
+
pageY: clientY + window.scrollY
|
|
1448
|
+
};
|
|
1449
|
+
overlay.hideHighlight();
|
|
1450
|
+
overlay.hideHud();
|
|
1451
|
+
let screenshotPromise = null;
|
|
1452
|
+
if (wantsScreenshots) {
|
|
1453
|
+
screenshotPromise = captureFn().catch(() => null);
|
|
1454
|
+
}
|
|
1455
|
+
overlay.popoverManager().showComposer(
|
|
1456
|
+
{ pageX: pendingPin.pageX, pageY: pendingPin.pageY },
|
|
1457
|
+
{
|
|
1458
|
+
initialAuthor: getAuthor(),
|
|
1459
|
+
onSubmit: async (body, author) => {
|
|
1460
|
+
const pin = pendingPin;
|
|
1461
|
+
if (!pin) return;
|
|
1462
|
+
try {
|
|
1463
|
+
const fb = await createPin(pin, { author, body });
|
|
1464
|
+
setAuthor(author);
|
|
1465
|
+
options.onPinCreate?.(fb);
|
|
1466
|
+
pendingPin = null;
|
|
1467
|
+
overlay.popoverManager().hide();
|
|
1468
|
+
if (screenshotPromise && transport.uploadScreenshot) {
|
|
1469
|
+
void screenshotPromise.then(async (blob) => {
|
|
1470
|
+
if (!blob) return;
|
|
1471
|
+
const updated = await transport.uploadScreenshot(fb.id, blob);
|
|
1472
|
+
replaceFeedback(updated);
|
|
1473
|
+
}).catch((err) => reportError(err));
|
|
1474
|
+
}
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
reportError(err);
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
onCancel: () => cancelComposer()
|
|
1480
|
+
}
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
function cancelComposer() {
|
|
1484
|
+
if (!pendingPin) return;
|
|
1485
|
+
pendingPin = null;
|
|
1486
|
+
overlay.popoverManager().hide();
|
|
1487
|
+
}
|
|
1488
|
+
function openThread(fb) {
|
|
1489
|
+
activeThreadId = fb.id;
|
|
1490
|
+
const anchor = computeThreadAnchor(fb);
|
|
1491
|
+
overlay.popoverManager().showThread(fb, anchor, {
|
|
1492
|
+
initialAuthor: getAuthor(),
|
|
1493
|
+
onReply: async (body, author) => {
|
|
1494
|
+
try {
|
|
1495
|
+
const updated = await transport.reply(fb.id, { author, body });
|
|
1496
|
+
setAuthor(author);
|
|
1497
|
+
replaceFeedback(updated);
|
|
1498
|
+
overlay.popoverManager().hide();
|
|
1499
|
+
activeThreadId = null;
|
|
1500
|
+
openThread(updated);
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
reportError(err);
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
onStatus: async (status) => {
|
|
1506
|
+
try {
|
|
1507
|
+
const updated = await transport.setStatus(fb.id, status);
|
|
1508
|
+
replaceFeedback(updated);
|
|
1509
|
+
overlay.popoverManager().hide();
|
|
1510
|
+
activeThreadId = null;
|
|
1511
|
+
openThread(updated);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
reportError(err);
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
onClose: () => closeThread()
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
function closeThread() {
|
|
1520
|
+
if (!activeThreadId) return;
|
|
1521
|
+
activeThreadId = null;
|
|
1522
|
+
overlay.popoverManager().hide();
|
|
1523
|
+
}
|
|
1524
|
+
function computeThreadAnchor(fb) {
|
|
1525
|
+
const target = findElement(fb.selector);
|
|
1526
|
+
if (target) {
|
|
1527
|
+
const rect = target.getBoundingClientRect();
|
|
1528
|
+
return {
|
|
1529
|
+
pageX: rect.left + window.scrollX + rect.width * fb.coordinates.xPercent,
|
|
1530
|
+
pageY: rect.top + window.scrollY + rect.height * fb.coordinates.yPercent
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
return { pageX: fb.coordinates.xPx, pageY: fb.coordinates.yPx };
|
|
1534
|
+
}
|
|
1535
|
+
async function createPin(pin, comment) {
|
|
1536
|
+
const viewport = getViewportInfo();
|
|
1537
|
+
const pageUrl = getPageUrl();
|
|
1538
|
+
const input = {
|
|
1539
|
+
projectId: "",
|
|
1540
|
+
// server resolves from API key — kept here only to satisfy the type
|
|
1541
|
+
pageUrl,
|
|
1542
|
+
selector: pin.selector,
|
|
1543
|
+
coordinates: pin.coordinates,
|
|
1544
|
+
viewport,
|
|
1545
|
+
comment
|
|
1546
|
+
};
|
|
1547
|
+
const fb = await transport.create(input);
|
|
1548
|
+
state.feedbacks = [...state.feedbacks, fb];
|
|
1549
|
+
overlay.renderPins(state.feedbacks, openThread, onPinDragEnd);
|
|
1550
|
+
return fb;
|
|
1551
|
+
}
|
|
1552
|
+
function replaceFeedback(updated) {
|
|
1553
|
+
state.feedbacks = state.feedbacks.map((f) => f.id === updated.id ? updated : f);
|
|
1554
|
+
overlay.renderPins(state.feedbacks, openThread, onPinDragEnd);
|
|
1555
|
+
}
|
|
1556
|
+
async function refresh() {
|
|
1557
|
+
try {
|
|
1558
|
+
const result = await transport.list({ projectId: "" });
|
|
1559
|
+
state.feedbacks = result.items;
|
|
1560
|
+
overlay.renderPins(state.feedbacks, openThread, onPinDragEnd);
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
reportError(err);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function onPinDragEnd(id, next) {
|
|
1566
|
+
const previous = state.feedbacks.find((f) => f.id === id);
|
|
1567
|
+
if (!previous) return;
|
|
1568
|
+
const optimistic = {
|
|
1569
|
+
...previous,
|
|
1570
|
+
coordinates: { ...previous.coordinates, ...next },
|
|
1571
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1572
|
+
};
|
|
1573
|
+
replaceFeedback(optimistic);
|
|
1574
|
+
if (!transport.move) return;
|
|
1575
|
+
try {
|
|
1576
|
+
const persisted = await transport.move(id, optimistic.coordinates);
|
|
1577
|
+
replaceFeedback(persisted);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
replaceFeedback(previous);
|
|
1580
|
+
reportError(err);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
document.addEventListener("mousemove", onMouseMove, true);
|
|
1584
|
+
document.addEventListener("click", onClick, true);
|
|
1585
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1586
|
+
window.addEventListener("scroll", onWindowReposition, true);
|
|
1587
|
+
window.addEventListener("resize", onWindowReposition);
|
|
1588
|
+
void refresh();
|
|
1589
|
+
overlay.setEnabledStyles(state.enabled);
|
|
1590
|
+
const wantsLauncher = options.showLauncher !== false;
|
|
1591
|
+
const launcherState = {
|
|
1592
|
+
pinsVisible: options.pinsVisible !== false,
|
|
1593
|
+
launcherVisible: true
|
|
1594
|
+
};
|
|
1595
|
+
overlay.setPinsVisible(launcherState.pinsVisible);
|
|
1596
|
+
function syncLauncher() {
|
|
1597
|
+
launcher?.setEnabled(state.enabled);
|
|
1598
|
+
launcher?.setPinsVisible(launcherState.pinsVisible);
|
|
1599
|
+
launcher?.setLauncherVisible(launcherState.launcherVisible);
|
|
1600
|
+
}
|
|
1601
|
+
const launcher = wantsLauncher ? overlay.installLauncher({
|
|
1602
|
+
onToggleFeedback: () => {
|
|
1603
|
+
publicSetEnabled(!state.enabled);
|
|
1604
|
+
},
|
|
1605
|
+
onTogglePins: () => {
|
|
1606
|
+
publicSetPinsVisible(!launcherState.pinsVisible);
|
|
1607
|
+
},
|
|
1608
|
+
onHideLauncher: () => {
|
|
1609
|
+
publicSetLauncherVisible(!launcherState.launcherVisible);
|
|
1610
|
+
}
|
|
1611
|
+
}) : null;
|
|
1612
|
+
syncLauncher();
|
|
1613
|
+
function publicSetEnabled(enabled) {
|
|
1614
|
+
if (state.destroyed) return;
|
|
1615
|
+
state.enabled = enabled;
|
|
1616
|
+
overlay.setEnabledStyles(enabled);
|
|
1617
|
+
if (!enabled) cancelComposer();
|
|
1618
|
+
syncLauncher();
|
|
1619
|
+
}
|
|
1620
|
+
function publicSetPinsVisible(visible) {
|
|
1621
|
+
if (state.destroyed) return;
|
|
1622
|
+
launcherState.pinsVisible = visible;
|
|
1623
|
+
overlay.setPinsVisible(visible);
|
|
1624
|
+
syncLauncher();
|
|
1625
|
+
}
|
|
1626
|
+
function publicSetLauncherVisible(visible) {
|
|
1627
|
+
if (state.destroyed) return;
|
|
1628
|
+
launcherState.launcherVisible = visible;
|
|
1629
|
+
syncLauncher();
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
setEnabled: publicSetEnabled,
|
|
1633
|
+
isEnabled() {
|
|
1634
|
+
return state.enabled;
|
|
1635
|
+
},
|
|
1636
|
+
setPinsVisible: publicSetPinsVisible,
|
|
1637
|
+
setLauncherVisible: publicSetLauncherVisible,
|
|
1638
|
+
async refresh() {
|
|
1639
|
+
if (state.destroyed) return;
|
|
1640
|
+
await refresh();
|
|
1641
|
+
},
|
|
1642
|
+
destroy() {
|
|
1643
|
+
if (state.destroyed) return;
|
|
1644
|
+
state.destroyed = true;
|
|
1645
|
+
document.removeEventListener("mousemove", onMouseMove, true);
|
|
1646
|
+
document.removeEventListener("click", onClick, true);
|
|
1647
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1648
|
+
window.removeEventListener("scroll", onWindowReposition, true);
|
|
1649
|
+
window.removeEventListener("resize", onWindowReposition);
|
|
1650
|
+
overlay.destroy();
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function elementAtFiltered(x, y, overlay) {
|
|
1655
|
+
const stack = document.elementsFromPoint(x, y);
|
|
1656
|
+
for (const node of stack) {
|
|
1657
|
+
if (overlay.ownsEvent({ composedPath: () => [node] })) continue;
|
|
1658
|
+
if (node === document.documentElement || node === document.body) continue;
|
|
1659
|
+
return node;
|
|
1660
|
+
}
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
function ssrNoopController() {
|
|
1664
|
+
return {
|
|
1665
|
+
setEnabled() {
|
|
1666
|
+
},
|
|
1667
|
+
isEnabled() {
|
|
1668
|
+
return false;
|
|
1669
|
+
},
|
|
1670
|
+
setPinsVisible() {
|
|
1671
|
+
},
|
|
1672
|
+
setLauncherVisible() {
|
|
1673
|
+
},
|
|
1674
|
+
async refresh() {
|
|
1675
|
+
},
|
|
1676
|
+
destroy() {
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/index.ts
|
|
1682
|
+
var _globalController = null;
|
|
1683
|
+
function _setGlobalController(c) {
|
|
1684
|
+
_globalController = c;
|
|
1685
|
+
}
|
|
1686
|
+
function setFeedbackEnabled(enabled) {
|
|
1687
|
+
_globalController?.setEnabled(enabled);
|
|
1688
|
+
}
|
|
1689
|
+
function destroyFeedback() {
|
|
1690
|
+
_globalController?.destroy();
|
|
1691
|
+
_globalController = null;
|
|
1692
|
+
}
|
|
1693
|
+
function initFeedbackGlobal(options) {
|
|
1694
|
+
const ctrl = initFeedback(options);
|
|
1695
|
+
_globalController = ctrl;
|
|
1696
|
+
return ctrl;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
export { _setGlobalController, captureViewport, computeCoordinates, createHttpTransport, destroyFeedback, findElement, getViewportInfo, initFeedback, initFeedbackGlobal, projectCoordinates, resolveSelector, setFeedbackEnabled };
|
|
1700
|
+
//# sourceMappingURL=index.js.map
|
|
1701
|
+
//# sourceMappingURL=index.js.map
|