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