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