comment-mode 0.1.2 → 0.1.4

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useCallback, useState, useMemo, useEffect, useRef, useContext } from 'react';
1
+ import { createContext, useState, useCallback, useLayoutEffect, useMemo, useEffect, useRef, useContext } from 'react';
2
2
  import { createClient } from '@supabase/supabase-js';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -15,6 +15,7 @@ function AuthProvider(props) {
15
15
  const [accessToken, setAccessToken] = useState(null);
16
16
  const [isReady, setIsReady] = useState(false);
17
17
  const [displayName, setDisplayNameState] = useState("");
18
+ const [avatarUrl, setAvatarUrl] = useState(null);
18
19
  const apiBaseUrl = (_a = config.apiBaseUrl) != null ? _a : DEFAULT_API_BASE_URL;
19
20
  useEffect(() => {
20
21
  let cancelled = false;
@@ -27,8 +28,13 @@ function AuthProvider(props) {
27
28
  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
29
  const emailPrefix = (_f = (_e = u.email) == null ? void 0 : _e.split("@")[0]) == null ? void 0 : _f.trim();
29
30
  return fullName || userName || emailPrefix || "";
31
+ }, getAvatarUrlFromUser2 = function(u) {
32
+ var _a2;
33
+ const meta = u.user_metadata;
34
+ const url = (_a2 = meta == null ? void 0 : meta.avatar_url) == null ? void 0 : _a2.trim();
35
+ return url || null;
30
36
  };
31
- var getDisplayNameFromUser = getDisplayNameFromUser2;
37
+ var getDisplayNameFromUser = getDisplayNameFromUser2, getAvatarUrlFromUser = getAvatarUrlFromUser2;
32
38
  const response = await fetch(`${apiBaseUrl}/auth/config`);
33
39
  if (!response.ok) {
34
40
  throw new Error(`Failed to load auth config: ${response.status}`);
@@ -48,16 +54,19 @@ function AuthProvider(props) {
48
54
  setUser({ id: session.user.id, email: session.user.email });
49
55
  setAccessToken(session.access_token);
50
56
  setDisplayNameState(getDisplayNameFromUser2(session.user));
57
+ setAvatarUrl(getAvatarUrlFromUser2(session.user));
51
58
  }
52
59
  client.auth.onAuthStateChange((_event, session2) => {
53
60
  if (!session2 || !session2.user) {
54
61
  setUser(null);
55
62
  setAccessToken(null);
56
63
  setDisplayNameState("");
64
+ setAvatarUrl(null);
57
65
  } else {
58
66
  setUser({ id: session2.user.id, email: session2.user.email });
59
67
  setAccessToken(session2.access_token);
60
68
  setDisplayNameState(getDisplayNameFromUser2(session2.user));
69
+ setAvatarUrl(getAvatarUrlFromUser2(session2.user));
61
70
  }
62
71
  });
63
72
  } catch (err) {
@@ -102,6 +111,7 @@ function AuthProvider(props) {
102
111
  setUser(null);
103
112
  setAccessToken(null);
104
113
  setDisplayNameState("");
114
+ setAvatarUrl(null);
105
115
  }, [supabase]);
106
116
  const value = useMemo(
107
117
  () => ({
@@ -110,6 +120,7 @@ function AuthProvider(props) {
110
120
  accessToken,
111
121
  isReady,
112
122
  displayName,
123
+ avatarUrl,
113
124
  signInWithEmail,
114
125
  signInWithGitHub,
115
126
  signOut
@@ -120,6 +131,7 @@ function AuthProvider(props) {
120
131
  accessToken,
121
132
  isReady,
122
133
  displayName,
134
+ avatarUrl,
123
135
  signInWithEmail,
124
136
  signInWithGitHub,
125
137
  signOut
@@ -134,6 +146,49 @@ function useAuthInternal() {
134
146
  }
135
147
  return ctx;
136
148
  }
149
+
150
+ // src/selector.ts
151
+ function getSelector(element, root) {
152
+ if (element === root) return ":scope";
153
+ const id = element.id;
154
+ if (id && /^[a-zA-Z][\w-]*$/.test(id) && !id.toLowerCase().startsWith("radix")) {
155
+ try {
156
+ const matches = root.querySelectorAll(`#${escapeSelectorId(id)}`);
157
+ if (matches.length === 1 && matches[0] === element) return `#${escapeSelectorId(id)}`;
158
+ } catch {
159
+ }
160
+ }
161
+ const path = [];
162
+ let current = element;
163
+ while (current && current !== root) {
164
+ let selector = current.tagName.toLowerCase();
165
+ if (current.id && /^[a-zA-Z][\w-]*$/.test(current.id)) {
166
+ selector += `#${escapeSelectorId(current.id)}`;
167
+ path.unshift(selector);
168
+ break;
169
+ }
170
+ const parent = current.parentElement;
171
+ if (!parent) break;
172
+ const siblings = Array.from(parent.children).filter(
173
+ (el) => el.tagName === current.tagName
174
+ );
175
+ const index = siblings.indexOf(current);
176
+ if (siblings.length > 1) selector += `:nth-of-type(${index + 1})`;
177
+ path.unshift(selector);
178
+ current = parent;
179
+ }
180
+ return path.join(" > ");
181
+ }
182
+ function escapeSelectorId(id) {
183
+ return CSS.escape(id);
184
+ }
185
+ function isElementVisible(el) {
186
+ const rect = el.getBoundingClientRect();
187
+ if (rect.width === 0 && rect.height === 0) return false;
188
+ const style = window.getComputedStyle(el);
189
+ if (style.display === "none" || style.visibility === "hidden") return false;
190
+ return true;
191
+ }
137
192
  var CommentsContext = createContext(void 0);
138
193
  function CommentsProvider(props) {
139
194
  var _a;
@@ -146,6 +201,7 @@ function CommentsProvider(props) {
146
201
  const [commentModeEnabled, setCommentModeEnabled] = useState(true);
147
202
  const [hoveredRect, setHoveredRect] = useState(null);
148
203
  const [pendingAnchor, setPendingAnchor] = useState(null);
204
+ const pendingAnchorElementRef = useRef(null);
149
205
  const surfaceRef = useRef(null);
150
206
  const surfaceClickHandlerRef = useRef(null);
151
207
  const onPinClickRef = useRef(null);
@@ -171,11 +227,17 @@ function CommentsProvider(props) {
171
227
  }
172
228
  const data = await response.json();
173
229
  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 : [];
230
+ const mapped = (_b = (_a2 = data.threads) == null ? void 0 : _a2.map((t) => {
231
+ var _a3, _b2;
232
+ return {
233
+ id: t.id,
234
+ anchor: { x: t.anchorX, y: t.anchorY },
235
+ anchorSelector: (_a3 = t.anchorSelector) != null ? _a3 : null,
236
+ anchorRelative: typeof t.anchorRelativeX === "number" && typeof t.anchorRelativeY === "number" ? { x: t.anchorRelativeX, y: t.anchorRelativeY } : null,
237
+ status: "open",
238
+ firstCommentAuthorAvatarUrl: (_b2 = t.firstCommentAuthorAvatarUrl) != null ? _b2 : null
239
+ };
240
+ })) != null ? _b : [];
179
241
  setThreads(mapped);
180
242
  } catch (err) {
181
243
  if (cancelled) return;
@@ -193,11 +255,30 @@ function CommentsProvider(props) {
193
255
  };
194
256
  }, [apiBaseUrl, config.projectSlug, config.surfaceId, accessToken, isAuthReady]);
195
257
  const createThread = useCallback(
196
- async (anchor, initialCommentBody, initialAuthorName) => {
258
+ async (anchor, initialCommentBody, initialAuthorName, anchorElement, initialAuthorAvatarUrl) => {
259
+ var _a2, _b, _c;
260
+ const root = surfaceRef.current;
261
+ const anchorSelector = root && anchorElement && root.contains(anchorElement) ? getSelector(anchorElement, root) : null;
262
+ let anchorRelative = null;
263
+ if (root && anchorElement && anchorSelector) {
264
+ const surfaceRect = root.getBoundingClientRect();
265
+ const elRect = anchorElement.getBoundingClientRect();
266
+ if (elRect.width > 0 && elRect.height > 0) {
267
+ const clickX = surfaceRect.left + anchor.x * surfaceRect.width;
268
+ const clickY = surfaceRect.top + anchor.y * surfaceRect.height;
269
+ anchorRelative = {
270
+ x: Math.max(0, Math.min(1, (clickX - elRect.left) / elRect.width)),
271
+ y: Math.max(0, Math.min(1, (clickY - elRect.top) / elRect.height))
272
+ };
273
+ }
274
+ }
197
275
  const optimisticThread = {
198
276
  id: `${Date.now()}`,
199
277
  anchor,
200
- status: "open"
278
+ anchorSelector: anchorSelector != null ? anchorSelector : void 0,
279
+ anchorRelative: anchorRelative != null ? anchorRelative : void 0,
280
+ status: "open",
281
+ firstCommentAuthorAvatarUrl: initialAuthorAvatarUrl != null ? initialAuthorAvatarUrl : null
201
282
  };
202
283
  setThreads((prev) => [...prev, optimisticThread]);
203
284
  try {
@@ -214,8 +295,12 @@ function CommentsProvider(props) {
214
295
  surfaceId: config.surfaceId,
215
296
  anchorX: anchor.x,
216
297
  anchorY: anchor.y,
298
+ anchorSelector: anchorSelector != null ? anchorSelector : null,
299
+ anchorRelativeX: (_a2 = anchorRelative == null ? void 0 : anchorRelative.x) != null ? _a2 : null,
300
+ anchorRelativeY: (_b = anchorRelative == null ? void 0 : anchorRelative.y) != null ? _b : null,
217
301
  initialCommentBody: initialCommentBody != null ? initialCommentBody : null,
218
- initialAuthorName: initialAuthorName != null ? initialAuthorName : null
302
+ initialAuthorName: initialAuthorName != null ? initialAuthorName : null,
303
+ initialAuthorAvatarUrl: initialAuthorAvatarUrl != null ? initialAuthorAvatarUrl : null
219
304
  })
220
305
  });
221
306
  if (!response.ok) {
@@ -224,7 +309,8 @@ function CommentsProvider(props) {
224
309
  const data = await response.json();
225
310
  const persistedThread = {
226
311
  ...optimisticThread,
227
- id: data.id
312
+ id: data.id,
313
+ firstCommentAuthorAvatarUrl: (_c = initialAuthorAvatarUrl != null ? initialAuthorAvatarUrl : optimisticThread.firstCommentAuthorAvatarUrl) != null ? _c : null
228
314
  };
229
315
  setThreads(
230
316
  (prev) => prev.map((t) => t === optimisticThread ? persistedThread : t)
@@ -258,13 +344,14 @@ function CommentsProvider(props) {
258
344
  }
259
345
  const data = await response.json();
260
346
  const mapped = (_b = (_a2 = data.comments) == null ? void 0 : _a2.map((c) => {
261
- var _a3;
347
+ var _a3, _b2;
262
348
  return {
263
349
  id: c.id,
264
350
  threadId: c.threadId,
265
351
  body: c.body,
266
352
  createdAt: c.createdAt,
267
- authorName: (_a3 = c.authorName) != null ? _a3 : null
353
+ authorName: (_a3 = c.authorName) != null ? _a3 : null,
354
+ authorAvatarUrl: (_b2 = c.authorAvatarUrl) != null ? _b2 : null
268
355
  };
269
356
  })) != null ? _b : [];
270
357
  setCommentsByThread((prev) => ({
@@ -280,7 +367,7 @@ function CommentsProvider(props) {
280
367
  [apiBaseUrl, accessToken]
281
368
  );
282
369
  const addComment = useCallback(
283
- async (threadId, body, authorName) => {
370
+ async (threadId, body, authorName, authorAvatarUrl) => {
284
371
  const trimmed = body.trim();
285
372
  if (!trimmed) {
286
373
  throw new Error("Comment body is empty");
@@ -289,7 +376,8 @@ function CommentsProvider(props) {
289
376
  id: `${Date.now()}`,
290
377
  threadId,
291
378
  body: trimmed,
292
- authorName: authorName != null ? authorName : null
379
+ authorName: authorName != null ? authorName : null,
380
+ authorAvatarUrl: authorAvatarUrl != null ? authorAvatarUrl : null
293
381
  };
294
382
  setCommentsByThread((prev) => {
295
383
  var _a2;
@@ -305,7 +393,7 @@ function CommentsProvider(props) {
305
393
  "Content-Type": "application/json",
306
394
  ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
307
395
  },
308
- body: JSON.stringify({ threadId, body: trimmed, authorName })
396
+ body: JSON.stringify({ threadId, body: trimmed, authorName, authorAvatarUrl: authorAvatarUrl != null ? authorAvatarUrl : null })
309
397
  });
310
398
  if (!response.ok) {
311
399
  throw new Error(`Failed to create comment: ${response.status}`);
@@ -315,7 +403,9 @@ function CommentsProvider(props) {
315
403
  id: data.id,
316
404
  threadId,
317
405
  body: trimmed,
318
- createdAt: data.createdAt
406
+ createdAt: data.createdAt,
407
+ authorName: authorName != null ? authorName : null,
408
+ authorAvatarUrl: authorAvatarUrl != null ? authorAvatarUrl : null
319
409
  };
320
410
  setCommentsByThread((prev) => {
321
411
  var _a2;
@@ -382,6 +472,7 @@ function CommentsProvider(props) {
382
472
  surfaceClickHandlerRef,
383
473
  pendingAnchor,
384
474
  setPendingAnchor,
475
+ pendingAnchorElementRef,
385
476
  onPinClickRef
386
477
  }),
387
478
  [
@@ -428,22 +519,33 @@ function useComments() {
428
519
  deleteThread: ctx.deleteThread
429
520
  };
430
521
  }
431
- var PIN_STYLE = {
522
+ var PIN_SIZE = 28;
523
+ var PIN_BASE_STYLE = {
432
524
  position: "absolute",
433
525
  transform: "translate(-50%, -50%)",
434
- width: 18,
435
- height: 18,
526
+ width: PIN_SIZE,
527
+ height: PIN_SIZE,
436
528
  borderRadius: "999px",
437
- background: "#f97316",
438
- border: "2px solid #fff",
439
- boxShadow: "0 4px 10px rgba(0,0,0,0.25)",
440
529
  display: "flex",
441
530
  alignItems: "center",
442
531
  justifyContent: "center",
532
+ cursor: "pointer"
533
+ };
534
+ var PIN_DOT_STYLE = {
535
+ ...PIN_BASE_STYLE,
536
+ background: "#f97316",
537
+ border: "2px solid #fff",
538
+ boxShadow: "0 4px 10px rgba(0,0,0,0.25)",
443
539
  color: "#fff",
444
540
  fontSize: 10,
445
- fontWeight: 600,
446
- cursor: "pointer"
541
+ fontWeight: 600
542
+ };
543
+ var PIN_AVATAR_STYLE = {
544
+ ...PIN_BASE_STYLE,
545
+ border: "2px solid #fff",
546
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
547
+ overflow: "hidden",
548
+ backgroundColor: "#e5e7eb"
447
549
  };
448
550
  function CommentSurface(props) {
449
551
  const {
@@ -455,6 +557,67 @@ function CommentSurface(props) {
455
557
  onPinClickRef
456
558
  } = useCommentsInternal();
457
559
  const { threads } = useComments();
560
+ const [resolvedPinPositions, setResolvedPinPositions] = useState({});
561
+ const recalcPinPositions = useCallback(() => {
562
+ const root = surfaceRef.current;
563
+ if (!root) return;
564
+ const surfaceRect = root.getBoundingClientRect();
565
+ if (surfaceRect.width === 0 && surfaceRect.height === 0) return;
566
+ const next = {};
567
+ threads.forEach((thread) => {
568
+ if (thread.anchorSelector) {
569
+ try {
570
+ const el = root.querySelector(thread.anchorSelector);
571
+ if (!el || !(el instanceof HTMLElement)) {
572
+ next[thread.id] = "hide";
573
+ return;
574
+ }
575
+ if (!isElementVisible(el)) {
576
+ next[thread.id] = "hide";
577
+ return;
578
+ }
579
+ const elRect = el.getBoundingClientRect();
580
+ const rel = thread.anchorRelative;
581
+ const rx = rel && typeof rel.x === "number" ? rel.x : 0.5;
582
+ const ry = rel && typeof rel.y === "number" ? rel.y : 0.5;
583
+ const left = (elRect.left - surfaceRect.left + rx * elRect.width) / surfaceRect.width;
584
+ const top = (elRect.top - surfaceRect.top + ry * elRect.height) / surfaceRect.height;
585
+ next[thread.id] = { left, top };
586
+ } catch {
587
+ next[thread.id] = "hide";
588
+ }
589
+ }
590
+ });
591
+ setResolvedPinPositions(next);
592
+ }, [threads, surfaceRef]);
593
+ useLayoutEffect(() => {
594
+ recalcPinPositions();
595
+ }, [recalcPinPositions]);
596
+ useLayoutEffect(() => {
597
+ const root = surfaceRef.current;
598
+ if (!root) return;
599
+ const scrollables = [root];
600
+ const walk = (el) => {
601
+ if (el === root) return;
602
+ const style = window.getComputedStyle(el);
603
+ const overflow = style.overflow + style.overflowX + style.overflowY;
604
+ if (overflow.includes("scroll") || overflow.includes("auto")) {
605
+ scrollables.push(el);
606
+ }
607
+ if (el instanceof HTMLElement && el.children.length) {
608
+ Array.from(el.children).forEach(walk);
609
+ }
610
+ };
611
+ walk(root);
612
+ const handleScroll = () => recalcPinPositions();
613
+ scrollables.forEach((el) => el.addEventListener("scroll", handleScroll, { passive: true }));
614
+ const ro = new ResizeObserver(handleScroll);
615
+ ro.observe(root);
616
+ return () => {
617
+ scrollables.forEach((el) => el.removeEventListener("scroll", handleScroll));
618
+ ro.disconnect();
619
+ };
620
+ }, [recalcPinPositions, surfaceRef]);
458
621
  const handleMouseMove = useCallback(
459
622
  (e) => {
460
623
  if (!commentModeEnabled) return;
@@ -506,42 +669,58 @@ function CommentSurface(props) {
506
669
  zIndex: 1
507
670
  },
508
671
  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();
672
+ threads.map((thread) => {
673
+ const resolved = resolvedPinPositions[thread.id];
674
+ if (resolved === "hide") return null;
675
+ const left = typeof resolved === "object" ? resolved.left : thread.anchor.x;
676
+ const top = typeof resolved === "object" ? resolved.top : thread.anchor.y;
677
+ const pinStyle = {
678
+ ...thread.firstCommentAuthorAvatarUrl ? PIN_AVATAR_STYLE : PIN_DOT_STYLE,
679
+ left: `${left * 100}%`,
680
+ top: `${top * 100}%`,
681
+ pointerEvents: "auto"
682
+ };
683
+ return /* @__PURE__ */ jsx(
684
+ "div",
685
+ {
686
+ "data-commentator-pin": true,
687
+ role: "button",
688
+ tabIndex: 0,
689
+ onClick: (e) => {
690
+ var _a;
524
691
  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"
692
+ (_a = onPinClickRef.current) == null ? void 0 : _a.call(onPinClickRef, thread.id, e.clientX, e.clientY);
693
+ },
694
+ onKeyDown: (e) => {
695
+ var _a;
696
+ if (e.key === "Enter" || e.key === " ") {
697
+ e.preventDefault();
698
+ e.stopPropagation();
699
+ (_a = onPinClickRef.current) == null ? void 0 : _a.call(onPinClickRef, thread.id, 0, 0);
700
+ }
701
+ },
702
+ style: pinStyle,
703
+ children: thread.firstCommentAuthorAvatarUrl ? /* @__PURE__ */ jsx(
704
+ "img",
705
+ {
706
+ src: thread.firstCommentAuthorAvatarUrl,
707
+ alt: "",
708
+ width: PIN_SIZE,
709
+ height: PIN_SIZE,
710
+ style: { display: "block", width: PIN_SIZE, height: PIN_SIZE, objectFit: "cover" }
711
+ }
712
+ ) : "\u25CF"
533
713
  },
534
- children: "\u25CF"
535
- },
536
- thread.id
537
- )),
714
+ thread.id
715
+ );
716
+ }),
538
717
  pendingAnchor && /* @__PURE__ */ jsx(
539
718
  "div",
540
719
  {
541
720
  "aria-hidden": true,
542
721
  "data-commentator-pin": true,
543
722
  style: {
544
- ...PIN_STYLE,
723
+ ...PIN_DOT_STYLE,
545
724
  left: `${pendingAnchor.x * 100}%`,
546
725
  top: `${pendingAnchor.y * 100}%`,
547
726
  cursor: "default",
@@ -567,11 +746,12 @@ function CommentSettings() {
567
746
  const {
568
747
  user,
569
748
  displayName,
749
+ avatarUrl,
570
750
  signOut,
571
751
  isReady: isAuthReady,
572
752
  signInWithGitHub
573
753
  } = useCommentAuth();
574
- return /* @__PURE__ */ jsxs(
754
+ return /* @__PURE__ */ jsx(
575
755
  "div",
576
756
  {
577
757
  style: {
@@ -583,90 +763,79 @@ function CommentSettings() {
583
763
  flexDirection: "column",
584
764
  gap: 8
585
765
  },
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"
766
+ children: user ? /* @__PURE__ */ jsxs(
767
+ "div",
768
+ {
769
+ style: {
770
+ display: "flex",
771
+ alignItems: "center",
772
+ gap: 8,
773
+ fontSize: 12,
774
+ color: "#6b7280"
775
+ },
776
+ children: [
777
+ avatarUrl && /* @__PURE__ */ jsx(
778
+ "img",
779
+ {
780
+ src: avatarUrl,
781
+ alt: "",
782
+ width: 24,
783
+ height: 24,
784
+ style: {
785
+ borderRadius: "50%",
786
+ flexShrink: 0
624
787
  }
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
- ]
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx("span", { children: displayName || user.email || "unknown user" }),
791
+ /* @__PURE__ */ jsx(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ onClick: () => signOut().catch(() => {
796
+ }),
797
+ style: {
798
+ borderRadius: 999,
799
+ border: "1px solid #e5e7eb",
800
+ backgroundColor: "#ffffff",
801
+ padding: "4px 10px",
802
+ fontSize: 11,
803
+ color: "#374151",
804
+ cursor: "pointer"
805
+ },
806
+ children: "Sign out"
807
+ }
808
+ )
809
+ ]
810
+ }
811
+ ) : /* @__PURE__ */ jsx("div", { style: { marginTop: 4 }, children: /* @__PURE__ */ jsxs(
812
+ "button",
813
+ {
814
+ type: "button",
815
+ onClick: () => signInWithGitHub().catch(() => {
816
+ }),
817
+ disabled: !isAuthReady,
818
+ style: {
819
+ width: "100%",
820
+ padding: "8px 12px",
821
+ borderRadius: 10,
822
+ border: "1px solid #e5e7eb",
823
+ backgroundColor: "#24292f",
824
+ color: "#fff",
825
+ fontSize: 13,
826
+ fontWeight: 500,
827
+ cursor: isAuthReady ? "pointer" : "default",
828
+ display: "flex",
829
+ alignItems: "center",
830
+ justifyContent: "center",
831
+ gap: 8
832
+ },
833
+ children: [
834
+ /* @__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" }) }),
835
+ "Sign in with GitHub"
836
+ ]
837
+ }
838
+ ) })
670
839
  }
671
840
  );
672
841
  }
@@ -714,13 +883,15 @@ function CommentOverlay(props) {
714
883
  surfaceClickHandlerRef,
715
884
  pendingAnchor,
716
885
  setPendingAnchor,
886
+ pendingAnchorElementRef,
717
887
  onPinClickRef
718
888
  } = useCommentsInternal();
719
889
  const {
720
890
  user,
721
891
  isReady: isAuthReady,
722
892
  signInWithGitHub,
723
- displayName
893
+ displayName,
894
+ avatarUrl
724
895
  } = useCommentAuth();
725
896
  const [activeThreadId, setActiveThreadId] = useState(null);
726
897
  const [draft, setDraft] = useState("");
@@ -741,18 +912,19 @@ function CommentOverlay(props) {
741
912
  }, [activeThreadId, pendingAnchor, commentsByThread, loadComments]);
742
913
  const handleSurfaceClick = useCallback(
743
914
  (event) => {
744
- var _a2;
915
+ var _a2, _b;
745
916
  setShowSettings(false);
746
917
  const rect = (_a2 = surfaceRef.current) == null ? void 0 : _a2.getBoundingClientRect();
747
918
  if (!rect) return;
748
919
  const x = (event.clientX - rect.left) / rect.width;
749
920
  const y = (event.clientY - rect.top) / rect.height;
750
921
  setPendingAnchor({ x, y });
922
+ pendingAnchorElementRef.current = (_b = event.target) != null ? _b : null;
751
923
  setActiveThreadId(null);
752
924
  setDraft("");
753
925
  setPanelPosition(computePanelPosition(event.clientX, event.clientY));
754
926
  },
755
- [setPendingAnchor, surfaceRef]
927
+ [setPendingAnchor, surfaceRef, pendingAnchorElementRef]
756
928
  );
757
929
  useEffect(() => {
758
930
  surfaceClickHandlerRef.current = handleSurfaceClick;
@@ -780,6 +952,19 @@ function CommentOverlay(props) {
780
952
  setDraft("");
781
953
  setPanelPosition(null);
782
954
  }, [commentModeEnabled, setCommentModeEnabled, setHoveredRect, setPendingAnchor]);
955
+ useEffect(() => {
956
+ const onKeyDown = (e) => {
957
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "m") {
958
+ const target = e.target;
959
+ const isEditable = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable;
960
+ if (isEditable) return;
961
+ e.preventDefault();
962
+ handleToggle();
963
+ }
964
+ };
965
+ window.addEventListener("keydown", onKeyDown);
966
+ return () => window.removeEventListener("keydown", onKeyDown);
967
+ }, [handleToggle]);
783
968
  return /* @__PURE__ */ jsxs(Fragment, { children: [
784
969
  /* @__PURE__ */ jsxs(
785
970
  "div",
@@ -843,7 +1028,36 @@ function CommentOverlay(props) {
843
1028
  fontSize: 14,
844
1029
  color: "#4b5563"
845
1030
  },
846
- children: "\u2699\uFE0F"
1031
+ children: /* @__PURE__ */ jsxs(
1032
+ "svg",
1033
+ {
1034
+ xmlns: "http://www.w3.org/2000/svg",
1035
+ fill: "none",
1036
+ viewBox: "0 0 24 24",
1037
+ strokeWidth: 1.5,
1038
+ stroke: "currentColor",
1039
+ style: { width: 18, height: 18 },
1040
+ "aria-hidden": true,
1041
+ children: [
1042
+ /* @__PURE__ */ jsx(
1043
+ "path",
1044
+ {
1045
+ strokeLinecap: "round",
1046
+ strokeLinejoin: "round",
1047
+ d: "M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
1048
+ }
1049
+ ),
1050
+ /* @__PURE__ */ jsx(
1051
+ "path",
1052
+ {
1053
+ strokeLinecap: "round",
1054
+ strokeLinejoin: "round",
1055
+ d: "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
1056
+ }
1057
+ )
1058
+ ]
1059
+ }
1060
+ )
847
1061
  }
848
1062
  )
849
1063
  ]
@@ -892,15 +1106,40 @@ function CommentOverlay(props) {
892
1106
  marginBottom: 4
893
1107
  },
894
1108
  children: [
895
- /* @__PURE__ */ jsx(
896
- "span",
1109
+ /* @__PURE__ */ jsxs(
1110
+ "div",
897
1111
  {
898
1112
  style: {
899
- fontSize: 13,
900
- fontWeight: 600,
901
- color: "#111827"
1113
+ display: "flex",
1114
+ alignItems: "center",
1115
+ gap: 8
902
1116
  },
903
- children: user ? activeThreadId ? "Thread" : "New comment" : "Sign in to comment"
1117
+ children: [
1118
+ user && avatarUrl && /* @__PURE__ */ jsx(
1119
+ "img",
1120
+ {
1121
+ src: avatarUrl,
1122
+ alt: "",
1123
+ width: 24,
1124
+ height: 24,
1125
+ style: {
1126
+ borderRadius: "50%",
1127
+ flexShrink: 0
1128
+ }
1129
+ }
1130
+ ),
1131
+ /* @__PURE__ */ jsx(
1132
+ "span",
1133
+ {
1134
+ style: {
1135
+ fontSize: 13,
1136
+ fontWeight: 600,
1137
+ color: "#111827"
1138
+ },
1139
+ children: user ? activeThreadId ? "Thread" : "New comment" : "Sign in to comment"
1140
+ }
1141
+ )
1142
+ ]
904
1143
  }
905
1144
  ),
906
1145
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
@@ -1007,19 +1246,24 @@ function CommentOverlay(props) {
1007
1246
  "form",
1008
1247
  {
1009
1248
  onSubmit: async (event) => {
1249
+ var _a2;
1010
1250
  event.preventDefault();
1011
1251
  if (!draft.trim()) return;
1012
1252
  try {
1013
1253
  if (activeThreadId) {
1014
- await addComment(activeThreadId, draft, displayName);
1254
+ await addComment(activeThreadId, draft, displayName, avatarUrl != null ? avatarUrl : null);
1015
1255
  } else if (pendingAnchor) {
1256
+ const element = (_a2 = pendingAnchorElementRef.current) != null ? _a2 : void 0;
1016
1257
  const thread = await createThread(
1017
1258
  pendingAnchor,
1018
1259
  draft,
1019
- displayName || null
1260
+ displayName || null,
1261
+ element,
1262
+ avatarUrl != null ? avatarUrl : null
1020
1263
  );
1021
1264
  setActiveThreadId(thread.id);
1022
1265
  setPendingAnchor(null);
1266
+ pendingAnchorElementRef.current = null;
1023
1267
  }
1024
1268
  setDraft("");
1025
1269
  } catch (err) {
@@ -1037,6 +1281,7 @@ function CommentOverlay(props) {
1037
1281
  placeholder: "Add a comment...",
1038
1282
  style: {
1039
1283
  width: "100%",
1284
+ color: "#000000",
1040
1285
  resize: "none",
1041
1286
  fontSize: 13,
1042
1287
  fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
@@ -1103,7 +1348,27 @@ function CommentOverlay(props) {
1103
1348
  )
1104
1349
  ] });
1105
1350
  }
1351
+ function Commentator(props) {
1352
+ const {
1353
+ config,
1354
+ position = "right",
1355
+ surfaceStyle,
1356
+ surfaceClassName,
1357
+ children
1358
+ } = props;
1359
+ return /* @__PURE__ */ jsxs(CommentProvider, { config, children: [
1360
+ /* @__PURE__ */ jsx(
1361
+ CommentSurface,
1362
+ {
1363
+ style: { minHeight: "100%", position: "relative", ...surfaceStyle },
1364
+ className: surfaceClassName,
1365
+ children
1366
+ }
1367
+ ),
1368
+ /* @__PURE__ */ jsx(CommentOverlay, { position })
1369
+ ] });
1370
+ }
1106
1371
 
1107
- export { CommentOverlay, CommentProvider, CommentSettings, CommentSurface, useCommentAuth, useComments };
1372
+ export { CommentOverlay, CommentProvider, CommentSettings, CommentSurface, Commentator, useCommentAuth, useComments };
1108
1373
  //# sourceMappingURL=index.mjs.map
1109
1374
  //# sourceMappingURL=index.mjs.map