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