proto-comments 0.2.0 → 0.2.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/README.md +3 -3
- package/dist/index.js +32 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Figma-style review comments on a running React prototype.
|
|
|
4
4
|
|
|
5
5
|
- **Comments are split by screen, not only by URL.** Prototypes often show every screen on one URL. Tell the plugin what a "screen" is, and a comment left in Settings won't show up on the deal page.
|
|
6
6
|
- **Pinned to elements.** A pin follows its element when the window is resized or scrolled. Each comment records the block it's in, for example "Review before I send these invites › Emails".
|
|
7
|
-
- **History.** All comments,
|
|
7
|
+
- **History.** All comments in one list, by number, each with the screen it was left on. Click one to reopen its screen in the saved state, scroll to the element and open the thread.
|
|
8
8
|
- **Made for a coding agent.** Comments live in one JSON file in your project. Mark the ones you want, tell the agent "apply the comments", and it resolves each with a reply saying what changed.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
@@ -71,7 +71,7 @@ Either way, comments are saved to `comments/comments.json`, relative to the fold
|
|
|
71
71
|
|
|
72
72
|
The shortcuts use physical keys, so they work in any keyboard layout. In text fields C types a letter.
|
|
73
73
|
|
|
74
|
-
In a comment you can reply, resolve, mark it for the agent, or delete it. In History you can filter (All, Open, For Agent, Resolved). Only open comments are numbered
|
|
74
|
+
In a comment you can reply, resolve, mark it for the agent, or delete it. In History you can filter (All, Open, For Agent, Resolved). Only open comments are numbered. In History they come first, in order of their numbers, then the resolved ones; each row shows the screen it's on. A resolved comment shows a check and who resolved it, so the next new comment can be #1 again. Each row has the same actions as an open comment, and you can select several comments to resolve, mark for the agent or delete them at once.
|
|
75
75
|
|
|
76
76
|
## Screens in single-URL prototypes
|
|
77
77
|
|
|
@@ -105,7 +105,7 @@ export default function Review() {
|
|
|
105
105
|
| --- | --- | --- |
|
|
106
106
|
| `endpoint` | `'/api/comments'` | Where comments are stored |
|
|
107
107
|
| `author` | `'Reviewer'` | Name on new comments and replies |
|
|
108
|
-
| `getScreen` | URL | `() => ({ key, label })`. Comments show only on a screen with the same `key`; `label`
|
|
108
|
+
| `getScreen` | URL | `() => ({ key, label })`. Comments show only on a screen with the same `key`; `label` is the screen's name in History |
|
|
109
109
|
| `getSnapshot` | `{ url }` | JSON saved with each comment |
|
|
110
110
|
| `restoreSnapshot` | go to `url` | `(snapshot, comment) => void \| Promise`. Called from History when the comment is on another screen |
|
|
111
111
|
| `getContext` | none | `() => string` shown in History, e.g. `"new client · admin"` |
|
package/dist/index.js
CHANGED
|
@@ -205,6 +205,14 @@ var when = (iso) => {
|
|
|
205
205
|
return d.toLocaleString("en-US", { year, month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
206
206
|
};
|
|
207
207
|
var byline = (author, iso) => [author, when(iso)].filter(Boolean).join(" \xB7 ");
|
|
208
|
+
function Clip({ as: Tag = "span", tip, children }) {
|
|
209
|
+
const ref = useRef(null);
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
const el = ref.current;
|
|
212
|
+
el.classList.toggle("pc-cut", el.scrollWidth > el.clientWidth);
|
|
213
|
+
});
|
|
214
|
+
return /* @__PURE__ */ jsx2(Tag, { ref, className: "pc-clip", "data-tip": tip, children });
|
|
215
|
+
}
|
|
208
216
|
function usePop(x, y, lost) {
|
|
209
217
|
const ref = useRef(null);
|
|
210
218
|
const [h, setH] = useState(0);
|
|
@@ -263,7 +271,7 @@ function Field({ placeholder, submit, onCancel, label, bare }) {
|
|
|
263
271
|
function Draft({ draft }) {
|
|
264
272
|
const pop = usePop(draft.x, draft.y);
|
|
265
273
|
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",
|
|
274
|
+
/* @__PURE__ */ jsx2("div", { className: "pc-head", children: /* @__PURE__ */ jsx2(Clip, { as: "b", tip: placeOf(draft.anchor), children: nameOf(draft.anchor) }) }),
|
|
267
275
|
/* @__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
276
|
] });
|
|
269
277
|
}
|
|
@@ -279,7 +287,7 @@ function Thread({ c, x, y, lost }) {
|
|
|
279
287
|
}, [c.replies.length]);
|
|
280
288
|
return /* @__PURE__ */ jsxs2("div", { className: "pc-pop", ref: pop.ref, style: pop.style, children: [
|
|
281
289
|
/* @__PURE__ */ jsxs2("div", { className: "pc-head", children: [
|
|
282
|
-
/* @__PURE__ */ jsx2("b",
|
|
290
|
+
/* @__PURE__ */ jsx2(Clip, { as: "b", tip: placeOf(c.anchor), children: nameOf(c.anchor) }),
|
|
283
291
|
c.queued && /* @__PURE__ */ jsx2("i", { className: "pc-tag", children: "For Agent" }),
|
|
284
292
|
/* @__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
293
|
/* @__PURE__ */ jsx2("button", { className: "pc-ibtn", onClick: () => setC({ openId: null }), "aria-label": "Close", "data-tip": "Close", children: /* @__PURE__ */ jsx2(Icon, { name: "x" }) })
|
|
@@ -320,17 +328,13 @@ function Pin({ c, x, y }) {
|
|
|
320
328
|
}
|
|
321
329
|
var FILTERS = [["all", "All"], ["open", "Open"], ["queued", "For Agent"], ["resolved", "Resolved"]];
|
|
322
330
|
var matches = (c, f) => f === "all" || (f === "queued" ? c.queued && c.status === "open" : c.status === f);
|
|
323
|
-
var
|
|
324
|
-
var _a;
|
|
325
|
-
(m[_a = key(it)] || (m[_a] = [])).push(it);
|
|
326
|
-
return m;
|
|
327
|
-
}, {});
|
|
331
|
+
var metaOf = (c, place2) => [c.screenLabel, place2, c.context, when(c.createdAt), c.replies.length && `${c.replies.length + 1} comments`].filter(Boolean).join(" \xB7 ");
|
|
328
332
|
function History() {
|
|
329
333
|
const isOpen = useC((s) => s.historyOpen);
|
|
330
334
|
const list = useC((s) => s.list);
|
|
331
335
|
const filter = useC((s) => s.filter);
|
|
332
|
-
const
|
|
333
|
-
const
|
|
336
|
+
const rank = (c) => c.status === "resolved" ? Infinity : c.n;
|
|
337
|
+
const shown = list.filter((c) => matches(c, filter)).sort((a, b) => rank(a) - rank(b) || 0);
|
|
334
338
|
const count = (f) => list.filter((c) => matches(c, f)).length;
|
|
335
339
|
const [picked, setPicked] = useState([]);
|
|
336
340
|
const selectable = shown.map((c) => c.id);
|
|
@@ -387,17 +391,17 @@ function History() {
|
|
|
387
391
|
/* @__PURE__ */ jsx2("button", { className: "pc-ibtn pc-trash", onClick: remove, "aria-label": "Delete selected", "data-tip": "Delete", children: /* @__PURE__ */ jsx2(Icon, { name: "trash" }) })
|
|
388
392
|
] })
|
|
389
393
|
] }),
|
|
390
|
-
/* @__PURE__ */ jsxs2("div", { className: "pc-
|
|
394
|
+
/* @__PURE__ */ jsxs2("div", { className: "pc-list", children: [
|
|
391
395
|
!shown.length && /* @__PURE__ */ jsx2("p", { className: "pc-empty", children: "No comments here. Press C and click an element to leave one." }),
|
|
392
|
-
|
|
396
|
+
shown.map((c) => /* @__PURE__ */ jsxs2("div", { className: `pc-row${c.status === "resolved" ? " done" : ""}${leaving.includes(c.id) ? " leaving" : ""}`, children: [
|
|
393
397
|
/* @__PURE__ */ jsx2("input", { type: "checkbox", className: "pc-pick", checked: sel.includes(c.id), onChange: () => toggle(c.id), "aria-label": `Select comment ${c.n}` }),
|
|
394
398
|
/* @__PURE__ */ jsxs2("button", { className: "pc-item", onClick: () => goToComment(c), children: [
|
|
395
399
|
/* @__PURE__ */ jsx2("span", { className: `pc-n${c.queued ? " queued" : ""}`, children: c.status === "resolved" ? /* @__PURE__ */ jsx2(Icon, { name: "check" }) : c.n }),
|
|
396
400
|
/* @__PURE__ */ jsxs2("span", { className: "pc-body", children: [
|
|
397
401
|
/* @__PURE__ */ jsx2("span", { className: "pc-text", children: c.text }),
|
|
398
|
-
/* @__PURE__ */ jsxs2("span", { className: "pc-meta",
|
|
402
|
+
/* @__PURE__ */ jsxs2("span", { className: "pc-meta", children: [
|
|
399
403
|
c.queued && /* @__PURE__ */ jsx2("i", { className: "pc-tag", children: "For Agent" }),
|
|
400
|
-
/* @__PURE__ */ jsx2(
|
|
404
|
+
/* @__PURE__ */ jsx2(Clip, { tip: metaOf(c, placeOf(c.anchor)), children: metaOf(c, nameOf(c.anchor)) })
|
|
401
405
|
] }),
|
|
402
406
|
c.status === "resolved" && c.resolvedBy && /* @__PURE__ */ jsxs2("span", { className: "pc-meta", children: [
|
|
403
407
|
"Resolved by ",
|
|
@@ -410,7 +414,7 @@ function History() {
|
|
|
410
414
|
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
415
|
/* @__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
416
|
] })
|
|
413
|
-
] }, c.id))
|
|
417
|
+
] }, c.id))
|
|
414
418
|
] })
|
|
415
419
|
] });
|
|
416
420
|
}
|
|
@@ -627,20 +631,21 @@ html.pc-mode .pc-grip{ cursor:grab !important; }
|
|
|
627
631
|
|
|
628
632
|
/* toolbar: standout surface, dark like the Tooltip */
|
|
629
633
|
.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{
|
|
634
|
+
.pc-bar button{ --pc-tint:transparent; 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); }
|
|
635
|
+
@media (hover:hover){ .pc-bar button:hover{ --pc-tint:#ffffff1b; background:var(--pc-tint); color:#ffffffed; } }
|
|
632
636
|
.pc-bar button:active{ transform:scale(.97); }
|
|
633
|
-
.pc-bar button[aria-pressed="true"]{
|
|
637
|
+
.pc-bar button[aria-pressed="true"]{ --pc-tint:#ffffff22; background:var(--pc-tint); color:#ffffffed; box-shadow:0 0 0 .5px #ffffff12 inset; }
|
|
634
638
|
.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); }
|
|
639
|
+
/* open comments: a dot on the History icon. Its ring is the button's own color: the bar, plus the button's tint when hovered or pressed. */
|
|
640
|
+
.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-tint), 0 0 0 1.5px var(--pc-dark); transition:box-shadow .15s; }
|
|
637
641
|
/* Tooltip (DS): standout surface, 8px radius, 12px medium; shows after a short delay, hides at once */
|
|
638
642
|
.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
643
|
.pc-bar[data-tips="below"] .pc-tip{ bottom:auto; top:calc(100% + 10px); transform:translate(-50%, -4px) scale(.95); transform-origin:50% 0; }
|
|
640
644
|
.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
645
|
@media (hover:none){ .pc-tip{ display:none; } }
|
|
642
646
|
/* The same Tooltip for [data-tip] controls: <Tooltip> places it by the control, so a scrolling panel can't clip it */
|
|
643
|
-
|
|
647
|
+
/* max-content: near the right edge, shrink-to-fit would wrap it narrower than it needs */
|
|
648
|
+
.pc-tip.float{ position:fixed; z-index:calc(var(--pc-z) + 5); bottom:auto; width:max-content; max-width:min(320px, calc(100vw - 16px)); white-space:normal; overflow-wrap:anywhere; }
|
|
644
649
|
.pc-tip.float[data-side="below"]{ transform:translate(-50%, -4px) scale(.95); transform-origin:50% 0; }
|
|
645
650
|
.pc-tip.float[data-open]{ opacity:1; transform:translate(-50%, 0) scale(1); transition-duration:.2s; }
|
|
646
651
|
.pc-grip{ display:grid; place-items:center; width:20px; height:28px; opacity:.5; cursor:grab; touch-action:none; transition:opacity .15s; }
|
|
@@ -670,7 +675,7 @@ html.pc-mode .pc-grip{ cursor:grab !important; }
|
|
|
670
675
|
/* transform-origin is set inline, at the pin or click point */
|
|
671
676
|
@keyframes pc-pop-in{ from{ opacity:0; transform:scale(.97) } }
|
|
672
677
|
.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;
|
|
678
|
+
.pc-head b{ flex:1; font-weight:500; }
|
|
674
679
|
.pc-head .pc-tag{ margin:0 6px; }
|
|
675
680
|
/* header, reply field and actions stay put; only the messages scroll */
|
|
676
681
|
.pc-pane{ flex:none; display:flex; flex-direction:column; gap:10px; padding:12px; }
|
|
@@ -693,12 +698,13 @@ html.pc-mode .pc-grip{ cursor:grab !important; }
|
|
|
693
698
|
/* narrow drawer or 4-digit counts: tabs shrink instead of overflowing */
|
|
694
699
|
.pc-history > .pc-tabs .pc-tab{ flex:1 1 auto; min-width:0; justify-content:center; gap:4px; padding:0 6px; overflow:hidden; }
|
|
695
700
|
.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
|
-
|
|
701
|
+
/* only the box and its text toggle; margin:0 against the page's own label styles */
|
|
702
|
+
.pc-check{ flex:none; display:inline-flex; align-items:center; gap:8px; margin:0; cursor:pointer; color:var(--pc-text); font-weight:500; user-select:none; }
|
|
697
703
|
.pc-check:has(:checked){ color:var(--pc-text-strong); }
|
|
698
|
-
.pc-bulk{ display:flex; gap:2px; margin-
|
|
704
|
+
.pc-bulk{ display:flex; gap:2px; margin:0 -4px 0 auto; }
|
|
699
705
|
/* Command list: 6px-radius items */
|
|
700
706
|
/* the bottom padding keeps the last row clear of the toolbar when the drawer is full width */
|
|
701
|
-
.pc-
|
|
707
|
+
.pc-list{ flex:1; overflow:auto; padding:8px 12px 72px; }
|
|
702
708
|
/* row: checkbox, pin and the first text line share one 20px line */
|
|
703
709
|
.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
710
|
/* fades before it leaves the filtered list */
|
|
@@ -719,7 +725,8 @@ html.pc-mode .pc-grip{ cursor:grab !important; }
|
|
|
719
725
|
.pc-text{ font-weight:450; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
|
720
726
|
.pc-row.done .pc-text{ color:var(--pc-faint); }
|
|
721
727
|
.pc-body .pc-meta{ display:flex; align-items:center; gap:6px; min-width:0; margin:0; }
|
|
722
|
-
.pc-
|
|
728
|
+
.pc-clip{ min-width:0; overflow:hidden; white-space:nowrap; }
|
|
729
|
+
.pc-clip.pc-cut{ -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 24px), transparent); mask-image:linear-gradient(to right, #000 calc(100% - 24px), transparent); }
|
|
723
730
|
.pc-body .pc-tag{ margin:0; }
|
|
724
731
|
.pc-empty{ margin:8px; color:var(--pc-faint); }
|
|
725
732
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proto-comments",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Figma-style review comments on a running React prototype, split by screen even in single-URL apps, stored in a JSON file a coding agent can apply.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|