@sylergydigital/issue-pin-sdk 0.6.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/LICENSE +74 -0
- package/README.md +376 -0
- package/dist/index.cjs +2500 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +2451 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2500 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ElementWhisperer: () => IssuePin,
|
|
34
|
+
FeedbackButton: () => FeedbackButton,
|
|
35
|
+
FeedbackOverlay: () => FeedbackOverlay,
|
|
36
|
+
FeedbackProvider: () => FeedbackProvider,
|
|
37
|
+
IssuePin: () => IssuePin,
|
|
38
|
+
ReviewSurfaceOverlay: () => ReviewSurfaceOverlay,
|
|
39
|
+
ScreenshotFeedback: () => ScreenshotFeedback,
|
|
40
|
+
SdkCommentPopover: () => SdkCommentPopover,
|
|
41
|
+
ThreadPins: () => ThreadPins,
|
|
42
|
+
Z: () => Z,
|
|
43
|
+
useFeedback: () => useFeedback,
|
|
44
|
+
useFeedbackSafe: () => useFeedbackSafe,
|
|
45
|
+
useIssuePinAnchor: () => useIssuePinAnchor
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/IssuePin.tsx
|
|
50
|
+
var import_react11 = require("react");
|
|
51
|
+
|
|
52
|
+
// src/FeedbackProvider.tsx
|
|
53
|
+
var import_react2 = require("react");
|
|
54
|
+
var import_supabase_js = require("@supabase/supabase-js");
|
|
55
|
+
var import_html2canvas = __toESM(require("html2canvas"), 1);
|
|
56
|
+
|
|
57
|
+
// src/useDocumentPathname.ts
|
|
58
|
+
var import_react = require("react");
|
|
59
|
+
var pathnameSubscribers = 0;
|
|
60
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
61
|
+
var origPush;
|
|
62
|
+
var origReplace;
|
|
63
|
+
function notify() {
|
|
64
|
+
listeners.forEach((l) => l());
|
|
65
|
+
}
|
|
66
|
+
function subscribePathname(onChange) {
|
|
67
|
+
if (pathnameSubscribers === 0) {
|
|
68
|
+
origPush = history.pushState.bind(history);
|
|
69
|
+
origReplace = history.replaceState.bind(history);
|
|
70
|
+
history.pushState = (...args) => {
|
|
71
|
+
origPush(...args);
|
|
72
|
+
notify();
|
|
73
|
+
};
|
|
74
|
+
history.replaceState = (...args) => {
|
|
75
|
+
origReplace(...args);
|
|
76
|
+
notify();
|
|
77
|
+
};
|
|
78
|
+
window.addEventListener("popstate", notify);
|
|
79
|
+
}
|
|
80
|
+
pathnameSubscribers++;
|
|
81
|
+
listeners.add(onChange);
|
|
82
|
+
return () => {
|
|
83
|
+
listeners.delete(onChange);
|
|
84
|
+
pathnameSubscribers--;
|
|
85
|
+
if (pathnameSubscribers === 0) {
|
|
86
|
+
history.pushState = origPush;
|
|
87
|
+
history.replaceState = origReplace;
|
|
88
|
+
window.removeEventListener("popstate", notify);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function getLocationCombinedSnapshot() {
|
|
93
|
+
if (typeof window === "undefined") return "/\n";
|
|
94
|
+
return `${window.location.pathname}
|
|
95
|
+
${window.location.search}`;
|
|
96
|
+
}
|
|
97
|
+
function getServerSnapshot() {
|
|
98
|
+
return "/\n";
|
|
99
|
+
}
|
|
100
|
+
function useDocumentLocationParts() {
|
|
101
|
+
const combined = (0, import_react.useSyncExternalStore)(subscribePathname, getLocationCombinedSnapshot, getServerSnapshot);
|
|
102
|
+
const [pathname, search] = combined.split("\n");
|
|
103
|
+
return { pathname, search: search ?? "" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/theme.ts
|
|
107
|
+
var Z = {
|
|
108
|
+
pins: 99990,
|
|
109
|
+
overlay: 99991,
|
|
110
|
+
popover: 99992,
|
|
111
|
+
launcher: 99993,
|
|
112
|
+
screenshotModal: 99994,
|
|
113
|
+
flash: 99998
|
|
114
|
+
};
|
|
115
|
+
var T = {
|
|
116
|
+
card: "#1a1a1f",
|
|
117
|
+
fg: "#e4e4e8",
|
|
118
|
+
border: "#2a2a30",
|
|
119
|
+
primary: "#6366f1",
|
|
120
|
+
primaryFg: "#ffffff",
|
|
121
|
+
accent: "#2e2e35",
|
|
122
|
+
muted: "#737380",
|
|
123
|
+
bg: "#141418",
|
|
124
|
+
input: "#2a2a30",
|
|
125
|
+
ring: "#6366f1"
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/FeedbackProvider.tsx
|
|
129
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
130
|
+
var FeedbackContext = (0, import_react2.createContext)(null);
|
|
131
|
+
var MODERN_THREAD_SELECT = "id, page_url, annotation_surface, review_url, selector, anchor_key, coordinate_space, container_key, element_text, element_tag, x_position, y_position, viewport_width, viewport_height, modal_trigger_selector, screenshot_path, status, visibility, created_at";
|
|
132
|
+
var LEGACY_THREAD_SELECT = "id, page_url, selector, element_text, element_tag, x_position, y_position, viewport_width, viewport_height, modal_trigger_selector, screenshot_path, status, visibility, created_at";
|
|
133
|
+
var threadsSchemaModeByWorkspace = /* @__PURE__ */ new Map();
|
|
134
|
+
function isMissingThreadsColumnError(error) {
|
|
135
|
+
if (!error || typeof error !== "object") return false;
|
|
136
|
+
const candidate = error;
|
|
137
|
+
if (candidate.code !== "PGRST204") return false;
|
|
138
|
+
return typeof candidate.message === "string" && candidate.message.includes("column");
|
|
139
|
+
}
|
|
140
|
+
function toLegacyThreadInsertPayload(payload) {
|
|
141
|
+
const legacy = { ...payload };
|
|
142
|
+
delete legacy.annotation_surface;
|
|
143
|
+
delete legacy.review_url;
|
|
144
|
+
delete legacy.anchor_key;
|
|
145
|
+
delete legacy.coordinate_space;
|
|
146
|
+
delete legacy.container_key;
|
|
147
|
+
return legacy;
|
|
148
|
+
}
|
|
149
|
+
function normalizeLegacyThread(data) {
|
|
150
|
+
return {
|
|
151
|
+
annotation_surface: "host-dom",
|
|
152
|
+
anchor_key: null,
|
|
153
|
+
coordinate_space: "document",
|
|
154
|
+
container_key: null,
|
|
155
|
+
id: String(data.id ?? ""),
|
|
156
|
+
page_url: String(data.page_url ?? ""),
|
|
157
|
+
review_url: null,
|
|
158
|
+
selector: typeof data.selector === "string" ? data.selector : null,
|
|
159
|
+
element_text: typeof data.element_text === "string" ? data.element_text : null,
|
|
160
|
+
element_tag: typeof data.element_tag === "string" ? data.element_tag : null,
|
|
161
|
+
x_position: typeof data.x_position === "number" ? data.x_position : data.x_position == null ? null : Number(data.x_position),
|
|
162
|
+
y_position: typeof data.y_position === "number" ? data.y_position : data.y_position == null ? null : Number(data.y_position),
|
|
163
|
+
viewport_width: typeof data.viewport_width === "number" ? data.viewport_width : data.viewport_width == null ? null : Number(data.viewport_width),
|
|
164
|
+
viewport_height: typeof data.viewport_height === "number" ? data.viewport_height : data.viewport_height == null ? null : Number(data.viewport_height),
|
|
165
|
+
modal_trigger_selector: typeof data.modal_trigger_selector === "string" ? data.modal_trigger_selector : null,
|
|
166
|
+
screenshot_path: typeof data.screenshot_path === "string" ? data.screenshot_path : null,
|
|
167
|
+
status: String(data.status ?? "open"),
|
|
168
|
+
visibility: String(data.visibility ?? "public"),
|
|
169
|
+
created_at: String(data.created_at ?? "")
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function getThreadsSchemaMode(workspaceId) {
|
|
173
|
+
return threadsSchemaModeByWorkspace.get(workspaceId) ?? "modern";
|
|
174
|
+
}
|
|
175
|
+
function setThreadsSchemaMode(workspaceId, mode) {
|
|
176
|
+
threadsSchemaModeByWorkspace.set(workspaceId, mode);
|
|
177
|
+
}
|
|
178
|
+
function resolveIssuePinActorUserId(opts) {
|
|
179
|
+
if (opts.explicitUserId) return opts.explicitUserId;
|
|
180
|
+
if (opts.hasApiKey && !opts.skipFederation && opts.autoIdentityUserId) {
|
|
181
|
+
return opts.federatedLocalUserId;
|
|
182
|
+
}
|
|
183
|
+
return opts.autoIdentityUserId;
|
|
184
|
+
}
|
|
185
|
+
function resolveIssuePinActorReady(opts) {
|
|
186
|
+
if (!opts.authReady) return false;
|
|
187
|
+
if (opts.explicitUserId) return true;
|
|
188
|
+
if (opts.hasApiKey && !opts.skipFederation && opts.autoIdentityUserId) {
|
|
189
|
+
return !!opts.federatedLocalUserId;
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
function resolveAnnotationSurface(opts) {
|
|
194
|
+
if (opts.annotationSurface) return opts.annotationSurface;
|
|
195
|
+
return opts.reviewUrl ? "review-iframe" : "host-dom";
|
|
196
|
+
}
|
|
197
|
+
function resolveCanPinOnPage(opts) {
|
|
198
|
+
if (!opts.allowPinOnPage) return false;
|
|
199
|
+
if (opts.annotationSurface === "review-iframe") {
|
|
200
|
+
return !!opts.reviewUrl;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function buildThreadInsertPayload(opts) {
|
|
205
|
+
const { pendingPin } = opts;
|
|
206
|
+
const isReviewSurface = pendingPin.annotationSurface === "review-iframe";
|
|
207
|
+
return {
|
|
208
|
+
workspace_id: opts.workspaceId,
|
|
209
|
+
created_by: opts.userId || null,
|
|
210
|
+
page_url: pendingPin.pageUrl,
|
|
211
|
+
annotation_surface: pendingPin.annotationSurface,
|
|
212
|
+
review_url: isReviewSurface ? pendingPin.reviewUrl : null,
|
|
213
|
+
selector: isReviewSurface ? null : pendingPin.selector,
|
|
214
|
+
anchor_key: isReviewSurface ? null : pendingPin.anchorKey,
|
|
215
|
+
coordinate_space: pendingPin.coordinateSpace,
|
|
216
|
+
container_key: isReviewSurface ? null : pendingPin.containerKey,
|
|
217
|
+
element_text: pendingPin.elementText,
|
|
218
|
+
element_tag: pendingPin.elementTag,
|
|
219
|
+
x_position: pendingPin.x,
|
|
220
|
+
y_position: pendingPin.y,
|
|
221
|
+
viewport_width: window.innerWidth,
|
|
222
|
+
viewport_height: window.innerHeight,
|
|
223
|
+
modal_trigger_selector: null,
|
|
224
|
+
visibility: opts.visibility,
|
|
225
|
+
sdk_user_email: opts.userEmail || null,
|
|
226
|
+
sdk_user_name: opts.userDisplayName || null
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function resolveApiKey(apiKey) {
|
|
230
|
+
const res = await fetch(
|
|
231
|
+
`https://jzdvmfemzpmgmxupcamh.supabase.co/functions/v1/sdk-resolve`,
|
|
232
|
+
{
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ apiKey })
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
const err = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
240
|
+
throw new Error(err.error || "Failed to resolve API key");
|
|
241
|
+
}
|
|
242
|
+
return res.json();
|
|
243
|
+
}
|
|
244
|
+
function getFunctionsBaseUrl(supabaseUrl) {
|
|
245
|
+
return `${supabaseUrl.replace(/\/+$/, "")}/functions/v1`;
|
|
246
|
+
}
|
|
247
|
+
async function uploadScreenshot(client, dataUrl, workspaceId) {
|
|
248
|
+
const res = await fetch(dataUrl);
|
|
249
|
+
const blob = await res.blob();
|
|
250
|
+
const fileName = `${workspaceId}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`;
|
|
251
|
+
const { error } = await client.storage.from("screenshots").upload(fileName, blob, {
|
|
252
|
+
contentType: "image/jpeg",
|
|
253
|
+
cacheControl: "31536000"
|
|
254
|
+
});
|
|
255
|
+
if (error) throw error;
|
|
256
|
+
return fileName;
|
|
257
|
+
}
|
|
258
|
+
async function uploadScreenshotBlob(client, blob, workspaceId) {
|
|
259
|
+
const fileName = `${workspaceId}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`;
|
|
260
|
+
const { error } = await client.storage.from("screenshots").upload(fileName, blob, {
|
|
261
|
+
contentType: "image/jpeg",
|
|
262
|
+
cacheControl: "31536000"
|
|
263
|
+
});
|
|
264
|
+
if (error) throw error;
|
|
265
|
+
return fileName;
|
|
266
|
+
}
|
|
267
|
+
function FeedbackProvider({
|
|
268
|
+
children,
|
|
269
|
+
...config
|
|
270
|
+
}) {
|
|
271
|
+
const directProps = config.supabaseUrl && config.supabaseAnonKey && config.workspaceId;
|
|
272
|
+
const [resolved, setResolved] = (0, import_react2.useState)(
|
|
273
|
+
directProps ? { supabaseUrl: config.supabaseUrl, supabaseAnonKey: config.supabaseAnonKey, workspaceId: config.workspaceId, autoScreenshotOnPin: false } : null
|
|
274
|
+
);
|
|
275
|
+
const [resolveError, setResolveError] = (0, import_react2.useState)(null);
|
|
276
|
+
const [autoIdentity, setAutoIdentity] = (0, import_react2.useState)({});
|
|
277
|
+
const [authReady, setAuthReady] = (0, import_react2.useState)(!config.supabaseClient || !!(config.userId || config.userEmail || config.userDisplayName));
|
|
278
|
+
(0, import_react2.useEffect)(() => {
|
|
279
|
+
if (!config.supabaseClient) {
|
|
280
|
+
setAuthReady(true);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (config.userId || config.userEmail || config.userDisplayName) {
|
|
284
|
+
setAuthReady(true);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
let cancelled = false;
|
|
288
|
+
const extractIdentity = (session) => ({
|
|
289
|
+
userId: session.user.id,
|
|
290
|
+
userEmail: session.user.email,
|
|
291
|
+
userDisplayName: session.user.user_metadata?.display_name ?? session.user.user_metadata?.full_name ?? session.user.user_metadata?.name ?? (session.user.email ? session.user.email.split("@")[0] : void 0),
|
|
292
|
+
accessToken: session.access_token
|
|
293
|
+
});
|
|
294
|
+
const initAuth = async () => {
|
|
295
|
+
try {
|
|
296
|
+
const { data: { session } } = await config.supabaseClient.auth.getSession();
|
|
297
|
+
if (cancelled) return;
|
|
298
|
+
if (session?.user) {
|
|
299
|
+
const identity = extractIdentity(session);
|
|
300
|
+
setAutoIdentity(identity);
|
|
301
|
+
console.log("[EW SDK] Auto-identity resolved:", { email: identity.userEmail, displayName: identity.userDisplayName });
|
|
302
|
+
} else {
|
|
303
|
+
console.log("[EW SDK] Auto-identity: no session found (user not logged in)");
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn("[IssuePin] Failed to auto-detect user from supabaseClient:", err);
|
|
307
|
+
} finally {
|
|
308
|
+
if (!cancelled) setAuthReady(true);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
initAuth();
|
|
312
|
+
const { data: { subscription } } = config.supabaseClient.auth.onAuthStateChange(
|
|
313
|
+
(_event, session) => {
|
|
314
|
+
if (cancelled) return;
|
|
315
|
+
if (session?.user) {
|
|
316
|
+
const identity = extractIdentity(session);
|
|
317
|
+
setAutoIdentity(identity);
|
|
318
|
+
console.log("[EW SDK] Auth state changed \u2014 identity updated:", { email: identity.userEmail, displayName: identity.userDisplayName });
|
|
319
|
+
} else {
|
|
320
|
+
setAutoIdentity({});
|
|
321
|
+
console.log("[EW SDK] Auth state changed \u2014 user signed out");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
return () => {
|
|
326
|
+
cancelled = true;
|
|
327
|
+
subscription.unsubscribe();
|
|
328
|
+
};
|
|
329
|
+
}, [config.supabaseClient, config.userId, config.userEmail, config.userDisplayName]);
|
|
330
|
+
const effectiveEmail = config.userEmail ?? autoIdentity.userEmail;
|
|
331
|
+
const effectiveDisplayName = config.userDisplayName ?? autoIdentity.userDisplayName;
|
|
332
|
+
const resolvedAnnotationSurface = resolveAnnotationSurface({
|
|
333
|
+
annotationSurface: config.annotationSurface,
|
|
334
|
+
reviewUrl: config.reviewUrl
|
|
335
|
+
});
|
|
336
|
+
(0, import_react2.useEffect)(() => {
|
|
337
|
+
if (resolved) return;
|
|
338
|
+
if (!config.apiKey) {
|
|
339
|
+
setResolveError("IssuePin: provide either apiKey or legacy supabase props");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
let cancelled = false;
|
|
343
|
+
resolveApiKey(config.apiKey).then((r) => {
|
|
344
|
+
if (cancelled) return;
|
|
345
|
+
if (!r.supabaseUrl || !r.supabaseAnonKey || !r.workspaceId) {
|
|
346
|
+
setResolveError("SDK resolved config is incomplete (missing supabaseUrl, supabaseAnonKey, or workspaceId)");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
setResolved(r);
|
|
350
|
+
}).catch((e) => {
|
|
351
|
+
if (!cancelled) setResolveError(e.message);
|
|
352
|
+
});
|
|
353
|
+
return () => {
|
|
354
|
+
cancelled = true;
|
|
355
|
+
};
|
|
356
|
+
}, [config.apiKey, config.supabaseUrl, config.supabaseAnonKey, config.workspaceId]);
|
|
357
|
+
const [federationDone, setFederationDone] = (0, import_react2.useState)(false);
|
|
358
|
+
const [federatedLocalUserId, setFederatedLocalUserId] = (0, import_react2.useState)();
|
|
359
|
+
const [federationError, setFederationError] = (0, import_react2.useState)(null);
|
|
360
|
+
(0, import_react2.useEffect)(() => {
|
|
361
|
+
if (federationDone) return;
|
|
362
|
+
if (!resolved || !config.apiKey) return;
|
|
363
|
+
if (config.skipFederation) return;
|
|
364
|
+
if (!autoIdentity.userId || !autoIdentity.userEmail || !autoIdentity.accessToken) return;
|
|
365
|
+
let cancelled = false;
|
|
366
|
+
const federate = async () => {
|
|
367
|
+
try {
|
|
368
|
+
const functionsBaseUrl = getFunctionsBaseUrl(resolved.supabaseUrl);
|
|
369
|
+
const res = await fetch(
|
|
370
|
+
`${functionsBaseUrl}/sdk-federate`,
|
|
371
|
+
{
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: {
|
|
374
|
+
"Content-Type": "application/json",
|
|
375
|
+
Authorization: `Bearer ${autoIdentity.accessToken}`
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
apiKey: config.apiKey,
|
|
379
|
+
externalId: autoIdentity.userId,
|
|
380
|
+
email: autoIdentity.userEmail,
|
|
381
|
+
displayName: autoIdentity.userDisplayName || void 0
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
if (!cancelled) {
|
|
386
|
+
if (res.ok) {
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
setFederatedLocalUserId(data.user_id || void 0);
|
|
389
|
+
setFederationError(null);
|
|
390
|
+
console.log("[EW SDK] Auto-federation complete:", { userId: data.user_id, isNew: data.is_new_user });
|
|
391
|
+
} else {
|
|
392
|
+
const err = await res.json().catch(() => ({}));
|
|
393
|
+
const message = err.error || `Auto-federation failed (${res.status})`;
|
|
394
|
+
setFederationError(message);
|
|
395
|
+
console.warn("[EW SDK] Auto-federation failed:", message);
|
|
396
|
+
}
|
|
397
|
+
setFederationDone(true);
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (!cancelled) {
|
|
401
|
+
const message = err instanceof Error ? err.message : "Auto-federation error";
|
|
402
|
+
setFederationError(message);
|
|
403
|
+
console.warn("[EW SDK] Auto-federation error:", message);
|
|
404
|
+
setFederationDone(true);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
federate();
|
|
409
|
+
return () => {
|
|
410
|
+
cancelled = true;
|
|
411
|
+
};
|
|
412
|
+
}, [resolved, config.apiKey, config.skipFederation, autoIdentity.userId, autoIdentity.userEmail, autoIdentity.userDisplayName, autoIdentity.accessToken, federationDone]);
|
|
413
|
+
const effectiveUserId = resolveIssuePinActorUserId({
|
|
414
|
+
explicitUserId: config.userId,
|
|
415
|
+
autoIdentityUserId: autoIdentity.userId,
|
|
416
|
+
federatedLocalUserId,
|
|
417
|
+
hasApiKey: !!config.apiKey,
|
|
418
|
+
skipFederation: config.skipFederation
|
|
419
|
+
});
|
|
420
|
+
const actorReady = resolveIssuePinActorReady({
|
|
421
|
+
authReady,
|
|
422
|
+
explicitUserId: config.userId,
|
|
423
|
+
autoIdentityUserId: autoIdentity.userId,
|
|
424
|
+
federatedLocalUserId,
|
|
425
|
+
hasApiKey: !!config.apiKey,
|
|
426
|
+
skipFederation: config.skipFederation
|
|
427
|
+
});
|
|
428
|
+
const actorError = federationDone && !actorReady && (federationError || "Unable to link your identity yet. Please retry in a moment.") ? federationError || "Unable to link your identity yet. Please retry in a moment." : null;
|
|
429
|
+
if (resolveError) {
|
|
430
|
+
console.error("[EW SDK]", resolveError);
|
|
431
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
432
|
+
}
|
|
433
|
+
if (!resolved) {
|
|
434
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
435
|
+
}
|
|
436
|
+
const onModeChangeUnified = config.onModeChange ?? (config.onFeedbackActiveChange ? ((m) => config.onFeedbackActiveChange(m === "annotate")) : void 0);
|
|
437
|
+
const controlledModeFromProps = config.mode !== void 0 ? config.mode : config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : void 0;
|
|
438
|
+
const initialModeUncontrolled = config.mode ?? (config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : "view");
|
|
439
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
440
|
+
FeedbackProviderInner,
|
|
441
|
+
{
|
|
442
|
+
supabaseUrl: resolved.supabaseUrl,
|
|
443
|
+
supabaseAnonKey: resolved.supabaseAnonKey,
|
|
444
|
+
workspaceId: resolved.workspaceId,
|
|
445
|
+
siteUrl: resolved.siteUrl,
|
|
446
|
+
initialAutoScreenshot: resolved.autoScreenshotOnPin ?? false,
|
|
447
|
+
allowPinOnPage: config.allowPinOnPage ?? true,
|
|
448
|
+
allowScreenshot: config.allowScreenshot ?? true,
|
|
449
|
+
showHints: config.showHints ?? true,
|
|
450
|
+
annotationSurface: resolvedAnnotationSurface,
|
|
451
|
+
reviewUrl: config.reviewUrl ?? null,
|
|
452
|
+
scrollContainer: config.scrollContainer,
|
|
453
|
+
resolveAnchor: config.resolveAnchor,
|
|
454
|
+
isModeControlled: onModeChangeUnified !== void 0,
|
|
455
|
+
controlledMode: controlledModeFromProps,
|
|
456
|
+
onModeChange: onModeChangeUnified,
|
|
457
|
+
initialModeUncontrolled,
|
|
458
|
+
debug: config.debug ?? false,
|
|
459
|
+
authReady,
|
|
460
|
+
actorReady,
|
|
461
|
+
actorError,
|
|
462
|
+
userId: effectiveUserId,
|
|
463
|
+
userEmail: effectiveEmail,
|
|
464
|
+
userDisplayName: effectiveDisplayName,
|
|
465
|
+
children
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
function FeedbackProviderInner({
|
|
470
|
+
children,
|
|
471
|
+
supabaseUrl,
|
|
472
|
+
supabaseAnonKey,
|
|
473
|
+
workspaceId,
|
|
474
|
+
siteUrl,
|
|
475
|
+
initialAutoScreenshot,
|
|
476
|
+
allowPinOnPage,
|
|
477
|
+
allowScreenshot,
|
|
478
|
+
showHints,
|
|
479
|
+
annotationSurface,
|
|
480
|
+
reviewUrl,
|
|
481
|
+
scrollContainer,
|
|
482
|
+
resolveAnchor,
|
|
483
|
+
isModeControlled,
|
|
484
|
+
controlledMode,
|
|
485
|
+
onModeChange,
|
|
486
|
+
initialModeUncontrolled,
|
|
487
|
+
authReady,
|
|
488
|
+
actorReady,
|
|
489
|
+
actorError,
|
|
490
|
+
debug,
|
|
491
|
+
userId,
|
|
492
|
+
userEmail,
|
|
493
|
+
userDisplayName
|
|
494
|
+
}) {
|
|
495
|
+
const debugLog = (0, import_react2.useCallback)((message, extra) => {
|
|
496
|
+
if (!debug) return;
|
|
497
|
+
if (extra === void 0) {
|
|
498
|
+
console.log(`[EW SDK] ${message}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
console.log(`[EW SDK] ${message}`, extra);
|
|
502
|
+
}, [debug]);
|
|
503
|
+
(0, import_react2.useEffect)(() => {
|
|
504
|
+
if (!userDisplayName && !userEmail) {
|
|
505
|
+
console.warn(
|
|
506
|
+
'[IssuePin] No user identity provided \u2014 comments will appear as "Unknown". Pass supabaseClient={supabase} for automatic identity, or user={{ email, displayName }} for manual attribution. See https://github.com/sylergydigital/issue-pin/blob/main/sdk/README.md#user-identity-avoiding-unknown-comments'
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}, []);
|
|
510
|
+
const client = (0, import_react2.useMemo)(() => {
|
|
511
|
+
try {
|
|
512
|
+
return (0, import_supabase_js.createClient)(supabaseUrl, supabaseAnonKey, {
|
|
513
|
+
auth: {
|
|
514
|
+
persistSession: false,
|
|
515
|
+
autoRefreshToken: false,
|
|
516
|
+
detectSessionInUrl: false
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.error("[EW SDK] Failed to create Supabase client:", err);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}, [supabaseUrl, supabaseAnonKey]);
|
|
524
|
+
const [autoScreenshotOnPin, setAutoScreenshotOnPin] = (0, import_react2.useState)(initialAutoScreenshot);
|
|
525
|
+
(0, import_react2.useEffect)(() => {
|
|
526
|
+
if (!client) return;
|
|
527
|
+
client.from("workspaces").select("auto_screenshot_on_pin").eq("id", workspaceId).single().then(({ data }) => {
|
|
528
|
+
if (data) setAutoScreenshotOnPin(data.auto_screenshot_on_pin);
|
|
529
|
+
});
|
|
530
|
+
}, [client, workspaceId]);
|
|
531
|
+
const [threads, setThreads] = (0, import_react2.useState)([]);
|
|
532
|
+
const [isLoadingThreads, setIsLoadingThreads] = (0, import_react2.useState)(false);
|
|
533
|
+
const [reviewOpen, setReviewOpen] = (0, import_react2.useState)(false);
|
|
534
|
+
const [internalMode, setInternalMode] = (0, import_react2.useState)(
|
|
535
|
+
() => isModeControlled ? "view" : initialModeUncontrolled
|
|
536
|
+
);
|
|
537
|
+
const [menuOpen, setMenuOpen] = (0, import_react2.useState)(false);
|
|
538
|
+
const [pendingPin, setPendingPin] = (0, import_react2.useState)(null);
|
|
539
|
+
const [screenshotDataUrl, setScreenshotDataUrl] = (0, import_react2.useState)(null);
|
|
540
|
+
const [pendingScreenshotPin, setPendingScreenshotPin] = (0, import_react2.useState)(null);
|
|
541
|
+
const [screenshotCapturing, setScreenshotCapturing] = (0, import_react2.useState)(false);
|
|
542
|
+
const [flashing, setFlashing] = (0, import_react2.useState)(false);
|
|
543
|
+
const mode = isModeControlled ? controlledMode ?? "view" : internalMode;
|
|
544
|
+
const setMode = (0, import_react2.useCallback)(
|
|
545
|
+
(m) => {
|
|
546
|
+
if (!isModeControlled) setInternalMode(m);
|
|
547
|
+
onModeChange?.(m);
|
|
548
|
+
},
|
|
549
|
+
[isModeControlled, onModeChange]
|
|
550
|
+
);
|
|
551
|
+
const feedbackActive = mode === "annotate";
|
|
552
|
+
const setFeedbackActive = (0, import_react2.useCallback)(
|
|
553
|
+
(active) => {
|
|
554
|
+
setMode(active ? "annotate" : "view");
|
|
555
|
+
},
|
|
556
|
+
[setMode]
|
|
557
|
+
);
|
|
558
|
+
const canPinOnPage = resolveCanPinOnPage({
|
|
559
|
+
allowPinOnPage,
|
|
560
|
+
annotationSurface,
|
|
561
|
+
reviewUrl
|
|
562
|
+
});
|
|
563
|
+
const openMenu = (0, import_react2.useCallback)(() => {
|
|
564
|
+
setMenuOpen(true);
|
|
565
|
+
}, []);
|
|
566
|
+
const closeMenu = (0, import_react2.useCallback)(() => {
|
|
567
|
+
setMenuOpen(false);
|
|
568
|
+
}, []);
|
|
569
|
+
const toggleMenu = (0, import_react2.useCallback)(() => {
|
|
570
|
+
setMenuOpen((prev) => !prev);
|
|
571
|
+
}, []);
|
|
572
|
+
const enterPinMode = (0, import_react2.useCallback)(() => {
|
|
573
|
+
if (!canPinOnPage) return;
|
|
574
|
+
setMode("annotate");
|
|
575
|
+
setMenuOpen(false);
|
|
576
|
+
if (annotationSurface === "review-iframe") {
|
|
577
|
+
setReviewOpen(true);
|
|
578
|
+
}
|
|
579
|
+
}, [annotationSurface, canPinOnPage, setMode]);
|
|
580
|
+
const exitPinMode = (0, import_react2.useCallback)(() => {
|
|
581
|
+
setMode("view");
|
|
582
|
+
setMenuOpen(false);
|
|
583
|
+
setPendingPin(null);
|
|
584
|
+
if (annotationSurface === "review-iframe") {
|
|
585
|
+
setReviewOpen(false);
|
|
586
|
+
}
|
|
587
|
+
}, [annotationSurface, setMode]);
|
|
588
|
+
(0, import_react2.useEffect)(() => {
|
|
589
|
+
if (annotationSurface !== "review-iframe") return;
|
|
590
|
+
if (mode === "view") {
|
|
591
|
+
setReviewOpen(false);
|
|
592
|
+
}
|
|
593
|
+
}, [annotationSurface, mode]);
|
|
594
|
+
const { pathname } = useDocumentLocationParts();
|
|
595
|
+
if (typeof document !== "undefined") {
|
|
596
|
+
document.documentElement.setAttribute("data-ew-sdk", "true");
|
|
597
|
+
}
|
|
598
|
+
(0, import_react2.useEffect)(() => {
|
|
599
|
+
const handleSdkPointerDownCapture = (event) => {
|
|
600
|
+
const target = event.target;
|
|
601
|
+
if (!(target instanceof Element)) return;
|
|
602
|
+
if (!target.closest('[data-ew-feedback-interactive="true"]')) return;
|
|
603
|
+
event.stopPropagation();
|
|
604
|
+
};
|
|
605
|
+
window.addEventListener("pointerdown", handleSdkPointerDownCapture, true);
|
|
606
|
+
return () => {
|
|
607
|
+
window.removeEventListener("pointerdown", handleSdkPointerDownCapture, true);
|
|
608
|
+
};
|
|
609
|
+
}, []);
|
|
610
|
+
(0, import_react2.useEffect)(() => {
|
|
611
|
+
debugLog("FeedbackProvider mounted", { workspaceId, path: pathname });
|
|
612
|
+
return () => {
|
|
613
|
+
debugLog("FeedbackProvider unmounted", { workspaceId, path: pathname });
|
|
614
|
+
};
|
|
615
|
+
}, [debugLog, workspaceId, pathname]);
|
|
616
|
+
(0, import_react2.useEffect)(() => {
|
|
617
|
+
debugLog("mode / feedbackActive", { mode, feedbackActive });
|
|
618
|
+
}, [debugLog, mode, feedbackActive]);
|
|
619
|
+
(0, import_react2.useEffect)(() => {
|
|
620
|
+
debugLog("menuOpen =", menuOpen);
|
|
621
|
+
}, [debugLog, menuOpen]);
|
|
622
|
+
const fetchThreads = (0, import_react2.useCallback)(async () => {
|
|
623
|
+
if (!client) return;
|
|
624
|
+
setIsLoadingThreads(true);
|
|
625
|
+
try {
|
|
626
|
+
const pageUrl = pathname;
|
|
627
|
+
const schemaMode = getThreadsSchemaMode(workspaceId);
|
|
628
|
+
const runQuery = async (mode2) => {
|
|
629
|
+
const threadsTable = client.from("threads");
|
|
630
|
+
let query = threadsTable.select(mode2 === "modern" ? MODERN_THREAD_SELECT : LEGACY_THREAD_SELECT).eq("workspace_id", workspaceId).eq("page_url", pageUrl).neq("status", "resolved").order("created_at", { ascending: true });
|
|
631
|
+
if (mode2 === "modern") {
|
|
632
|
+
if (annotationSurface === "review-iframe") {
|
|
633
|
+
query = query.eq("annotation_surface", "review-iframe").eq("review_url", reviewUrl ?? "");
|
|
634
|
+
} else {
|
|
635
|
+
query = query.or("annotation_surface.is.null,annotation_surface.eq.host-dom");
|
|
636
|
+
}
|
|
637
|
+
} else if (annotationSurface === "review-iframe") {
|
|
638
|
+
return { data: [], error: null };
|
|
639
|
+
}
|
|
640
|
+
const result2 = await query;
|
|
641
|
+
if (result2.error) return { data: null, error: result2.error };
|
|
642
|
+
const rows = result2.data ?? [];
|
|
643
|
+
return {
|
|
644
|
+
data: mode2 === "modern" ? rows : rows.map(normalizeLegacyThread),
|
|
645
|
+
error: null
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
let result = await runQuery(schemaMode);
|
|
649
|
+
if (result.error && schemaMode === "modern" && isMissingThreadsColumnError(result.error)) {
|
|
650
|
+
console.warn("[EW SDK] Falling back to legacy threads schema compatibility mode.");
|
|
651
|
+
setThreadsSchemaMode(workspaceId, "legacy");
|
|
652
|
+
result = await runQuery("legacy");
|
|
653
|
+
}
|
|
654
|
+
if (result.error) throw result.error;
|
|
655
|
+
setThreads(result.data || []);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.error("[EW SDK] Failed to fetch threads:", err);
|
|
658
|
+
} finally {
|
|
659
|
+
setIsLoadingThreads(false);
|
|
660
|
+
}
|
|
661
|
+
}, [annotationSurface, client, reviewUrl, workspaceId, pathname]);
|
|
662
|
+
(0, import_react2.useEffect)(() => {
|
|
663
|
+
fetchThreads();
|
|
664
|
+
}, [fetchThreads]);
|
|
665
|
+
(0, import_react2.useEffect)(() => {
|
|
666
|
+
if (!client) return;
|
|
667
|
+
const channel = client.channel(`sdk-threads-${workspaceId}`).on("postgres_changes", {
|
|
668
|
+
event: "*",
|
|
669
|
+
schema: "public",
|
|
670
|
+
table: "threads",
|
|
671
|
+
filter: `workspace_id=eq.${workspaceId}`
|
|
672
|
+
}, () => {
|
|
673
|
+
fetchThreads();
|
|
674
|
+
}).subscribe();
|
|
675
|
+
return () => {
|
|
676
|
+
client.removeChannel(channel);
|
|
677
|
+
};
|
|
678
|
+
}, [client, workspaceId, fetchThreads]);
|
|
679
|
+
const captureAndAttachScreenshot = (0, import_react2.useCallback)(async (threadId) => {
|
|
680
|
+
if (!client) return;
|
|
681
|
+
try {
|
|
682
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
683
|
+
const canvas = await (0, import_html2canvas.default)(document.body, {
|
|
684
|
+
useCORS: true,
|
|
685
|
+
allowTaint: true,
|
|
686
|
+
logging: false,
|
|
687
|
+
scale: window.devicePixelRatio || 1,
|
|
688
|
+
width: window.innerWidth,
|
|
689
|
+
height: window.innerHeight,
|
|
690
|
+
scrollX: window.scrollX,
|
|
691
|
+
scrollY: window.scrollY,
|
|
692
|
+
x: window.scrollX,
|
|
693
|
+
y: window.scrollY,
|
|
694
|
+
ignoreElements: (el) => {
|
|
695
|
+
return el.closest?.('[data-ew-feedback-interactive="true"]') !== null;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
const blob = await new Promise((resolve, reject) => {
|
|
699
|
+
canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/jpeg", 0.8);
|
|
700
|
+
});
|
|
701
|
+
const screenshotPath = await uploadScreenshotBlob(client, blob, workspaceId);
|
|
702
|
+
await client.from("threads").update({ screenshot_path: screenshotPath }).eq("id", threadId);
|
|
703
|
+
setFlashing(true);
|
|
704
|
+
setTimeout(() => setFlashing(false), 300);
|
|
705
|
+
console.log("[EW SDK] Auto-screenshot attached to thread", threadId);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
console.error("[EW SDK] Auto-screenshot failed:", err);
|
|
708
|
+
}
|
|
709
|
+
}, [client, workspaceId]);
|
|
710
|
+
const startScreenshotCapture = (0, import_react2.useCallback)(async () => {
|
|
711
|
+
if (!allowScreenshot) return;
|
|
712
|
+
setMenuOpen(false);
|
|
713
|
+
setScreenshotCapturing(true);
|
|
714
|
+
try {
|
|
715
|
+
const canvas = await (0, import_html2canvas.default)(document.body, {
|
|
716
|
+
useCORS: true,
|
|
717
|
+
scale: window.devicePixelRatio,
|
|
718
|
+
logging: false,
|
|
719
|
+
ignoreElements: (el) => el.closest?.('[data-ew-feedback-interactive="true"]') !== null
|
|
720
|
+
});
|
|
721
|
+
const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
|
|
722
|
+
setScreenshotDataUrl(dataUrl);
|
|
723
|
+
} catch (err) {
|
|
724
|
+
console.error("[EW SDK] Screenshot capture failed:", err);
|
|
725
|
+
} finally {
|
|
726
|
+
setScreenshotCapturing(false);
|
|
727
|
+
}
|
|
728
|
+
}, [allowScreenshot]);
|
|
729
|
+
const submitThread = (0, import_react2.useCallback)(
|
|
730
|
+
async (body, visibility) => {
|
|
731
|
+
if (!pendingPin || !client) return;
|
|
732
|
+
if (!actorReady) throw new Error(actorError || "Unable to link your identity yet. Please retry in a moment.");
|
|
733
|
+
const threadPayload = buildThreadInsertPayload({
|
|
734
|
+
workspaceId,
|
|
735
|
+
userId,
|
|
736
|
+
userEmail,
|
|
737
|
+
userDisplayName,
|
|
738
|
+
pendingPin,
|
|
739
|
+
visibility
|
|
740
|
+
});
|
|
741
|
+
const insertThread = async (payload) => client.from("threads").insert(payload).select().single();
|
|
742
|
+
let { data: thread, error: threadErr } = await insertThread(
|
|
743
|
+
getThreadsSchemaMode(workspaceId) === "legacy" ? toLegacyThreadInsertPayload(threadPayload) : threadPayload
|
|
744
|
+
);
|
|
745
|
+
if (threadErr && getThreadsSchemaMode(workspaceId) === "modern" && isMissingThreadsColumnError(threadErr)) {
|
|
746
|
+
if (pendingPin.annotationSurface === "review-iframe") {
|
|
747
|
+
throw new Error("This workspace is missing required review-surface thread columns. Apply the latest Issue Pin SDK migrations and retry.");
|
|
748
|
+
}
|
|
749
|
+
console.warn("[EW SDK] Retrying thread insert against legacy threads schema.");
|
|
750
|
+
setThreadsSchemaMode(workspaceId, "legacy");
|
|
751
|
+
({ data: thread, error: threadErr } = await insertThread(toLegacyThreadInsertPayload(threadPayload)));
|
|
752
|
+
}
|
|
753
|
+
if (threadErr) throw threadErr;
|
|
754
|
+
const { error: commentErr } = await client.from("thread_comments").insert({
|
|
755
|
+
thread_id: thread.id,
|
|
756
|
+
author_id: userId || null,
|
|
757
|
+
body,
|
|
758
|
+
visibility,
|
|
759
|
+
sdk_user_email: userEmail || null,
|
|
760
|
+
sdk_user_name: userDisplayName || null
|
|
761
|
+
});
|
|
762
|
+
if (commentErr) throw commentErr;
|
|
763
|
+
setPendingPin(null);
|
|
764
|
+
if (pendingPin.annotationSurface === "review-iframe") {
|
|
765
|
+
setMode("view");
|
|
766
|
+
}
|
|
767
|
+
await fetchThreads();
|
|
768
|
+
debugLog("submitThread complete", { autoScreenshotOnPin, threadId: thread.id });
|
|
769
|
+
if (autoScreenshotOnPin) {
|
|
770
|
+
captureAndAttachScreenshot(thread.id);
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
[pendingPin, client, actorReady, actorError, workspaceId, userId, userEmail, userDisplayName, fetchThreads, autoScreenshotOnPin, captureAndAttachScreenshot, setMode]
|
|
774
|
+
);
|
|
775
|
+
const submitScreenshotThread = (0, import_react2.useCallback)(
|
|
776
|
+
async (body, visibility) => {
|
|
777
|
+
if (!pendingScreenshotPin || !screenshotDataUrl || !client) return;
|
|
778
|
+
if (!actorReady) throw new Error(actorError || "Unable to link your identity yet. Please retry in a moment.");
|
|
779
|
+
const { x, y, pageUrl } = pendingScreenshotPin;
|
|
780
|
+
const screenshotUrl = await uploadScreenshot(client, screenshotDataUrl, workspaceId);
|
|
781
|
+
const threadPayload = {
|
|
782
|
+
workspace_id: workspaceId,
|
|
783
|
+
created_by: userId || null,
|
|
784
|
+
page_url: pageUrl,
|
|
785
|
+
selector: null,
|
|
786
|
+
anchor_key: null,
|
|
787
|
+
annotation_surface: annotationSurface,
|
|
788
|
+
coordinate_space: "document",
|
|
789
|
+
container_key: null,
|
|
790
|
+
element_text: null,
|
|
791
|
+
element_tag: "screenshot",
|
|
792
|
+
review_url: annotationSurface === "review-iframe" ? reviewUrl : null,
|
|
793
|
+
x_position: x,
|
|
794
|
+
y_position: y,
|
|
795
|
+
viewport_width: window.innerWidth,
|
|
796
|
+
viewport_height: window.innerHeight,
|
|
797
|
+
screenshot_path: screenshotUrl,
|
|
798
|
+
visibility,
|
|
799
|
+
sdk_user_email: userEmail || null,
|
|
800
|
+
sdk_user_name: userDisplayName || null
|
|
801
|
+
};
|
|
802
|
+
const insertThread = async (payload) => client.from("threads").insert(payload).select().single();
|
|
803
|
+
let { data: thread, error: threadErr } = await insertThread(
|
|
804
|
+
getThreadsSchemaMode(workspaceId) === "legacy" ? toLegacyThreadInsertPayload(threadPayload) : threadPayload
|
|
805
|
+
);
|
|
806
|
+
if (threadErr && getThreadsSchemaMode(workspaceId) === "modern" && isMissingThreadsColumnError(threadErr)) {
|
|
807
|
+
if (annotationSurface === "review-iframe") {
|
|
808
|
+
throw new Error("This workspace is missing required review-surface thread columns. Apply the latest Issue Pin SDK migrations and retry.");
|
|
809
|
+
}
|
|
810
|
+
console.warn("[EW SDK] Retrying screenshot thread insert against legacy threads schema.");
|
|
811
|
+
setThreadsSchemaMode(workspaceId, "legacy");
|
|
812
|
+
({ data: thread, error: threadErr } = await insertThread(toLegacyThreadInsertPayload(threadPayload)));
|
|
813
|
+
}
|
|
814
|
+
if (threadErr) throw threadErr;
|
|
815
|
+
const { error: commentErr } = await client.from("thread_comments").insert({
|
|
816
|
+
thread_id: thread.id,
|
|
817
|
+
author_id: userId || null,
|
|
818
|
+
body,
|
|
819
|
+
visibility,
|
|
820
|
+
sdk_user_email: userEmail || null,
|
|
821
|
+
sdk_user_name: userDisplayName || null
|
|
822
|
+
});
|
|
823
|
+
if (commentErr) throw commentErr;
|
|
824
|
+
setPendingScreenshotPin(null);
|
|
825
|
+
setScreenshotDataUrl(null);
|
|
826
|
+
await fetchThreads();
|
|
827
|
+
},
|
|
828
|
+
[pendingScreenshotPin, screenshotDataUrl, client, actorReady, actorError, workspaceId, userId, userEmail, userDisplayName, fetchThreads, annotationSurface, reviewUrl]
|
|
829
|
+
);
|
|
830
|
+
if (!client) {
|
|
831
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
832
|
+
}
|
|
833
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
834
|
+
FeedbackContext.Provider,
|
|
835
|
+
{
|
|
836
|
+
value: {
|
|
837
|
+
client,
|
|
838
|
+
workspaceId,
|
|
839
|
+
siteUrl,
|
|
840
|
+
userId,
|
|
841
|
+
userEmail,
|
|
842
|
+
userDisplayName,
|
|
843
|
+
authReady,
|
|
844
|
+
actorReady,
|
|
845
|
+
actorError,
|
|
846
|
+
debug,
|
|
847
|
+
canPinOnPage,
|
|
848
|
+
canScreenshot: allowScreenshot,
|
|
849
|
+
showHints,
|
|
850
|
+
annotationSurface,
|
|
851
|
+
reviewUrl,
|
|
852
|
+
reviewOpen,
|
|
853
|
+
setReviewOpen,
|
|
854
|
+
scrollContainer,
|
|
855
|
+
resolveAnchor,
|
|
856
|
+
screenshotCapturing,
|
|
857
|
+
threads,
|
|
858
|
+
isLoadingThreads,
|
|
859
|
+
mode,
|
|
860
|
+
setMode,
|
|
861
|
+
feedbackActive,
|
|
862
|
+
setFeedbackActive,
|
|
863
|
+
menuOpen,
|
|
864
|
+
setMenuOpen,
|
|
865
|
+
openMenu,
|
|
866
|
+
closeMenu,
|
|
867
|
+
toggleMenu,
|
|
868
|
+
enterPinMode,
|
|
869
|
+
exitPinMode,
|
|
870
|
+
startScreenshotCapture,
|
|
871
|
+
pendingPin,
|
|
872
|
+
setPendingPin,
|
|
873
|
+
screenshotDataUrl,
|
|
874
|
+
setScreenshotDataUrl,
|
|
875
|
+
pendingScreenshotPin,
|
|
876
|
+
setPendingScreenshotPin,
|
|
877
|
+
submitThread,
|
|
878
|
+
submitScreenshotThread,
|
|
879
|
+
refreshThreads: fetchThreads
|
|
880
|
+
},
|
|
881
|
+
children: [
|
|
882
|
+
children,
|
|
883
|
+
flashing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
884
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ew-flash-fade { 0% { opacity: 0.85; } 100% { opacity: 0; } }` }),
|
|
885
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
886
|
+
"div",
|
|
887
|
+
{
|
|
888
|
+
style: {
|
|
889
|
+
position: "fixed",
|
|
890
|
+
inset: 0,
|
|
891
|
+
background: "white",
|
|
892
|
+
pointerEvents: "none",
|
|
893
|
+
zIndex: Z.flash,
|
|
894
|
+
animation: "ew-flash-fade 300ms ease-out forwards"
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
)
|
|
898
|
+
] })
|
|
899
|
+
]
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
function useFeedback() {
|
|
904
|
+
const ctx = (0, import_react2.useContext)(FeedbackContext);
|
|
905
|
+
if (!ctx) throw new Error("useFeedback must be used within <FeedbackProvider>");
|
|
906
|
+
return ctx;
|
|
907
|
+
}
|
|
908
|
+
function useFeedbackSafe() {
|
|
909
|
+
return (0, import_react2.useContext)(FeedbackContext);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/FeedbackOverlay.tsx
|
|
913
|
+
var import_react3 = require("react");
|
|
914
|
+
var import_react_dom = require("react-dom");
|
|
915
|
+
|
|
916
|
+
// src/positioning.ts
|
|
917
|
+
var positionedContainers = /* @__PURE__ */ new WeakMap();
|
|
918
|
+
function clampPercent(value) {
|
|
919
|
+
return Math.min(100, Math.max(0, value));
|
|
920
|
+
}
|
|
921
|
+
function docPercentToViewport(xPct, yPct) {
|
|
922
|
+
const doc = document.documentElement;
|
|
923
|
+
const left = xPct / 100 * doc.scrollWidth - window.scrollX;
|
|
924
|
+
const top = yPct / 100 * doc.scrollHeight - window.scrollY;
|
|
925
|
+
return { left, top };
|
|
926
|
+
}
|
|
927
|
+
function getContainerContentSize(container) {
|
|
928
|
+
return {
|
|
929
|
+
width: Math.max(container.scrollWidth, 1),
|
|
930
|
+
height: Math.max(container.scrollHeight, 1)
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function containerPercentToViewport(container, xPct, yPct) {
|
|
934
|
+
const rect = container.getBoundingClientRect();
|
|
935
|
+
const { width, height } = getContainerContentSize(container);
|
|
936
|
+
return {
|
|
937
|
+
left: rect.left + xPct / 100 * width - container.scrollLeft,
|
|
938
|
+
top: rect.top + yPct / 100 * height - container.scrollTop
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function elementCenterToViewport(element) {
|
|
942
|
+
const rect = element.getBoundingClientRect();
|
|
943
|
+
return {
|
|
944
|
+
left: rect.left + rect.width / 2,
|
|
945
|
+
top: rect.top + rect.height / 2
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function elementCenterToContainer(container, element) {
|
|
949
|
+
const containerRect = container.getBoundingClientRect();
|
|
950
|
+
const rect = element.getBoundingClientRect();
|
|
951
|
+
return {
|
|
952
|
+
left: rect.left - containerRect.left + container.scrollLeft + rect.width / 2,
|
|
953
|
+
top: rect.top - containerRect.top + container.scrollTop + rect.height / 2
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function containerPercentToContent(container, xPct, yPct) {
|
|
957
|
+
const { width, height } = getContainerContentSize(container);
|
|
958
|
+
return {
|
|
959
|
+
left: xPct / 100 * width,
|
|
960
|
+
top: yPct / 100 * height
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function ensurePositionedContainer(container) {
|
|
964
|
+
const computed = window.getComputedStyle(container);
|
|
965
|
+
if (computed.position !== "static") {
|
|
966
|
+
return () => {
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const existing = positionedContainers.get(container);
|
|
970
|
+
if (existing) {
|
|
971
|
+
existing.count += 1;
|
|
972
|
+
return () => {
|
|
973
|
+
existing.count -= 1;
|
|
974
|
+
if (existing.count === 0) {
|
|
975
|
+
container.style.position = existing.previousInlinePosition;
|
|
976
|
+
positionedContainers.delete(container);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const state = {
|
|
981
|
+
count: 1,
|
|
982
|
+
previousInlinePosition: container.style.position
|
|
983
|
+
};
|
|
984
|
+
positionedContainers.set(container, state);
|
|
985
|
+
container.style.position = "relative";
|
|
986
|
+
return () => {
|
|
987
|
+
state.count -= 1;
|
|
988
|
+
if (state.count === 0) {
|
|
989
|
+
container.style.position = state.previousInlinePosition;
|
|
990
|
+
positionedContainers.delete(container);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/FeedbackOverlay.tsx
|
|
996
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
997
|
+
function getElementTag(el) {
|
|
998
|
+
let tag = el.tagName.toLowerCase();
|
|
999
|
+
if (el.className && typeof el.className === "string") {
|
|
1000
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 3).join(".");
|
|
1001
|
+
if (classes) tag += `.${classes}`;
|
|
1002
|
+
}
|
|
1003
|
+
return tag;
|
|
1004
|
+
}
|
|
1005
|
+
function escapeSelectorValue(value) {
|
|
1006
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
1007
|
+
return CSS.escape(value);
|
|
1008
|
+
}
|
|
1009
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
1010
|
+
}
|
|
1011
|
+
function generateFallbackSelector(el) {
|
|
1012
|
+
if (!(el instanceof HTMLElement)) return null;
|
|
1013
|
+
if (el.id) return `#${escapeSelectorValue(el.id)}`;
|
|
1014
|
+
const stableAttr = ["data-testid", "data-qa", "data-cy"].find((name) => el.getAttribute(name));
|
|
1015
|
+
if (stableAttr) {
|
|
1016
|
+
return `[${stableAttr}="${escapeSelectorValue(el.getAttribute(stableAttr) || "")}"]`;
|
|
1017
|
+
}
|
|
1018
|
+
const segments = [];
|
|
1019
|
+
let current = el;
|
|
1020
|
+
while (current && current !== document.body && segments.length < 5) {
|
|
1021
|
+
let segment = current.tagName.toLowerCase();
|
|
1022
|
+
if (current.classList.length > 0) {
|
|
1023
|
+
segment += `.${escapeSelectorValue(current.classList[0])}`;
|
|
1024
|
+
segments.unshift(segment);
|
|
1025
|
+
current = current.parentElement;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
const parent = current.parentElement;
|
|
1029
|
+
if (!parent) break;
|
|
1030
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
|
|
1031
|
+
const siblingIndex = siblings.indexOf(current) + 1;
|
|
1032
|
+
segment += `:nth-of-type(${siblingIndex})`;
|
|
1033
|
+
segments.unshift(segment);
|
|
1034
|
+
current = parent;
|
|
1035
|
+
}
|
|
1036
|
+
return segments.length > 0 ? segments.join(" > ") : null;
|
|
1037
|
+
}
|
|
1038
|
+
function FeedbackOverlay() {
|
|
1039
|
+
const ctx = useFeedbackSafe();
|
|
1040
|
+
const [containerVersion, setContainerVersion] = (0, import_react3.useState)(0);
|
|
1041
|
+
const scrollContainer = ctx?.scrollContainer;
|
|
1042
|
+
const container = scrollContainer?.ref.current ?? null;
|
|
1043
|
+
(0, import_react3.useEffect)(() => {
|
|
1044
|
+
if (!container) return;
|
|
1045
|
+
const restorePosition = ensurePositionedContainer(container);
|
|
1046
|
+
const resizeObserver = new ResizeObserver(() => setContainerVersion((value) => value + 1));
|
|
1047
|
+
resizeObserver.observe(container);
|
|
1048
|
+
const schedule = () => setContainerVersion((value) => value + 1);
|
|
1049
|
+
container.addEventListener("scroll", schedule, { passive: true });
|
|
1050
|
+
return () => {
|
|
1051
|
+
container.removeEventListener("scroll", schedule);
|
|
1052
|
+
resizeObserver.disconnect();
|
|
1053
|
+
restorePosition();
|
|
1054
|
+
};
|
|
1055
|
+
}, [container]);
|
|
1056
|
+
const containerLayerStyle = (0, import_react3.useMemo)(() => {
|
|
1057
|
+
if (!container) return null;
|
|
1058
|
+
const { width, height } = getContainerContentSize(container);
|
|
1059
|
+
return {
|
|
1060
|
+
position: "absolute",
|
|
1061
|
+
inset: 0,
|
|
1062
|
+
width,
|
|
1063
|
+
height,
|
|
1064
|
+
background: "transparent",
|
|
1065
|
+
zIndex: Z.overlay,
|
|
1066
|
+
cursor: "crosshair"
|
|
1067
|
+
};
|
|
1068
|
+
}, [container, containerVersion]);
|
|
1069
|
+
const handleClick = (0, import_react3.useCallback)(
|
|
1070
|
+
(e) => {
|
|
1071
|
+
if (!ctx) return;
|
|
1072
|
+
const { mode, setPendingPin, resolveAnchor } = ctx;
|
|
1073
|
+
if (mode !== "annotate") return;
|
|
1074
|
+
e.preventDefault();
|
|
1075
|
+
e.stopPropagation();
|
|
1076
|
+
const overlay2 = e.currentTarget;
|
|
1077
|
+
overlay2.style.pointerEvents = "none";
|
|
1078
|
+
let el = null;
|
|
1079
|
+
try {
|
|
1080
|
+
el = document.elementFromPoint(e.clientX, e.clientY);
|
|
1081
|
+
} finally {
|
|
1082
|
+
overlay2.style.pointerEvents = "auto";
|
|
1083
|
+
}
|
|
1084
|
+
if (!el || el === overlay2) return;
|
|
1085
|
+
const anchor = resolveAnchor?.(el) ?? null;
|
|
1086
|
+
const selector = anchor?.selector ?? generateFallbackSelector(el);
|
|
1087
|
+
let x = 0;
|
|
1088
|
+
let y = 0;
|
|
1089
|
+
let coordinateSpace = "document";
|
|
1090
|
+
let containerKey = null;
|
|
1091
|
+
if (scrollContainer?.ref.current) {
|
|
1092
|
+
const containerEl = scrollContainer.ref.current;
|
|
1093
|
+
const { width, height } = getContainerContentSize(containerEl);
|
|
1094
|
+
const rect = overlay2.getBoundingClientRect();
|
|
1095
|
+
const localX = e.clientX - rect.left;
|
|
1096
|
+
const localY = e.clientY - rect.top;
|
|
1097
|
+
x = clampPercent(localX / width * 100);
|
|
1098
|
+
y = clampPercent(localY / height * 100);
|
|
1099
|
+
coordinateSpace = "container";
|
|
1100
|
+
containerKey = scrollContainer.key;
|
|
1101
|
+
} else {
|
|
1102
|
+
const doc = document.documentElement;
|
|
1103
|
+
const cw = Math.max(doc.scrollWidth, 1);
|
|
1104
|
+
const ch = Math.max(doc.scrollHeight, 1);
|
|
1105
|
+
const cx = e.clientX + window.scrollX;
|
|
1106
|
+
const cy = e.clientY + window.scrollY;
|
|
1107
|
+
x = clampPercent(cx / cw * 100);
|
|
1108
|
+
y = clampPercent(cy / ch * 100);
|
|
1109
|
+
}
|
|
1110
|
+
const elementText = (el.textContent || "").trim().slice(0, 200);
|
|
1111
|
+
const elementTag = getElementTag(el);
|
|
1112
|
+
setPendingPin({
|
|
1113
|
+
annotationSurface: "host-dom",
|
|
1114
|
+
anchorKey: anchor?.key ?? null,
|
|
1115
|
+
coordinateSpace,
|
|
1116
|
+
containerKey,
|
|
1117
|
+
x,
|
|
1118
|
+
y,
|
|
1119
|
+
selector,
|
|
1120
|
+
elementText,
|
|
1121
|
+
elementTag,
|
|
1122
|
+
pageUrl: window.location.pathname,
|
|
1123
|
+
reviewUrl: null
|
|
1124
|
+
});
|
|
1125
|
+
},
|
|
1126
|
+
[ctx, scrollContainer]
|
|
1127
|
+
);
|
|
1128
|
+
if (!ctx || ctx.mode !== "annotate") return null;
|
|
1129
|
+
const overlay = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1130
|
+
"div",
|
|
1131
|
+
{
|
|
1132
|
+
"data-ew-feedback-interactive": "true",
|
|
1133
|
+
onPointerDownCapture: (e) => e.stopPropagation(),
|
|
1134
|
+
className: container ? "" : "fixed inset-0 cursor-crosshair",
|
|
1135
|
+
style: container ? containerLayerStyle ?? void 0 : { background: "transparent", zIndex: Z.overlay },
|
|
1136
|
+
onClick: handleClick
|
|
1137
|
+
}
|
|
1138
|
+
);
|
|
1139
|
+
if (container) {
|
|
1140
|
+
return (0, import_react_dom.createPortal)(overlay, container);
|
|
1141
|
+
}
|
|
1142
|
+
return overlay;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/ThreadPins.tsx
|
|
1146
|
+
var import_react5 = require("react");
|
|
1147
|
+
var import_react_dom2 = require("react-dom");
|
|
1148
|
+
var import_lucide_react = require("lucide-react");
|
|
1149
|
+
|
|
1150
|
+
// src/anchorRegistry.ts
|
|
1151
|
+
var import_react4 = require("react");
|
|
1152
|
+
var anchors = /* @__PURE__ */ new Map();
|
|
1153
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
1154
|
+
function emitChange() {
|
|
1155
|
+
listeners2.forEach((listener) => listener());
|
|
1156
|
+
}
|
|
1157
|
+
function getRegisteredAnchor(anchorKey) {
|
|
1158
|
+
if (!anchorKey) return null;
|
|
1159
|
+
return anchors.get(anchorKey) ?? null;
|
|
1160
|
+
}
|
|
1161
|
+
function subscribeAnchorRegistry(listener) {
|
|
1162
|
+
listeners2.add(listener);
|
|
1163
|
+
return () => {
|
|
1164
|
+
listeners2.delete(listener);
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function useIssuePinAnchor(anchorKey) {
|
|
1168
|
+
const currentNodeRef = (0, import_react4.useRef)(null);
|
|
1169
|
+
return (0, import_react4.useCallback)((node) => {
|
|
1170
|
+
const currentNode = currentNodeRef.current;
|
|
1171
|
+
if (currentNode && currentNode !== node && anchors.get(anchorKey) === currentNode) {
|
|
1172
|
+
anchors.delete(anchorKey);
|
|
1173
|
+
emitChange();
|
|
1174
|
+
}
|
|
1175
|
+
currentNodeRef.current = node;
|
|
1176
|
+
if (node && anchors.get(anchorKey) !== node) {
|
|
1177
|
+
anchors.set(anchorKey, node);
|
|
1178
|
+
emitChange();
|
|
1179
|
+
}
|
|
1180
|
+
}, [anchorKey]);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/ThreadPins.tsx
|
|
1184
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1185
|
+
var EMPTY_THREADS = [];
|
|
1186
|
+
function resolveSelector(selector) {
|
|
1187
|
+
if (!selector) return null;
|
|
1188
|
+
try {
|
|
1189
|
+
return document.querySelector(selector);
|
|
1190
|
+
} catch {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function arePositionsEqual(prev, next) {
|
|
1195
|
+
return prev.length === next.length && prev.every(
|
|
1196
|
+
(pin, index) => pin.threadId === next[index].threadId && Math.round(pin.top) === Math.round(next[index].top) && Math.round(pin.left) === Math.round(next[index].left)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
function PinMarker({
|
|
1200
|
+
pin,
|
|
1201
|
+
isHighlighted,
|
|
1202
|
+
onClick,
|
|
1203
|
+
containerMode
|
|
1204
|
+
}) {
|
|
1205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1206
|
+
"div",
|
|
1207
|
+
{
|
|
1208
|
+
"data-ew-feedback-interactive": "true",
|
|
1209
|
+
className: `${containerMode ? "absolute" : "fixed"} pointer-events-auto`,
|
|
1210
|
+
style: { top: pin.top - 12, left: pin.left - 12, zIndex: Z.pins },
|
|
1211
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1212
|
+
"div",
|
|
1213
|
+
{
|
|
1214
|
+
onClick,
|
|
1215
|
+
className: `flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold shadow-lg cursor-pointer transition-transform hover:scale-125 ${pin.isScreenshot ? "bg-accent text-accent-foreground border border-border" : "bg-primary text-primary-foreground"} ${isHighlighted ? "scale-150 ring-4 ring-primary/50 animate-pulse" : ""}`,
|
|
1216
|
+
title: pin.isScreenshot ? `Thread #${pin.index} (screenshot \u2014 click to preview)` : `Thread #${pin.index}`,
|
|
1217
|
+
children: pin.isScreenshot ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react.Camera, { className: "h-3 w-3" }) : pin.index
|
|
1218
|
+
}
|
|
1219
|
+
)
|
|
1220
|
+
},
|
|
1221
|
+
pin.threadId
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
function ThreadPins() {
|
|
1225
|
+
const ctx = useFeedbackSafe();
|
|
1226
|
+
const [documentPositions, setDocumentPositions] = (0, import_react5.useState)([]);
|
|
1227
|
+
const [containerPositions, setContainerPositions] = (0, import_react5.useState)([]);
|
|
1228
|
+
const [containerVersion, setContainerVersion] = (0, import_react5.useState)(0);
|
|
1229
|
+
const rafRef = (0, import_react5.useRef)();
|
|
1230
|
+
const { search } = useDocumentLocationParts();
|
|
1231
|
+
const [highlightedThreadId, setHighlightedThreadId] = (0, import_react5.useState)(null);
|
|
1232
|
+
const highlightTimeoutRef = (0, import_react5.useRef)();
|
|
1233
|
+
const lastHighlightedRef = (0, import_react5.useRef)(null);
|
|
1234
|
+
const getSignedUrlRef = (0, import_react5.useRef)(() => Promise.resolve(null));
|
|
1235
|
+
const [previewScreenshot, setPreviewScreenshot] = (0, import_react5.useState)(null);
|
|
1236
|
+
const signedUrlCache = (0, import_react5.useRef)({});
|
|
1237
|
+
const threads = ctx?.threads ?? EMPTY_THREADS;
|
|
1238
|
+
const client = ctx?.client;
|
|
1239
|
+
const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
|
|
1240
|
+
const scrollContainer = ctx?.scrollContainer;
|
|
1241
|
+
const container = scrollContainer?.ref.current ?? null;
|
|
1242
|
+
const getSignedUrl = (0, import_react5.useCallback)(async (path) => {
|
|
1243
|
+
if (!client) return null;
|
|
1244
|
+
const storagePath = path.includes("/object/public/screenshots/") ? path.split("/object/public/screenshots/")[1] : path.startsWith("http") ? null : path;
|
|
1245
|
+
if (!storagePath) return path;
|
|
1246
|
+
const cached = signedUrlCache.current[storagePath];
|
|
1247
|
+
if (cached && cached.expires > Date.now()) return cached.url;
|
|
1248
|
+
const { data, error } = await client.storage.from("screenshots").createSignedUrl(storagePath, 3600);
|
|
1249
|
+
if (error || !data) return null;
|
|
1250
|
+
signedUrlCache.current[storagePath] = { url: data.signedUrl, expires: Date.now() + 50 * 60 * 1e3 };
|
|
1251
|
+
return data.signedUrl;
|
|
1252
|
+
}, [client]);
|
|
1253
|
+
getSignedUrlRef.current = getSignedUrl;
|
|
1254
|
+
(0, import_react5.useEffect)(() => {
|
|
1255
|
+
if (!container) return;
|
|
1256
|
+
const restorePosition = ensurePositionedContainer(container);
|
|
1257
|
+
const resizeObserver = new ResizeObserver(() => setContainerVersion((value) => value + 1));
|
|
1258
|
+
resizeObserver.observe(container);
|
|
1259
|
+
return () => {
|
|
1260
|
+
resizeObserver.disconnect();
|
|
1261
|
+
restorePosition();
|
|
1262
|
+
};
|
|
1263
|
+
}, [container]);
|
|
1264
|
+
(0, import_react5.useEffect)(() => subscribeAnchorRegistry(() => {
|
|
1265
|
+
setContainerVersion((value) => value + 1);
|
|
1266
|
+
}), []);
|
|
1267
|
+
(0, import_react5.useEffect)(() => {
|
|
1268
|
+
const threadId = new URLSearchParams(search).get("highlight_thread");
|
|
1269
|
+
if (!threadId || threads.length === 0) return;
|
|
1270
|
+
if (lastHighlightedRef.current === threadId) return;
|
|
1271
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
1272
|
+
if (!thread) return;
|
|
1273
|
+
lastHighlightedRef.current = threadId;
|
|
1274
|
+
const params = new URLSearchParams(search);
|
|
1275
|
+
params.delete("highlight_thread");
|
|
1276
|
+
const next = `${window.location.pathname}${params.toString() ? `?${params}` : ""}${window.location.hash}`;
|
|
1277
|
+
window.history.replaceState({}, "", next);
|
|
1278
|
+
setHighlightedThreadId(threadId);
|
|
1279
|
+
if (thread.element_tag === "screenshot" && thread.screenshot_path && thread.x_position != null && thread.y_position != null) {
|
|
1280
|
+
getSignedUrlRef.current(thread.screenshot_path).then((url) => {
|
|
1281
|
+
if (url) {
|
|
1282
|
+
setPreviewScreenshot({
|
|
1283
|
+
url,
|
|
1284
|
+
x: thread.x_position,
|
|
1285
|
+
y: thread.y_position
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
highlightTimeoutRef.current = setTimeout(() => {
|
|
1291
|
+
setHighlightedThreadId(null);
|
|
1292
|
+
}, 3e3);
|
|
1293
|
+
return () => {
|
|
1294
|
+
if (highlightTimeoutRef.current) clearTimeout(highlightTimeoutRef.current);
|
|
1295
|
+
};
|
|
1296
|
+
}, [search, threads]);
|
|
1297
|
+
const recalculate = (0, import_react5.useCallback)(() => {
|
|
1298
|
+
const nextDocumentPins = [];
|
|
1299
|
+
const nextContainerPins = [];
|
|
1300
|
+
threads.forEach((thread, index) => {
|
|
1301
|
+
const isScreenshot = thread.element_tag === "screenshot";
|
|
1302
|
+
const x = thread.x_position ?? 0;
|
|
1303
|
+
const y = thread.y_position ?? 0;
|
|
1304
|
+
const pin = {
|
|
1305
|
+
threadId: thread.id,
|
|
1306
|
+
top: 0,
|
|
1307
|
+
left: 0,
|
|
1308
|
+
index: index + 1,
|
|
1309
|
+
isScreenshot,
|
|
1310
|
+
screenshotPath: thread.screenshot_path,
|
|
1311
|
+
screenshotX: thread.x_position,
|
|
1312
|
+
screenshotY: thread.y_position
|
|
1313
|
+
};
|
|
1314
|
+
if (!isScreenshot && thread.coordinate_space === "container") {
|
|
1315
|
+
if (!container || !thread.container_key || thread.container_key !== scrollContainer?.key) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const anchorElement2 = getRegisteredAnchor(thread.anchor_key) ?? resolveSelector(thread.selector);
|
|
1319
|
+
const resolved2 = anchorElement2 ? elementCenterToContainer(container, anchorElement2) : containerPercentToContent(container, x, y);
|
|
1320
|
+
pin.left = resolved2.left;
|
|
1321
|
+
pin.top = resolved2.top;
|
|
1322
|
+
nextContainerPins.push(pin);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const anchorElement = !isScreenshot ? getRegisteredAnchor(thread.anchor_key) ?? resolveSelector(thread.selector) : null;
|
|
1326
|
+
const resolved = anchorElement ? elementCenterToViewport(anchorElement) : docPercentToViewport(x, y);
|
|
1327
|
+
pin.left = resolved.left;
|
|
1328
|
+
pin.top = resolved.top;
|
|
1329
|
+
nextDocumentPins.push(pin);
|
|
1330
|
+
});
|
|
1331
|
+
setDocumentPositions((prev) => arePositionsEqual(prev, nextDocumentPins) ? prev : nextDocumentPins);
|
|
1332
|
+
setContainerPositions((prev) => arePositionsEqual(prev, nextContainerPins) ? prev : nextContainerPins);
|
|
1333
|
+
}, [container, scrollContainer?.key, threads]);
|
|
1334
|
+
const scheduleRecalculate = (0, import_react5.useCallback)(() => {
|
|
1335
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
1336
|
+
rafRef.current = requestAnimationFrame(recalculate);
|
|
1337
|
+
}, [recalculate]);
|
|
1338
|
+
(0, import_react5.useLayoutEffect)(() => {
|
|
1339
|
+
recalculate();
|
|
1340
|
+
}, [recalculate, containerVersion]);
|
|
1341
|
+
(0, import_react5.useEffect)(() => {
|
|
1342
|
+
window.addEventListener("scroll", scheduleRecalculate, true);
|
|
1343
|
+
window.addEventListener("resize", scheduleRecalculate);
|
|
1344
|
+
window.addEventListener("load", scheduleRecalculate);
|
|
1345
|
+
container?.addEventListener("scroll", scheduleRecalculate, { passive: true });
|
|
1346
|
+
return () => {
|
|
1347
|
+
window.removeEventListener("scroll", scheduleRecalculate, true);
|
|
1348
|
+
window.removeEventListener("resize", scheduleRecalculate);
|
|
1349
|
+
window.removeEventListener("load", scheduleRecalculate);
|
|
1350
|
+
container?.removeEventListener("scroll", scheduleRecalculate);
|
|
1351
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
1352
|
+
};
|
|
1353
|
+
}, [container, scheduleRecalculate]);
|
|
1354
|
+
const handlePinClick = (0, import_react5.useCallback)((pin) => {
|
|
1355
|
+
if (pin.isScreenshot && pin.screenshotPath && pin.screenshotX != null && pin.screenshotY != null) {
|
|
1356
|
+
getSignedUrl(pin.screenshotPath).then((signedUrl) => {
|
|
1357
|
+
if (signedUrl) {
|
|
1358
|
+
setPreviewScreenshot({ url: signedUrl, x: pin.screenshotX, y: pin.screenshotY });
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
window.open(`${threadBaseUrl}/threads/${pin.threadId}`, "_blank", "noopener,noreferrer");
|
|
1364
|
+
}, [getSignedUrl, threadBaseUrl]);
|
|
1365
|
+
const containerLayer = (0, import_react5.useMemo)(() => {
|
|
1366
|
+
if (!container || containerPositions.length === 0) return null;
|
|
1367
|
+
const { width, height } = getContainerContentSize(container);
|
|
1368
|
+
return (0, import_react_dom2.createPortal)(
|
|
1369
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1370
|
+
"div",
|
|
1371
|
+
{
|
|
1372
|
+
"data-ew-feedback-interactive": "true",
|
|
1373
|
+
style: {
|
|
1374
|
+
position: "absolute",
|
|
1375
|
+
inset: 0,
|
|
1376
|
+
width,
|
|
1377
|
+
height,
|
|
1378
|
+
pointerEvents: "none",
|
|
1379
|
+
zIndex: Z.pins
|
|
1380
|
+
},
|
|
1381
|
+
children: containerPositions.map((pin) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1382
|
+
PinMarker,
|
|
1383
|
+
{
|
|
1384
|
+
pin,
|
|
1385
|
+
isHighlighted: pin.threadId === highlightedThreadId,
|
|
1386
|
+
onClick: () => handlePinClick(pin),
|
|
1387
|
+
containerMode: true
|
|
1388
|
+
},
|
|
1389
|
+
pin.threadId
|
|
1390
|
+
))
|
|
1391
|
+
}
|
|
1392
|
+
),
|
|
1393
|
+
container
|
|
1394
|
+
);
|
|
1395
|
+
}, [container, containerPositions, handlePinClick, highlightedThreadId, containerVersion]);
|
|
1396
|
+
if (!ctx || documentPositions.length === 0 && containerPositions.length === 0 && !previewScreenshot) return null;
|
|
1397
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1398
|
+
documentPositions.map((pin) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1399
|
+
PinMarker,
|
|
1400
|
+
{
|
|
1401
|
+
pin,
|
|
1402
|
+
isHighlighted: pin.threadId === highlightedThreadId,
|
|
1403
|
+
onClick: () => handlePinClick(pin),
|
|
1404
|
+
containerMode: false
|
|
1405
|
+
},
|
|
1406
|
+
pin.threadId
|
|
1407
|
+
)),
|
|
1408
|
+
containerLayer,
|
|
1409
|
+
previewScreenshot && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1410
|
+
"div",
|
|
1411
|
+
{
|
|
1412
|
+
"data-ew-feedback-interactive": "true",
|
|
1413
|
+
onPointerDownCapture: (e) => e.stopPropagation(),
|
|
1414
|
+
className: "fixed inset-0 flex items-center justify-center bg-black/70",
|
|
1415
|
+
style: { zIndex: Z.screenshotModal },
|
|
1416
|
+
onClick: () => setPreviewScreenshot(null),
|
|
1417
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative max-h-[85vh] max-w-[90vw] overflow-hidden rounded-lg shadow-2xl", onClick: (e) => e.stopPropagation(), children: [
|
|
1418
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1419
|
+
"img",
|
|
1420
|
+
{
|
|
1421
|
+
src: previewScreenshot.url,
|
|
1422
|
+
alt: "Screenshot context",
|
|
1423
|
+
className: "max-h-[85vh] max-w-[90vw] object-contain",
|
|
1424
|
+
draggable: false
|
|
1425
|
+
}
|
|
1426
|
+
),
|
|
1427
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1428
|
+
"div",
|
|
1429
|
+
{
|
|
1430
|
+
className: "absolute pointer-events-none",
|
|
1431
|
+
style: {
|
|
1432
|
+
left: `${previewScreenshot.x}%`,
|
|
1433
|
+
top: `${previewScreenshot.y}%`,
|
|
1434
|
+
transform: "translate(-50%, -50%)"
|
|
1435
|
+
},
|
|
1436
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-6 w-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-[10px] font-bold shadow-lg ring-4 ring-primary/30 animate-pulse", children: "\u2022" })
|
|
1437
|
+
}
|
|
1438
|
+
),
|
|
1439
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1440
|
+
"button",
|
|
1441
|
+
{
|
|
1442
|
+
onClick: () => setPreviewScreenshot(null),
|
|
1443
|
+
className: "absolute top-2 right-2 text-[10px] px-2 py-1 rounded bg-card text-foreground border border-border hover:bg-accent",
|
|
1444
|
+
children: "Close"
|
|
1445
|
+
}
|
|
1446
|
+
)
|
|
1447
|
+
] })
|
|
1448
|
+
}
|
|
1449
|
+
)
|
|
1450
|
+
] });
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/ReviewSurfaceOverlay.tsx
|
|
1454
|
+
var import_react6 = require("react");
|
|
1455
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1456
|
+
var EMPTY_THREADS2 = [];
|
|
1457
|
+
function PinMarker2({
|
|
1458
|
+
index,
|
|
1459
|
+
left,
|
|
1460
|
+
top,
|
|
1461
|
+
onClick
|
|
1462
|
+
}) {
|
|
1463
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1464
|
+
"div",
|
|
1465
|
+
{
|
|
1466
|
+
className: "absolute pointer-events-auto",
|
|
1467
|
+
style: { left: left - 12, top: top - 12, zIndex: Z.pins },
|
|
1468
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1469
|
+
"button",
|
|
1470
|
+
{
|
|
1471
|
+
type: "button",
|
|
1472
|
+
onClick,
|
|
1473
|
+
className: "flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground shadow-lg transition-transform hover:scale-125",
|
|
1474
|
+
title: `Thread #${index}`,
|
|
1475
|
+
children: index
|
|
1476
|
+
}
|
|
1477
|
+
)
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
function ReviewSurfaceOverlay() {
|
|
1482
|
+
const ctx = useFeedbackSafe();
|
|
1483
|
+
const iframeRef = (0, import_react6.useRef)(null);
|
|
1484
|
+
const [metrics, setMetrics] = (0, import_react6.useState)({ width: 0, height: 0, scrollX: 0, scrollY: 0 });
|
|
1485
|
+
const [frameReady, setFrameReady] = (0, import_react6.useState)(false);
|
|
1486
|
+
const [frameError, setFrameError] = (0, import_react6.useState)(null);
|
|
1487
|
+
const open = ctx?.reviewOpen ?? false;
|
|
1488
|
+
const mode = ctx?.mode ?? "view";
|
|
1489
|
+
const reviewUrl = ctx?.reviewUrl ?? null;
|
|
1490
|
+
const threads = ctx?.threads ?? EMPTY_THREADS2;
|
|
1491
|
+
const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
|
|
1492
|
+
const updateMetrics = (0, import_react6.useCallback)(() => {
|
|
1493
|
+
const iframe = iframeRef.current;
|
|
1494
|
+
if (!iframe) return;
|
|
1495
|
+
try {
|
|
1496
|
+
const doc = iframe.contentDocument;
|
|
1497
|
+
const win = iframe.contentWindow;
|
|
1498
|
+
const docEl = doc?.documentElement;
|
|
1499
|
+
if (!doc || !win || !docEl) {
|
|
1500
|
+
setFrameReady(false);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
setMetrics({
|
|
1504
|
+
width: Math.max(docEl.scrollWidth, 1),
|
|
1505
|
+
height: Math.max(docEl.scrollHeight, 1),
|
|
1506
|
+
scrollX: win.scrollX,
|
|
1507
|
+
scrollY: win.scrollY
|
|
1508
|
+
});
|
|
1509
|
+
setFrameReady(true);
|
|
1510
|
+
setFrameError(null);
|
|
1511
|
+
} catch {
|
|
1512
|
+
setFrameReady(false);
|
|
1513
|
+
setFrameError("Review URL must be same-origin and iframe-embeddable.");
|
|
1514
|
+
}
|
|
1515
|
+
}, []);
|
|
1516
|
+
(0, import_react6.useEffect)(() => {
|
|
1517
|
+
if (!open) return;
|
|
1518
|
+
if (!reviewUrl) {
|
|
1519
|
+
setFrameReady(false);
|
|
1520
|
+
setFrameError("Review mode requires a reviewUrl.");
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const iframe = iframeRef.current;
|
|
1524
|
+
if (!iframe) return;
|
|
1525
|
+
let cleanupResizeObserver;
|
|
1526
|
+
let cleanupScrollListener;
|
|
1527
|
+
const install = () => {
|
|
1528
|
+
updateMetrics();
|
|
1529
|
+
try {
|
|
1530
|
+
const doc = iframe.contentDocument;
|
|
1531
|
+
const win = iframe.contentWindow;
|
|
1532
|
+
const docEl = doc?.documentElement;
|
|
1533
|
+
if (!doc || !win || !docEl) {
|
|
1534
|
+
setFrameError("Unable to access the review surface.");
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const handleScroll = () => updateMetrics();
|
|
1538
|
+
win.addEventListener("scroll", handleScroll, { passive: true });
|
|
1539
|
+
window.addEventListener("resize", updateMetrics);
|
|
1540
|
+
cleanupScrollListener = () => {
|
|
1541
|
+
win.removeEventListener("scroll", handleScroll);
|
|
1542
|
+
window.removeEventListener("resize", updateMetrics);
|
|
1543
|
+
};
|
|
1544
|
+
const resizeObserver = new ResizeObserver(() => updateMetrics());
|
|
1545
|
+
resizeObserver.observe(docEl);
|
|
1546
|
+
if (doc.body) resizeObserver.observe(doc.body);
|
|
1547
|
+
cleanupResizeObserver = () => resizeObserver.disconnect();
|
|
1548
|
+
} catch {
|
|
1549
|
+
setFrameReady(false);
|
|
1550
|
+
setFrameError("Review URL must be same-origin and iframe-embeddable.");
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
const handleLoad = () => install();
|
|
1554
|
+
iframe.addEventListener("load", handleLoad);
|
|
1555
|
+
if (iframe.contentDocument?.readyState === "complete") {
|
|
1556
|
+
install();
|
|
1557
|
+
}
|
|
1558
|
+
return () => {
|
|
1559
|
+
iframe.removeEventListener("load", handleLoad);
|
|
1560
|
+
cleanupResizeObserver?.();
|
|
1561
|
+
cleanupScrollListener?.();
|
|
1562
|
+
};
|
|
1563
|
+
}, [open, reviewUrl, updateMetrics]);
|
|
1564
|
+
const handleClose = (0, import_react6.useCallback)(() => {
|
|
1565
|
+
if (!ctx) return;
|
|
1566
|
+
ctx.setPendingPin(null);
|
|
1567
|
+
ctx.setReviewOpen(false);
|
|
1568
|
+
ctx.setMode("view");
|
|
1569
|
+
}, [ctx]);
|
|
1570
|
+
const handleOverlayClick = (0, import_react6.useCallback)((event) => {
|
|
1571
|
+
if (!ctx || mode !== "annotate") return;
|
|
1572
|
+
if (!reviewUrl || !frameReady || frameError) return;
|
|
1573
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1574
|
+
const x = clampPercent((event.clientX - rect.left) / Math.max(metrics.width, 1) * 100);
|
|
1575
|
+
const y = clampPercent((event.clientY - rect.top) / Math.max(metrics.height, 1) * 100);
|
|
1576
|
+
ctx.setPendingPin({
|
|
1577
|
+
annotationSurface: "review-iframe",
|
|
1578
|
+
anchorKey: null,
|
|
1579
|
+
coordinateSpace: "document",
|
|
1580
|
+
containerKey: null,
|
|
1581
|
+
x,
|
|
1582
|
+
y,
|
|
1583
|
+
selector: null,
|
|
1584
|
+
elementText: reviewUrl,
|
|
1585
|
+
elementTag: "iframe.review-surface",
|
|
1586
|
+
pageUrl: window.location.pathname,
|
|
1587
|
+
reviewUrl,
|
|
1588
|
+
viewportLeft: event.clientX,
|
|
1589
|
+
viewportTop: event.clientY
|
|
1590
|
+
});
|
|
1591
|
+
}, [ctx, frameError, frameReady, metrics.height, metrics.width, mode, reviewUrl]);
|
|
1592
|
+
const handleWheel = (0, import_react6.useCallback)((event) => {
|
|
1593
|
+
const win = iframeRef.current?.contentWindow;
|
|
1594
|
+
if (!win) return;
|
|
1595
|
+
win.scrollBy(event.deltaX, event.deltaY);
|
|
1596
|
+
event.preventDefault();
|
|
1597
|
+
}, []);
|
|
1598
|
+
const layerStyle = (0, import_react6.useMemo)(() => ({
|
|
1599
|
+
position: "absolute",
|
|
1600
|
+
top: 0,
|
|
1601
|
+
left: 0,
|
|
1602
|
+
width: metrics.width,
|
|
1603
|
+
height: metrics.height,
|
|
1604
|
+
transform: `translate(${-metrics.scrollX}px, ${-metrics.scrollY}px)`
|
|
1605
|
+
}), [metrics.height, metrics.scrollX, metrics.scrollY, metrics.width]);
|
|
1606
|
+
if (!ctx || ctx.annotationSurface !== "review-iframe" || !open) return null;
|
|
1607
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1608
|
+
"div",
|
|
1609
|
+
{
|
|
1610
|
+
"data-ew-feedback-interactive": "true",
|
|
1611
|
+
onPointerDownCapture: (e) => e.stopPropagation(),
|
|
1612
|
+
className: "fixed inset-0",
|
|
1613
|
+
style: { zIndex: Z.overlay, background: "rgba(8, 8, 10, 0.82)" },
|
|
1614
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex h-full flex-col p-4", children: [
|
|
1615
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1616
|
+
"div",
|
|
1617
|
+
{
|
|
1618
|
+
className: "mb-3 flex items-center justify-between rounded-lg border px-3 py-2",
|
|
1619
|
+
style: { borderColor: T.border, background: T.card, color: T.fg },
|
|
1620
|
+
children: [
|
|
1621
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "min-w-0", children: [
|
|
1622
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-sm font-medium", children: "Review Surface" }),
|
|
1623
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "truncate text-xs", style: { color: T.muted }, children: reviewUrl ?? "Missing review URL" })
|
|
1624
|
+
] }),
|
|
1625
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1626
|
+
"button",
|
|
1627
|
+
{
|
|
1628
|
+
type: "button",
|
|
1629
|
+
onClick: handleClose,
|
|
1630
|
+
className: "rounded px-3 py-1 text-xs",
|
|
1631
|
+
style: { background: T.accent, color: T.fg },
|
|
1632
|
+
children: "Close"
|
|
1633
|
+
}
|
|
1634
|
+
)
|
|
1635
|
+
]
|
|
1636
|
+
}
|
|
1637
|
+
),
|
|
1638
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative min-h-0 flex-1 overflow-hidden rounded-xl border", style: { borderColor: T.border, background: "#fff" }, children: [
|
|
1639
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1640
|
+
"iframe",
|
|
1641
|
+
{
|
|
1642
|
+
ref: iframeRef,
|
|
1643
|
+
src: reviewUrl ?? void 0,
|
|
1644
|
+
title: "Issue Pin review surface",
|
|
1645
|
+
className: "h-full w-full border-0"
|
|
1646
|
+
}
|
|
1647
|
+
),
|
|
1648
|
+
reviewUrl && !frameError && mode === "annotate" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1649
|
+
"div",
|
|
1650
|
+
{
|
|
1651
|
+
"data-ew-feedback-interactive": "true",
|
|
1652
|
+
className: "absolute inset-0 cursor-crosshair overflow-hidden",
|
|
1653
|
+
onClick: handleOverlayClick,
|
|
1654
|
+
onWheel: handleWheel,
|
|
1655
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: layerStyle })
|
|
1656
|
+
}
|
|
1657
|
+
),
|
|
1658
|
+
reviewUrl && !frameError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `${mode === "annotate" ? "pointer-events-none " : ""}absolute inset-0 overflow-hidden`, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: layerStyle, children: threads.map((thread, index) => {
|
|
1659
|
+
const x = thread.x_position ?? 0;
|
|
1660
|
+
const y = thread.y_position ?? 0;
|
|
1661
|
+
const left = x / 100 * metrics.width;
|
|
1662
|
+
const top = y / 100 * metrics.height;
|
|
1663
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1664
|
+
PinMarker2,
|
|
1665
|
+
{
|
|
1666
|
+
index: index + 1,
|
|
1667
|
+
left,
|
|
1668
|
+
top,
|
|
1669
|
+
onClick: () => window.open(`${threadBaseUrl}/threads/${thread.id}`, "_blank", "noopener,noreferrer")
|
|
1670
|
+
},
|
|
1671
|
+
thread.id
|
|
1672
|
+
);
|
|
1673
|
+
}) }) }),
|
|
1674
|
+
!reviewUrl && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm", style: { color: T.fg, background: "rgba(20,20,24,0.9)" }, children: "Review mode requires a `reviewUrl`." }),
|
|
1675
|
+
reviewUrl && frameError && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm", style: { color: T.fg, background: "rgba(20,20,24,0.9)" }, children: frameError }),
|
|
1676
|
+
reviewUrl && !frameError && !frameReady && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm", style: { color: T.fg, background: "rgba(20,20,24,0.5)" }, children: "Loading review surface\u2026" })
|
|
1677
|
+
] })
|
|
1678
|
+
] })
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/SdkCommentPopover.tsx
|
|
1684
|
+
var import_react7 = require("react");
|
|
1685
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1686
|
+
function SdkCommentPopover() {
|
|
1687
|
+
const ctx = useFeedbackSafe();
|
|
1688
|
+
const [body, setBody] = (0, import_react7.useState)("");
|
|
1689
|
+
const [visibility, setVisibility] = (0, import_react7.useState)("public");
|
|
1690
|
+
const [submitting, setSubmitting] = (0, import_react7.useState)(false);
|
|
1691
|
+
const [pos, setPos] = (0, import_react7.useState)({ top: 0, left: 0 });
|
|
1692
|
+
const pendingPin = ctx?.pendingPin ?? null;
|
|
1693
|
+
const recalc = (0, import_react7.useCallback)(() => {
|
|
1694
|
+
if (!pendingPin) return;
|
|
1695
|
+
if (pendingPin.annotationSurface === "review-iframe" && pendingPin.viewportLeft != null && pendingPin.viewportTop != null) {
|
|
1696
|
+
const pad2 = 16;
|
|
1697
|
+
const w2 = 280;
|
|
1698
|
+
const maxLeft2 = Math.max(pad2, window.innerWidth - w2 - pad2);
|
|
1699
|
+
setPos({
|
|
1700
|
+
top: pendingPin.viewportTop + pad2,
|
|
1701
|
+
left: Math.min(Math.max(pad2, pendingPin.viewportLeft), maxLeft2)
|
|
1702
|
+
});
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const container = ctx?.scrollContainer?.ref.current ?? null;
|
|
1706
|
+
const { left, top } = pendingPin.coordinateSpace === "container" && pendingPin.containerKey && container && ctx?.scrollContainer?.key === pendingPin.containerKey ? containerPercentToViewport(container, pendingPin.x, pendingPin.y) : docPercentToViewport(pendingPin.x, pendingPin.y);
|
|
1707
|
+
const pad = 16;
|
|
1708
|
+
const w = 280;
|
|
1709
|
+
const maxLeft = Math.max(pad, window.innerWidth - w - pad);
|
|
1710
|
+
setPos({
|
|
1711
|
+
top: top + pad,
|
|
1712
|
+
left: Math.min(Math.max(pad, left), maxLeft)
|
|
1713
|
+
});
|
|
1714
|
+
}, [ctx, pendingPin]);
|
|
1715
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
1716
|
+
recalc();
|
|
1717
|
+
}, [recalc]);
|
|
1718
|
+
(0, import_react7.useEffect)(() => {
|
|
1719
|
+
if (!pendingPin) return;
|
|
1720
|
+
window.addEventListener("scroll", recalc, true);
|
|
1721
|
+
window.addEventListener("resize", recalc);
|
|
1722
|
+
return () => {
|
|
1723
|
+
window.removeEventListener("scroll", recalc, true);
|
|
1724
|
+
window.removeEventListener("resize", recalc);
|
|
1725
|
+
};
|
|
1726
|
+
}, [pendingPin, recalc]);
|
|
1727
|
+
if (!ctx || !pendingPin) return null;
|
|
1728
|
+
const { setPendingPin, submitThread, authReady, actorReady, actorError, userDisplayName, userEmail } = ctx;
|
|
1729
|
+
const identityLabel = userDisplayName || userEmail || null;
|
|
1730
|
+
const handleSubmit = async () => {
|
|
1731
|
+
if (!body.trim() || submitting) return;
|
|
1732
|
+
if (!actorReady) {
|
|
1733
|
+
console.warn("[EW SDK] Cannot submit:", actorError || "still linking user identity\u2026");
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
setSubmitting(true);
|
|
1737
|
+
try {
|
|
1738
|
+
await submitThread(body.trim(), visibility);
|
|
1739
|
+
setBody("");
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
console.error("[EW SDK] Submit failed:", err);
|
|
1742
|
+
} finally {
|
|
1743
|
+
setSubmitting(false);
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1747
|
+
"div",
|
|
1748
|
+
{
|
|
1749
|
+
"data-ew-feedback-interactive": "true",
|
|
1750
|
+
onPointerDownCapture: (e) => e.stopPropagation(),
|
|
1751
|
+
style: { position: "fixed", zIndex: Z.popover, top: pos.top, left: pos.left },
|
|
1752
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { width: 256, borderRadius: 8, border: `1px solid ${T.border}`, background: T.card, boxShadow: "0 10px 25px -5px rgba(0,0,0,.5)", padding: 12 }, children: [
|
|
1753
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { marginBottom: 8, fontSize: 10, color: T.muted, fontFamily: "monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
|
|
1754
|
+
pendingPin.elementTag,
|
|
1755
|
+
pendingPin.elementText && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { marginLeft: 4, color: T.fg }, children: [
|
|
1756
|
+
'"',
|
|
1757
|
+
pendingPin.elementText.slice(0, 40),
|
|
1758
|
+
'"'
|
|
1759
|
+
] })
|
|
1760
|
+
] }),
|
|
1761
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { marginBottom: 6, fontSize: 10, color: identityLabel ? T.fg : T.muted }, children: !authReady ? "Identifying\u2026" : !actorReady ? actorError || "Linking identity\u2026" : identityLabel ? `Commenting as ${identityLabel}` : "Anonymous" }),
|
|
1762
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1763
|
+
"textarea",
|
|
1764
|
+
{
|
|
1765
|
+
style: {
|
|
1766
|
+
width: "100%",
|
|
1767
|
+
borderRadius: 6,
|
|
1768
|
+
border: `1px solid ${T.input}`,
|
|
1769
|
+
background: T.bg,
|
|
1770
|
+
color: T.fg,
|
|
1771
|
+
padding: "6px 8px",
|
|
1772
|
+
fontSize: 12,
|
|
1773
|
+
resize: "none",
|
|
1774
|
+
outline: "none",
|
|
1775
|
+
boxSizing: "border-box"
|
|
1776
|
+
},
|
|
1777
|
+
placeholder: "Add a comment\u2026",
|
|
1778
|
+
rows: 3,
|
|
1779
|
+
value: body,
|
|
1780
|
+
onChange: (e) => setBody(e.target.value),
|
|
1781
|
+
onKeyDown: (e) => {
|
|
1782
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
|
1783
|
+
},
|
|
1784
|
+
autoFocus: true
|
|
1785
|
+
}
|
|
1786
|
+
),
|
|
1787
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 8 }, children: [
|
|
1788
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1789
|
+
"select",
|
|
1790
|
+
{
|
|
1791
|
+
style: { fontSize: 10, background: T.bg, border: `1px solid ${T.input}`, borderRadius: 4, padding: "2px 4px", color: T.fg },
|
|
1792
|
+
value: visibility,
|
|
1793
|
+
onChange: (e) => setVisibility(e.target.value),
|
|
1794
|
+
children: [
|
|
1795
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "public", children: "Public" }),
|
|
1796
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "internal", children: "Internal" })
|
|
1797
|
+
]
|
|
1798
|
+
}
|
|
1799
|
+
),
|
|
1800
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
1801
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1802
|
+
"button",
|
|
1803
|
+
{
|
|
1804
|
+
onClick: () => setPendingPin(null),
|
|
1805
|
+
style: { fontSize: 10, padding: "4px 8px", borderRadius: 4, background: T.accent, color: T.muted, border: "none", cursor: "pointer" },
|
|
1806
|
+
children: "Cancel"
|
|
1807
|
+
}
|
|
1808
|
+
),
|
|
1809
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1810
|
+
"button",
|
|
1811
|
+
{
|
|
1812
|
+
onClick: handleSubmit,
|
|
1813
|
+
disabled: !body.trim() || submitting || !actorReady,
|
|
1814
|
+
style: { fontSize: 10, padding: "4px 8px", borderRadius: 4, background: T.primary, color: T.primaryFg, border: "none", cursor: "pointer", opacity: !body.trim() || submitting || !actorReady ? 0.5 : 1 },
|
|
1815
|
+
children: submitting ? "\u2026" : "Pin"
|
|
1816
|
+
}
|
|
1817
|
+
)
|
|
1818
|
+
] })
|
|
1819
|
+
] })
|
|
1820
|
+
] })
|
|
1821
|
+
}
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/ScreenshotFeedback.tsx
|
|
1826
|
+
var import_react8 = require("react");
|
|
1827
|
+
var import_lucide_react2 = require("lucide-react");
|
|
1828
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1829
|
+
function ScreenshotFeedback() {
|
|
1830
|
+
const ctx = useFeedbackSafe();
|
|
1831
|
+
const [body, setBody] = (0, import_react8.useState)("");
|
|
1832
|
+
const [visibility, setVisibility] = (0, import_react8.useState)("public");
|
|
1833
|
+
const [submitting, setSubmitting] = (0, import_react8.useState)(false);
|
|
1834
|
+
const containerRef = (0, import_react8.useRef)(null);
|
|
1835
|
+
const [dragStart, setDragStart] = (0, import_react8.useState)(null);
|
|
1836
|
+
const [dragEnd, setDragEnd] = (0, import_react8.useState)(null);
|
|
1837
|
+
const [isDragging, setIsDragging] = (0, import_react8.useState)(false);
|
|
1838
|
+
const screenshotDataUrl = ctx?.screenshotDataUrl ?? null;
|
|
1839
|
+
const setScreenshotDataUrl = ctx?.setScreenshotDataUrl;
|
|
1840
|
+
const pendingScreenshotPin = ctx?.pendingScreenshotPin ?? null;
|
|
1841
|
+
const setPendingScreenshotPin = ctx?.setPendingScreenshotPin;
|
|
1842
|
+
const submitScreenshotThread = ctx?.submitScreenshotThread;
|
|
1843
|
+
const actorReady = ctx?.actorReady ?? false;
|
|
1844
|
+
const actorError = ctx?.actorError ?? null;
|
|
1845
|
+
const authReady = ctx?.authReady ?? false;
|
|
1846
|
+
const showHints = ctx?.showHints ?? true;
|
|
1847
|
+
const getRelativeCoords = (0, import_react8.useCallback)(
|
|
1848
|
+
(e) => {
|
|
1849
|
+
const container = containerRef.current;
|
|
1850
|
+
if (!container) return null;
|
|
1851
|
+
const rect = container.getBoundingClientRect();
|
|
1852
|
+
return {
|
|
1853
|
+
x: (e.clientX - rect.left) / rect.width * 100,
|
|
1854
|
+
y: (e.clientY - rect.top) / rect.height * 100
|
|
1855
|
+
};
|
|
1856
|
+
},
|
|
1857
|
+
[]
|
|
1858
|
+
);
|
|
1859
|
+
const handleMouseDown = (0, import_react8.useCallback)(
|
|
1860
|
+
(e) => {
|
|
1861
|
+
if (e.button !== 0) return;
|
|
1862
|
+
const pt = getRelativeCoords(e);
|
|
1863
|
+
if (!pt) return;
|
|
1864
|
+
setDragStart(pt);
|
|
1865
|
+
setDragEnd(pt);
|
|
1866
|
+
setIsDragging(true);
|
|
1867
|
+
setPendingScreenshotPin?.(null);
|
|
1868
|
+
setBody("");
|
|
1869
|
+
},
|
|
1870
|
+
[getRelativeCoords, setPendingScreenshotPin]
|
|
1871
|
+
);
|
|
1872
|
+
const handleMouseMove = (0, import_react8.useCallback)(
|
|
1873
|
+
(e) => {
|
|
1874
|
+
if (!isDragging) return;
|
|
1875
|
+
const pt = getRelativeCoords(e);
|
|
1876
|
+
if (pt) setDragEnd(pt);
|
|
1877
|
+
},
|
|
1878
|
+
[isDragging, getRelativeCoords]
|
|
1879
|
+
);
|
|
1880
|
+
const handleMouseUp = (0, import_react8.useCallback)(() => {
|
|
1881
|
+
if (!isDragging || !dragStart || !dragEnd) return;
|
|
1882
|
+
setIsDragging(false);
|
|
1883
|
+
const x = Math.min(dragStart.x, dragEnd.x);
|
|
1884
|
+
const y = Math.min(dragStart.y, dragEnd.y);
|
|
1885
|
+
const w = Math.abs(dragEnd.x - dragStart.x);
|
|
1886
|
+
const h = Math.abs(dragEnd.y - dragStart.y);
|
|
1887
|
+
if (w < 1 && h < 1) {
|
|
1888
|
+
setPendingScreenshotPin?.({ x: dragStart.x, y: dragStart.y, width: 0, height: 0, pageUrl: window.location.pathname });
|
|
1889
|
+
} else {
|
|
1890
|
+
setPendingScreenshotPin?.({ x, y, width: w, height: h, pageUrl: window.location.pathname });
|
|
1891
|
+
}
|
|
1892
|
+
}, [isDragging, dragStart, dragEnd, setPendingScreenshotPin]);
|
|
1893
|
+
const handleClose = () => {
|
|
1894
|
+
setScreenshotDataUrl?.(null);
|
|
1895
|
+
setPendingScreenshotPin?.(null);
|
|
1896
|
+
setBody("");
|
|
1897
|
+
setDragStart(null);
|
|
1898
|
+
setDragEnd(null);
|
|
1899
|
+
};
|
|
1900
|
+
const handleSubmit = async () => {
|
|
1901
|
+
if (!body.trim() || submitting || !submitScreenshotThread) return;
|
|
1902
|
+
if (!actorReady) {
|
|
1903
|
+
console.warn("[EW SDK] Cannot submit screenshot:", actorError || "still linking user identity\u2026");
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
setSubmitting(true);
|
|
1907
|
+
try {
|
|
1908
|
+
await submitScreenshotThread(body.trim(), visibility);
|
|
1909
|
+
setBody("");
|
|
1910
|
+
} catch (err) {
|
|
1911
|
+
console.error("[EW SDK] Screenshot submit failed:", err);
|
|
1912
|
+
} finally {
|
|
1913
|
+
setSubmitting(false);
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
if (!ctx || !screenshotDataUrl) return null;
|
|
1917
|
+
const selRect = isDragging && dragStart && dragEnd ? {
|
|
1918
|
+
x: Math.min(dragStart.x, dragEnd.x),
|
|
1919
|
+
y: Math.min(dragStart.y, dragEnd.y),
|
|
1920
|
+
w: Math.abs(dragEnd.x - dragStart.x),
|
|
1921
|
+
h: Math.abs(dragEnd.y - dragStart.y)
|
|
1922
|
+
} : null;
|
|
1923
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1924
|
+
"div",
|
|
1925
|
+
{
|
|
1926
|
+
"data-ew-feedback-interactive": "true",
|
|
1927
|
+
onPointerDownCapture: (e) => e.stopPropagation(),
|
|
1928
|
+
style: { position: "fixed", inset: 0, zIndex: Z.screenshotModal, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.8)" },
|
|
1929
|
+
children: [
|
|
1930
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1931
|
+
"button",
|
|
1932
|
+
{
|
|
1933
|
+
onClick: handleClose,
|
|
1934
|
+
style: {
|
|
1935
|
+
position: "absolute",
|
|
1936
|
+
top: 16,
|
|
1937
|
+
right: 16,
|
|
1938
|
+
zIndex: 10,
|
|
1939
|
+
display: "flex",
|
|
1940
|
+
height: 32,
|
|
1941
|
+
width: 32,
|
|
1942
|
+
alignItems: "center",
|
|
1943
|
+
justifyContent: "center",
|
|
1944
|
+
borderRadius: "50%",
|
|
1945
|
+
background: T.card,
|
|
1946
|
+
color: T.fg,
|
|
1947
|
+
border: `1px solid ${T.border}`,
|
|
1948
|
+
cursor: "pointer"
|
|
1949
|
+
},
|
|
1950
|
+
title: "Cancel screenshot feedback",
|
|
1951
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react2.X, { style: { width: 16, height: 16 } })
|
|
1952
|
+
}
|
|
1953
|
+
),
|
|
1954
|
+
showHints && !pendingScreenshotPin && !isDragging && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1955
|
+
position: "absolute",
|
|
1956
|
+
top: 16,
|
|
1957
|
+
left: "50%",
|
|
1958
|
+
transform: "translateX(-50%)",
|
|
1959
|
+
zIndex: 10,
|
|
1960
|
+
borderRadius: 8,
|
|
1961
|
+
background: T.card,
|
|
1962
|
+
border: `1px solid ${T.border}`,
|
|
1963
|
+
padding: "8px 16px",
|
|
1964
|
+
boxShadow: "0 4px 12px rgba(0,0,0,.4)"
|
|
1965
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: { fontSize: 12, color: T.fg, fontWeight: 500, margin: 0 }, children: "Click and drag to select an area" }) }),
|
|
1966
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1967
|
+
"div",
|
|
1968
|
+
{
|
|
1969
|
+
ref: containerRef,
|
|
1970
|
+
style: { position: "relative", maxHeight: "90vh", maxWidth: "95vw", overflow: "hidden", borderRadius: 8, boxShadow: "0 25px 50px -12px rgba(0,0,0,.5)", userSelect: "none", cursor: "crosshair" },
|
|
1971
|
+
onMouseDown: handleMouseDown,
|
|
1972
|
+
onMouseMove: handleMouseMove,
|
|
1973
|
+
onMouseUp: handleMouseUp,
|
|
1974
|
+
children: [
|
|
1975
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: screenshotDataUrl, alt: "Screenshot for feedback", style: { display: "block", maxHeight: "90vh", maxWidth: "95vw", pointerEvents: "none" }, draggable: false }),
|
|
1976
|
+
selRect && selRect.w + selRect.h > 0.5 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1977
|
+
position: "absolute",
|
|
1978
|
+
left: `${selRect.x}%`,
|
|
1979
|
+
top: `${selRect.y}%`,
|
|
1980
|
+
width: `${selRect.w}%`,
|
|
1981
|
+
height: `${selRect.h}%`,
|
|
1982
|
+
border: `2px solid ${T.primary}`,
|
|
1983
|
+
background: `${T.primary}33`,
|
|
1984
|
+
pointerEvents: "none"
|
|
1985
|
+
} }),
|
|
1986
|
+
pendingScreenshotPin && !isDragging && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, { children: pendingScreenshotPin.width > 0 || pendingScreenshotPin.height > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1987
|
+
position: "absolute",
|
|
1988
|
+
left: `${pendingScreenshotPin.x}%`,
|
|
1989
|
+
top: `${pendingScreenshotPin.y}%`,
|
|
1990
|
+
width: `${pendingScreenshotPin.width}%`,
|
|
1991
|
+
height: `${pendingScreenshotPin.height}%`,
|
|
1992
|
+
border: `2px solid ${T.primary}`,
|
|
1993
|
+
background: `${T.primary}33`,
|
|
1994
|
+
pointerEvents: "none"
|
|
1995
|
+
} }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
1996
|
+
position: "absolute",
|
|
1997
|
+
left: `${pendingScreenshotPin.x}%`,
|
|
1998
|
+
top: `${pendingScreenshotPin.y}%`,
|
|
1999
|
+
transform: "translate(-50%, -50%)",
|
|
2000
|
+
pointerEvents: "none"
|
|
2001
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: {
|
|
2002
|
+
height: 24,
|
|
2003
|
+
width: 24,
|
|
2004
|
+
borderRadius: "50%",
|
|
2005
|
+
background: T.primary,
|
|
2006
|
+
color: T.primaryFg,
|
|
2007
|
+
display: "flex",
|
|
2008
|
+
alignItems: "center",
|
|
2009
|
+
justifyContent: "center",
|
|
2010
|
+
fontSize: 10,
|
|
2011
|
+
fontWeight: 700,
|
|
2012
|
+
boxShadow: `0 0 0 4px ${T.primary}4d, 0 4px 12px rgba(0,0,0,.4)`
|
|
2013
|
+
}, children: "\u2022" }) }) })
|
|
2014
|
+
]
|
|
2015
|
+
}
|
|
2016
|
+
),
|
|
2017
|
+
pendingScreenshotPin && !isDragging && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { position: "absolute", bottom: 24, left: "50%", transform: "translateX(-50%)", zIndex: 10, width: 288 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { borderRadius: 8, border: `1px solid ${T.border}`, background: T.card, boxShadow: "0 10px 25px -5px rgba(0,0,0,.5)", padding: 12 }, children: [
|
|
2018
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { marginBottom: 8, fontSize: 10, color: T.muted, fontFamily: "monospace" }, children: [
|
|
2019
|
+
"\u{1F4F8}",
|
|
2020
|
+
" ",
|
|
2021
|
+
pendingScreenshotPin.width > 0 ? `Area (${Math.round(pendingScreenshotPin.x)}%, ${Math.round(pendingScreenshotPin.y)}%) ${Math.round(pendingScreenshotPin.width)}\xD7${Math.round(pendingScreenshotPin.height)}%` : `Pin at (${Math.round(pendingScreenshotPin.x)}%, ${Math.round(pendingScreenshotPin.y)}%)`
|
|
2022
|
+
] }),
|
|
2023
|
+
!actorReady && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { marginBottom: 8, fontSize: 10, color: T.muted }, children: !authReady ? "Identifying\u2026" : actorError || "Linking identity\u2026" }),
|
|
2024
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2025
|
+
"textarea",
|
|
2026
|
+
{
|
|
2027
|
+
style: {
|
|
2028
|
+
width: "100%",
|
|
2029
|
+
borderRadius: 6,
|
|
2030
|
+
border: `1px solid ${T.input}`,
|
|
2031
|
+
background: T.bg,
|
|
2032
|
+
color: T.fg,
|
|
2033
|
+
padding: "6px 8px",
|
|
2034
|
+
fontSize: 12,
|
|
2035
|
+
resize: "none",
|
|
2036
|
+
outline: "none",
|
|
2037
|
+
boxSizing: "border-box"
|
|
2038
|
+
},
|
|
2039
|
+
placeholder: "Add a comment\u2026",
|
|
2040
|
+
rows: 3,
|
|
2041
|
+
value: body,
|
|
2042
|
+
onChange: (e) => setBody(e.target.value),
|
|
2043
|
+
onKeyDown: (e) => {
|
|
2044
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) handleSubmit();
|
|
2045
|
+
},
|
|
2046
|
+
autoFocus: true
|
|
2047
|
+
}
|
|
2048
|
+
),
|
|
2049
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 8 }, children: [
|
|
2050
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
2051
|
+
"select",
|
|
2052
|
+
{
|
|
2053
|
+
style: { fontSize: 10, background: T.bg, border: `1px solid ${T.input}`, borderRadius: 4, padding: "2px 4px", color: T.fg },
|
|
2054
|
+
value: visibility,
|
|
2055
|
+
onChange: (e) => setVisibility(e.target.value),
|
|
2056
|
+
children: [
|
|
2057
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "public", children: "Public" }),
|
|
2058
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "internal", children: "Internal" })
|
|
2059
|
+
]
|
|
2060
|
+
}
|
|
2061
|
+
),
|
|
2062
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
2063
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2064
|
+
"button",
|
|
2065
|
+
{
|
|
2066
|
+
onClick: () => {
|
|
2067
|
+
setPendingScreenshotPin?.(null);
|
|
2068
|
+
setDragStart(null);
|
|
2069
|
+
setDragEnd(null);
|
|
2070
|
+
},
|
|
2071
|
+
style: { fontSize: 10, padding: "4px 8px", borderRadius: 4, background: T.accent, color: T.muted, border: "none", cursor: "pointer" },
|
|
2072
|
+
children: "Re-select"
|
|
2073
|
+
}
|
|
2074
|
+
),
|
|
2075
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
2076
|
+
"button",
|
|
2077
|
+
{
|
|
2078
|
+
onClick: handleSubmit,
|
|
2079
|
+
disabled: !body.trim() || submitting || !actorReady,
|
|
2080
|
+
style: { fontSize: 10, padding: "4px 8px", borderRadius: 4, background: T.primary, color: T.primaryFg, border: "none", cursor: "pointer", opacity: !body.trim() || submitting || !actorReady ? 0.5 : 1 },
|
|
2081
|
+
children: submitting ? "\u2026" : "Submit"
|
|
2082
|
+
}
|
|
2083
|
+
)
|
|
2084
|
+
] })
|
|
2085
|
+
] })
|
|
2086
|
+
] }) })
|
|
2087
|
+
]
|
|
2088
|
+
}
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/FeedbackButton.tsx
|
|
2093
|
+
var import_react9 = require("react");
|
|
2094
|
+
var import_lucide_react3 = require("lucide-react");
|
|
2095
|
+
|
|
2096
|
+
// src/launcher.ts
|
|
2097
|
+
function getLauncherCapabilities({
|
|
2098
|
+
allowPinOnPage = true,
|
|
2099
|
+
allowScreenshot = true
|
|
2100
|
+
}) {
|
|
2101
|
+
const canPinOnPage = allowPinOnPage;
|
|
2102
|
+
const canScreenshot = allowScreenshot;
|
|
2103
|
+
const actionCount = Number(canPinOnPage) + Number(canScreenshot);
|
|
2104
|
+
const singleAction = actionCount === 1 ? canPinOnPage ? "pin" : "screenshot" : null;
|
|
2105
|
+
return {
|
|
2106
|
+
canPinOnPage,
|
|
2107
|
+
canScreenshot,
|
|
2108
|
+
actionCount,
|
|
2109
|
+
singleAction
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/FeedbackButton.tsx
|
|
2114
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
2115
|
+
function FeedbackButton({ position = "bottom-right" }) {
|
|
2116
|
+
const ctx = useFeedbackSafe();
|
|
2117
|
+
const menuRef = (0, import_react9.useRef)(null);
|
|
2118
|
+
const [hintDismissed, setHintDismissed] = (0, import_react9.useState)(false);
|
|
2119
|
+
const menuOpenState = ctx?.menuOpen ?? false;
|
|
2120
|
+
const debug = ctx?.debug ?? false;
|
|
2121
|
+
(0, import_react9.useEffect)(() => {
|
|
2122
|
+
if (!menuOpenState || !ctx) return;
|
|
2123
|
+
const handler = (e) => {
|
|
2124
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
2125
|
+
ctx.setMenuOpen(false);
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
document.addEventListener("mousedown", handler);
|
|
2129
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
2130
|
+
}, [menuOpenState, ctx]);
|
|
2131
|
+
(0, import_react9.useEffect)(() => {
|
|
2132
|
+
if (!debug) return;
|
|
2133
|
+
console.log("[EW SDK] FeedbackButton mounted");
|
|
2134
|
+
return () => console.log("[EW SDK] FeedbackButton unmounted");
|
|
2135
|
+
}, [debug]);
|
|
2136
|
+
if (!ctx) return null;
|
|
2137
|
+
const {
|
|
2138
|
+
mode,
|
|
2139
|
+
menuOpen,
|
|
2140
|
+
canPinOnPage,
|
|
2141
|
+
canScreenshot,
|
|
2142
|
+
showHints,
|
|
2143
|
+
screenshotCapturing,
|
|
2144
|
+
toggleMenu,
|
|
2145
|
+
enterPinMode,
|
|
2146
|
+
exitPinMode,
|
|
2147
|
+
startScreenshotCapture
|
|
2148
|
+
} = ctx;
|
|
2149
|
+
const annotate = mode === "annotate";
|
|
2150
|
+
const capabilities = getLauncherCapabilities({ allowPinOnPage: canPinOnPage, allowScreenshot: canScreenshot });
|
|
2151
|
+
const posStyle = position === "bottom-left" ? { left: 20, bottom: 20 } : { right: 20, bottom: 20 };
|
|
2152
|
+
const hintStyle = position === "bottom-left" ? { left: 56, bottom: 8 } : { right: 56, bottom: 8 };
|
|
2153
|
+
const handleToggle = () => {
|
|
2154
|
+
if (debug) console.log("[EW SDK] handleToggle", { mode, menuOpen });
|
|
2155
|
+
setHintDismissed(true);
|
|
2156
|
+
if (annotate) {
|
|
2157
|
+
exitPinMode();
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (capabilities.actionCount === 1) {
|
|
2161
|
+
if (capabilities.singleAction === "pin") {
|
|
2162
|
+
enterPinMode();
|
|
2163
|
+
} else if (capabilities.singleAction === "screenshot") {
|
|
2164
|
+
void startScreenshotCapture();
|
|
2165
|
+
}
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
toggleMenu();
|
|
2169
|
+
};
|
|
2170
|
+
const capturing = screenshotCapturing;
|
|
2171
|
+
const showLauncherHint = showHints && !hintDismissed && !annotate && !menuOpen;
|
|
2172
|
+
if (capabilities.actionCount === 0) return null;
|
|
2173
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2174
|
+
"div",
|
|
2175
|
+
{
|
|
2176
|
+
ref: menuRef,
|
|
2177
|
+
"data-ew-feedback-interactive": "true",
|
|
2178
|
+
style: { position: "fixed", ...posStyle, zIndex: Z.launcher },
|
|
2179
|
+
children: [
|
|
2180
|
+
showLauncherHint && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2181
|
+
"div",
|
|
2182
|
+
{
|
|
2183
|
+
style: {
|
|
2184
|
+
position: "absolute",
|
|
2185
|
+
...hintStyle,
|
|
2186
|
+
width: 280,
|
|
2187
|
+
borderRadius: 10,
|
|
2188
|
+
border: `1px solid ${T.border}`,
|
|
2189
|
+
background: T.card,
|
|
2190
|
+
color: T.fg,
|
|
2191
|
+
boxShadow: "0 10px 25px -5px rgba(0,0,0,.5)",
|
|
2192
|
+
padding: "12px 14px"
|
|
2193
|
+
},
|
|
2194
|
+
children: [
|
|
2195
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 16, fontWeight: 600, marginBottom: 6 }, children: "Open Issue Pin" }),
|
|
2196
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 12, color: T.muted, lineHeight: 1.45 }, children: "Click the feedback bubble to pin comments on the page or capture a screenshot." })
|
|
2197
|
+
]
|
|
2198
|
+
}
|
|
2199
|
+
),
|
|
2200
|
+
menuOpen && !annotate && capabilities.actionCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2201
|
+
"div",
|
|
2202
|
+
{
|
|
2203
|
+
style: {
|
|
2204
|
+
position: "absolute",
|
|
2205
|
+
bottom: 56,
|
|
2206
|
+
right: 0,
|
|
2207
|
+
marginBottom: 4,
|
|
2208
|
+
width: 176,
|
|
2209
|
+
borderRadius: 8,
|
|
2210
|
+
border: `1px solid ${T.border}`,
|
|
2211
|
+
background: T.card,
|
|
2212
|
+
boxShadow: "0 10px 25px -5px rgba(0,0,0,.5)",
|
|
2213
|
+
overflow: "hidden"
|
|
2214
|
+
},
|
|
2215
|
+
children: [
|
|
2216
|
+
canPinOnPage && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2217
|
+
"button",
|
|
2218
|
+
{
|
|
2219
|
+
onClick: enterPinMode,
|
|
2220
|
+
style: {
|
|
2221
|
+
display: "flex",
|
|
2222
|
+
width: "100%",
|
|
2223
|
+
alignItems: "center",
|
|
2224
|
+
gap: 10,
|
|
2225
|
+
padding: "10px 12px",
|
|
2226
|
+
fontSize: 12,
|
|
2227
|
+
color: T.fg,
|
|
2228
|
+
background: "transparent",
|
|
2229
|
+
border: "none",
|
|
2230
|
+
cursor: "pointer",
|
|
2231
|
+
textAlign: "left"
|
|
2232
|
+
},
|
|
2233
|
+
onMouseEnter: (e) => e.currentTarget.style.background = T.accent,
|
|
2234
|
+
onMouseLeave: (e) => e.currentTarget.style.background = "transparent",
|
|
2235
|
+
children: [
|
|
2236
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.MapPin, { style: { width: 16, height: 16, color: T.primary, flexShrink: 0 } }),
|
|
2237
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
2238
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: "Pin on page" }),
|
|
2239
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: T.muted }, children: "Click an element" })
|
|
2240
|
+
] })
|
|
2241
|
+
]
|
|
2242
|
+
}
|
|
2243
|
+
),
|
|
2244
|
+
canPinOnPage && canScreenshot && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { borderTop: `1px solid ${T.border}` } }),
|
|
2245
|
+
canScreenshot && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2246
|
+
"button",
|
|
2247
|
+
{
|
|
2248
|
+
onClick: () => void startScreenshotCapture(),
|
|
2249
|
+
disabled: capturing,
|
|
2250
|
+
style: {
|
|
2251
|
+
display: "flex",
|
|
2252
|
+
width: "100%",
|
|
2253
|
+
alignItems: "center",
|
|
2254
|
+
gap: 10,
|
|
2255
|
+
padding: "10px 12px",
|
|
2256
|
+
fontSize: 12,
|
|
2257
|
+
color: T.fg,
|
|
2258
|
+
background: "transparent",
|
|
2259
|
+
border: "none",
|
|
2260
|
+
cursor: capturing ? "default" : "pointer",
|
|
2261
|
+
opacity: capturing ? 0.5 : 1,
|
|
2262
|
+
textAlign: "left"
|
|
2263
|
+
},
|
|
2264
|
+
onMouseEnter: (e) => {
|
|
2265
|
+
if (!capturing) e.currentTarget.style.background = T.accent;
|
|
2266
|
+
},
|
|
2267
|
+
onMouseLeave: (e) => e.currentTarget.style.background = "transparent",
|
|
2268
|
+
children: [
|
|
2269
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.Camera, { style: { width: 16, height: 16, color: T.primary, flexShrink: 0 } }),
|
|
2270
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
2271
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing\u2026" : "Screenshot" }),
|
|
2272
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: T.muted }, children: "Capture current view" })
|
|
2273
|
+
] })
|
|
2274
|
+
]
|
|
2275
|
+
}
|
|
2276
|
+
)
|
|
2277
|
+
]
|
|
2278
|
+
}
|
|
2279
|
+
),
|
|
2280
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2281
|
+
"button",
|
|
2282
|
+
{
|
|
2283
|
+
onClick: handleToggle,
|
|
2284
|
+
style: {
|
|
2285
|
+
display: "flex",
|
|
2286
|
+
height: 48,
|
|
2287
|
+
width: 48,
|
|
2288
|
+
alignItems: "center",
|
|
2289
|
+
justifyContent: "center",
|
|
2290
|
+
borderRadius: "50%",
|
|
2291
|
+
boxShadow: "0 4px 12px rgba(0,0,0,.4)",
|
|
2292
|
+
transition: "all 200ms",
|
|
2293
|
+
border: annotate ? "none" : `1px solid ${T.border}`,
|
|
2294
|
+
background: annotate ? T.primary : T.card,
|
|
2295
|
+
color: annotate ? T.primaryFg : T.fg,
|
|
2296
|
+
cursor: "pointer",
|
|
2297
|
+
transform: annotate ? "scale(1.1)" : "scale(1)"
|
|
2298
|
+
},
|
|
2299
|
+
title: annotate ? "Exit feedback mode" : "Open feedback menu",
|
|
2300
|
+
"aria-label": "Toggle feedback mode",
|
|
2301
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.MessageSquarePlus, { style: { width: 20, height: 20 } })
|
|
2302
|
+
}
|
|
2303
|
+
)
|
|
2304
|
+
]
|
|
2305
|
+
}
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/ModalFeedbackInjector.tsx
|
|
2310
|
+
var import_react10 = require("react");
|
|
2311
|
+
var import_react_dom3 = require("react-dom");
|
|
2312
|
+
var import_lucide_react4 = require("lucide-react");
|
|
2313
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
2314
|
+
function ModalFeedbackInjector({
|
|
2315
|
+
renderButton
|
|
2316
|
+
}) {
|
|
2317
|
+
const ctx = useFeedbackSafe();
|
|
2318
|
+
const [openModals, setOpenModals] = (0, import_react10.useState)([]);
|
|
2319
|
+
(0, import_react10.useEffect)(() => {
|
|
2320
|
+
const query = () => {
|
|
2321
|
+
const modals = document.querySelectorAll('[role="dialog"], [data-radix-dialog-content]');
|
|
2322
|
+
setOpenModals(Array.from(modals));
|
|
2323
|
+
};
|
|
2324
|
+
query();
|
|
2325
|
+
const observer = new MutationObserver(query);
|
|
2326
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
2327
|
+
return () => observer.disconnect();
|
|
2328
|
+
}, []);
|
|
2329
|
+
const handleCapture = (0, import_react10.useCallback)(async () => {
|
|
2330
|
+
if (!ctx) return;
|
|
2331
|
+
await ctx.startScreenshotCapture();
|
|
2332
|
+
}, [ctx]);
|
|
2333
|
+
if (!ctx || openModals.length === 0) return null;
|
|
2334
|
+
const renderProps = {
|
|
2335
|
+
captureScreenshot: handleCapture,
|
|
2336
|
+
closeLauncherMenu: ctx.closeMenu
|
|
2337
|
+
};
|
|
2338
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_jsx_runtime8.Fragment, { children: openModals.map(
|
|
2339
|
+
(modal, i) => (0, import_react_dom3.createPortal)(
|
|
2340
|
+
renderButton ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { "data-ew-feedback-interactive": "true", onPointerDownCapture: (e) => e.stopPropagation(), children: renderButton(renderProps) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
2341
|
+
"button",
|
|
2342
|
+
{
|
|
2343
|
+
"data-ew-feedback-interactive": "true",
|
|
2344
|
+
onClick: (e) => {
|
|
2345
|
+
e.stopPropagation();
|
|
2346
|
+
void handleCapture();
|
|
2347
|
+
},
|
|
2348
|
+
style: {
|
|
2349
|
+
position: "absolute",
|
|
2350
|
+
bottom: 12,
|
|
2351
|
+
right: 12,
|
|
2352
|
+
zIndex: Z.launcher,
|
|
2353
|
+
display: "flex",
|
|
2354
|
+
height: 32,
|
|
2355
|
+
width: 32,
|
|
2356
|
+
alignItems: "center",
|
|
2357
|
+
justifyContent: "center",
|
|
2358
|
+
borderRadius: "50%",
|
|
2359
|
+
border: `1px solid ${T.border}`,
|
|
2360
|
+
background: T.card,
|
|
2361
|
+
color: T.fg,
|
|
2362
|
+
boxShadow: "0 2px 8px rgba(0,0,0,.3)",
|
|
2363
|
+
cursor: "pointer"
|
|
2364
|
+
},
|
|
2365
|
+
title: "Capture feedback screenshot",
|
|
2366
|
+
"aria-label": "Capture feedback screenshot",
|
|
2367
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.Camera, { style: { width: 16, height: 16, color: T.primary } })
|
|
2368
|
+
},
|
|
2369
|
+
i
|
|
2370
|
+
),
|
|
2371
|
+
modal,
|
|
2372
|
+
`modal-feedback-${i}`
|
|
2373
|
+
)
|
|
2374
|
+
) });
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// src/IssuePin.tsx
|
|
2378
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
2379
|
+
var SdkErrorBoundary = class extends import_react11.Component {
|
|
2380
|
+
constructor() {
|
|
2381
|
+
super(...arguments);
|
|
2382
|
+
this.state = { hasError: false };
|
|
2383
|
+
}
|
|
2384
|
+
static getDerivedStateFromError() {
|
|
2385
|
+
return { hasError: true };
|
|
2386
|
+
}
|
|
2387
|
+
componentDidCatch(error) {
|
|
2388
|
+
console.warn(`[IssuePin] ${this.props.name} crashed and was disabled:`, error.message);
|
|
2389
|
+
}
|
|
2390
|
+
render() {
|
|
2391
|
+
return this.state.hasError ? null : this.props.children;
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
function IssuePin({
|
|
2395
|
+
apiKey,
|
|
2396
|
+
enabled = false,
|
|
2397
|
+
user,
|
|
2398
|
+
supabaseClient,
|
|
2399
|
+
allowPinOnPage = true,
|
|
2400
|
+
allowScreenshot = true,
|
|
2401
|
+
showHints = true,
|
|
2402
|
+
scrollContainer,
|
|
2403
|
+
resolveAnchor,
|
|
2404
|
+
reviewUrl,
|
|
2405
|
+
annotationSurface,
|
|
2406
|
+
pinsVisible = true,
|
|
2407
|
+
buttonPosition = "bottom-right",
|
|
2408
|
+
renderLauncher,
|
|
2409
|
+
renderModalCaptureButton,
|
|
2410
|
+
showModalCaptureButton = true,
|
|
2411
|
+
mode,
|
|
2412
|
+
onModeChange,
|
|
2413
|
+
feedbackActive,
|
|
2414
|
+
onFeedbackActiveChange,
|
|
2415
|
+
debug = false,
|
|
2416
|
+
// Legacy props
|
|
2417
|
+
supabaseUrl,
|
|
2418
|
+
supabaseAnonKey,
|
|
2419
|
+
workspaceId,
|
|
2420
|
+
userId,
|
|
2421
|
+
userEmail,
|
|
2422
|
+
userDisplayName
|
|
2423
|
+
}) {
|
|
2424
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2425
|
+
FeedbackProvider,
|
|
2426
|
+
{
|
|
2427
|
+
apiKey,
|
|
2428
|
+
supabaseUrl,
|
|
2429
|
+
supabaseAnonKey,
|
|
2430
|
+
workspaceId,
|
|
2431
|
+
supabaseClient,
|
|
2432
|
+
debug,
|
|
2433
|
+
allowPinOnPage,
|
|
2434
|
+
allowScreenshot,
|
|
2435
|
+
showHints,
|
|
2436
|
+
scrollContainer,
|
|
2437
|
+
resolveAnchor,
|
|
2438
|
+
reviewUrl,
|
|
2439
|
+
annotationSurface,
|
|
2440
|
+
mode,
|
|
2441
|
+
onModeChange,
|
|
2442
|
+
feedbackActive,
|
|
2443
|
+
onFeedbackActiveChange,
|
|
2444
|
+
userId: user?.id ?? userId,
|
|
2445
|
+
userEmail: user?.email ?? userEmail,
|
|
2446
|
+
userDisplayName: user?.displayName ?? userDisplayName,
|
|
2447
|
+
children: enabled && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
2448
|
+
annotationSurface === "review-iframe" || !!reviewUrl && !annotationSurface ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ReviewSurfaceOverlay, {}) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeedbackOverlay, {}),
|
|
2449
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(SdkCommentPopover, {}),
|
|
2450
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScreenshotFeedback, {}),
|
|
2451
|
+
pinsVisible && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(SdkErrorBoundary, { name: "ThreadPins", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PinsSlot, {}) }),
|
|
2452
|
+
allowScreenshot && showModalCaptureButton && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ModalFeedbackInjector, { renderButton: renderModalCaptureButton }),
|
|
2453
|
+
renderLauncher ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CustomLauncherSlot, { renderLauncher }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeedbackButton, { position: buttonPosition })
|
|
2454
|
+
] })
|
|
2455
|
+
}
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
function CustomLauncherSlot({
|
|
2459
|
+
renderLauncher
|
|
2460
|
+
}) {
|
|
2461
|
+
const ctx = useFeedbackSafe();
|
|
2462
|
+
if (!ctx) return null;
|
|
2463
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "data-ew-feedback-interactive": "true", onPointerDownCapture: (e) => e.stopPropagation(), children: renderLauncher({
|
|
2464
|
+
mode: ctx.mode,
|
|
2465
|
+
setMode: ctx.setMode,
|
|
2466
|
+
feedbackActive: ctx.feedbackActive,
|
|
2467
|
+
menuOpen: ctx.menuOpen,
|
|
2468
|
+
canPinOnPage: ctx.canPinOnPage,
|
|
2469
|
+
canScreenshot: ctx.canScreenshot,
|
|
2470
|
+
openMenu: ctx.openMenu,
|
|
2471
|
+
closeMenu: ctx.closeMenu,
|
|
2472
|
+
toggleMenu: ctx.toggleMenu,
|
|
2473
|
+
enterPinMode: ctx.enterPinMode,
|
|
2474
|
+
exitPinMode: ctx.exitPinMode,
|
|
2475
|
+
startScreenshotCapture: ctx.startScreenshotCapture
|
|
2476
|
+
}) });
|
|
2477
|
+
}
|
|
2478
|
+
function PinsSlot() {
|
|
2479
|
+
const ctx = useFeedbackSafe();
|
|
2480
|
+
if (!ctx) return null;
|
|
2481
|
+
if (ctx.annotationSurface === "review-iframe") return null;
|
|
2482
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ThreadPins, {});
|
|
2483
|
+
}
|
|
2484
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2485
|
+
0 && (module.exports = {
|
|
2486
|
+
ElementWhisperer,
|
|
2487
|
+
FeedbackButton,
|
|
2488
|
+
FeedbackOverlay,
|
|
2489
|
+
FeedbackProvider,
|
|
2490
|
+
IssuePin,
|
|
2491
|
+
ReviewSurfaceOverlay,
|
|
2492
|
+
ScreenshotFeedback,
|
|
2493
|
+
SdkCommentPopover,
|
|
2494
|
+
ThreadPins,
|
|
2495
|
+
Z,
|
|
2496
|
+
useFeedback,
|
|
2497
|
+
useFeedbackSafe,
|
|
2498
|
+
useIssuePinAnchor
|
|
2499
|
+
});
|
|
2500
|
+
//# sourceMappingURL=index.cjs.map
|