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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 proto-comments contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,161 @@
1
- # Temporary Holding Version
1
+ # proto-comments
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Figma-style review comments on a running React prototype.
4
+
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
+ - **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.
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
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -D proto-comments
14
+ ```
15
+
16
+ React 18+. Mount it once, in development only.
17
+
18
+ ### Next.js (App Router)
19
+
20
+ Store comments through an API route:
21
+
22
+ ```js
23
+ // app/api/comments/route.js
24
+ import { createNextHandlers } from 'proto-comments/next';
25
+
26
+ export const dynamic = 'force-dynamic';
27
+ export const { GET, POST, PATCH, DELETE } = createNextHandlers(); // off outside `next dev`
28
+ ```
29
+
30
+ ```jsx
31
+ // app/layout.js
32
+ import { ProtoComments } from 'proto-comments';
33
+
34
+ export default function RootLayout({ children }) {
35
+ return (
36
+ <html lang="en">
37
+ <body>
38
+ {children}
39
+ {process.env.NODE_ENV === 'development' && <ProtoComments author="Anna" />}
40
+ </body>
41
+ </html>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ### Vite and other apps
47
+
48
+ Run the bundled local server next to your dev server:
49
+
50
+ ```bash
51
+ npx proto-comments serve # http://localhost:4848/api/comments
52
+ ```
53
+
54
+ ```jsx
55
+ {import.meta.env.DEV && <ProtoComments endpoint="http://localhost:4848/api/comments" author="Anna" />}
56
+ ```
57
+
58
+ The server only accepts requests from `localhost` pages.
59
+
60
+ Either way, comments are saved to `comments/comments.json`, relative to the folder you run the dev server from. Use `createNextHandlers({ file })` or `serve --file` to change it.
61
+
62
+ ## Use
63
+
64
+ | | |
65
+ | --- | --- |
66
+ | **C** | Start commenting: hover outlines an element, click it to leave a comment |
67
+ | **V** | Stop commenting |
68
+ | **H** | Open or close History |
69
+ | **Esc** | Close the draft, the open comment or History |
70
+ | Toolbar | Bottom center. Drag it by the dots; it stays where you put it |
71
+
72
+ The shortcuts use physical keys, so they work in any keyboard layout. In text fields C types a letter.
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.
75
+
76
+ ## Screens in single-URL prototypes
77
+
78
+ By default a screen is `location.pathname + location.hash`, and History reopens a comment by going to its URL. If your prototype switches screens in state, pass three functions:
79
+
80
+ ```jsx
81
+ 'use client';
82
+ import { ProtoComments } from 'proto-comments';
83
+ import { useApp } from '@/lib/store'; // your store
84
+
85
+ export default function Review() {
86
+ return (
87
+ <ProtoComments
88
+ author="Anna"
89
+ getScreen={() => {
90
+ const s = useApp.getState();
91
+ return { key: s.view === 'settings' ? `settings:${s.tab}` : s.view, label: s.view };
92
+ }}
93
+ getSnapshot={() => structuredClone(useApp.getState())} // must be JSON-serializable
94
+ restoreSnapshot={snap => useApp.setState(snap)} // reopen that exact state
95
+ />
96
+ );
97
+ }
98
+ ```
99
+
100
+ `getScreen` is called on every animation frame, so keep it cheap.
101
+
102
+ ### All props
103
+
104
+ | Prop | Default | |
105
+ | --- | --- | --- |
106
+ | `endpoint` | `'/api/comments'` | Where comments are stored |
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 |
109
+ | `getSnapshot` | `{ url }` | JSON saved with each comment |
110
+ | `restoreSnapshot` | go to `url` | `(snapshot, comment) => void \| Promise`. Called from History when the comment is on another screen |
111
+ | `getContext` | none | `() => string` shown in History, e.g. `"new client · admin"` |
112
+ | `headingSelector` | none | Extra selectors for block titles, e.g. `'.card-title'`. Built-in: `h1`–`h6`, `legend`, `[class*="title"]`, `label` |
113
+
114
+ **Better anchors.** A pin is found by a CSS path from the nearest ancestor with an `id` or `data-cid`, plus the element's text. Add `data-cid="…"` to key containers if your markup has few ids. If the element can't be found any more, the comment has no pin, but it still opens from History.
115
+
116
+ **Theming.** Colors are CSS variables on `.pc-root` (`--pc-primary`, `--pc-dark`, `--pc-agent`, …). Override them in your own CSS.
117
+
118
+ ## Working with a coding agent
119
+
120
+ The agent reads and resolves comments in the JSON file directly, or through the CLI:
121
+
122
+ ```bash
123
+ npx proto-comments list # marked "For Agent" if any, otherwise all open
124
+ npx proto-comments list --all # also --open, --queued
125
+ npx proto-comments show 3 # selector, block, screen, replies
126
+ npx proto-comments resolve 3 "Shortened the subtitle to one sentence"
127
+ npx proto-comments reply 4 "Should this apply to the popup too?"
128
+ ```
129
+
130
+ Options: `--file <path>`, `--author <name>` (default `Agent`). A comment or reply can be up to 5000 characters.
131
+
132
+ The page doesn't poll. It reloads comments when its tab becomes active again, so the agent's replies show up when you come back.
133
+
134
+ Add this to your `CLAUDE.md` or `AGENTS.md`:
135
+
136
+ ```md
137
+ ## Review comments
138
+ Comments on the prototype live in `comments/comments.json` (proto-comments).
139
+ - "apply the comments": apply the ones marked "For Agent" (`queued: true`); if none are marked, every open comment. "apply 2 and 5": only those numbers.
140
+ - `npx proto-comments list` shows that set; `show <n>` gives the selector, block and screen to find the component.
141
+ - After a change: `npx proto-comments resolve <n> "what changed"`. If a comment is unclear, don't change code: `reply <n> "question"` and leave it open.
142
+ ```
143
+
144
+ ### Comment format
145
+
146
+ ```json
147
+ {
148
+ "id": "c1a2b3", "n": 3, "status": "open", "queued": true,
149
+ "screen": "settings:preferences", "screenLabel": "Settings · Preferences", "context": "",
150
+ "anchor": { "selector": "#main > div.card > h3", "block": "Homepage", "text": "Default page", "tag": "h3", "offset": { "x": 0.4, "y": 0.5 } },
151
+ "text": "Rename to Start page", "author": "Anna", "createdAt": "2026-10-22T10:00:00.000Z",
152
+ "replies": [{ "author": "Agent", "text": "Renamed", "createdAt": "…" }],
153
+ "snapshot": {}
154
+ }
155
+ ```
156
+
157
+ A resolved comment also has `resolvedBy` (the `author` prop, or `--author` in the CLI) and `resolvedAt`. Reopening clears them.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import http from 'node:http';
3
+ import { createStorage, DEFAULT_FILE } from '../server/storage.js';
4
+
5
+ const HELP = `proto-comments — review comments on a prototype
6
+
7
+ proto-comments serve [--port 4848] local server for apps without their own API route (Vite etc.)
8
+ proto-comments list [--queued|--open|--all] default: what "apply the comments" covers
9
+ proto-comments show <n> one comment with its anchor and replies
10
+ proto-comments resolve <n> "what changed" close it with a reply
11
+ proto-comments reply <n> "question" reply and keep it open
12
+
13
+ Options: --file <path> (default ${DEFAULT_FILE}), --author <name> (default Agent)`;
14
+
15
+ function parse(argv) {
16
+ const opts = { _: [] };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a.startsWith('--')) {
20
+ const key = a.slice(2);
21
+ const next = argv[i + 1];
22
+ if (['file', 'port', 'author'].includes(key)) { opts[key] = next; i++; } else opts[key] = true;
23
+ } else opts._.push(a);
24
+ }
25
+ return opts;
26
+ }
27
+
28
+ const place = a => [...new Set([a.block, a.text].filter(Boolean))].join(' › ') || `<${a.tag}>`;
29
+ const line = c => `${c.status === 'resolved' ? '✓' : `#${c.n}`} ${c.status}${c.resolvedBy ? ` by ${c.resolvedBy}` : ''}${c.queued ? ' [For Agent]' : ''} · ${c.screenLabel} · ${place(c.anchor)}\n ${c.text}`;
30
+ /** Marked "For Agent" if any are marked, otherwise every open comment. */
31
+ const todo = list => { const open = list.filter(c => c.status === 'open'); const q = open.filter(c => c.queued); return q.length ? q : open; };
32
+
33
+ function serve(storage, port) {
34
+ const server = http.createServer(async (req, res) => {
35
+ const origin = req.headers.origin;
36
+ /* Local prototypes only: a page on another site must not be able to write comments, even with a "simple" request. */
37
+ const local = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin || '');
38
+ if (origin && !local) return res.writeHead(403).end();
39
+ if (local) {
40
+ res.setHeader('Access-Control-Allow-Origin', origin);
41
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
42
+ res.setHeader('Access-Control-Allow-Headers', 'content-type');
43
+ }
44
+ if (req.method === 'OPTIONS') return res.writeHead(204).end();
45
+ if (req.method !== 'GET' && !String(req.headers['content-type']).startsWith('application/json')) return res.writeHead(415).end();
46
+ if (!req.url.startsWith('/api/comments')) return res.writeHead(404).end();
47
+ let raw = '';
48
+ for await (const chunk of req) raw += chunk;
49
+ try {
50
+ const { status, json } = await storage.handle(req.method, raw ? JSON.parse(raw) : null);
51
+ res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(json));
52
+ } catch (e) {
53
+ res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({ error: e.message }));
54
+ }
55
+ });
56
+ server.listen(port, '127.0.0.1', () => {
57
+ console.log(`proto-comments: http://localhost:${port}/api/comments → ${storage.file}`);
58
+ });
59
+ }
60
+
61
+ async function main() {
62
+ const opts = parse(process.argv.slice(2));
63
+ const [cmd, n, ...words] = opts._;
64
+ const storage = createStorage(opts.file);
65
+ /* Resolved comments can share a number with an open one; the open one wins. */
66
+ const find = list => list.find(c => c.n === Number(n) && c.status === 'open') || list.findLast(c => c.n === Number(n)) || (console.error(`No comment #${n}`), process.exit(1));
67
+
68
+ if (cmd === 'serve') return serve(storage, Number(opts.port) || 4848);
69
+ const list = await storage.read();
70
+ if (cmd === 'list') {
71
+ const shown = opts.queued ? list.filter(c => c.queued && c.status === 'open')
72
+ : opts.open ? list.filter(c => c.status === 'open') : opts.all ? list : todo(list);
73
+ return console.log(shown.map(line).join('\n') || 'No comments.');
74
+ }
75
+ if (cmd === 'show') {
76
+ const c = find(list);
77
+ console.log(`${line(c)}\n screen: ${c.screen}${c.context ? ` (${c.context})` : ''}\n selector: ${c.anchor.selector}`);
78
+ return c.replies.forEach(r => console.log(` ↳ ${r.author}: ${r.text}`));
79
+ }
80
+ if (cmd === 'resolve' || cmd === 'reply') {
81
+ const c = find(list);
82
+ const author = opts.author || 'Agent';
83
+ const at = new Date().toISOString();
84
+ c.replies.push({ author, text: words.join(' ') || 'Done.', createdAt: at });
85
+ if (cmd === 'resolve') Object.assign(c, { status: 'resolved', queued: false, resolvedBy: author, resolvedAt: at });
86
+ await storage.write(list);
87
+ return console.log(line(c));
88
+ }
89
+ console.log(HELP);
90
+ }
91
+
92
+ main();