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