proto-comments 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,934 @@
1
+ "use client";
2
+
3
+ // src/index.jsx
4
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
5
+ import { createPortal } from "react-dom";
6
+
7
+ // src/store.js
8
+ import { useSyncExternalStore } from "react";
9
+
10
+ // src/anchors.js
11
+ var ROOT_SEL = (el) => el.id ? `#${CSS.escape(el.id)}` : el.dataset?.cid ? `[data-cid="${el.dataset.cid}"]` : null;
12
+ var segmenter = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter() : null;
13
+ var cut = (s, n) => {
14
+ if (s.length <= n || !segmenter) return s.slice(0, n);
15
+ let out = "";
16
+ for (const { segment } of segmenter.segment(s)) {
17
+ if (out.length + segment.length > n) break;
18
+ out += segment;
19
+ }
20
+ return out;
21
+ };
22
+ var textOf = (el) => cut((el.innerText || el.getAttribute?.("aria-label") || "").replace(/\s+/g, " ").trim(), 60);
23
+ function selectorFor(el) {
24
+ const parts = [];
25
+ for (let n = el; n && n !== document.body; n = n.parentElement) {
26
+ const root = ROOT_SEL(n);
27
+ if (root) {
28
+ parts.unshift(root);
29
+ break;
30
+ }
31
+ const tag = n.tagName.toLowerCase();
32
+ const cls = [...n.classList].find((c) => !c.startsWith("pc-"));
33
+ const same = n.parentElement ? [...n.parentElement.children].filter((c) => c.tagName === n.tagName) : [];
34
+ parts.unshift(`${tag}${cls ? "." + CSS.escape(cls) : ""}${same.length > 1 ? `:nth-of-type(${same.indexOf(n) + 1})` : ""}`);
35
+ }
36
+ return parts.join(" > ");
37
+ }
38
+ var HEADING = 'h1, h2, h3, h4, h5, h6, legend, [class*="title"], label';
39
+ function blockFor(el, extra = "") {
40
+ const sel = extra ? `${extra}, ${HEADING}` : HEADING;
41
+ if (el.matches(sel) || el.closest(extra ? `${extra}, h1, h2, h3, h4, h5, h6, legend` : "h1, h2, h3, h4, h5, h6, legend")) return "";
42
+ const own = textOf(el);
43
+ const before = (h) => h.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING;
44
+ for (let n = el.parentElement; n && n !== document.body; n = n.parentElement) {
45
+ const h = [...n.querySelectorAll(sel)].find((h2) => before(h2) && !h2.closest(".pc-root") && textOf(h2) && textOf(h2) !== own);
46
+ if (h) return textOf(h);
47
+ if (n.tagName === "MAIN") break;
48
+ }
49
+ return "";
50
+ }
51
+ var placeOf = (a) => [...new Set([a.block, a.text].filter(Boolean))].join(" \u203A ") || `<${a.tag}>`;
52
+ function anchorFor(el, x, y, headingSelector) {
53
+ const r = el.getBoundingClientRect();
54
+ return {
55
+ selector: selectorFor(el),
56
+ text: textOf(el),
57
+ block: blockFor(el, headingSelector),
58
+ tag: el.tagName.toLowerCase(),
59
+ offset: { x: r.width ? (x - r.left) / r.width : 0, y: r.height ? (y - r.top) / r.height : 0 }
60
+ };
61
+ }
62
+ function findAnchor(a) {
63
+ let el = null;
64
+ try {
65
+ el = document.querySelector(a.selector);
66
+ } catch {
67
+ }
68
+ if (el && (!a.text || textOf(el) === a.text)) return el;
69
+ if (a.text) {
70
+ const same = [...document.querySelectorAll(a.tag)].filter((n) => !n.closest(".pc-root") && textOf(n) === a.text);
71
+ if (same.length === 1) return same[0];
72
+ }
73
+ return el;
74
+ }
75
+
76
+ // src/store.js
77
+ var state = { list: [], mode: false, historyOpen: false, openId: null, draft: null, flashId: null, bornId: null, filter: "all" };
78
+ var subs = /* @__PURE__ */ new Set();
79
+ var getC = () => state;
80
+ var setC = (patch) => {
81
+ state = { ...state, ...patch };
82
+ subs.forEach((f) => f());
83
+ };
84
+ var subscribe = (f) => {
85
+ subs.add(f);
86
+ return () => subs.delete(f);
87
+ };
88
+ var useC = (sel) => useSyncExternalStore(subscribe, () => sel(state), () => sel(state));
89
+ var GO_KEY = "proto-comments:go";
90
+ var DEFAULTS = {
91
+ endpoint: "/api/comments",
92
+ author: "Reviewer",
93
+ getScreen: () => ({ key: location.pathname + location.hash, label: location.pathname + location.hash }),
94
+ getSnapshot: () => ({ url: location.href }),
95
+ restoreSnapshot: (snap) => {
96
+ if (!snap?.url || snap.url === location.href) return;
97
+ try {
98
+ sessionStorage.setItem(GO_KEY, pendingId);
99
+ } catch {
100
+ }
101
+ location.assign(snap.url);
102
+ return "navigating";
103
+ },
104
+ getContext: () => "",
105
+ headingSelector: ""
106
+ };
107
+ var cfg = DEFAULTS;
108
+ var pendingId = null;
109
+ var setConfig = (c) => {
110
+ cfg = c;
111
+ };
112
+ var getConfig = () => cfg;
113
+ async function api(method, body) {
114
+ const res = await fetch(cfg.endpoint, { method, headers: { "content-type": "application/json" }, body: body && JSON.stringify(body) });
115
+ if (!res.ok) throw new Error(`proto-comments: ${method} ${cfg.endpoint} \u2192 ${res.status}`);
116
+ return res.json();
117
+ }
118
+ async function loadComments() {
119
+ const list = await api("GET");
120
+ if (JSON.stringify(list) !== JSON.stringify(state.list)) setC({ list });
121
+ }
122
+ async function addComment(anchor, text) {
123
+ const screen = cfg.getScreen();
124
+ const c = await api("POST", { screen: screen.key, screenLabel: screen.label, context: cfg.getContext(), snapshot: cfg.getSnapshot(), anchor, text, author: cfg.author });
125
+ setC({ list: [...state.list, c], draft: null, bornId: c.id });
126
+ setTimeout(() => {
127
+ if (state.bornId === c.id) setC({ bornId: null });
128
+ }, 400);
129
+ }
130
+ async function patchComment(id, patch) {
131
+ const c = await api("PATCH", { id, author: cfg.author, ...patch });
132
+ setC({ list: state.list.map((x) => x.id === id ? c : x) });
133
+ }
134
+ async function markComments(ids, queued) {
135
+ setC({ list: await api("PATCH", { ids, queued }) });
136
+ }
137
+ async function deleteComment(id) {
138
+ await api("DELETE", { id });
139
+ setC({ list: state.list.filter((x) => x.id !== id), openId: null });
140
+ }
141
+ function reveal(c) {
142
+ const smooth = !matchMedia("(prefers-reduced-motion: reduce)").matches;
143
+ findAnchor(c.anchor)?.scrollIntoView({ block: "center", behavior: smooth ? "smooth" : "auto" });
144
+ setC({ openId: c.id, flashId: c.id });
145
+ setTimeout(() => setC({ flashId: null }), 1600);
146
+ }
147
+ async function goToComment(c) {
148
+ const onScreen = cfg.getScreen().key === c.screen && findAnchor(c.anchor);
149
+ setC({ openId: null, historyOpen: false });
150
+ if (onScreen) return reveal(c);
151
+ pendingId = c.id;
152
+ if (await cfg.restoreSnapshot(c.snapshot, c) === "navigating") return;
153
+ setTimeout(() => reveal(c), 450);
154
+ }
155
+ function resumePendingGo() {
156
+ let id = null;
157
+ try {
158
+ id = sessionStorage.getItem(GO_KEY);
159
+ sessionStorage.removeItem(GO_KEY);
160
+ } catch {
161
+ }
162
+ const c = id && state.list.find((x) => x.id === id);
163
+ if (c) setTimeout(() => reveal(c), 450);
164
+ }
165
+
166
+ // src/ui.jsx
167
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
168
+
169
+ // src/icons.jsx
170
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
171
+ var PATHS = {
172
+ comment: /* @__PURE__ */ jsx("path", { d: "M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" }),
173
+ history: /* @__PURE__ */ jsxs(Fragment, { children: [
174
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
175
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" }),
176
+ /* @__PURE__ */ jsx("path", { d: "M12 7v5l4 2" })
177
+ ] }),
178
+ x: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }),
179
+ check: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }),
180
+ reopen: /* @__PURE__ */ jsxs(Fragment, { children: [
181
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
182
+ /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
183
+ ] }),
184
+ agent: /* @__PURE__ */ jsx("path", { d: "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" }),
185
+ trash: /* @__PURE__ */ jsxs(Fragment, { children: [
186
+ /* @__PURE__ */ jsx("path", { d: "M10 11v6M14 11v6M3 6h18" }),
187
+ /* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
188
+ /* @__PURE__ */ jsx("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
189
+ ] })
190
+ };
191
+ function Icon({ name }) {
192
+ return /* @__PURE__ */ jsx("svg", { className: "pc-i", viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: PATHS[name] });
193
+ }
194
+ function Grip() {
195
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: [5, 12, 19].map((y) => [9, 15].map((x) => /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r: "1.75" }, `${x}${y}`))) });
196
+ }
197
+
198
+ // src/ui.jsx
199
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
200
+ var nameOf = (a) => a.text || a.block || `<${a.tag}>`;
201
+ var when = (iso) => {
202
+ const d = new Date(iso);
203
+ if (Number.isNaN(d.getTime())) return "";
204
+ const year = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear() ? void 0 : "numeric";
205
+ return d.toLocaleString("en-US", { year, month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
206
+ };
207
+ var byline = (author, iso) => [author, when(iso)].filter(Boolean).join(" \xB7 ");
208
+ function usePop(x, y, lost) {
209
+ const ref = useRef(null);
210
+ const [h, setH] = useState(0);
211
+ useLayoutEffect(() => {
212
+ const el = ref.current;
213
+ setH(el.offsetHeight);
214
+ const ro = new ResizeObserver(() => setH(el.offsetHeight));
215
+ ro.observe(el);
216
+ return () => ro.disconnect();
217
+ }, []);
218
+ const w = Math.min(320, window.innerWidth - 16);
219
+ const left = Math.max(8, Math.min(x + 18, window.innerWidth - w - 8));
220
+ const top = Math.max(8, Math.min(y - 8, window.innerHeight - h - 8));
221
+ return { ref, style: { left, top, transformOrigin: lost ? "center top" : `${x - left}px ${y - top}px` } };
222
+ }
223
+ var reducedMotion = () => matchMedia("(prefers-reduced-motion: reduce)").matches;
224
+ var MAX_TEXT = 5e3;
225
+ function Field({ placeholder, submit, onCancel, label, bare }) {
226
+ const [text, setText] = useState("");
227
+ const [busy, setBusy] = useState(false);
228
+ const go = async () => {
229
+ if (!text.trim() || busy) return;
230
+ setBusy(true);
231
+ try {
232
+ await submit(text.trim());
233
+ setText("");
234
+ } finally {
235
+ setBusy(false);
236
+ }
237
+ };
238
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
239
+ /* @__PURE__ */ jsx2(
240
+ "textarea",
241
+ {
242
+ className: "pc-input",
243
+ autoFocus: true,
244
+ rows: 3,
245
+ maxLength: MAX_TEXT,
246
+ placeholder,
247
+ value: text,
248
+ onChange: (e) => setText(e.target.value),
249
+ onKeyDown: (e) => {
250
+ if (e.key === "Enter" && !e.shiftKey) {
251
+ e.preventDefault();
252
+ go();
253
+ }
254
+ }
255
+ }
256
+ ),
257
+ !bare && /* @__PURE__ */ jsxs2("div", { className: "pc-actions", children: [
258
+ onCancel && /* @__PURE__ */ jsx2("button", { className: "pc-btn", onClick: onCancel, children: "Cancel" }),
259
+ /* @__PURE__ */ jsx2("button", { className: "pc-btn primary", disabled: !text.trim() || busy, onClick: go, children: label })
260
+ ] })
261
+ ] });
262
+ }
263
+ function Draft({ draft }) {
264
+ const pop = usePop(draft.x, draft.y);
265
+ return /* @__PURE__ */ jsxs2("div", { className: "pc-pop", ref: pop.ref, style: pop.style, children: [
266
+ /* @__PURE__ */ jsx2("div", { className: "pc-head", children: /* @__PURE__ */ jsx2("b", { "data-tip": placeOf(draft.anchor), children: nameOf(draft.anchor) }) }),
267
+ /* @__PURE__ */ jsx2("div", { className: "pc-pane", children: /* @__PURE__ */ jsx2(Field, { placeholder: "Leave a comment", label: "Comment", submit: (text) => addComment(draft.anchor, text), onCancel: () => setC({ draft: null }) }) })
268
+ ] });
269
+ }
270
+ var resolvedOf = (c) => ({ status: c.status === "resolved" ? "open" : "resolved" });
271
+ var toggleResolved = (c) => patchComment(c.id, resolvedOf(c));
272
+ var toggleQueued = (c) => markComments([c.id], !c.queued);
273
+ function Thread({ c, x, y, lost }) {
274
+ const resolved = c.status === "resolved";
275
+ const pop = usePop(x, y, lost);
276
+ const msgs = useRef(null);
277
+ useLayoutEffect(() => {
278
+ msgs.current.scrollTop = msgs.current.scrollHeight;
279
+ }, [c.replies.length]);
280
+ return /* @__PURE__ */ jsxs2("div", { className: "pc-pop", ref: pop.ref, style: pop.style, children: [
281
+ /* @__PURE__ */ jsxs2("div", { className: "pc-head", children: [
282
+ /* @__PURE__ */ jsx2("b", { "data-tip": placeOf(c.anchor), children: nameOf(c.anchor) }),
283
+ c.queued && /* @__PURE__ */ jsx2("i", { className: "pc-tag", children: "For Agent" }),
284
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-trash", onClick: () => deleteComment(c.id), "aria-label": "Delete comment", "data-tip": "Delete", children: /* @__PURE__ */ jsx2(Icon, { name: "trash" }) }),
285
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn", onClick: () => setC({ openId: null }), "aria-label": "Close", "data-tip": "Close", children: /* @__PURE__ */ jsx2(Icon, { name: "x" }) })
286
+ ] }),
287
+ /* @__PURE__ */ jsxs2("div", { className: "pc-pane msgs", ref: msgs, children: [
288
+ lost && /* @__PURE__ */ jsx2("div", { className: "pc-lost", children: "The element isn't on this screen any more." }),
289
+ /* @__PURE__ */ jsxs2("div", { className: "pc-msg", children: [
290
+ /* @__PURE__ */ jsx2("div", { className: "pc-meta", children: byline(c.author, c.createdAt) }),
291
+ c.text
292
+ ] }),
293
+ c.replies.map((r, i) => /* @__PURE__ */ jsxs2("div", { className: "pc-msg", children: [
294
+ /* @__PURE__ */ jsx2("div", { className: "pc-meta", children: byline(r.author, r.createdAt) }),
295
+ r.text
296
+ ] }, i)),
297
+ resolved && c.resolvedBy && /* @__PURE__ */ jsx2("div", { className: "pc-meta", children: ["Resolved by " + c.resolvedBy, when(c.resolvedAt)].filter(Boolean).join(" \xB7 ") })
298
+ ] }),
299
+ /* @__PURE__ */ jsx2("div", { className: "pc-pane", children: /* @__PURE__ */ jsx2(Field, { bare: true, placeholder: "Add a comment", submit: (text) => patchComment(c.id, { reply: { author: getConfig().author, text } }) }) }),
300
+ /* @__PURE__ */ jsxs2("div", { className: "pc-actions foot", children: [
301
+ /* @__PURE__ */ jsx2("button", { className: "pc-btn sm tertiary", onClick: () => toggleResolved(c), children: resolved ? "Reopen" : "Resolve" }),
302
+ !resolved && /* @__PURE__ */ jsx2("button", { className: "pc-btn sm primary", onClick: () => toggleQueued(c), children: c.queued ? "Unmark" : "Mark for Agent" })
303
+ ] })
304
+ ] });
305
+ }
306
+ function Pin({ c, x, y }) {
307
+ const open = useC((s) => s.openId === c.id);
308
+ const flash = useC((s) => s.flashId === c.id);
309
+ const born = useC((s) => s.bornId === c.id);
310
+ return /* @__PURE__ */ jsx2(
311
+ "button",
312
+ {
313
+ className: `pc-pin${c.status === "resolved" ? " done" : ""}${c.queued ? " queued" : ""}${open ? " on" : ""}${flash ? " flash" : ""}${born ? " born" : ""}`,
314
+ style: { left: x, top: y },
315
+ onClick: () => setC({ openId: open ? null : c.id, draft: null }),
316
+ "aria-label": c.status === "resolved" ? "Resolved comment" : `Comment ${c.n}`,
317
+ children: c.status === "resolved" ? /* @__PURE__ */ jsx2(Icon, { name: "check" }) : c.n
318
+ }
319
+ );
320
+ }
321
+ var FILTERS = [["all", "All"], ["open", "Open"], ["queued", "For Agent"], ["resolved", "Resolved"]];
322
+ var matches = (c, f) => f === "all" || (f === "queued" ? c.queued && c.status === "open" : c.status === f);
323
+ var groupBy = (items, key) => items.reduce((m, it) => {
324
+ var _a;
325
+ (m[_a = key(it)] || (m[_a] = [])).push(it);
326
+ return m;
327
+ }, {});
328
+ function History() {
329
+ const isOpen = useC((s) => s.historyOpen);
330
+ const list = useC((s) => s.list);
331
+ const filter = useC((s) => s.filter);
332
+ const shown = list.filter((c) => matches(c, filter)).sort((a, b) => (a.status === "resolved") - (b.status === "resolved"));
333
+ const groups = Object.entries(groupBy(shown, (c) => c.screenLabel));
334
+ const count = (f) => list.filter((c) => matches(c, f)).length;
335
+ const [picked, setPicked] = useState([]);
336
+ const selectable = shown.map((c) => c.id);
337
+ const sel = picked.filter((id) => selectable.includes(id));
338
+ const allPicked = selectable.length > 0 && sel.length === selectable.length;
339
+ const toggle = (id) => setPicked((p) => p.includes(id) ? p.filter((x) => x !== id) : [...p, id]);
340
+ useEffect(() => {
341
+ if (!isOpen) setPicked([]);
342
+ }, [isOpen]);
343
+ const [leaving, setLeaving] = useState([]);
344
+ const leave = async (items, next, action) => {
345
+ const ids = items.filter((c) => !next || !matches({ ...c, ...next(c) }, filter)).map((c) => c.id);
346
+ if (ids.length) {
347
+ setLeaving((l) => [...l, ...ids]);
348
+ if (!reducedMotion()) await new Promise((res) => setTimeout(res, 150));
349
+ }
350
+ try {
351
+ await action();
352
+ } finally {
353
+ setLeaving((l) => l.filter((id) => !ids.includes(id)));
354
+ }
355
+ };
356
+ const picks = shown.filter((c) => sel.includes(c.id));
357
+ const open = picks.filter((c) => c.status === "open");
358
+ const allResolved = picks.length > 0 && !open.length;
359
+ const allQueued = open.length > 0 && open.every((c) => c.queued);
360
+ const done = () => setPicked([]);
361
+ const setStatus = (status) => leave(picks, () => ({ status }), async () => {
362
+ for (const c of picks) if (c.status !== status) await patchComment(c.id, { status });
363
+ done();
364
+ });
365
+ const mark = () => leave(open, () => ({ queued: !allQueued }), () => markComments(open.map((c) => c.id), !allQueued).then(done));
366
+ const remove = () => leave(picks, null, async () => {
367
+ for (const c of picks) await deleteComment(c.id);
368
+ done();
369
+ });
370
+ return /* @__PURE__ */ jsxs2("aside", { className: "pc-history", "data-open": isOpen || void 0, "aria-label": "Comments", children: [
371
+ /* @__PURE__ */ jsxs2("header", { children: [
372
+ /* @__PURE__ */ jsx2("b", { children: "Comments" }),
373
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn", onClick: () => setC({ historyOpen: false }), "aria-label": "Close", "data-tip": "Close", children: /* @__PURE__ */ jsx2(Icon, { name: "x" }) })
374
+ ] }),
375
+ /* @__PURE__ */ jsx2("div", { className: "pc-tabs", role: "tablist", children: FILTERS.map(([f, l]) => /* @__PURE__ */ jsxs2("button", { className: "pc-tab", role: "tab", "aria-selected": filter === f, onClick: () => setC({ filter: f }), children: [
376
+ l,
377
+ /* @__PURE__ */ jsx2("span", { children: count(f) })
378
+ ] }, f)) }),
379
+ selectable.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "pc-selbar", children: [
380
+ /* @__PURE__ */ jsxs2("label", { className: "pc-check", children: [
381
+ /* @__PURE__ */ jsx2("input", { type: "checkbox", checked: allPicked, onChange: () => setPicked(allPicked ? [] : selectable) }),
382
+ sel.length ? `${sel.length} selected` : "Select all"
383
+ ] }),
384
+ sel.length > 0 && /* @__PURE__ */ jsxs2("span", { className: "pc-bulk", children: [
385
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn", onClick: () => setStatus(allResolved ? "open" : "resolved"), "aria-label": allResolved ? "Reopen" : "Resolve", "data-tip": allResolved ? "Reopen" : "Resolve", children: /* @__PURE__ */ jsx2(Icon, { name: allResolved ? "reopen" : "check" }) }),
386
+ open.length > 0 && /* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-agent", "aria-pressed": allQueued, onClick: mark, "aria-label": allQueued ? "Unmark" : "Mark for Agent", "data-tip": allQueued ? "Unmark" : "Mark for Agent", children: /* @__PURE__ */ jsx2(Icon, { name: "agent" }) }),
387
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-trash", onClick: remove, "aria-label": "Delete selected", "data-tip": "Delete", children: /* @__PURE__ */ jsx2(Icon, { name: "trash" }) })
388
+ ] })
389
+ ] }),
390
+ /* @__PURE__ */ jsxs2("div", { className: "pc-groups", children: [
391
+ !shown.length && /* @__PURE__ */ jsx2("p", { className: "pc-empty", children: "No comments here. Press C and click an element to leave one." }),
392
+ groups.map(([label, items]) => /* @__PURE__ */ jsx2("section", { children: items.map((c) => /* @__PURE__ */ jsxs2("div", { className: `pc-row${c.status === "resolved" ? " done" : ""}${leaving.includes(c.id) ? " leaving" : ""}`, children: [
393
+ /* @__PURE__ */ jsx2("input", { type: "checkbox", className: "pc-pick", checked: sel.includes(c.id), onChange: () => toggle(c.id), "aria-label": `Select comment ${c.n}` }),
394
+ /* @__PURE__ */ jsxs2("button", { className: "pc-item", onClick: () => goToComment(c), children: [
395
+ /* @__PURE__ */ jsx2("span", { className: `pc-n${c.queued ? " queued" : ""}`, children: c.status === "resolved" ? /* @__PURE__ */ jsx2(Icon, { name: "check" }) : c.n }),
396
+ /* @__PURE__ */ jsxs2("span", { className: "pc-body", children: [
397
+ /* @__PURE__ */ jsx2("span", { className: "pc-text", children: c.text }),
398
+ /* @__PURE__ */ jsxs2("span", { className: "pc-meta", "data-tip": placeOf(c.anchor), children: [
399
+ c.queued && /* @__PURE__ */ jsx2("i", { className: "pc-tag", children: "For Agent" }),
400
+ /* @__PURE__ */ jsx2("span", { children: [nameOf(c.anchor), c.context, when(c.createdAt), c.replies.length && `${c.replies.length + 1} comments`].filter(Boolean).join(" \xB7 ") })
401
+ ] }),
402
+ c.status === "resolved" && c.resolvedBy && /* @__PURE__ */ jsxs2("span", { className: "pc-meta", children: [
403
+ "Resolved by ",
404
+ c.resolvedBy
405
+ ] })
406
+ ] })
407
+ ] }),
408
+ /* @__PURE__ */ jsxs2("span", { className: "pc-row-actions", children: [
409
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn", onClick: () => leave([c], resolvedOf, () => toggleResolved(c)), "aria-label": c.status === "resolved" ? "Reopen" : "Resolve", "data-tip": c.status === "resolved" ? "Reopen" : "Resolve", children: /* @__PURE__ */ jsx2(Icon, { name: c.status === "resolved" ? "reopen" : "check" }) }),
410
+ c.status === "open" && /* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-agent", "aria-pressed": !!c.queued, onClick: () => leave([c], (x) => ({ queued: !x.queued }), () => toggleQueued(c)), "aria-label": c.queued ? "Unmark" : "Mark for Agent", "data-tip": c.queued ? "Unmark" : "Mark for Agent", children: /* @__PURE__ */ jsx2(Icon, { name: "agent" }) }),
411
+ /* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-trash", onClick: () => leave([c], null, () => deleteComment(c.id)), "aria-label": "Delete comment", "data-tip": "Delete", children: /* @__PURE__ */ jsx2(Icon, { name: "trash" }) })
412
+ ] })
413
+ ] }, c.id)) }, label))
414
+ ] })
415
+ ] });
416
+ }
417
+ var POS_KEY = "proto-comments:toolbar";
418
+ function Toolbar() {
419
+ const mode = useC((c) => c.mode);
420
+ const historyOpen = useC((c) => c.historyOpen);
421
+ const open = useC((c) => c.list.filter((x) => x.status === "open").length);
422
+ const [pos, setPos] = useState(null);
423
+ const drag = useRef(null);
424
+ useEffect(() => {
425
+ try {
426
+ const p = JSON.parse(localStorage.getItem(POS_KEY));
427
+ if (p) setPos(p);
428
+ } catch {
429
+ }
430
+ }, []);
431
+ const onDown = (e) => {
432
+ const r = e.currentTarget.parentElement.getBoundingClientRect();
433
+ drag.current = { dx: e.clientX - r.left, dy: e.clientY - r.top, w: r.width, h: r.height };
434
+ e.currentTarget.setPointerCapture(e.pointerId);
435
+ };
436
+ const onMove = (e) => {
437
+ const d = drag.current;
438
+ if (!d) return;
439
+ setPos({
440
+ x: Math.max(8, Math.min(e.clientX - d.dx, window.innerWidth - d.w - 8)),
441
+ y: Math.max(8, Math.min(e.clientY - d.dy, window.innerHeight - d.h - 8))
442
+ });
443
+ };
444
+ const onUp = () => {
445
+ drag.current = null;
446
+ try {
447
+ localStorage.setItem(POS_KEY, JSON.stringify(pos));
448
+ } catch {
449
+ }
450
+ };
451
+ return /* @__PURE__ */ jsxs2("div", { className: "pc-bar", "data-tips": pos && pos.y < 48 ? "below" : void 0, style: pos ? { left: pos.x, top: pos.y, bottom: "auto", transform: "none" } : void 0, role: "toolbar", "aria-label": "Comments", children: [
452
+ /* @__PURE__ */ jsx2("span", { className: "pc-grip", onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, "aria-hidden": "true", children: /* @__PURE__ */ jsx2(Grip, {}) }),
453
+ /* @__PURE__ */ jsxs2("button", { "aria-pressed": mode, onClick: () => setC({ mode: !getC().mode, draft: null, openId: null }), "aria-label": mode ? "Stop commenting" : "Comment", "aria-keyshortcuts": mode ? "V" : "C", children: [
454
+ /* @__PURE__ */ jsx2(Icon, { name: "comment" }),
455
+ /* @__PURE__ */ jsxs2("span", { className: "pc-tip", "aria-hidden": "true", children: [
456
+ mode ? "Stop commenting" : "Comment",
457
+ /* @__PURE__ */ jsx2("kbd", { children: mode ? "V" : "C" })
458
+ ] })
459
+ ] }),
460
+ /* @__PURE__ */ jsxs2("button", { "aria-pressed": historyOpen, onClick: () => setC({ historyOpen: !historyOpen }), "aria-label": open ? `History, ${open} open` : "History", "aria-keyshortcuts": "H", children: [
461
+ /* @__PURE__ */ jsx2(Icon, { name: "history" }),
462
+ open > 0 && /* @__PURE__ */ jsx2("i", { className: "pc-dot" }),
463
+ /* @__PURE__ */ jsxs2("span", { className: "pc-tip", "aria-hidden": "true", children: [
464
+ open ? `History \xB7 ${open} open` : "History",
465
+ /* @__PURE__ */ jsx2("kbd", { children: "H" })
466
+ ] })
467
+ ] })
468
+ ] });
469
+ }
470
+ var TIP_DELAY = 300;
471
+ function Tooltip() {
472
+ const [tip, setTip] = useState(null);
473
+ const ref = useRef(null);
474
+ useEffect(() => {
475
+ if (!matchMedia("(hover: hover)").matches) return;
476
+ let el = null, timer, hiddenAt = 0;
477
+ const hide = () => {
478
+ clearTimeout(timer);
479
+ if (el) hiddenAt = Date.now();
480
+ el = null;
481
+ setTip(null);
482
+ };
483
+ const over = (e) => {
484
+ const next = e.target.closest?.(".pc-root [data-tip]");
485
+ if (next === el) return;
486
+ if (!next) return hide();
487
+ const warm = el || Date.now() - hiddenAt < TIP_DELAY;
488
+ clearTimeout(timer);
489
+ el = next;
490
+ timer = setTimeout(() => setTip({ text: next.dataset.tip, rect: next.getBoundingClientRect() }), warm ? 0 : TIP_DELAY);
491
+ };
492
+ document.addEventListener("pointerover", over);
493
+ document.addEventListener("pointerdown", hide, true);
494
+ window.addEventListener("scroll", hide, true);
495
+ return () => {
496
+ clearTimeout(timer);
497
+ document.removeEventListener("pointerover", over);
498
+ document.removeEventListener("pointerdown", hide, true);
499
+ window.removeEventListener("scroll", hide, true);
500
+ };
501
+ }, []);
502
+ useLayoutEffect(() => {
503
+ const node = ref.current;
504
+ if (!tip || !node) return;
505
+ const { rect } = tip;
506
+ const half = node.offsetWidth / 2;
507
+ const below = rect.top - node.offsetHeight - 10 < 8;
508
+ node.style.left = `${Math.max(8 + half, Math.min(rect.left + rect.width / 2, window.innerWidth - 8 - half))}px`;
509
+ if (below) {
510
+ node.dataset.side = "below";
511
+ node.style.top = `${rect.bottom + 10}px`;
512
+ } else node.style.bottom = `${window.innerHeight - rect.top + 10}px`;
513
+ node.getBoundingClientRect();
514
+ node.setAttribute("data-open", "");
515
+ }, [tip]);
516
+ return tip && /* @__PURE__ */ jsx2("span", { ref, className: "pc-tip float", role: "tooltip", children: tip.text }, tip.text + tip.rect.x + tip.rect.y);
517
+ }
518
+
519
+ // src/styles.js
520
+ var styles_default = `
521
+ .pc-root{
522
+ /* surfaces */
523
+ --pc-popover:#FFFFFF; /* component-on-sunken: Popover */
524
+ --pc-container:#FFFFFF; /* container: Drawer */
525
+ --pc-field:#FFFFFF; /* component-on-container-strong: Textarea, secondary Button, active Tab */
526
+ --pc-field-hover:#F9F9F9;
527
+ --pc-component:#0000000f; /* component: Tabs track, Badge, Kbd */
528
+ --pc-hover:#0000000f; /* transparent-hover */
529
+ --pc-active:#0000001f;
530
+ --pc-row-hover:#F0F0F0; /* --pc-hover over the container, as a solid */
531
+ --pc-dark:#202020; /* standout: toolbar, Tooltip */
532
+ --pc-inverted:#000000f2; --pc-inverted-hover:#000000cc; --pc-inverted-active:#000000b3; --pc-on-inverted:#FFFFFF;
533
+ /* text */
534
+ --pc-text-strong:#000000df; /* primary-foreground */
535
+ --pc-text:#0000009b; /* secondary-foreground */
536
+ --pc-faint:#00000072; /* tertiary-foreground */
537
+ --pc-disabled:#00000044;
538
+ /* lines */
539
+ --pc-border:#0000000f; /* border-default */
540
+ --pc-border-strong:#00000026;
541
+ --pc-tab-border:#D9D9D9; /* gray-6: Tabs indicator */
542
+ /* accents */
543
+ --pc-primary:#0090FF; --pc-primary-hover:#0588F0; --pc-primary-soft:#008FF519; --pc-primary-text:#0D74CE;
544
+ --pc-focus:#0080FF8c; /* brand-border-focus */
545
+ --pc-danger:#C12400cc; --pc-danger-solid:#E54D2E; --pc-danger-solid-hover:#DD4425; --pc-danger-solid-active:#D13415;
546
+ --pc-danger-soft:#FF200008; --pc-danger-soft-hover:#F52B0018; --pc-danger-soft-active:#FF35003a;
547
+ --pc-warn:#CC4E00; --pc-warn-soft:#FF9C0026;
548
+ --pc-agent:#6E56CF; --pc-agent-soft:#3201FF1a; --pc-agent-text:#6550B9;
549
+ --pc-done:#BBBBBB;
550
+ /* shadows: s-default, s-extra-large */
551
+ --pc-shadow-sm:0 0 0 .5px #0000000f, 0 1px 1px -1px #0000001a, 0 1px 2px 0 #0000000d;
552
+ --pc-shadow:0 0 0 .5px #0000000f, 0 8px 40px 0 #0000000d, 0 12px 32px -16px #00000026;
553
+ --pc-z:2147483000;
554
+ font:400 13px/20px InterVariable, Inter, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-feature-settings:"ss03";
555
+ color:var(--pc-text-strong); -webkit-font-smoothing:antialiased;
556
+ }
557
+ :root.dark .pc-root, .dark .pc-root, .pc-root.dark{
558
+ --pc-popover:#2A2A2A; --pc-container:#111111; --pc-field:#ffffff12; --pc-field-hover:#ffffff1b;
559
+ --pc-component:#ffffff12; --pc-hover:#ffffff1b; --pc-active:#ffffff22; --pc-row-hover:#282828;
560
+ --pc-dark:#222222;
561
+ --pc-inverted:#fffffff2; --pc-inverted-hover:#ffffffcc; --pc-inverted-active:#ffffffb3; --pc-on-inverted:#000000;
562
+ --pc-text-strong:#ffffffed; --pc-text:#ffffffaf; --pc-faint:#ffffff64; --pc-disabled:#ffffff55;
563
+ --pc-border:#ffffff12; --pc-border-strong:#ffffff2c; --pc-tab-border:#484848;
564
+ --pc-primary:#0090FF; --pc-primary-hover:#3B9EFF; --pc-primary-soft:#0077FF3a; --pc-primary-text:#70B8FF; --pc-focus:#3B94FFf7;
565
+ --pc-danger:#FF7A57d9; --pc-danger-solid:#E54D2E; --pc-danger-solid-hover:#EC6142; --pc-danger-solid-active:#FF977D;
566
+ --pc-danger-soft:#FF60390d; --pc-danger-soft-hover:#FF482D26; --pc-danger-soft-active:#FF6F4F4a;
567
+ --pc-warn:#FFA057; --pc-warn-soft:#FF7C0021;
568
+ --pc-agent-soft:#7A5BFF26; --pc-agent-text:#BAA7FF; --pc-done:#606060;
569
+ --pc-shadow-sm:0 0 0 .5px #ffffff12 inset;
570
+ --pc-shadow:0 0 0 .5px #ffffff12 inset, 0 8px 40px 0 #0000000d, 0 12px 32px -16px #00000026;
571
+ }
572
+ .pc-root *, .pc-root *::before, .pc-root *::after{ box-sizing:border-box; }
573
+ /* :where() keeps these resets weaker than the component classes below */
574
+ :where(.pc-root) button{ font:inherit; color:inherit; cursor:pointer; }
575
+ :where(.pc-root) :is(textarea, input){ font:inherit; color:inherit; }
576
+ .pc-root :focus-visible{ outline:2px solid var(--pc-focus); outline-offset:1px; }
577
+ .pc-i{ flex:none; }
578
+
579
+ html.pc-mode, html.pc-mode *{ cursor:crosshair !important; }
580
+ html.pc-mode .pc-root, html.pc-mode .pc-root *{ cursor:auto !important; }
581
+ html.pc-mode .pc-root button, html.pc-mode .pc-root label{ cursor:pointer !important; }
582
+ html.pc-mode .pc-grip{ cursor:grab !important; }
583
+
584
+ /* Button (int-btn). Default = secondary, md: 28px, 10px padding, 6px radius, 13px medium. */
585
+ .pc-btn{ --bg:var(--pc-field); --bg-hover:var(--pc-field-hover); --bg-active:var(--bg-hover); --fg:var(--pc-text-strong); --fg-hover:var(--fg);
586
+ display:inline-flex; align-items:center; justify-content:center; gap:6px; height:28px; padding:0 10px; border:0; border-radius:6px;
587
+ background:var(--bg); color:var(--fg); box-shadow:var(--pc-shadow-sm); font-size:13px; line-height:20px; font-weight:500; white-space:nowrap; user-select:none;
588
+ transition:color .15s cubic-bezier(0,0,.2,1), background-color .15s cubic-bezier(0,0,.2,1), transform .15s cubic-bezier(0,0,.2,1); }
589
+ @media (hover:hover){ .pc-btn:enabled:hover{ background:var(--bg-hover); color:var(--fg-hover); } }
590
+ .pc-btn:enabled:active{ background:var(--bg-active); transform:scale(.97); }
591
+ .pc-btn.sm{ height:24px; padding:0 8px; gap:4px; }
592
+ .pc-btn.primary{ --bg:var(--pc-inverted); --bg-hover:var(--pc-inverted-hover); --bg-active:var(--pc-inverted-active); --fg:var(--pc-on-inverted); box-shadow:none; }
593
+ .pc-btn.tertiary{ --bg:var(--pc-component); --bg-hover:var(--pc-active); box-shadow:none; }
594
+ .pc-btn.ghost{ --bg:transparent; --bg-hover:var(--pc-hover); --fg:var(--pc-text); --fg-hover:var(--pc-text-strong); box-shadow:none; }
595
+ .pc-btn:disabled{ background:var(--pc-component); color:var(--pc-disabled); box-shadow:none; cursor:not-allowed; }
596
+ /* icon-only transparent, sm: 24px */
597
+ .pc-ibtn{ width:24px; height:24px; flex:none; display:inline-grid; place-items:center; padding:0; border:0; border-radius:6px; background:transparent; color:var(--pc-text); transition:background-color .15s, color .15s, transform .15s cubic-bezier(0,0,.2,1); }
598
+ @media (hover:hover){ .pc-ibtn:hover{ background:var(--pc-hover); color:var(--pc-text-strong); } }
599
+ .pc-ibtn:active{ transform:scale(.97); }
600
+ @media (hover:hover){ .pc-trash:hover{ color:var(--pc-danger); background:var(--pc-danger-soft-hover); } }
601
+
602
+ /* Badge: 20px, 6px padding, 12px, 4px radius */
603
+ .pc-tag{ display:inline-flex; align-items:center; flex:none; height:20px; padding:0 6px; margin-right:6px; border-radius:4px; background:var(--pc-agent-soft); color:var(--pc-agent-text); font-style:normal; font-size:12px; line-height:16px; font-weight:400; user-select:none; }
604
+
605
+ /* Tabs: gray track, white indicator with a gray-6 border */
606
+ .pc-tabs{ display:inline-flex; align-self:flex-start; padding:0; border-radius:6px; background:var(--pc-component); font-weight:500; }
607
+ .pc-tab{ display:inline-flex; align-items:center; gap:6px; height:28px; padding:0 10px; border:1px solid transparent; border-radius:6px; background:transparent; color:var(--pc-text); white-space:nowrap; transition:color .2s cubic-bezier(.4,0,.2,1); }
608
+ .pc-tab span{ color:var(--pc-faint); font-variant-numeric:tabular-nums; }
609
+ @media (hover:hover){ .pc-tab:hover{ color:var(--pc-text-strong); } }
610
+ .pc-tab[aria-selected="true"]{ background:var(--pc-field); border-color:var(--pc-tab-border); color:var(--pc-text-strong); }
611
+
612
+ /* Kbd: 16px, 0.5px border, 4px radius, 11px medium uppercase */
613
+ .pc-root kbd{ display:inline-flex; align-items:center; justify-content:center; flex:none; height:16px; min-width:16px; padding:0 3px; border:.5px solid var(--pc-border); border-radius:4px; background:var(--pc-component); color:var(--pc-text); font-family:inherit; font-size:11px; line-height:16px; font-weight:500; font-feature-settings:"ss02"; text-transform:uppercase; }
614
+
615
+ /* Checkbox: 14px, 1.5px border, 4px radius, brand fill when checked */
616
+ .pc-root input[type=checkbox]{ appearance:none; -webkit-appearance:none; position:relative; width:14px; height:14px; margin:0; flex:none; border:1.5px solid var(--pc-border-strong); border-radius:4px; background:transparent; background-size:60%; cursor:pointer; transition:background-color .2s cubic-bezier(.23,1,.32,1), background-size .15s cubic-bezier(.23,1,.32,1), transform .15s cubic-bezier(.23,1,.32,1); }
617
+ @media (hover:hover){ .pc-root input[type=checkbox]:hover{ background-color:var(--pc-hover); } }
618
+ .pc-root input[type=checkbox]:active{ transform:scale(.97); }
619
+ .pc-root input[type=checkbox]:checked{ border-color:transparent; background:var(--pc-primary) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3.5 7.2l2.3 2.3 4.7-5' fill='none' stroke='white' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/100% no-repeat; }
620
+ @media (hover:hover){ .pc-root input[type=checkbox]:checked:hover{ background-color:var(--pc-primary-hover); } }
621
+
622
+ /* Textarea: white field with s-default, 8px radius; blue ring on focus */
623
+ .pc-input{ display:block; width:100%; min-height:80px; resize:none; field-sizing:content; max-height:200px; border:0; border-radius:8px; padding:4px 8px; background:var(--pc-field); color:var(--pc-text-strong); caret-color:var(--pc-text); box-shadow:var(--pc-shadow-sm); font-size:13px; line-height:20px; transition:color .15s, background-color .15s, box-shadow .15s; }
624
+ .pc-input::placeholder{ color:var(--pc-faint); }
625
+ @media (hover:hover){ .pc-input:hover{ background:var(--pc-field-hover); } }
626
+ .pc-input:focus{ outline:none; background:var(--pc-field); box-shadow:0 0 0 1px var(--pc-focus), inset 0 0 0 .5px var(--pc-focus); }
627
+
628
+ /* toolbar: standout surface, dark like the Tooltip */
629
+ .pc-bar{ position:fixed; z-index:calc(var(--pc-z) + 4); left:50%; transform:translateX(-50%); bottom:calc(16px + env(safe-area-inset-bottom,0px)); display:flex; align-items:center; gap:2px; padding:4px; border-radius:10px; background:var(--pc-dark); color:#ffffffaf; box-shadow:0 0 0 .5px #ffffff12 inset, 0 8px 40px 0 #0000000d, 0 12px 32px -16px #00000026; user-select:none; }
630
+ .pc-bar button{ position:relative; display:inline-grid; place-items:center; width:28px; height:28px; padding:0; border:0; border-radius:6px; background:transparent; color:inherit; transition:background-color .15s, color .15s, transform .15s cubic-bezier(0,0,.2,1); }
631
+ @media (hover:hover){ .pc-bar button:hover{ background:#ffffff1b; color:#ffffffed; } }
632
+ .pc-bar button:active{ transform:scale(.97); }
633
+ .pc-bar button[aria-pressed="true"]{ background:#ffffff22; color:#ffffffed; box-shadow:0 0 0 .5px #ffffff12 inset; }
634
+ .pc-bar kbd{ border-color:#ffffff12; background:#ffffff12; color:#ffffffaf; }
635
+ /* open comments: a dot on the History icon */
636
+ .pc-dot{ position:absolute; top:5px; right:5px; width:6px; height:6px; border-radius:50%; background:var(--pc-primary); box-shadow:0 0 0 1.5px var(--pc-dark); }
637
+ /* Tooltip (DS): standout surface, 8px radius, 12px medium; shows after a short delay, hides at once */
638
+ .pc-tip{ position:absolute; left:50%; bottom:calc(100% + 10px); display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:8px; background:var(--pc-dark); color:#ffffffed; box-shadow:0 0 0 .5px #ffffff12 inset; font-size:12px; line-height:16px; font-weight:500; white-space:nowrap; pointer-events:none; opacity:0; transform:translate(-50%, 4px) scale(.95); transform-origin:50% 100%; transition:opacity .15s, transform .15s cubic-bezier(.23,1,.32,1); }
639
+ .pc-bar[data-tips="below"] .pc-tip{ bottom:auto; top:calc(100% + 10px); transform:translate(-50%, -4px) scale(.95); transform-origin:50% 0; }
640
+ .pc-bar button:hover .pc-tip, .pc-bar button:focus-visible .pc-tip{ opacity:1; transform:translate(-50%, 0) scale(1); transition-duration:.2s; transition-delay:.3s; }
641
+ @media (hover:none){ .pc-tip{ display:none; } }
642
+ /* The same Tooltip for [data-tip] controls: <Tooltip> places it by the control, so a scrolling panel can't clip it */
643
+ .pc-tip.float{ position:fixed; z-index:calc(var(--pc-z) + 5); bottom:auto; }
644
+ .pc-tip.float[data-side="below"]{ transform:translate(-50%, -4px) scale(.95); transform-origin:50% 0; }
645
+ .pc-tip.float[data-open]{ opacity:1; transform:translate(-50%, 0) scale(1); transition-duration:.2s; }
646
+ .pc-grip{ display:grid; place-items:center; width:20px; height:28px; opacity:.5; cursor:grab; touch-action:none; transition:opacity .15s; }
647
+ @media (hover:hover){ .pc-grip:hover{ opacity:1; } }
648
+ /* Tooltip */
649
+ .pc-hint{ position:fixed; z-index:calc(var(--pc-z) + 1); left:50%; bottom:calc(60px + env(safe-area-inset-bottom,0px)); transform:translateX(-50%); padding:6px 10px; border-radius:8px; background:var(--pc-dark); color:#ffffffed; box-shadow:0 0 0 .5px #ffffff12 inset; font-size:12px; line-height:16px; font-weight:500; white-space:nowrap; animation:pc-hint-in .15s cubic-bezier(0,0,.2,1); }
650
+ /* translate (the property) composes with the centering transform above */
651
+ @keyframes pc-hint-in{ from{ opacity:0; translate:0 4px; } }
652
+
653
+ /* pins */
654
+ .pc-outline{ position:fixed; z-index:var(--pc-z); pointer-events:none; outline:1.5px solid var(--pc-primary); outline-offset:1px; border-radius:4px; background:var(--pc-primary-soft); }
655
+ .pc-outline.flash{ animation:pc-flash 1.6s cubic-bezier(0,0,.2,1) forwards; }
656
+ @keyframes pc-flash{ 0%,60%{ opacity:1 } 100%{ opacity:0 } }
657
+ .pc-pin{ position:fixed; z-index:calc(var(--pc-z) + 1); transform:translate(-2px,-100%); min-width:24px; height:24px; padding:0 7px; border:2px solid #fff; border-radius:12px 12px 12px 2px; background:var(--pc-primary); color:#fff !important; font-size:12px !important; font-weight:500; font-variant-numeric:tabular-nums; box-shadow:var(--pc-shadow); transform-origin:left bottom; }
658
+ .pc-pin.on{ background:#202020; z-index:calc(var(--pc-z) + 2); }
659
+ .pc-pin.queued, .pc-n.queued{ background:var(--pc-agent); }
660
+ .pc-pin.done{ background:var(--pc-done); padding:0; width:24px; display:grid; place-items:center; }
661
+ .pc-pin .pc-i, .pc-n .pc-i{ width:12px; height:12px; }
662
+ .pc-pin.flash{ animation:pc-pin .5s cubic-bezier(.23,1,.32,1) 2; }
663
+ @keyframes pc-pin{ 50%{ transform:translate(-2px,-100%) scale(1.15) } }
664
+ /* a pin that was just left: grows out of its tail, where the draft popover was */
665
+ .pc-pin.born{ animation:pc-pin-in .2s cubic-bezier(.32,.72,0,1); }
666
+ @keyframes pc-pin-in{ from{ opacity:0; transform:translate(-2px,-100%) scale(.8) } }
667
+
668
+ /* Popover: 10px radius, s-extra-large, 0.5px dividers */
669
+ .pc-pop{ position:fixed; z-index:calc(var(--pc-z) + 5); width:min(320px, calc(100vw - 16px)); padding:0; display:flex; flex-direction:column; background:var(--pc-popover); color:var(--pc-text-strong); border-radius:10px; box-shadow:var(--pc-shadow); max-height:calc(100vh - 16px); overflow:hidden; animation:pc-pop-in .18s cubic-bezier(.32,.72,0,1); }
670
+ /* transform-origin is set inline, at the pin or click point */
671
+ @keyframes pc-pop-in{ from{ opacity:0; transform:scale(.97) } }
672
+ .pc-head{ flex:none; display:flex; align-items:center; gap:2px; min-height:40px; padding:8px 8px 8px 12px; border-bottom:.5px solid var(--pc-border); }
673
+ .pc-head b{ flex:1; min-width:0; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
674
+ .pc-head .pc-tag{ margin:0 6px; }
675
+ /* header, reply field and actions stay put; only the messages scroll */
676
+ .pc-pane{ flex:none; display:flex; flex-direction:column; gap:10px; padding:12px; }
677
+ .pc-pane.msgs{ flex:0 1 auto; min-height:48px; overflow:auto; overscroll-behavior:contain; padding-bottom:0; }
678
+ .pc-pane.msgs + .pc-pane{ padding-top:10px; }
679
+ .pc-actions{ display:flex; justify-content:flex-end; gap:8px; }
680
+ .pc-actions.foot{ flex:none; padding:6px 8px; border-top:.5px solid var(--pc-border); }
681
+ .pc-msg{ white-space:pre-wrap; overflow-wrap:anywhere; }
682
+ .pc-meta{ display:block; font-size:12px; line-height:16px; color:var(--pc-faint); margin-bottom:2px; }
683
+ .pc-lost{ font-size:12px; line-height:16px; color:var(--pc-warn); background:var(--pc-warn-soft); border-radius:6px; padding:6px 8px; }
684
+
685
+ /* History: right Drawer */
686
+ .pc-history{ position:fixed; z-index:calc(var(--pc-z) + 3); top:0; right:0; bottom:0; width:384px; max-width:100%; background:var(--pc-container); color:var(--pc-text-strong); border-left:.5px solid var(--pc-border); box-shadow:var(--pc-shadow); display:flex; flex-direction:column;
687
+ transform:translateX(100%); visibility:hidden; transition:transform .18s cubic-bezier(.32,.72,0,1), visibility 0s .18s; }
688
+ /* a transition, not keyframes: toggling History mid-slide reverses it smoothly. Closing is faster than opening */
689
+ .pc-history[data-open]{ transform:none; visibility:visible; transition:transform .25s cubic-bezier(.32,.72,0,1); }
690
+ .pc-history header{ display:flex; align-items:center; gap:6px; padding:16px 16px 12px 20px; }
691
+ .pc-history header b{ flex:1; font-size:18px; line-height:24px; font-weight:500; }
692
+ .pc-history > .pc-tabs{ display:flex; align-self:stretch; margin:0 20px 12px; }
693
+ /* narrow drawer or 4-digit counts: tabs shrink instead of overflowing */
694
+ .pc-history > .pc-tabs .pc-tab{ flex:1 1 auto; min-width:0; justify-content:center; gap:4px; padding:0 6px; overflow:hidden; }
695
+ .pc-selbar{ display:flex; align-items:center; gap:6px; min-height:40px; padding:6px 16px 6px 20px; border-top:.5px solid var(--pc-border); border-bottom:.5px solid var(--pc-border); }
696
+ .pc-check{ flex:1; display:inline-flex; align-items:center; gap:8px; cursor:pointer; color:var(--pc-text); font-weight:500; user-select:none; }
697
+ .pc-check:has(:checked){ color:var(--pc-text-strong); }
698
+ .pc-bulk{ display:flex; gap:2px; margin-right:-4px; }
699
+ /* Command list: 6px-radius items */
700
+ /* the bottom padding keeps the last row clear of the toolbar when the drawer is full width */
701
+ .pc-groups{ flex:1; overflow:auto; padding:8px 12px 72px; }
702
+ /* row: checkbox, pin and the first text line share one 20px line */
703
+ .pc-row{ position:relative; display:flex; align-items:flex-start; gap:10px; padding:8px; border-radius:6px; transition:background-color .15s, opacity .15s cubic-bezier(.4,0,1,1); }
704
+ /* fades before it leaves the filtered list */
705
+ .pc-row.leaving{ opacity:0; pointer-events:none; }
706
+ @media (hover:hover){ .pc-row:hover{ background:var(--pc-hover); } }
707
+ .pc-root .pc-row .pc-pick{ width:14px; height:14px; flex:none; margin:3px 0 0; }
708
+ .pc-item{ flex:1; min-width:0; display:flex; align-items:flex-start; gap:10px; padding:0; border:0; background:transparent; text-align:left; }
709
+ .pc-n{ flex:none; min-width:20px; height:20px; padding:0 5px; border-radius:10px 10px 10px 2px; background:var(--pc-primary); color:#fff; font-size:11px; font-weight:500; font-variant-numeric:tabular-nums; display:grid; place-items:center; }
710
+ /* row actions: the thread's Resolve, Mark for Agent and Delete; shown on hover or focus, always on touch */
711
+ /* overlaid at the row's right edge so hidden actions take no width; the fade matches the row's hover fill */
712
+ .pc-row-actions{ position:absolute; top:6px; right:6px; display:flex; gap:2px; padding-left:20px; border-radius:6px; background:linear-gradient(to right, transparent, var(--pc-row-hover) 20px); opacity:0; pointer-events:none; transition:opacity .15s; }
713
+ .pc-row:hover .pc-row-actions, .pc-row:focus-within .pc-row-actions{ opacity:1; pointer-events:auto; }
714
+ /* on touch they are always there, so they take their own column instead of covering the text */
715
+ @media (hover:none){ .pc-row-actions{ position:static; flex:none; opacity:1; pointer-events:auto; padding-left:0; background:none; margin:-2px -2px 0 0; } }
716
+ .pc-agent[aria-pressed="true"]{ color:var(--pc-agent-text); background:var(--pc-agent-soft); }
717
+ .pc-row.done .pc-n{ background:var(--pc-done); padding:0; width:20px; }
718
+ .pc-body{ flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; }
719
+ .pc-text{ font-weight:450; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
720
+ .pc-row.done .pc-text{ color:var(--pc-faint); }
721
+ .pc-body .pc-meta{ display:flex; align-items:center; gap:6px; min-width:0; margin:0; }
722
+ .pc-body .pc-meta > span{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
723
+ .pc-body .pc-tag{ margin:0; }
724
+ .pc-empty{ margin:8px; color:var(--pc-faint); }
725
+
726
+ @media (prefers-reduced-motion:reduce){ .pc-root *{ transition:none !important; animation:none !important; } }
727
+ `;
728
+
729
+ // src/index.jsx
730
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
731
+ var TEXT_INPUT = 'input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]):not([type=range]):not([type=color]), textarea, [contenteditable="true"]';
732
+ var typing = (t) => t.closest?.(TEXT_INPUT);
733
+ var ours = (t) => t.closest?.(".pc-root");
734
+ var BLOCK = ["pointerdown", "mousedown", "mouseup", "click", "dblclick"];
735
+ var rectOf = (el) => {
736
+ const r = el.getBoundingClientRect();
737
+ return { left: r.left, top: r.top, width: r.width, height: r.height };
738
+ };
739
+ function useStyles() {
740
+ useEffect2(() => {
741
+ if (document.getElementById("proto-comments-css")) return;
742
+ const s = document.createElement("style");
743
+ s.id = "proto-comments-css";
744
+ s.textContent = styles_default;
745
+ document.head.appendChild(s);
746
+ }, []);
747
+ }
748
+ var clippers = (el) => {
749
+ const out = [];
750
+ for (let n = el.parentElement; n && n !== document.body; n = n.parentElement) {
751
+ const s = getComputedStyle(n);
752
+ if (s.overflowX !== "visible" || s.overflowY !== "visible") out.push(n);
753
+ }
754
+ return out;
755
+ };
756
+ var inside = (x, y, els) => els.every((n) => {
757
+ const r = n.getBoundingClientRect();
758
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
759
+ });
760
+ var PIN_H = 24;
761
+ var pinWidth = (c) => c.status === "resolved" ? 24 : Math.max(24, 18 + 7 * String(c.n).length);
762
+ function place(list, pos) {
763
+ const placed = [];
764
+ list.filter((c) => pos[c.id]).sort((a, b) => a.n - b.n).forEach((c) => {
765
+ const p = pos[c.id], w = pinWidth(c);
766
+ p.x = Math.max(4, Math.min(p.x, window.innerWidth - w - 2));
767
+ p.y = Math.max(PIN_H + 4, Math.min(p.y, window.innerHeight - 4));
768
+ const startX = p.x;
769
+ for (let moved = true; moved; ) {
770
+ moved = false;
771
+ for (const q of placed) {
772
+ if (Math.abs(p.y - q.y) < PIN_H && p.x < q.x + q.w + 2 && p.x + w + 2 > q.x) {
773
+ p.x = q.x + q.w + 2;
774
+ moved = true;
775
+ }
776
+ }
777
+ if (p.x + w > window.innerWidth - 2) {
778
+ p.x = startX;
779
+ p.y += PIN_H + 2;
780
+ moved = true;
781
+ }
782
+ }
783
+ placed.push({ x: p.x, y: p.y, w });
784
+ });
785
+ }
786
+ function useFrame(list, openId, flashId) {
787
+ const [frame, setFrame] = useState2({ screen: null, pos: {} });
788
+ useEffect2(() => {
789
+ let raf, last = "", lastScreen = null, dirty = true, found = {};
790
+ const mo = new MutationObserver((records) => {
791
+ if (records.some((r) => !ours(r.target.nodeType === 1 ? r.target : r.target.parentElement))) dirty = true;
792
+ });
793
+ mo.observe(document.body, { childList: true, characterData: true, subtree: true });
794
+ const timer = setInterval(() => {
795
+ dirty = true;
796
+ }, 1e3);
797
+ const tick = () => {
798
+ const screen = getConfig().getScreen().key;
799
+ const shown = list.filter((c) => c.screen === screen && (c.status === "open" || c.id === openId));
800
+ if (dirty || screen !== lastScreen) {
801
+ found = {};
802
+ shown.forEach((c) => {
803
+ const el = findAnchor(c.anchor);
804
+ if (el) found[c.id] = { el, clip: clippers(el) };
805
+ });
806
+ dirty = false;
807
+ lastScreen = screen;
808
+ }
809
+ const pos = {};
810
+ shown.forEach((c) => {
811
+ const f = found[c.id];
812
+ if (!f || !f.el.isConnected || !f.el.getClientRects().length) return;
813
+ const r = rectOf(f.el);
814
+ const x = r.left + c.anchor.offset.x * r.width, y = r.top + c.anchor.offset.y * r.height;
815
+ if (c.id !== openId && !inside(x, y, f.clip)) return;
816
+ pos[c.id] = { x, y, rect: c.id === flashId ? r : null };
817
+ });
818
+ place(shown, pos);
819
+ const key = JSON.stringify([screen, pos]);
820
+ if (key !== last) {
821
+ last = key;
822
+ setFrame({ screen, pos });
823
+ }
824
+ raf = requestAnimationFrame(tick);
825
+ };
826
+ tick();
827
+ return () => {
828
+ cancelAnimationFrame(raf);
829
+ clearInterval(timer);
830
+ mo.disconnect();
831
+ };
832
+ }, [list, openId, flashId]);
833
+ return frame;
834
+ }
835
+ function ProtoComments(props) {
836
+ const cfg2 = { ...DEFAULTS, ...Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0)) };
837
+ setConfig(cfg2);
838
+ useStyles();
839
+ const [root, setRoot] = useState2(null);
840
+ useEffect2(() => {
841
+ const el = document.createElement("div");
842
+ el.className = "pc-root";
843
+ document.body.appendChild(el);
844
+ setRoot(el);
845
+ return () => el.remove();
846
+ }, []);
847
+ const { list, mode, draft, openId, flashId } = useC((s) => s);
848
+ const [hover, setHover] = useState2(null);
849
+ useEffect2(() => {
850
+ const load = () => {
851
+ if (document.visibilityState === "visible") return loadComments().catch((e) => console.warn(e.message));
852
+ };
853
+ Promise.resolve(load()).then(resumePendingGo);
854
+ document.addEventListener("visibilitychange", load);
855
+ window.addEventListener("focus", load);
856
+ return () => {
857
+ document.removeEventListener("visibilitychange", load);
858
+ window.removeEventListener("focus", load);
859
+ };
860
+ }, []);
861
+ useEffect2(() => {
862
+ const onKey = (e) => {
863
+ const c = getC();
864
+ if (e.key === "Escape" && (c.draft || c.openId || c.historyOpen)) {
865
+ e.stopPropagation();
866
+ if (c.draft) setC({ draft: null });
867
+ else if (c.openId) setC({ openId: null });
868
+ else setC({ historyOpen: false });
869
+ return;
870
+ }
871
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
872
+ if (e.code === "KeyV" && c.mode && !(typing(e.target) && ours(e.target))) {
873
+ e.preventDefault();
874
+ e.stopPropagation();
875
+ setC({ mode: false, draft: null });
876
+ }
877
+ if (e.code === "KeyC" && !c.mode && !typing(e.target)) setC({ mode: true, openId: null });
878
+ if (e.code === "KeyH" && !typing(e.target)) setC({ historyOpen: !c.historyOpen });
879
+ };
880
+ window.addEventListener("keydown", onKey, true);
881
+ return () => window.removeEventListener("keydown", onKey, true);
882
+ }, []);
883
+ useEffect2(() => {
884
+ if (!mode) {
885
+ setHover(null);
886
+ return;
887
+ }
888
+ const onMove = (e) => setHover(ours(e.target) ? null : rectOf(e.target));
889
+ const block = (e) => {
890
+ if (ours(e.target)) return;
891
+ e.preventDefault();
892
+ e.stopPropagation();
893
+ if (e.type === "click") setC({ draft: { anchor: anchorFor(e.target, e.clientX, e.clientY, cfg2.headingSelector), x: e.clientX, y: e.clientY }, openId: null });
894
+ };
895
+ document.addEventListener("mousemove", onMove, true);
896
+ BLOCK.forEach((t) => document.addEventListener(t, block, true));
897
+ if (!ours(document.activeElement)) document.activeElement?.blur();
898
+ document.documentElement.classList.add("pc-mode");
899
+ return () => {
900
+ document.removeEventListener("mousemove", onMove, true);
901
+ BLOCK.forEach((t) => document.removeEventListener(t, block, true));
902
+ document.documentElement.classList.remove("pc-mode");
903
+ };
904
+ }, [mode, cfg2.headingSelector]);
905
+ const { screen, pos } = useFrame(list, openId, flashId);
906
+ const lastScreen = useRef2(screen);
907
+ useEffect2(() => {
908
+ if (lastScreen.current !== null && screen !== lastScreen.current) setC({ openId: null, draft: null });
909
+ lastScreen.current = screen;
910
+ }, [screen]);
911
+ const here = list.filter((c) => pos[c.id]);
912
+ const open = list.find((c) => c.id === openId);
913
+ if (!root) return null;
914
+ return createPortal(
915
+ /* @__PURE__ */ jsxs3(Fragment3, { children: [
916
+ hover && !draft && /* @__PURE__ */ jsx3("div", { className: "pc-outline", style: hover }),
917
+ here.map((c) => /* @__PURE__ */ jsx3(Pin, { c, x: pos[c.id].x, y: pos[c.id].y }, c.id)),
918
+ here.map((c) => pos[c.id].rect && /* @__PURE__ */ jsx3("div", { className: "pc-outline flash", style: pos[c.id].rect }, `f${c.id}`)),
919
+ draft && /* @__PURE__ */ jsx3(Draft, { draft }),
920
+ open && (pos[open.id] ? /* @__PURE__ */ jsx3(Thread, { c: open, x: pos[open.id].x, y: pos[open.id].y }, open.id) : /* @__PURE__ */ jsx3(Thread, { c: open, x: window.innerWidth / 2 - 160, y: 120, lost: true }, open.id)),
921
+ /* @__PURE__ */ jsx3(History, {}),
922
+ /* @__PURE__ */ jsx3(Toolbar, {}),
923
+ /* @__PURE__ */ jsx3(Tooltip, {}),
924
+ mode && /* @__PURE__ */ jsx3("div", { className: "pc-hint", children: "Click an element to comment \xB7 V to stop" })
925
+ ] }),
926
+ root
927
+ );
928
+ }
929
+ var index_default = ProtoComments;
930
+ export {
931
+ ProtoComments,
932
+ index_default as default,
933
+ placeOf
934
+ };