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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +32 -25
  3. 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, grouped by screen. Click one to reopen its screen in the saved state, scroll to the element and open the thread.
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, and they come first in History. 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.
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` groups History |
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", { "data-tip": placeOf(draft.anchor), children: nameOf(draft.anchor) }) }),
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", { "data-tip": placeOf(c.anchor), children: nameOf(c.anchor) }),
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 groupBy = (items, key) => items.reduce((m, it) => {
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 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));
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-groups", children: [
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
- 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: [
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", "data-tip": placeOf(c.anchor), children: [
402
+ /* @__PURE__ */ jsxs2("span", { className: "pc-meta", children: [
399
403
  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 ") })
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)) }, label))
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{ background:#ffffff1b; color:#ffffffed; } }
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"]{ background:#ffffff22; color:#ffffffed; box-shadow:0 0 0 .5px #ffffff12 inset; }
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
- .pc-tip.float{ position:fixed; z-index:calc(var(--pc-z) + 5); bottom:auto; }
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; min-width:0; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
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
- .pc-check{ flex:1; display:inline-flex; align-items:center; gap:8px; cursor:pointer; color:var(--pc-text); font-weight:500; user-select:none; }
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-right:-4px; }
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-groups{ flex:1; overflow:auto; padding:8px 12px 72px; }
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-body .pc-meta > span{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
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.0",
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",