comment-mode 0.1.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,1116 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var supabaseJs = require('@supabase/supabase-js');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/context/CommentsContext.tsx
8
+
9
+ // src/constants.ts
10
+ var DEFAULT_API_BASE_URL = "https://api.commentmode.dev/api/commentator";
11
+ var AuthContext = react.createContext(void 0);
12
+ function AuthProvider(props) {
13
+ var _a;
14
+ const { config, children } = props;
15
+ const [supabase, setSupabase] = react.useState(null);
16
+ const [user, setUser] = react.useState(null);
17
+ const [accessToken, setAccessToken] = react.useState(null);
18
+ const [isReady, setIsReady] = react.useState(false);
19
+ const [displayName, setDisplayNameState] = react.useState("");
20
+ const apiBaseUrl = (_a = config.apiBaseUrl) != null ? _a : DEFAULT_API_BASE_URL;
21
+ react.useEffect(() => {
22
+ let cancelled = false;
23
+ async function init() {
24
+ try {
25
+ let getDisplayNameFromUser2 = function(u) {
26
+ var _a2, _b, _c, _d, _e, _f;
27
+ const meta = u.user_metadata;
28
+ const fullName = ((_a2 = meta == null ? void 0 : meta.full_name) == null ? void 0 : _a2.trim()) || ((_b = meta == null ? void 0 : meta.name) == null ? void 0 : _b.trim());
29
+ const userName = ((_c = meta == null ? void 0 : meta.user_name) == null ? void 0 : _c.trim()) || ((_d = meta == null ? void 0 : meta.preferred_username) == null ? void 0 : _d.trim());
30
+ const emailPrefix = (_f = (_e = u.email) == null ? void 0 : _e.split("@")[0]) == null ? void 0 : _f.trim();
31
+ return fullName || userName || emailPrefix || "";
32
+ };
33
+ var getDisplayNameFromUser = getDisplayNameFromUser2;
34
+ const response = await fetch(`${apiBaseUrl}/auth/config`);
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to load auth config: ${response.status}`);
37
+ }
38
+ const data = await response.json();
39
+ if (cancelled) return;
40
+ const client = supabaseJs.createClient(data.supabaseUrl, data.supabaseAnonKey, {
41
+ auth: {
42
+ persistSession: true
43
+ }
44
+ });
45
+ setSupabase(client);
46
+ const {
47
+ data: { session }
48
+ } = await client.auth.getSession();
49
+ if (session == null ? void 0 : session.user) {
50
+ setUser({ id: session.user.id, email: session.user.email });
51
+ setAccessToken(session.access_token);
52
+ setDisplayNameState(getDisplayNameFromUser2(session.user));
53
+ }
54
+ client.auth.onAuthStateChange((_event, session2) => {
55
+ if (!session2 || !session2.user) {
56
+ setUser(null);
57
+ setAccessToken(null);
58
+ setDisplayNameState("");
59
+ } else {
60
+ setUser({ id: session2.user.id, email: session2.user.email });
61
+ setAccessToken(session2.access_token);
62
+ setDisplayNameState(getDisplayNameFromUser2(session2.user));
63
+ }
64
+ });
65
+ } catch (err) {
66
+ console.error("[commentator] Failed to initialise auth", err);
67
+ } finally {
68
+ if (!cancelled) {
69
+ setIsReady(true);
70
+ }
71
+ }
72
+ }
73
+ void init();
74
+ return () => {
75
+ cancelled = true;
76
+ };
77
+ }, [apiBaseUrl]);
78
+ const signInWithEmail = react.useCallback(
79
+ async (email) => {
80
+ if (!supabase) return;
81
+ const trimmed = email.trim();
82
+ if (!trimmed) return;
83
+ const redirectTo = typeof window !== "undefined" ? window.location.origin : void 0;
84
+ await supabase.auth.signInWithOtp({
85
+ email: trimmed,
86
+ options: {
87
+ emailRedirectTo: redirectTo
88
+ }
89
+ });
90
+ },
91
+ [supabase]
92
+ );
93
+ const signInWithGitHub = react.useCallback(async () => {
94
+ if (!supabase) return;
95
+ const redirectTo = typeof window !== "undefined" ? window.location.origin : void 0;
96
+ await supabase.auth.signInWithOAuth({
97
+ provider: "github",
98
+ options: { redirectTo }
99
+ });
100
+ }, [supabase]);
101
+ const signOut = react.useCallback(async () => {
102
+ if (!supabase) return;
103
+ await supabase.auth.signOut();
104
+ setUser(null);
105
+ setAccessToken(null);
106
+ setDisplayNameState("");
107
+ }, [supabase]);
108
+ const value = react.useMemo(
109
+ () => ({
110
+ supabase,
111
+ user,
112
+ accessToken,
113
+ isReady,
114
+ displayName,
115
+ signInWithEmail,
116
+ signInWithGitHub,
117
+ signOut
118
+ }),
119
+ [
120
+ supabase,
121
+ user,
122
+ accessToken,
123
+ isReady,
124
+ displayName,
125
+ signInWithEmail,
126
+ signInWithGitHub,
127
+ signOut
128
+ ]
129
+ );
130
+ return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value, children });
131
+ }
132
+ function useAuthInternal() {
133
+ const ctx = react.useContext(AuthContext);
134
+ if (!ctx) {
135
+ throw new Error("useAuthInternal must be used within an AuthProvider");
136
+ }
137
+ return ctx;
138
+ }
139
+ var CommentsContext = react.createContext(void 0);
140
+ function CommentsProvider(props) {
141
+ var _a;
142
+ const { config, children } = props;
143
+ const { accessToken, isReady: isAuthReady } = useAuthInternal();
144
+ const [threads, setThreads] = react.useState([]);
145
+ const [commentsByThread, setCommentsByThread] = react.useState({});
146
+ const [isLoading, setIsLoading] = react.useState(false);
147
+ const [error, setError] = react.useState(null);
148
+ const [commentModeEnabled, setCommentModeEnabled] = react.useState(true);
149
+ const [hoveredRect, setHoveredRect] = react.useState(null);
150
+ const [pendingAnchor, setPendingAnchor] = react.useState(null);
151
+ const surfaceRef = react.useRef(null);
152
+ const surfaceClickHandlerRef = react.useRef(null);
153
+ const onPinClickRef = react.useRef(null);
154
+ const apiBaseUrl = (_a = config.apiBaseUrl) != null ? _a : DEFAULT_API_BASE_URL;
155
+ react.useEffect(() => {
156
+ let cancelled = false;
157
+ async function fetchThreads() {
158
+ var _a2, _b;
159
+ if (!isAuthReady) return;
160
+ try {
161
+ setIsLoading(true);
162
+ setError(null);
163
+ const url = new URL(`${apiBaseUrl}/threads`);
164
+ url.searchParams.set("projectSlug", config.projectSlug);
165
+ url.searchParams.set("surfaceId", config.surfaceId);
166
+ const headers = {};
167
+ if (accessToken) {
168
+ headers.Authorization = `Bearer ${accessToken}`;
169
+ }
170
+ const response = await fetch(url.toString(), { headers });
171
+ if (!response.ok) {
172
+ throw new Error(`Failed to load threads: ${response.status}`);
173
+ }
174
+ const data = await response.json();
175
+ if (cancelled) return;
176
+ const mapped = (_b = (_a2 = data.threads) == null ? void 0 : _a2.map((t) => ({
177
+ id: t.id,
178
+ anchor: { x: t.anchorX, y: t.anchorY },
179
+ status: "open"
180
+ }))) != null ? _b : [];
181
+ setThreads(mapped);
182
+ } catch (err) {
183
+ if (cancelled) return;
184
+ const e = err instanceof Error ? err : new Error("Unknown error");
185
+ setError(e);
186
+ } finally {
187
+ if (!cancelled) {
188
+ setIsLoading(false);
189
+ }
190
+ }
191
+ }
192
+ fetchThreads();
193
+ return () => {
194
+ cancelled = true;
195
+ };
196
+ }, [apiBaseUrl, config.projectSlug, config.surfaceId, accessToken, isAuthReady]);
197
+ const createThread = react.useCallback(
198
+ async (anchor, initialCommentBody, initialAuthorName) => {
199
+ const optimisticThread = {
200
+ id: `${Date.now()}`,
201
+ anchor,
202
+ status: "open"
203
+ };
204
+ setThreads((prev) => [...prev, optimisticThread]);
205
+ try {
206
+ setIsLoading(true);
207
+ setError(null);
208
+ const response = await fetch(`${apiBaseUrl}/threads`, {
209
+ method: "POST",
210
+ headers: {
211
+ "Content-Type": "application/json",
212
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
213
+ },
214
+ body: JSON.stringify({
215
+ projectSlug: config.projectSlug,
216
+ surfaceId: config.surfaceId,
217
+ anchorX: anchor.x,
218
+ anchorY: anchor.y,
219
+ initialCommentBody: initialCommentBody != null ? initialCommentBody : null,
220
+ initialAuthorName: initialAuthorName != null ? initialAuthorName : null
221
+ })
222
+ });
223
+ if (!response.ok) {
224
+ throw new Error(`Failed to create thread: ${response.status}`);
225
+ }
226
+ const data = await response.json();
227
+ const persistedThread = {
228
+ ...optimisticThread,
229
+ id: data.id
230
+ };
231
+ setThreads(
232
+ (prev) => prev.map((t) => t === optimisticThread ? persistedThread : t)
233
+ );
234
+ return persistedThread;
235
+ } catch (err) {
236
+ const e = err instanceof Error ? err : new Error("Unknown error");
237
+ setError(e);
238
+ setThreads((prev) => prev.filter((t) => t !== optimisticThread));
239
+ throw e;
240
+ } finally {
241
+ setIsLoading(false);
242
+ }
243
+ },
244
+ [apiBaseUrl, config.projectSlug, config.surfaceId, accessToken]
245
+ );
246
+ const loadComments = react.useCallback(
247
+ async (threadId) => {
248
+ var _a2, _b;
249
+ try {
250
+ setError(null);
251
+ const url = new URL(`${apiBaseUrl}/comments`);
252
+ url.searchParams.set("threadId", threadId);
253
+ const headers = {};
254
+ if (accessToken) {
255
+ headers.Authorization = `Bearer ${accessToken}`;
256
+ }
257
+ const response = await fetch(url.toString(), { headers });
258
+ if (!response.ok) {
259
+ throw new Error(`Failed to load comments: ${response.status}`);
260
+ }
261
+ const data = await response.json();
262
+ const mapped = (_b = (_a2 = data.comments) == null ? void 0 : _a2.map((c) => {
263
+ var _a3;
264
+ return {
265
+ id: c.id,
266
+ threadId: c.threadId,
267
+ body: c.body,
268
+ createdAt: c.createdAt,
269
+ authorName: (_a3 = c.authorName) != null ? _a3 : null
270
+ };
271
+ })) != null ? _b : [];
272
+ setCommentsByThread((prev) => ({
273
+ ...prev,
274
+ [threadId]: mapped
275
+ }));
276
+ } catch (err) {
277
+ const e = err instanceof Error ? err : new Error("Unknown error");
278
+ setError(e);
279
+ throw e;
280
+ }
281
+ },
282
+ [apiBaseUrl, accessToken]
283
+ );
284
+ const addComment = react.useCallback(
285
+ async (threadId, body, authorName) => {
286
+ const trimmed = body.trim();
287
+ if (!trimmed) {
288
+ throw new Error("Comment body is empty");
289
+ }
290
+ const optimistic = {
291
+ id: `${Date.now()}`,
292
+ threadId,
293
+ body: trimmed,
294
+ authorName: authorName != null ? authorName : null
295
+ };
296
+ setCommentsByThread((prev) => {
297
+ var _a2;
298
+ return {
299
+ ...prev,
300
+ [threadId]: [...(_a2 = prev[threadId]) != null ? _a2 : [], optimistic]
301
+ };
302
+ });
303
+ try {
304
+ const response = await fetch(`${apiBaseUrl}/comments`, {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/json",
308
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
309
+ },
310
+ body: JSON.stringify({ threadId, body: trimmed, authorName })
311
+ });
312
+ if (!response.ok) {
313
+ throw new Error(`Failed to create comment: ${response.status}`);
314
+ }
315
+ const data = await response.json();
316
+ const persisted = {
317
+ id: data.id,
318
+ threadId,
319
+ body: trimmed,
320
+ createdAt: data.createdAt
321
+ };
322
+ setCommentsByThread((prev) => {
323
+ var _a2;
324
+ return {
325
+ ...prev,
326
+ [threadId]: ((_a2 = prev[threadId]) != null ? _a2 : []).map(
327
+ (c) => c === optimistic ? persisted : c
328
+ )
329
+ };
330
+ });
331
+ return persisted;
332
+ } catch (err) {
333
+ const e = err instanceof Error ? err : new Error("Unknown error");
334
+ setError(e);
335
+ setCommentsByThread((prev) => {
336
+ var _a2;
337
+ return {
338
+ ...prev,
339
+ [threadId]: ((_a2 = prev[threadId]) != null ? _a2 : []).filter((c) => c !== optimistic)
340
+ };
341
+ });
342
+ throw e;
343
+ }
344
+ },
345
+ [apiBaseUrl, accessToken]
346
+ );
347
+ const deleteThread = react.useCallback(
348
+ async (threadId) => {
349
+ const headers = {
350
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
351
+ };
352
+ const response = await fetch(`${apiBaseUrl}/threads/${threadId}`, {
353
+ method: "DELETE",
354
+ headers
355
+ });
356
+ if (!response.ok) {
357
+ throw new Error(`Failed to delete thread: ${response.status}`);
358
+ }
359
+ setThreads((prev) => prev.filter((t) => t.id !== threadId));
360
+ setCommentsByThread((prev) => {
361
+ const next = { ...prev };
362
+ delete next[threadId];
363
+ return next;
364
+ });
365
+ },
366
+ [apiBaseUrl, accessToken]
367
+ );
368
+ const value = react.useMemo(
369
+ () => ({
370
+ config,
371
+ threads,
372
+ commentsByThread,
373
+ isLoading,
374
+ error,
375
+ createThread,
376
+ loadComments,
377
+ addComment,
378
+ deleteThread,
379
+ commentModeEnabled,
380
+ setCommentModeEnabled,
381
+ surfaceRef,
382
+ hoveredRect,
383
+ setHoveredRect,
384
+ surfaceClickHandlerRef,
385
+ pendingAnchor,
386
+ setPendingAnchor,
387
+ onPinClickRef
388
+ }),
389
+ [
390
+ config,
391
+ threads,
392
+ commentsByThread,
393
+ isLoading,
394
+ error,
395
+ createThread,
396
+ loadComments,
397
+ addComment,
398
+ deleteThread,
399
+ commentModeEnabled,
400
+ hoveredRect,
401
+ pendingAnchor
402
+ ]
403
+ );
404
+ return /* @__PURE__ */ jsxRuntime.jsx(CommentsContext.Provider, { value, children });
405
+ }
406
+ function useCommentsInternal() {
407
+ const ctx = react.useContext(CommentsContext);
408
+ if (!ctx) {
409
+ throw new Error("useCommentsInternal must be used within a CommentsProvider");
410
+ }
411
+ return ctx;
412
+ }
413
+ function CommentProvider(props) {
414
+ const { config, children } = props;
415
+ return /* @__PURE__ */ jsxRuntime.jsx(AuthProvider, { config, children: /* @__PURE__ */ jsxRuntime.jsx(CommentsProvider, { config, children }) });
416
+ }
417
+
418
+ // src/hooks.ts
419
+ function useComments() {
420
+ const ctx = useCommentsInternal();
421
+ return {
422
+ config: ctx.config,
423
+ threads: ctx.threads,
424
+ commentsByThread: ctx.commentsByThread,
425
+ isLoading: ctx.isLoading,
426
+ error: ctx.error,
427
+ createThread: ctx.createThread,
428
+ loadComments: ctx.loadComments,
429
+ addComment: ctx.addComment,
430
+ deleteThread: ctx.deleteThread
431
+ };
432
+ }
433
+ var PIN_STYLE = {
434
+ position: "absolute",
435
+ transform: "translate(-50%, -50%)",
436
+ width: 18,
437
+ height: 18,
438
+ borderRadius: "999px",
439
+ background: "#f97316",
440
+ border: "2px solid #fff",
441
+ boxShadow: "0 4px 10px rgba(0,0,0,0.25)",
442
+ display: "flex",
443
+ alignItems: "center",
444
+ justifyContent: "center",
445
+ color: "#fff",
446
+ fontSize: 10,
447
+ fontWeight: 600,
448
+ cursor: "pointer"
449
+ };
450
+ function CommentSurface(props) {
451
+ const {
452
+ commentModeEnabled,
453
+ setHoveredRect,
454
+ surfaceRef,
455
+ surfaceClickHandlerRef,
456
+ pendingAnchor,
457
+ onPinClickRef
458
+ } = useCommentsInternal();
459
+ const { threads } = useComments();
460
+ const handleMouseMove = react.useCallback(
461
+ (e) => {
462
+ if (!commentModeEnabled) return;
463
+ const el = e.target;
464
+ const rect = el.getBoundingClientRect();
465
+ setHoveredRect({
466
+ left: rect.left,
467
+ top: rect.top,
468
+ width: rect.width,
469
+ height: rect.height
470
+ });
471
+ },
472
+ [commentModeEnabled, setHoveredRect]
473
+ );
474
+ const handleMouseLeave = react.useCallback(() => {
475
+ setHoveredRect(null);
476
+ }, [setHoveredRect]);
477
+ const handleClick = react.useCallback(
478
+ (e) => {
479
+ var _a, _b;
480
+ if (!commentModeEnabled) return;
481
+ const target = e.target;
482
+ if ((_a = target.closest) == null ? void 0 : _a.call(target, "[data-commentator-pin]")) return;
483
+ e.preventDefault();
484
+ e.stopPropagation();
485
+ (_b = surfaceClickHandlerRef.current) == null ? void 0 : _b.call(surfaceClickHandlerRef, e);
486
+ },
487
+ [commentModeEnabled, surfaceClickHandlerRef]
488
+ );
489
+ return /* @__PURE__ */ jsxRuntime.jsxs(
490
+ "div",
491
+ {
492
+ ref: surfaceRef,
493
+ onMouseMove: handleMouseMove,
494
+ onMouseLeave: handleMouseLeave,
495
+ onClickCapture: handleClick,
496
+ style: { ...props.style, minHeight: "100%", position: "relative" },
497
+ className: props.className,
498
+ children: [
499
+ props.children,
500
+ /* @__PURE__ */ jsxRuntime.jsxs(
501
+ "div",
502
+ {
503
+ "aria-hidden": true,
504
+ style: {
505
+ position: "absolute",
506
+ inset: 0,
507
+ pointerEvents: "none",
508
+ zIndex: 1
509
+ },
510
+ children: [
511
+ threads.map((thread) => /* @__PURE__ */ jsxRuntime.jsx(
512
+ "div",
513
+ {
514
+ "data-commentator-pin": true,
515
+ role: "button",
516
+ tabIndex: 0,
517
+ onClick: (e) => {
518
+ var _a;
519
+ e.stopPropagation();
520
+ (_a = onPinClickRef.current) == null ? void 0 : _a.call(onPinClickRef, thread.id, e.clientX, e.clientY);
521
+ },
522
+ onKeyDown: (e) => {
523
+ var _a;
524
+ if (e.key === "Enter" || e.key === " ") {
525
+ e.preventDefault();
526
+ e.stopPropagation();
527
+ (_a = onPinClickRef.current) == null ? void 0 : _a.call(onPinClickRef, thread.id, 0, 0);
528
+ }
529
+ },
530
+ style: {
531
+ ...PIN_STYLE,
532
+ left: `${thread.anchor.x * 100}%`,
533
+ top: `${thread.anchor.y * 100}%`,
534
+ pointerEvents: "auto"
535
+ },
536
+ children: "\u25CF"
537
+ },
538
+ thread.id
539
+ )),
540
+ pendingAnchor && /* @__PURE__ */ jsxRuntime.jsx(
541
+ "div",
542
+ {
543
+ "aria-hidden": true,
544
+ "data-commentator-pin": true,
545
+ style: {
546
+ ...PIN_STYLE,
547
+ left: `${pendingAnchor.x * 100}%`,
548
+ top: `${pendingAnchor.y * 100}%`,
549
+ cursor: "default",
550
+ opacity: 0.7,
551
+ pointerEvents: "auto"
552
+ },
553
+ children: "\u25CF"
554
+ }
555
+ )
556
+ ]
557
+ }
558
+ )
559
+ ]
560
+ }
561
+ );
562
+ }
563
+
564
+ // src/auth.ts
565
+ function useCommentAuth() {
566
+ return useAuthInternal();
567
+ }
568
+ function CommentSettings() {
569
+ const {
570
+ user,
571
+ displayName,
572
+ signOut,
573
+ isReady: isAuthReady,
574
+ signInWithGitHub
575
+ } = useCommentAuth();
576
+ return /* @__PURE__ */ jsxRuntime.jsxs(
577
+ "div",
578
+ {
579
+ style: {
580
+ borderRadius: 16,
581
+ padding: 16,
582
+ backgroundColor: "#f9fafb",
583
+ border: "1px solid #e5e7eb",
584
+ display: "flex",
585
+ flexDirection: "column",
586
+ gap: 8
587
+ },
588
+ children: [
589
+ /* @__PURE__ */ jsxRuntime.jsxs(
590
+ "div",
591
+ {
592
+ style: {
593
+ display: "flex",
594
+ justifyContent: "space-between",
595
+ alignItems: "center",
596
+ marginBottom: 4
597
+ },
598
+ children: [
599
+ /* @__PURE__ */ jsxRuntime.jsx(
600
+ "span",
601
+ {
602
+ style: {
603
+ fontSize: 14,
604
+ fontWeight: 600,
605
+ color: "#111827"
606
+ },
607
+ children: "Commentator settings"
608
+ }
609
+ ),
610
+ user && /* @__PURE__ */ jsxRuntime.jsx(
611
+ "button",
612
+ {
613
+ type: "button",
614
+ onClick: () => signOut().catch(() => {
615
+ }),
616
+ style: {
617
+ borderRadius: 999,
618
+ border: "1px solid #e5e7eb",
619
+ backgroundColor: "#ffffff",
620
+ padding: "4px 10px",
621
+ fontSize: 11,
622
+ color: "#374151",
623
+ cursor: "pointer"
624
+ },
625
+ children: "Sign out"
626
+ }
627
+ )
628
+ ]
629
+ }
630
+ ),
631
+ user ? /* @__PURE__ */ jsxRuntime.jsx(
632
+ "div",
633
+ {
634
+ style: {
635
+ fontSize: 12,
636
+ color: "#6b7280"
637
+ },
638
+ children: /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
639
+ "Signed in as ",
640
+ displayName || user.email || "unknown user"
641
+ ] })
642
+ }
643
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 4 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
644
+ "button",
645
+ {
646
+ type: "button",
647
+ onClick: () => signInWithGitHub().catch(() => {
648
+ }),
649
+ disabled: !isAuthReady,
650
+ style: {
651
+ width: "100%",
652
+ padding: "8px 12px",
653
+ borderRadius: 10,
654
+ border: "1px solid #e5e7eb",
655
+ backgroundColor: "#24292f",
656
+ color: "#fff",
657
+ fontSize: 13,
658
+ fontWeight: 500,
659
+ cursor: isAuthReady ? "pointer" : "default",
660
+ display: "flex",
661
+ alignItems: "center",
662
+ justifyContent: "center",
663
+ gap: 8
664
+ },
665
+ children: [
666
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" }) }),
667
+ "Sign in with GitHub"
668
+ ]
669
+ }
670
+ ) })
671
+ ]
672
+ }
673
+ );
674
+ }
675
+ var PANEL_WIDTH = 280;
676
+ var PANEL_HEIGHT = 260;
677
+ var PANEL_MARGIN = 16;
678
+ function computePanelPosition(clientX, clientY) {
679
+ if (typeof window === "undefined") {
680
+ return { top: 72, left: void 0 };
681
+ }
682
+ const vw = window.innerWidth;
683
+ const vh = window.innerHeight;
684
+ let left = clientX + 24;
685
+ let top = clientY - PANEL_HEIGHT / 2;
686
+ if (left + PANEL_WIDTH + PANEL_MARGIN > vw) {
687
+ left = clientX - PANEL_WIDTH - 24;
688
+ }
689
+ left = Math.min(
690
+ Math.max(PANEL_MARGIN, left),
691
+ vw - PANEL_WIDTH - PANEL_MARGIN
692
+ );
693
+ top = Math.min(
694
+ Math.max(PANEL_MARGIN, top),
695
+ vh - PANEL_HEIGHT - PANEL_MARGIN
696
+ );
697
+ return { top, left };
698
+ }
699
+ function CommentOverlay(props) {
700
+ var _a;
701
+ const { position = "right" } = props;
702
+ const {
703
+ threads,
704
+ commentsByThread,
705
+ createThread,
706
+ loadComments,
707
+ addComment,
708
+ deleteThread
709
+ } = useComments();
710
+ const {
711
+ commentModeEnabled,
712
+ setCommentModeEnabled,
713
+ surfaceRef,
714
+ hoveredRect,
715
+ setHoveredRect,
716
+ surfaceClickHandlerRef,
717
+ pendingAnchor,
718
+ setPendingAnchor,
719
+ onPinClickRef
720
+ } = useCommentsInternal();
721
+ const {
722
+ user,
723
+ isReady: isAuthReady,
724
+ signInWithGitHub,
725
+ displayName
726
+ } = useCommentAuth();
727
+ const [activeThreadId, setActiveThreadId] = react.useState(null);
728
+ const [draft, setDraft] = react.useState("");
729
+ const [panelPosition, setPanelPosition] = react.useState(null);
730
+ const [showSettings, setShowSettings] = react.useState(false);
731
+ const activeComments = react.useMemo(
732
+ () => {
733
+ var _a2;
734
+ return activeThreadId ? (_a2 = commentsByThread[activeThreadId]) != null ? _a2 : [] : [];
735
+ },
736
+ [activeThreadId, commentsByThread]
737
+ );
738
+ react.useEffect(() => {
739
+ var _a2;
740
+ if (!activeThreadId || pendingAnchor) return;
741
+ if ((_a2 = commentsByThread[activeThreadId]) == null ? void 0 : _a2.length) return;
742
+ void loadComments(activeThreadId);
743
+ }, [activeThreadId, pendingAnchor, commentsByThread, loadComments]);
744
+ const handleSurfaceClick = react.useCallback(
745
+ (event) => {
746
+ var _a2;
747
+ setShowSettings(false);
748
+ const rect = (_a2 = surfaceRef.current) == null ? void 0 : _a2.getBoundingClientRect();
749
+ if (!rect) return;
750
+ const x = (event.clientX - rect.left) / rect.width;
751
+ const y = (event.clientY - rect.top) / rect.height;
752
+ setPendingAnchor({ x, y });
753
+ setActiveThreadId(null);
754
+ setDraft("");
755
+ setPanelPosition(computePanelPosition(event.clientX, event.clientY));
756
+ },
757
+ [setPendingAnchor, surfaceRef]
758
+ );
759
+ react.useEffect(() => {
760
+ surfaceClickHandlerRef.current = handleSurfaceClick;
761
+ return () => {
762
+ surfaceClickHandlerRef.current = null;
763
+ };
764
+ }, [surfaceClickHandlerRef, handleSurfaceClick]);
765
+ react.useEffect(() => {
766
+ onPinClickRef.current = (threadId, clientX, clientY) => {
767
+ setShowSettings(false);
768
+ setActiveThreadId(threadId);
769
+ setPendingAnchor(null);
770
+ setPanelPosition(computePanelPosition(clientX, clientY));
771
+ };
772
+ return () => {
773
+ onPinClickRef.current = null;
774
+ };
775
+ }, [onPinClickRef]);
776
+ const handleToggle = react.useCallback(() => {
777
+ const next = !commentModeEnabled;
778
+ if (!next) setHoveredRect(null);
779
+ setCommentModeEnabled(next);
780
+ setActiveThreadId(null);
781
+ setPendingAnchor(null);
782
+ setDraft("");
783
+ setPanelPosition(null);
784
+ }, [commentModeEnabled, setCommentModeEnabled, setHoveredRect, setPendingAnchor]);
785
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
786
+ /* @__PURE__ */ jsxRuntime.jsxs(
787
+ "div",
788
+ {
789
+ style: {
790
+ position: "fixed",
791
+ top: 16,
792
+ [position]: 16,
793
+ zIndex: 2147483e3,
794
+ display: "flex",
795
+ alignItems: "center",
796
+ gap: 6
797
+ },
798
+ children: [
799
+ /* @__PURE__ */ jsxRuntime.jsx(
800
+ "button",
801
+ {
802
+ type: "button",
803
+ onClick: handleToggle,
804
+ "aria-pressed": commentModeEnabled,
805
+ style: {
806
+ padding: "8px 12px",
807
+ borderRadius: 999,
808
+ border: "1px solid rgba(0,0,0,0.12)",
809
+ background: commentModeEnabled ? "#111827" : "#f9fafb",
810
+ color: commentModeEnabled ? "#f9fafb" : "#111827",
811
+ fontSize: 12,
812
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
813
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
814
+ cursor: "pointer"
815
+ },
816
+ children: commentModeEnabled ? "Comment mode: on" : "Comment mode: off"
817
+ }
818
+ ),
819
+ /* @__PURE__ */ jsxRuntime.jsx(
820
+ "button",
821
+ {
822
+ type: "button",
823
+ onClick: () => setShowSettings((prev) => {
824
+ const next = !prev;
825
+ if (next) {
826
+ setActiveThreadId(null);
827
+ setPendingAnchor(null);
828
+ setDraft("");
829
+ setPanelPosition(null);
830
+ }
831
+ return next;
832
+ }),
833
+ "aria-label": "Commentator settings",
834
+ style: {
835
+ width: 28,
836
+ height: 28,
837
+ borderRadius: 999,
838
+ border: "1px solid rgba(0,0,0,0.12)",
839
+ background: "#ffffff",
840
+ display: "flex",
841
+ alignItems: "center",
842
+ justifyContent: "center",
843
+ boxShadow: "0 4px 12px rgba(0,0,0,0.12)",
844
+ cursor: "pointer",
845
+ fontSize: 14,
846
+ color: "#4b5563"
847
+ },
848
+ children: "\u2699\uFE0F"
849
+ }
850
+ )
851
+ ]
852
+ }
853
+ ),
854
+ showSettings && /* @__PURE__ */ jsxRuntime.jsx(
855
+ "div",
856
+ {
857
+ style: {
858
+ position: "fixed",
859
+ top: 54,
860
+ [position]: 16,
861
+ zIndex: 2147483e3,
862
+ maxWidth: 280
863
+ },
864
+ children: /* @__PURE__ */ jsxRuntime.jsx(CommentSettings, {})
865
+ }
866
+ ),
867
+ !showSettings && (activeThreadId || pendingAnchor) && /* @__PURE__ */ jsxRuntime.jsxs(
868
+ "div",
869
+ {
870
+ style: {
871
+ position: "fixed",
872
+ top: (_a = panelPosition == null ? void 0 : panelPosition.top) != null ? _a : 72,
873
+ left: panelPosition == null ? void 0 : panelPosition.left,
874
+ width: PANEL_WIDTH,
875
+ maxHeight: "70vh",
876
+ padding: 16,
877
+ borderRadius: 16,
878
+ backgroundColor: "#ffffff",
879
+ boxShadow: "0 18px 45px rgba(15,23,42,0.25)",
880
+ border: "1px solid rgba(148,163,184,0.35)",
881
+ display: "flex",
882
+ flexDirection: "column",
883
+ gap: 8,
884
+ zIndex: 2147483500
885
+ },
886
+ children: [
887
+ /* @__PURE__ */ jsxRuntime.jsxs(
888
+ "div",
889
+ {
890
+ style: {
891
+ display: "flex",
892
+ justifyContent: "space-between",
893
+ alignItems: "center",
894
+ marginBottom: 4
895
+ },
896
+ children: [
897
+ /* @__PURE__ */ jsxRuntime.jsx(
898
+ "span",
899
+ {
900
+ style: {
901
+ fontSize: 13,
902
+ fontWeight: 600,
903
+ color: "#111827"
904
+ },
905
+ children: user ? activeThreadId ? "Thread" : "New comment" : "Sign in to comment"
906
+ }
907
+ ),
908
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
909
+ activeThreadId && /* @__PURE__ */ jsxRuntime.jsx(
910
+ "button",
911
+ {
912
+ type: "button",
913
+ onClick: async () => {
914
+ try {
915
+ await deleteThread(activeThreadId);
916
+ setActiveThreadId(null);
917
+ setPendingAnchor(null);
918
+ setDraft("");
919
+ setPanelPosition(null);
920
+ } catch (err) {
921
+ console.error("[commentator] Failed to delete thread", err);
922
+ }
923
+ },
924
+ style: {
925
+ border: "none",
926
+ background: "transparent",
927
+ color: "#b91c1c",
928
+ fontSize: 12,
929
+ cursor: "pointer"
930
+ },
931
+ children: "Delete thread"
932
+ }
933
+ ),
934
+ /* @__PURE__ */ jsxRuntime.jsx(
935
+ "button",
936
+ {
937
+ type: "button",
938
+ onClick: () => {
939
+ setActiveThreadId(null);
940
+ setPendingAnchor(null);
941
+ setDraft("");
942
+ setPanelPosition(null);
943
+ },
944
+ style: {
945
+ border: "none",
946
+ background: "transparent",
947
+ color: "#6b7280",
948
+ fontSize: 12,
949
+ cursor: "pointer"
950
+ },
951
+ children: "Close"
952
+ }
953
+ )
954
+ ] })
955
+ ]
956
+ }
957
+ ),
958
+ user ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
959
+ /* @__PURE__ */ jsxRuntime.jsx(
960
+ "div",
961
+ {
962
+ style: {
963
+ flex: 1,
964
+ overflowY: "auto",
965
+ paddingRight: 4
966
+ },
967
+ children: activeComments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
968
+ "p",
969
+ {
970
+ style: {
971
+ fontSize: 13,
972
+ color: "#9ca3af",
973
+ margin: 0
974
+ },
975
+ children: "No comments yet. Be the first to comment."
976
+ }
977
+ ) : activeComments.map((comment) => /* @__PURE__ */ jsxRuntime.jsxs(
978
+ "div",
979
+ {
980
+ style: {
981
+ fontSize: 13,
982
+ color: "#111827",
983
+ padding: "6px 8px",
984
+ borderRadius: 10,
985
+ backgroundColor: "#f9fafb",
986
+ border: "1px solid #e5e7eb",
987
+ marginBottom: 6
988
+ },
989
+ children: [
990
+ comment.authorName && /* @__PURE__ */ jsxRuntime.jsx(
991
+ "div",
992
+ {
993
+ style: {
994
+ fontSize: 11,
995
+ color: "#6b7280",
996
+ marginBottom: 2
997
+ },
998
+ children: comment.authorName
999
+ }
1000
+ ),
1001
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: comment.body })
1002
+ ]
1003
+ },
1004
+ comment.id
1005
+ ))
1006
+ }
1007
+ ),
1008
+ /* @__PURE__ */ jsxRuntime.jsxs(
1009
+ "form",
1010
+ {
1011
+ onSubmit: async (event) => {
1012
+ event.preventDefault();
1013
+ if (!draft.trim()) return;
1014
+ try {
1015
+ if (activeThreadId) {
1016
+ await addComment(activeThreadId, draft, displayName);
1017
+ } else if (pendingAnchor) {
1018
+ const thread = await createThread(
1019
+ pendingAnchor,
1020
+ draft,
1021
+ displayName || null
1022
+ );
1023
+ setActiveThreadId(thread.id);
1024
+ setPendingAnchor(null);
1025
+ }
1026
+ setDraft("");
1027
+ } catch (err) {
1028
+ console.error("[commentator] Failed to save comment", err);
1029
+ }
1030
+ },
1031
+ style: { marginTop: 4 },
1032
+ children: [
1033
+ /* @__PURE__ */ jsxRuntime.jsx(
1034
+ "textarea",
1035
+ {
1036
+ value: draft,
1037
+ onChange: (event) => setDraft(event.target.value),
1038
+ rows: 3,
1039
+ placeholder: "Add a comment...",
1040
+ style: {
1041
+ width: "100%",
1042
+ resize: "none",
1043
+ fontSize: 13,
1044
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
1045
+ padding: "6px 8px",
1046
+ borderRadius: 10,
1047
+ border: "1px solid #d1d5db",
1048
+ boxSizing: "border-box",
1049
+ marginBottom: 6
1050
+ }
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsxRuntime.jsx(
1054
+ "button",
1055
+ {
1056
+ type: "submit",
1057
+ disabled: !draft.trim(),
1058
+ style: {
1059
+ width: "100%",
1060
+ padding: "6px 10px",
1061
+ borderRadius: 999,
1062
+ border: "none",
1063
+ backgroundColor: draft.trim() ? "#111827" : "#e5e7eb",
1064
+ color: draft.trim() ? "#f9fafb" : "#9ca3af",
1065
+ fontSize: 13,
1066
+ fontWeight: 500,
1067
+ cursor: draft.trim() ? "pointer" : "default"
1068
+ },
1069
+ children: "Post comment"
1070
+ }
1071
+ )
1072
+ ]
1073
+ }
1074
+ )
1075
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
1076
+ "button",
1077
+ {
1078
+ type: "button",
1079
+ onClick: () => signInWithGitHub().catch(() => {
1080
+ }),
1081
+ disabled: !isAuthReady,
1082
+ style: {
1083
+ width: "100%",
1084
+ padding: "8px 12px",
1085
+ borderRadius: 10,
1086
+ border: "1px solid #e5e7eb",
1087
+ backgroundColor: "#24292f",
1088
+ color: "#fff",
1089
+ fontSize: 13,
1090
+ fontWeight: 500,
1091
+ cursor: isAuthReady ? "pointer" : "default",
1092
+ display: "flex",
1093
+ alignItems: "center",
1094
+ justifyContent: "center",
1095
+ gap: 8
1096
+ },
1097
+ children: [
1098
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" }) }),
1099
+ "Sign in with GitHub"
1100
+ ]
1101
+ }
1102
+ ) })
1103
+ ]
1104
+ }
1105
+ )
1106
+ ] });
1107
+ }
1108
+
1109
+ exports.CommentOverlay = CommentOverlay;
1110
+ exports.CommentProvider = CommentProvider;
1111
+ exports.CommentSettings = CommentSettings;
1112
+ exports.CommentSurface = CommentSurface;
1113
+ exports.useCommentAuth = useCommentAuth;
1114
+ exports.useComments = useComments;
1115
+ //# sourceMappingURL=index.js.map
1116
+ //# sourceMappingURL=index.js.map