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