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