@react-spot/plugin-comments 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,716 @@
1
+ import { useDeactivateInspector, useProjectRoot, useWidgetPortalContainer } from "@react-spot/core";
2
+ import { Button, ChatBubbleIcon, ClipboardIcon, DropdownMenu, IconButton, OpencodeIcon, PanelHeader, Popover, Select, Separator, Textarea, ToolbarButton, Tooltip, TrashIcon, XIcon } from "@react-spot/ui-components";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { atom, useAtomValue, useSetAtom } from "jotai";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { createOpencodeClient } from "@opencode-ai/sdk";
7
+
8
+ //#region src/store.ts
9
+ const commentEntriesAtom = atom([]);
10
+ const pendingCommentAtom = atom(null);
11
+ const commentsSnapshotAtom = atom((get) => ({
12
+ comments: get(commentEntriesAtom),
13
+ pending: get(pendingCommentAtom)
14
+ }));
15
+ const addCommentAtom = atom(null, (get, set, entry) => {
16
+ const id = crypto.randomUUID();
17
+ const comment = {
18
+ ...entry,
19
+ id,
20
+ createdAt: Date.now()
21
+ };
22
+ set(commentEntriesAtom, [...get(commentEntriesAtom), comment]);
23
+ return id;
24
+ });
25
+ const updateCommentAtom = atom(null, (get, set, payload) => {
26
+ set(commentEntriesAtom, get(commentEntriesAtom).map((comment) => comment.id === payload.id ? {
27
+ ...comment,
28
+ comment: payload.text
29
+ } : comment));
30
+ });
31
+ const removeCommentAtom = atom(null, (get, set, id) => {
32
+ set(commentEntriesAtom, get(commentEntriesAtom).filter((comment) => comment.id !== id));
33
+ });
34
+ const clearAllCommentsAtom = atom(null, (_get, set) => {
35
+ set(commentEntriesAtom, []);
36
+ });
37
+ function useCommentsSnapshot() {
38
+ return useAtomValue(commentsSnapshotAtom);
39
+ }
40
+ function useCommentEntries() {
41
+ return useCommentsSnapshot().comments;
42
+ }
43
+ function usePendingComment() {
44
+ return useCommentsSnapshot().pending;
45
+ }
46
+ function useCommentsActions() {
47
+ return {
48
+ addComment: useSetAtom(addCommentAtom),
49
+ updateComment: useSetAtom(updateCommentAtom),
50
+ removeComment: useSetAtom(removeCommentAtom),
51
+ clearAllComments: useSetAtom(clearAllCommentsAtom),
52
+ setPending: useSetAtom(pendingCommentAtom)
53
+ };
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/CommentEditor.tsx
58
+ function CommentEditorOverlay() {
59
+ const portalContainer = useWidgetPortalContainer();
60
+ const pending = usePendingComment();
61
+ const { addComment, setPending } = useCommentsActions();
62
+ const [text, setText] = useState("");
63
+ const textareaRef = useRef(null);
64
+ useEffect(() => {
65
+ if (pending) {
66
+ setText("");
67
+ requestAnimationFrame(() => {
68
+ textareaRef.current?.focus();
69
+ });
70
+ }
71
+ }, [pending]);
72
+ const handleCancel = () => {
73
+ setPending(null);
74
+ };
75
+ const handleSubmit = () => {
76
+ if (!pending || !text.trim()) return;
77
+ addComment({
78
+ filePath: pending.filePath,
79
+ lineNumber: pending.lineNumber,
80
+ comment: text.trim()
81
+ });
82
+ setPending(null);
83
+ };
84
+ const handleKeyDown = (e) => {
85
+ if (e.key === "Escape") {
86
+ e.preventDefault();
87
+ handleCancel();
88
+ } else if (e.key === "Enter" && !e.shiftKey) {
89
+ e.preventDefault();
90
+ handleSubmit();
91
+ }
92
+ };
93
+ if (!pending) return null;
94
+ return /* @__PURE__ */ jsx(Popover.Root, {
95
+ open: true,
96
+ onOpenChange: (open) => {
97
+ if (!open) setPending(null);
98
+ },
99
+ children: /* @__PURE__ */ jsx(Popover.Portal, {
100
+ container: portalContainer,
101
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
102
+ anchor: pending.anchorEl,
103
+ side: "bottom",
104
+ align: "start",
105
+ sideOffset: 8,
106
+ collisionPadding: 8,
107
+ positionMethod: "fixed",
108
+ style: {
109
+ zIndex: 9999999,
110
+ pointerEvents: "auto"
111
+ },
112
+ children: /* @__PURE__ */ jsxs(Popover.Popup, {
113
+ initialFocus: false,
114
+ style: {
115
+ width: 480,
116
+ border: "1px solid #3b82f6",
117
+ padding: 8,
118
+ fontFamily: "system-ui, sans-serif",
119
+ boxSizing: "border-box"
120
+ },
121
+ onClick: (e) => e.stopPropagation(),
122
+ onKeyDown: (e) => e.stopPropagation(),
123
+ children: [
124
+ /* @__PURE__ */ jsx(Textarea, {
125
+ ref: textareaRef,
126
+ value: text,
127
+ onChange: (e) => setText(e.target.value),
128
+ onKeyDown: handleKeyDown,
129
+ placeholder: "Add comment",
130
+ rows: 3
131
+ }),
132
+ /* @__PURE__ */ jsx(Separator, { style: { margin: "4px 0" } }),
133
+ /* @__PURE__ */ jsxs("div", {
134
+ style: {
135
+ display: "flex",
136
+ alignItems: "center",
137
+ justifyContent: "space-between",
138
+ paddingTop: 4
139
+ },
140
+ children: [/* @__PURE__ */ jsxs("span", {
141
+ style: {
142
+ fontSize: 11,
143
+ color: "#71717a",
144
+ fontFamily: "ui-monospace, monospace"
145
+ },
146
+ children: ["Commenting on line ", pending.lineNumber]
147
+ }), /* @__PURE__ */ jsxs("div", {
148
+ style: {
149
+ display: "flex",
150
+ gap: 6
151
+ },
152
+ children: [/* @__PURE__ */ jsx(Button, {
153
+ variant: "secondary",
154
+ onClick: handleCancel,
155
+ children: "Cancel"
156
+ }), /* @__PURE__ */ jsx(Button, {
157
+ variant: "primary",
158
+ onClick: handleSubmit,
159
+ disabled: !text.trim(),
160
+ children: "Comment"
161
+ })]
162
+ })]
163
+ })
164
+ ]
165
+ })
166
+ })
167
+ })
168
+ });
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/utils.ts
173
+ /**
174
+ * Formats a comment as a compact `file:line: comment` string.
175
+ * Used for both copy-to-clipboard and the synthetic text part sent to OpenCode.
176
+ */
177
+ function formatCommentNote(filePath, lineNumber, comment) {
178
+ return `${filePath}:${lineNumber}: ${comment}`;
179
+ }
180
+
181
+ //#endregion
182
+ //#region src/SendToOpencode.tsx
183
+ function SendToOpencodeForm({ root, comments, onClearComments, onDone }) {
184
+ const portalContainer = useWidgetPortalContainer();
185
+ const [sessions, setSessions] = useState([]);
186
+ const [selectedId, setSelectedId] = useState("__new__");
187
+ const [generalComment, setGeneralComment] = useState("");
188
+ const [status, setStatus] = useState("loading-sessions");
189
+ const [errorMsg, setErrorMsg] = useState("");
190
+ const textareaRef = useRef(null);
191
+ const [client] = useState(() => createOpencodeClient({
192
+ baseUrl: "http://localhost:4096",
193
+ ...root ? { directory: root } : {}
194
+ }));
195
+ useEffect(() => {
196
+ client.session.list().then((res) => {
197
+ const list = (res.data ?? []).filter((s) => !s.parentID).toSorted((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0));
198
+ setSessions(list);
199
+ setStatus("ready");
200
+ if (list.length > 0) setSelectedId(list[0].id);
201
+ requestAnimationFrame(() => textareaRef.current?.focus());
202
+ }).catch(() => {
203
+ setStatus("error");
204
+ setErrorMsg("Could not connect to OpenCode. Is it running?");
205
+ });
206
+ }, [client]);
207
+ const handleSend = async () => {
208
+ if (!comments.length) return;
209
+ setStatus("sending");
210
+ setErrorMsg("");
211
+ try {
212
+ const previews = await Promise.all(comments.map(async (c) => {
213
+ try {
214
+ return ((await client.file.read({ query: { path: c.filePath } })).data?.content ?? "").split("\n")[c.lineNumber - 1] ?? "";
215
+ } catch {
216
+ return "";
217
+ }
218
+ }));
219
+ const parts = [];
220
+ const trimmed = generalComment.trim();
221
+ if (trimmed) parts.push({
222
+ type: "text",
223
+ text: trimmed
224
+ });
225
+ for (let i = 0; i < comments.length; i++) {
226
+ const c = comments[i];
227
+ const preview = previews[i];
228
+ const absUrl = root ? `file://${root.replace(/\/$/, "")}/${c.filePath}?start=${c.lineNumber}&end=${c.lineNumber}` : `file:///${c.filePath}?start=${c.lineNumber}&end=${c.lineNumber}`;
229
+ const filename = c.filePath.split("/").pop() ?? c.filePath;
230
+ parts.push({
231
+ type: "text",
232
+ text: formatCommentNote(c.filePath, c.lineNumber, c.comment),
233
+ synthetic: true,
234
+ metadata: { opencodeComment: {
235
+ path: c.filePath,
236
+ selection: {
237
+ startLine: c.lineNumber,
238
+ endLine: c.lineNumber,
239
+ startChar: 0,
240
+ endChar: 0
241
+ },
242
+ comment: c.comment,
243
+ preview,
244
+ origin: "file"
245
+ } }
246
+ });
247
+ parts.push({
248
+ type: "file",
249
+ mime: "text/plain",
250
+ filename,
251
+ url: absUrl
252
+ });
253
+ }
254
+ let sessionId = selectedId;
255
+ if (sessionId === "__new__") {
256
+ const res = await client.session.create({ body: {} });
257
+ if (!res.data?.id) throw new Error("Failed to create session");
258
+ sessionId = res.data.id;
259
+ }
260
+ await client.session.promptAsync({
261
+ path: { id: sessionId },
262
+ body: { parts }
263
+ });
264
+ onClearComments();
265
+ setStatus("sent");
266
+ setTimeout(onDone, 1200);
267
+ } catch (e) {
268
+ setStatus("error");
269
+ setErrorMsg(e instanceof Error ? e.message : "Failed to send to OpenCode");
270
+ }
271
+ };
272
+ const handleKeyDown = (e) => {
273
+ if (e.key === "Escape") {
274
+ e.preventDefault();
275
+ onDone();
276
+ } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
277
+ e.preventDefault();
278
+ handleSend();
279
+ }
280
+ };
281
+ if (status === "sent") return /* @__PURE__ */ jsx("div", {
282
+ style: {
283
+ padding: "12px",
284
+ textAlign: "center",
285
+ fontSize: 12,
286
+ color: "#4ade80",
287
+ fontFamily: "system-ui, sans-serif"
288
+ },
289
+ children: "Sent to OpenCode!"
290
+ });
291
+ const loading = status === "loading-sessions";
292
+ const sending = status === "sending";
293
+ const disabled = loading || sending;
294
+ return /* @__PURE__ */ jsxs("div", {
295
+ style: {
296
+ padding: "10px 12px 8px",
297
+ display: "flex",
298
+ flexDirection: "column",
299
+ gap: 8
300
+ },
301
+ onKeyDown: (e) => e.stopPropagation(),
302
+ children: [
303
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
304
+ style: {
305
+ display: "block",
306
+ fontSize: 11,
307
+ color: "#71717a",
308
+ fontFamily: "system-ui, sans-serif",
309
+ marginBottom: 4
310
+ },
311
+ children: "Session"
312
+ }), /* @__PURE__ */ jsxs(Select.Root, {
313
+ value: selectedId,
314
+ onValueChange: (v) => v && setSelectedId(v),
315
+ disabled,
316
+ children: [/* @__PURE__ */ jsx(Select.Trigger, {
317
+ style: { borderRadius: 4 },
318
+ children: /* @__PURE__ */ jsx(Select.Value, {
319
+ placeholder: "Select a session…",
320
+ children: (value) => sessions.find((s) => s.id === value)?.title ?? value
321
+ })
322
+ }), /* @__PURE__ */ jsx(Select.Portal, {
323
+ container: portalContainer,
324
+ children: /* @__PURE__ */ jsx(Select.Positioner, {
325
+ style: {
326
+ zIndex: 9999999,
327
+ pointerEvents: "auto"
328
+ },
329
+ children: /* @__PURE__ */ jsx(Select.Popup, { children: /* @__PURE__ */ jsxs(Select.List, { children: [/* @__PURE__ */ jsx(Select.Item, {
330
+ value: "__new__",
331
+ children: /* @__PURE__ */ jsx(Select.ItemText, { children: "+ New session" })
332
+ }), loading ? /* @__PURE__ */ jsx(Select.Item, {
333
+ value: "__loading__",
334
+ disabled: true,
335
+ children: /* @__PURE__ */ jsx(Select.ItemText, { children: "Loading sessions…" })
336
+ }) : sessions.map((s) => /* @__PURE__ */ jsx(Select.Item, {
337
+ value: s.id,
338
+ children: /* @__PURE__ */ jsx(Select.ItemText, { children: s.title || `Session ${s.id.slice(0, 8)}` })
339
+ }, s.id))] }) })
340
+ })
341
+ })]
342
+ })] }),
343
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
344
+ style: {
345
+ display: "block",
346
+ fontSize: 11,
347
+ color: "#71717a",
348
+ fontFamily: "system-ui, sans-serif",
349
+ marginBottom: 4
350
+ },
351
+ children: ["Message ", /* @__PURE__ */ jsx("span", {
352
+ style: { color: "#52525b" },
353
+ children: "(optional)"
354
+ })]
355
+ }), /* @__PURE__ */ jsx(Textarea, {
356
+ ref: textareaRef,
357
+ value: generalComment,
358
+ onChange: (e) => setGeneralComment(e.target.value),
359
+ onKeyDown: handleKeyDown,
360
+ placeholder: "e.g. Left the comments below, please address",
361
+ rows: 2,
362
+ disabled
363
+ })] }),
364
+ status === "error" && errorMsg && /* @__PURE__ */ jsx("p", {
365
+ style: {
366
+ margin: 0,
367
+ fontSize: 11,
368
+ color: "#f87171",
369
+ fontFamily: "system-ui, sans-serif"
370
+ },
371
+ children: errorMsg
372
+ }),
373
+ /* @__PURE__ */ jsxs("div", {
374
+ style: {
375
+ display: "flex",
376
+ justifyContent: "flex-end",
377
+ gap: 6
378
+ },
379
+ children: [/* @__PURE__ */ jsx(Button, {
380
+ variant: "secondary",
381
+ onClick: onDone,
382
+ disabled: sending,
383
+ children: "Cancel"
384
+ }), /* @__PURE__ */ jsx(Button, {
385
+ variant: "primary",
386
+ onClick: handleSend,
387
+ disabled,
388
+ children: sending ? "Sending…" : "Send"
389
+ })]
390
+ })
391
+ ]
392
+ });
393
+ }
394
+
395
+ //#endregion
396
+ //#region src/CommentsMenu.tsx
397
+ function CommentRow({ comment, onDelete, onUpdate }) {
398
+ const [hovered, setHovered] = useState(false);
399
+ const [editing, setEditing] = useState(false);
400
+ const [draft, setDraft] = useState(comment.comment);
401
+ const textareaRef = useRef(null);
402
+ useEffect(() => {
403
+ if (!editing) setDraft(comment.comment);
404
+ }, [comment.comment, editing]);
405
+ const startEditing = () => {
406
+ setDraft(comment.comment);
407
+ setEditing(true);
408
+ requestAnimationFrame(() => {
409
+ const el = textareaRef.current;
410
+ if (!el) return;
411
+ el.focus();
412
+ el.selectionStart = el.selectionEnd = el.value.length;
413
+ });
414
+ };
415
+ const handleSave = () => {
416
+ const trimmed = draft.trim();
417
+ if (trimmed && trimmed !== comment.comment) onUpdate(trimmed);
418
+ setEditing(false);
419
+ };
420
+ const handleCancel = () => {
421
+ setDraft(comment.comment);
422
+ setEditing(false);
423
+ };
424
+ const handleKeyDown = (e) => {
425
+ if (e.key === "Escape") {
426
+ e.preventDefault();
427
+ handleCancel();
428
+ } else if (e.key === "Enter" && !e.shiftKey) {
429
+ e.preventDefault();
430
+ handleSave();
431
+ }
432
+ };
433
+ return /* @__PURE__ */ jsxs("div", {
434
+ style: {
435
+ padding: "6px 12px",
436
+ background: hovered ? "rgba(255,255,255,0.03)" : "transparent",
437
+ transition: "background 0.1s"
438
+ },
439
+ onMouseEnter: () => setHovered(true),
440
+ onMouseLeave: () => setHovered(false),
441
+ children: [/* @__PURE__ */ jsxs("div", {
442
+ style: {
443
+ display: "flex",
444
+ alignItems: "flex-start",
445
+ justifyContent: "space-between",
446
+ gap: 8
447
+ },
448
+ children: [/* @__PURE__ */ jsxs("span", {
449
+ style: {
450
+ fontSize: 11,
451
+ fontFamily: "ui-monospace, monospace",
452
+ color: "#3b82f6",
453
+ flexShrink: 0
454
+ },
455
+ children: [
456
+ comment.filePath,
457
+ ":",
458
+ comment.lineNumber
459
+ ]
460
+ }), hovered && !editing && /* @__PURE__ */ jsx(IconButton, {
461
+ onClick: onDelete,
462
+ title: "Remove comment",
463
+ style: {
464
+ padding: 0,
465
+ flexShrink: 0
466
+ },
467
+ children: /* @__PURE__ */ jsx(XIcon, { size: 12 })
468
+ })]
469
+ }), editing ? /* @__PURE__ */ jsxs("div", {
470
+ style: { marginTop: 4 },
471
+ children: [/* @__PURE__ */ jsx(Textarea, {
472
+ ref: textareaRef,
473
+ value: draft,
474
+ onChange: (e) => setDraft(e.target.value),
475
+ onKeyDown: handleKeyDown,
476
+ rows: 3
477
+ }), /* @__PURE__ */ jsxs("div", {
478
+ style: {
479
+ display: "flex",
480
+ justifyContent: "flex-end",
481
+ gap: 6,
482
+ marginTop: 4
483
+ },
484
+ children: [/* @__PURE__ */ jsx(Button, {
485
+ variant: "secondary",
486
+ onClick: handleCancel,
487
+ style: {
488
+ height: 24,
489
+ fontSize: 11
490
+ },
491
+ children: "Cancel"
492
+ }), /* @__PURE__ */ jsx(Button, {
493
+ variant: "primary",
494
+ onClick: handleSave,
495
+ disabled: !draft.trim(),
496
+ style: {
497
+ height: 24,
498
+ fontSize: 11
499
+ },
500
+ children: "Save"
501
+ })]
502
+ })]
503
+ }) : /* @__PURE__ */ jsx("p", {
504
+ onClick: startEditing,
505
+ title: "Click to edit",
506
+ style: {
507
+ margin: "3px 0 0",
508
+ fontSize: 12,
509
+ color: "#d4d4d8",
510
+ fontFamily: "system-ui, sans-serif",
511
+ lineHeight: 1.4,
512
+ whiteSpace: "pre-wrap",
513
+ wordBreak: "break-word",
514
+ cursor: "text"
515
+ },
516
+ children: comment.comment
517
+ })]
518
+ });
519
+ }
520
+ function CommentsMenu({ onClose }) {
521
+ const root = useProjectRoot();
522
+ const comments = useCommentEntries();
523
+ const { clearAllComments, removeComment, updateComment } = useCommentsActions();
524
+ const [copyFeedback, setCopyFeedback] = useState(false);
525
+ const [showSendForm, setShowSendForm] = useState(false);
526
+ useEffect(() => {
527
+ if (!comments.length) setShowSendForm(false);
528
+ }, [comments.length]);
529
+ const handleCopy = () => {
530
+ if (!comments.length) return;
531
+ const text = comments.map((c) => formatCommentNote(c.filePath, c.lineNumber, c.comment)).join("\n\n");
532
+ navigator.clipboard.writeText(text).then(() => {
533
+ setCopyFeedback(true);
534
+ setTimeout(() => setCopyFeedback(false), 1500);
535
+ }).catch(() => {});
536
+ };
537
+ const handleClear = () => {
538
+ clearAllComments();
539
+ onClose();
540
+ };
541
+ return /* @__PURE__ */ jsxs("div", {
542
+ style: {
543
+ width: 320,
544
+ overflow: "hidden"
545
+ },
546
+ onClick: (e) => e.stopPropagation(),
547
+ children: [
548
+ /* @__PURE__ */ jsx(PanelHeader, {
549
+ title: /* @__PURE__ */ jsxs(Fragment, { children: ["Comments", comments.length > 0 && /* @__PURE__ */ jsxs("span", {
550
+ style: {
551
+ marginLeft: 6,
552
+ fontSize: 11,
553
+ color: "#71717a",
554
+ fontWeight: 400
555
+ },
556
+ children: [
557
+ "(",
558
+ comments.length,
559
+ ")"
560
+ ]
561
+ })] }),
562
+ actionsRender: /* @__PURE__ */ jsx(IconButton, {
563
+ onClick: onClose,
564
+ title: "Close",
565
+ children: /* @__PURE__ */ jsx(XIcon, {})
566
+ })
567
+ }),
568
+ comments.length === 0 ? /* @__PURE__ */ jsx("div", {
569
+ style: {
570
+ padding: "16px 12px",
571
+ fontSize: 12,
572
+ lineHeight: 1.5,
573
+ color: "#97979b",
574
+ fontFamily: "system-ui, sans-serif",
575
+ textAlign: "center",
576
+ textWrap: "balance"
577
+ },
578
+ children: "No comments yet. Inspect an element and click \"Add comment\"."
579
+ }) : /* @__PURE__ */ jsx("div", {
580
+ style: {
581
+ maxHeight: 280,
582
+ overflowY: "auto",
583
+ paddingBlock: 6
584
+ },
585
+ onKeyDown: (e) => e.stopPropagation(),
586
+ children: comments.map((c) => /* @__PURE__ */ jsx(CommentRow, {
587
+ comment: c,
588
+ onDelete: () => removeComment(c.id),
589
+ onUpdate: (text) => updateComment({
590
+ id: c.id,
591
+ text
592
+ })
593
+ }, c.id))
594
+ }),
595
+ comments.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { style: {
596
+ borderTop: "1px solid #27272a",
597
+ margin: "4px 0 0"
598
+ } }), showSendForm ? /* @__PURE__ */ jsx(SendToOpencodeForm, {
599
+ root,
600
+ comments,
601
+ onClearComments: clearAllComments,
602
+ onDone: () => {
603
+ setShowSendForm(false);
604
+ onClose();
605
+ }
606
+ }) : /* @__PURE__ */ jsxs("div", {
607
+ style: { padding: "4px 6px" },
608
+ children: [
609
+ /* @__PURE__ */ jsxs(DropdownMenu.Item, {
610
+ closeOnClick: false,
611
+ onClick: () => setShowSendForm(true),
612
+ children: [/* @__PURE__ */ jsx(OpencodeIcon, { size: 13 }), "Send to OpenCode"]
613
+ }),
614
+ /* @__PURE__ */ jsxs(DropdownMenu.Item, {
615
+ closeOnClick: false,
616
+ onClick: handleCopy,
617
+ children: [/* @__PURE__ */ jsx(ClipboardIcon, {}), copyFeedback ? "Copied!" : "Copy to clipboard"]
618
+ }),
619
+ /* @__PURE__ */ jsxs(DropdownMenu.Item, {
620
+ style: { color: "#ef4444" },
621
+ onClick: handleClear,
622
+ children: [/* @__PURE__ */ jsx(TrashIcon, {}), "Clear all"]
623
+ })
624
+ ]
625
+ })] })
626
+ ]
627
+ });
628
+ }
629
+
630
+ //#endregion
631
+ //#region src/index.tsx
632
+ function CommentsToolbarIcon({ count }) {
633
+ return /* @__PURE__ */ jsxs("span", {
634
+ style: {
635
+ position: "relative",
636
+ display: "flex",
637
+ alignItems: "center",
638
+ justifyContent: "center"
639
+ },
640
+ children: [/* @__PURE__ */ jsx(ChatBubbleIcon, {}), count > 0 && /* @__PURE__ */ jsx("span", {
641
+ style: {
642
+ position: "absolute",
643
+ top: -6,
644
+ right: -6,
645
+ background: "#ef4444",
646
+ color: "#fff",
647
+ fontSize: 9,
648
+ fontWeight: 700,
649
+ fontFamily: "system-ui, sans-serif",
650
+ lineHeight: 1,
651
+ minWidth: 14,
652
+ height: 14,
653
+ borderRadius: 7,
654
+ display: "flex",
655
+ alignItems: "center",
656
+ justifyContent: "center",
657
+ padding: "0 3px",
658
+ boxSizing: "border-box",
659
+ pointerEvents: "none"
660
+ },
661
+ children: count > 99 ? "99+" : count
662
+ })]
663
+ });
664
+ }
665
+ function CommentsToolbar() {
666
+ const comments = useCommentEntries();
667
+ const portalContainer = useWidgetPortalContainer();
668
+ const deactivateInspector = useDeactivateInspector();
669
+ const buttonRef = useRef(null);
670
+ const [isOpen, setIsOpen] = useState(false);
671
+ const count = comments.length;
672
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
673
+ /* @__PURE__ */ jsx(Tooltip, {
674
+ label: "Comments",
675
+ container: portalContainer,
676
+ render: /* @__PURE__ */ jsx(ToolbarButton, { ref: buttonRef }),
677
+ "aria-label": "Comments",
678
+ onClick: () => {
679
+ deactivateInspector();
680
+ setIsOpen((open) => !open);
681
+ },
682
+ children: /* @__PURE__ */ jsx(CommentsToolbarIcon, { count })
683
+ }),
684
+ /* @__PURE__ */ jsx(DropdownMenu.Root, {
685
+ open: isOpen,
686
+ onOpenChange: setIsOpen,
687
+ children: /* @__PURE__ */ jsx(DropdownMenu.Portal, {
688
+ container: portalContainer,
689
+ children: /* @__PURE__ */ jsx(DropdownMenu.Positioner, {
690
+ anchor: buttonRef.current,
691
+ side: "top",
692
+ align: "end",
693
+ sideOffset: 8,
694
+ collisionPadding: 8,
695
+ positionMethod: "fixed",
696
+ style: {
697
+ zIndex: 9999999,
698
+ pointerEvents: "auto"
699
+ },
700
+ children: /* @__PURE__ */ jsx(DropdownMenu.Popup, { children: /* @__PURE__ */ jsx(CommentsMenu, { onClose: () => setIsOpen(false) }) })
701
+ })
702
+ })
703
+ }),
704
+ /* @__PURE__ */ jsx(CommentEditorOverlay, {})
705
+ ] });
706
+ }
707
+ function CommentsPlugin() {
708
+ return {
709
+ name: "comments",
710
+ toolbar: CommentsToolbar
711
+ };
712
+ }
713
+
714
+ //#endregion
715
+ export { CommentsPlugin };
716
+ //# sourceMappingURL=index.js.map