@augurworks/augur 0.15.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 (190) hide show
  1. package/CANVAS.md +858 -0
  2. package/INSTALL.md +603 -0
  3. package/LICENSE +27 -0
  4. package/README.md +150 -0
  5. package/TRADEMARKS.md +72 -0
  6. package/agents/README.md +65 -0
  7. package/agents/canon-extract.md +117 -0
  8. package/agents/canon.md +151 -0
  9. package/agents/canvas.md +88 -0
  10. package/agents/currency.md +74 -0
  11. package/agents/drafts.md +92 -0
  12. package/agents/identity.md +148 -0
  13. package/agents/prototype-contract.md +69 -0
  14. package/agents/publishing.md +324 -0
  15. package/agents/review-feedback.md +76 -0
  16. package/agents/space-json.md +132 -0
  17. package/agents/ui-skill.md +101 -0
  18. package/agents/working-marks.md +86 -0
  19. package/brand/augur-eye.svg +3 -0
  20. package/brand/augur-icon.svg +41 -0
  21. package/brand/augur-mark.svg +5 -0
  22. package/brand/lentianova-bulky.otf +0 -0
  23. package/brand/lentianova-median.otf +0 -0
  24. package/brand/lentianova.woff2 +0 -0
  25. package/build.js +8409 -0
  26. package/changelog.md +133 -0
  27. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/package.json +57 -0
  29. package/pitis/PETS.md +121 -0
  30. package/pitis/README.md +117 -0
  31. package/pitis/index.html +266 -0
  32. package/pitis/piti.build.js +81 -0
  33. package/pitis/piti.js +929 -0
  34. package/pitis/roast-agent.md +185 -0
  35. package/realtime/src/index.js +119 -0
  36. package/realtime/wrangler.example.toml +44 -0
  37. package/scripts/adopt.mjs +106 -0
  38. package/scripts/board-snapshot.mjs +197 -0
  39. package/scripts/build-seed-pack.mjs +33 -0
  40. package/scripts/bundle-rekey.mjs +112 -0
  41. package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
  42. package/scripts/canon-extract.mjs +578 -0
  43. package/scripts/canon.mjs +599 -0
  44. package/scripts/canvas-screen.mjs +333 -0
  45. package/scripts/check-local.mjs +136 -0
  46. package/scripts/clawd-art.mjs +75 -0
  47. package/scripts/clawd-canvas.mjs +741 -0
  48. package/scripts/cli.mjs +100 -0
  49. package/scripts/clone.mjs +422 -0
  50. package/scripts/close.mjs +30 -0
  51. package/scripts/connect.mjs +103 -0
  52. package/scripts/deploy.mjs +157 -0
  53. package/scripts/dev.mjs +54 -0
  54. package/scripts/doc-lint.mjs +143 -0
  55. package/scripts/export.mjs +300 -0
  56. package/scripts/fork.mjs +64 -0
  57. package/scripts/freeze.mjs +74 -0
  58. package/scripts/frontdoor-parity.mjs +180 -0
  59. package/scripts/hook.mjs +67 -0
  60. package/scripts/identity-rekey.mjs +112 -0
  61. package/scripts/init.mjs +122 -0
  62. package/scripts/land.mjs +40 -0
  63. package/scripts/lib/adapters.mjs +156 -0
  64. package/scripts/lib/board-snapshot.mjs +511 -0
  65. package/scripts/lib/draft.mjs +385 -0
  66. package/scripts/lib/graduate.mjs +276 -0
  67. package/scripts/lib/instance.mjs +74 -0
  68. package/scripts/lib/marks.mjs +107 -0
  69. package/scripts/lib/materialize.mjs +138 -0
  70. package/scripts/lib/merge3.mjs +124 -0
  71. package/scripts/lib/offline-posture.mjs +120 -0
  72. package/scripts/lib/offline-respawn.mjs +18 -0
  73. package/scripts/lib/offline-wrangler.mjs +78 -0
  74. package/scripts/lib/publish-compose.mjs +12 -0
  75. package/scripts/lib/publish-conflict.mjs +78 -0
  76. package/scripts/lib/publish-evidence.mjs +144 -0
  77. package/scripts/lib/refine-compare.mjs +140 -0
  78. package/scripts/lib/refine-ledger.mjs +150 -0
  79. package/scripts/lib/refine-png.mjs +149 -0
  80. package/scripts/lib/seed-pack-build.mjs +141 -0
  81. package/scripts/lib/state-compare.mjs +68 -0
  82. package/scripts/lib/store.mjs +122 -0
  83. package/scripts/login.mjs +84 -0
  84. package/scripts/mark.mjs +112 -0
  85. package/scripts/migrate-board-keys.mjs +164 -0
  86. package/scripts/migrate.mjs +265 -0
  87. package/scripts/no-foreign-vocabulary.mjs +216 -0
  88. package/scripts/no-product-names.mjs +157 -0
  89. package/scripts/no-tenant-globals.mjs +1134 -0
  90. package/scripts/offline.mjs +247 -0
  91. package/scripts/og.mjs +170 -0
  92. package/scripts/one-tenant-resolver.mjs +146 -0
  93. package/scripts/open.mjs +63 -0
  94. package/scripts/publish.mjs +1041 -0
  95. package/scripts/read.mjs +31 -0
  96. package/scripts/refine.mjs +455 -0
  97. package/scripts/release-drift.mjs +138 -0
  98. package/scripts/restore.mjs +322 -0
  99. package/scripts/review.mjs +125 -0
  100. package/scripts/save.mjs +21 -0
  101. package/scripts/shell-lint.mjs +189 -0
  102. package/scripts/ship.mjs +460 -0
  103. package/scripts/shoot.mjs +264 -0
  104. package/scripts/smoke-invite.mjs +113 -0
  105. package/scripts/state-inventory.mjs +223 -0
  106. package/scripts/status.mjs +177 -0
  107. package/scripts/sync.mjs +27 -0
  108. package/scripts/tenant-do-rehearsal.mjs +1314 -0
  109. package/scripts/ui-copy-lint.mjs +202 -0
  110. package/scripts/watch.mjs +32 -0
  111. package/scripts/wrangler-preflight.mjs +246 -0
  112. package/seed/CANON.md +43 -0
  113. package/seed/README.md +119 -0
  114. package/seed/prototype-status.json +9 -0
  115. package/seed/registry.json +88 -0
  116. package/seed/skills/starter-ui/SKILL.md +52 -0
  117. package/seed/skills/starter-ui/skill.json +11 -0
  118. package/seed/skills/starter-ui/starter-tokens.css +73 -0
  119. package/seed/skills/starter-ui/starter-ui.css +318 -0
  120. package/seed/skills/starter-ui/starter-ui.js +50 -0
  121. package/seed/space.json +5 -0
  122. package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
  123. package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
  124. package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
  125. package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
  126. package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
  127. package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
  128. package/seed/threads.json +52 -0
  129. package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
  130. package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
  131. package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
  132. package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
  133. package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
  134. package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
  135. package/src/_worker.js +12668 -0
  136. package/src/board-key.mjs +57 -0
  137. package/src/board-room.mjs +881 -0
  138. package/src/bundle-keys.mjs +138 -0
  139. package/src/canon/collect-in-browser.js +215 -0
  140. package/src/canon/emit.mjs +232 -0
  141. package/src/canon/observe.mjs +260 -0
  142. package/src/canon/schema.mjs +451 -0
  143. package/src/canvas/DSEG-LICENSE.txt +95 -0
  144. package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
  145. package/src/canvas/canvas.css +885 -0
  146. package/src/canvas/canvas.js +5780 -0
  147. package/src/canvas/capture.js +485 -0
  148. package/src/chrome/appchrome.mjs +568 -0
  149. package/src/currency.mjs +203 -0
  150. package/src/delete-confirmation.mjs +254 -0
  151. package/src/drafts/drafts.js +326 -0
  152. package/src/entry.js +58 -0
  153. package/src/galleries.mjs +400 -0
  154. package/src/health-cron.mjs +173 -0
  155. package/src/identity.json +1 -0
  156. package/src/kv-codec.mjs +135 -0
  157. package/src/kv-identity.mjs +259 -0
  158. package/src/mail.mjs +599 -0
  159. package/src/provenance.mjs +74 -0
  160. package/src/publish-compose.mjs +363 -0
  161. package/src/publish-fork.mjs +221 -0
  162. package/src/publish-units.mjs +44 -0
  163. package/src/purge.mjs +81 -0
  164. package/src/review/COMMENTING-UX.md +306 -0
  165. package/src/review/cat.png +0 -0
  166. package/src/review/comment-cursor.svg +7 -0
  167. package/src/review/comments.js +1738 -0
  168. package/src/room-ticket.mjs +96 -0
  169. package/src/roster-chip.mjs +32 -0
  170. package/src/seed-pack.mjs +261 -0
  171. package/src/state-inventory.mjs +249 -0
  172. package/src/sw-logic.mjs +48 -0
  173. package/src/tenant-cache.mjs +141 -0
  174. package/src/tenant-context.mjs +386 -0
  175. package/src/tenant-do.js +2940 -0
  176. package/src/tenant-host.mjs +144 -0
  177. package/src/tenant-quotas.mjs +140 -0
  178. package/src/unit-core.mjs +103 -0
  179. package/src/unit-object.mjs +338 -0
  180. package/templates/README.md +67 -0
  181. package/templates/shell/deploy.yml +145 -0
  182. package/templates/shell/engine-bump.yml +142 -0
  183. package/templates/shell/health.yml +233 -0
  184. package/templates/shell/kv-backup.yml +201 -0
  185. package/templates/shell/roster-update.yml +110 -0
  186. package/templates/shell/space-preflight.yml +35 -0
  187. package/templates/shell/space-rebake.yml +79 -0
  188. package/templates/shell/store-backup.yml +152 -0
  189. package/templates/shell/wrangler.example.toml +266 -0
  190. package/templates/space/publish.yml +91 -0
package/src/purge.mjs ADDED
@@ -0,0 +1,81 @@
1
+ // Redacting one person out of a conversation other people are part of.
2
+ //
3
+ // `E-gdpr-purge-user`. Extracted from src/_worker.js so the WORKSPACE OBJECT can run the
4
+ // same sweep the worker runs. Under Decision 2 an erasure has to happen in every workspace
5
+ // the account belongs to, and only the control plane knows which those are — so the sweep
6
+ // has to be reachable as a workspace verb, not only as an admin action inside one workspace
7
+ // by somebody who happens to administer it. Two copies of this logic would be two answers
8
+ // to "was this person erased", and the one that gets used less is the one that rots.
9
+ //
10
+ // ── WHAT AN ERASURE KEEPS ───────────────────────────────────────────────────────────
11
+ //
12
+ // `body` and `at` survive, and so does thread structure. Deleting the messages would erase
13
+ // other people's conversation — a reply that answers a question is unreadable once the
14
+ // question is gone — and the request is to stop identifying somebody, not to rewrite a
15
+ // record other people are part of. So the message stays and stops carrying a person:
16
+ // `author` becomes a fixed sentinel, `by` is cleared, and `verified` goes false so nothing
17
+ // renders it as a confirmed identity.
18
+ //
19
+ // ── ⚠️ IDENTIFICATION IS BY A 32-BIT HASH, AND THAT IS THE SHARP EDGE ───────────────
20
+ //
21
+ // Messages store `by: personId(email)`, a one-way djb2 hash — deliberately, because an
22
+ // address in every stored message would be reversible PII and `/__people` is ungated on
23
+ // public prototypes precisely because ids cannot be reversed. Do NOT "fix" that by storing
24
+ // the address.
25
+ //
26
+ // The consequence is that two addresses can share an id, and a purge keyed on it would then
27
+ // redact an innocent third party's messages too. A machine cannot choose between them, so
28
+ // nothing here tries: the caller checks the workspace roster for any OTHER member sharing
29
+ // the id and REFUSES, naming the count. That turns a silent over-redaction into a question
30
+ // for a person, which is the only honest answer available.
31
+
32
+ /** What a redacted message says instead of a name. One string, so nothing invents a second. */
33
+ export const PURGED_AUTHOR = "Deleted user";
34
+
35
+ /** The address folding this module uses. Matches the worker's `lcEmail`. */
36
+ export const lcAddress = (e) => String(e == null ? "" : e).trim().toLowerCase();
37
+
38
+ /**
39
+ * The one-way author id. djb2 over the folded address, base 36.
40
+ *
41
+ * ⚠️ ITS OUTPUT IS STORED IN EVERY MESSAGE EVER WRITTEN, so this function is a data format
42
+ * and not an implementation detail. Changing it orphans every stored `by`, which means every
43
+ * past erasure silently stops matching and every "is this mine" check answers no.
44
+ */
45
+ export function personIdFor(email) {
46
+ const s = lcAddress(email);
47
+ let h = 5381;
48
+ for (let i = 0; i < s.length; i++) h = (h * 33 + s.charCodeAt(i)) >>> 0;
49
+ return h.toString(36);
50
+ }
51
+
52
+ /** Redact one person from one thread array. Pure — every caller does its own I/O. */
53
+ export function purgeThreads(threads, id) {
54
+ let redacted = 0;
55
+ const out = (Array.isArray(threads) ? threads : []).map((t) => {
56
+ if (!t || !Array.isArray(t.messages)) return t;
57
+ let touched = false;
58
+ const messages = t.messages.map((m) => {
59
+ if (!m || m.by !== id) return m;
60
+ touched = true; redacted++;
61
+ // Spread first so any field a future version adds survives an erasure written before
62
+ // it existed; the three that identify are then overwritten by name.
63
+ return { ...m, author: PURGED_AUTHOR, by: null, verified: false };
64
+ });
65
+ return touched ? { ...t, messages } : t;
66
+ });
67
+ return { threads: out, redacted };
68
+ }
69
+
70
+ /**
71
+ * Which OTHER members share this address's author id. Empty means the sweep is safe.
72
+ * `members` is anything with an `email` — the worker's roster rows and the workspace
73
+ * object's `members` table both qualify.
74
+ */
75
+ export function idCollisions(members, email) {
76
+ const addr = lcAddress(email);
77
+ const id = personIdFor(addr);
78
+ return (members || [])
79
+ .map((u) => lcAddress(u && u.email))
80
+ .filter((e) => e && e !== addr && personIdFor(e) === id);
81
+ }
@@ -0,0 +1,306 @@
1
+ # Commenting UX — compose & thread redesign (build plan)
2
+
3
+ > **Historical** — the original build plan for the comments overlay, kept as a design record.
4
+ > Current behavior is the code (`src/review/comments.js`); do not treat "what's there today" claims below as current.
5
+
6
+ Target spec for upgrading the review overlay (`comments.js`) to a modern
7
+ commenting interaction model, captured from a set of annotated reference
8
+ screenshots. This is a **build plan**, not a description of
9
+ current behavior: each state below states the *target*, the *interaction detail*
10
+ (transitions, focus, keyboard, edge cases), and **what's there today vs. what to
11
+ build** against the current implementation.
12
+
13
+ > Scope note: the overlay lives entirely in `src/review/comments.js` (shadow-DOM,
14
+ > single IIFE). Everything here is implementable inside that file + its stylesheet
15
+ > block (`root.innerHTML` `<style>`, comments.js:211). KV/worker changes are flagged
16
+ > where a state needs persisted data we don't store yet.
17
+
18
+ ---
19
+
20
+ ## State machine (overview)
21
+
22
+ ```
23
+ click canvas first keypress
24
+ (browse) ─────────────▶ [pin + collapsed input] ─────────────▶ [compose box + toolbar]
25
+ │ Esc / blur-empty │ text grows box
26
+ └──────────────┐ │ @ / emoji / image
27
+ ▼ ▼
28
+ (discard) submit (⏎ / arrow)
29
+
30
+ hover pin ▼
31
+ [numbered pin] ───────────▶ [hover preview card] ──click──▶ [open thread]
32
+
33
+ ┌───────────────────────┤
34
+ reply box (same compose UI) resolve ✓ / close ✕ / ⋯
35
+ ```
36
+
37
+ Five visible compose/read surfaces, mapped to the reference image's six panels:
38
+
39
+ | Reference panel | Surface |
40
+ |------------------------|------------------------------------------|
41
+ | *when dropping pin* | Collapsed compose (pin + 1-line input) |
42
+ | *when starting to write* | Expanded compose (box + toolbar) |
43
+ | *longer write* | Expanded compose, auto-grown + @mentions |
44
+ | (emoji grid) | Emoji picker popover |
45
+ | *preview on hover* | Hover preview card (read-only) |
46
+ | (open thread + reply) | Open thread + reply compose |
47
+
48
+ ---
49
+
50
+ ## 1. Dropping a pin — collapsed compose
51
+
52
+ **Target.** Clicking the canvas drops the pin glyph and, immediately to its right,
53
+ a single-line pill input reading **"Add a comment"** with a circular send arrow on
54
+ the trailing edge (disabled/greyed until there's text). No toolbar yet, no name
55
+ field, nothing else. It reads as one lightweight affordance, not a form.
56
+
57
+ **Interaction detail.**
58
+ - The pin appears at the click point; the input anchors beside it (right by
59
+ default, flips left if it would overflow the viewport — reuse the clamp logic in
60
+ `positionCard`, comments.js:481).
61
+ - Input autofocuses. Caret is in the field; the user can type immediately.
62
+ - **Esc** or **clicking away while empty** discards the whole thing (pin + input)
63
+ with no persistence — this is a *pending* pin, not a saved comment.
64
+ - Send arrow is disabled while empty; pressing **⏎** with text submits (see §2).
65
+ - The pin has no number yet — it's numbered only once the comment is saved and
66
+ enters `listed()` (comments.js:338).
67
+
68
+ **Today vs. build.**
69
+ - Today: the catcher click goes straight to `composeNew` (comments.js:631, :490),
70
+ which renders the *full* card (heading, optional name input, multi-line
71
+ textarea, anno toggle, Cancel/Comment buttons). There is no collapsed step.
72
+ - Build: split `composeNew` into a **collapsed → expanded** progression. Start
73
+ with a compact `.compose.collapsed` element (pin + 1-line input + send). Keep the
74
+ `anchorAt()` capture (comments.js:179) exactly as-is — that's the pin's anchor and
75
+ is orthogonal to the compose chrome.
76
+
77
+ ---
78
+
79
+ ## 2. Starting to write — expanded compose + toolbar
80
+
81
+ **Target.** On the first keypress the collapsed pill **expands** into a rounded
82
+ card: a multi-line text area on top, and a toolbar row beneath it with three icon
83
+ buttons — **emoji** (☺), **@mention**, **image** — on the left, and the blue
84
+ circular **send** arrow on the right (now enabled).
85
+
86
+ **Interaction detail.**
87
+ - Transition is the input *growing into* the box, not a swap — the typed text
88
+ stays, caret position preserved. Animate height if cheap; never lose focus.
89
+ - Toolbar buttons:
90
+ - **Emoji** → opens the emoji picker popover (§4); inserts at caret.
91
+ - **@mention** → inserts `@` at caret and opens the mention autocomplete (§3).
92
+ - **Image** → file picker; see §6 for the storage decision (currently unsolved).
93
+ - **Send**: blue filled circle + up-arrow. Enabled iff trimmed text is non-empty.
94
+ Click or **⏎** submits. **Shift+⏎** inserts a newline (don't submit).
95
+ - On submit: persist via `mutate({op:"add", …})` (comments.js:522), collapse the
96
+ compose, and render the saved pin (numbered) + open thread — first send drops
97
+ you into the open thread.
98
+
99
+ **Today vs. build.**
100
+ - Today: the textarea exists (`.tx`, comments.js:498) but there's **no toolbar**,
101
+ no emoji/@/image, and submit is a labelled "Comment" button (comments.js:500) with
102
+ no ⏎-to-send.
103
+ - Build: add the toolbar row + send-arrow button; wire ⏎/Shift+⏎ key handling on
104
+ the textarea; auto-grow (§3). The name capture (`needName`, comments.js:496) should
105
+ move out of the inline form — the reference identifies the author by avatar, not a
106
+ text field. Capture the name **once** (first-run prompt or a small identity chip), then
107
+ represent the author as an avatar everywhere (see §7, Open questions).
108
+
109
+ ---
110
+
111
+ ## 3. Longer write — auto-grow + @mentions
112
+
113
+ **Target.** As content grows the box grows with it (the *"auto-box increase"* in
114
+ the reference). `@`-mentions render inline as a coloured token (the name in the
115
+ accent colour, e.g. "Ana" in blue), distinct from body text.
116
+
117
+ **Interaction detail.**
118
+ - **Auto-grow:** textarea height tracks content up to a max (then it scrolls).
119
+ Implement by setting `height:auto; height:scrollHeight` on each `input` event, or
120
+ a mirror/`field-sizing` approach. The toolbar stays pinned to the bottom edge.
121
+ - **@mention flow:** typing `@` opens an autocomplete list of known
122
+ participants/teammates; arrow-keys + ⏎ to pick; Esc closes the list without
123
+ inserting. A committed mention is a styled, atomic token — backspace deletes the
124
+ whole token, not one character.
125
+ - Rendering mentions requires storing the message as **rich content** (text +
126
+ mention spans), not the current plain `body` string (comments.js:520). Either a
127
+ lightweight markup (`@[name](id)`) parsed at render, or a structured
128
+ `{ text, mentions:[{offset,len,id,name}] }`. Pick one and use it for both compose
129
+ and the message list (§6). **Never** `innerHTML` raw user text — the current code
130
+ correctly uses `textContent` (comments.js:549); the mention renderer must tokenise
131
+ and build nodes, not interpolate strings.
132
+
133
+ **Today vs. build.**
134
+ - Today: fixed `min-height:64px; resize:vertical` textarea (comments.js:273); plain
135
+ string body; no mentions.
136
+ - Build: auto-grow handler; rich-content model; mention autocomplete + token
137
+ rendering. **Mention directory is an open question** — see §7.
138
+
139
+ ---
140
+
141
+ ## 4. Emoji picker
142
+
143
+ **Target.** A popover with: a category tab strip across the top (recent ⏱, smileys,
144
+ nature, food, activity, travel, objects, symbols, flags), a **Search** field, a
145
+ **Frequently used** row, then **Smileys & People** and the rest in a scrollable
146
+ grid. A skin-tone selector sits bottom-right.
147
+
148
+ **Interaction detail.**
149
+ - Opens anchored to the emoji toolbar button; flips to stay on-screen (reuse the
150
+ viewport clamp pattern from `showTip`, comments.js:455).
151
+ - Picking an emoji inserts it at the caret and keeps the picker
152
+ open for multi-pick; click-away or Esc closes it.
153
+ - Search filters the grid live by name/keyword.
154
+ - "Frequently used" is per-user, persisted to `localStorage` (same pattern as
155
+ `LS_NAME`, comments.js:42) — no server needed.
156
+
157
+ **Today vs. build.** Not present at all. This is the heaviest new piece.
158
+ - Build: ship a small emoji dataset (categories + keywords) as a local module —
159
+ **do not** pull a CDN/npm picker; prototypes are self-contained static files
160
+ (CLAUDE.md). A compact JSON of common emoji + a simple grid renderer inside the
161
+ shadow root keeps it dependency-free. Defer the full Unicode set unless asked;
162
+ "Frequently used + Smileys & People + search" covers the reference.
163
+
164
+ ---
165
+
166
+ ## 5. Preview on hover
167
+
168
+ **Target.** Hovering a pin shows a small read-only **preview card**: author
169
+ name, relative time ("just now"), and the comment body truncated with an ellipsis
170
+ (*"Adding a really long comment, I really don't need the @ or the images (well, can
171
+ you highlight…"*). It does **not** show replies or any input — it's a peek. Click
172
+ opens the full thread (§6).
173
+
174
+ **Interaction detail.**
175
+ - **Grow-from-pin animation (the reference image):** the card **unfurls out of the
176
+ pin, left edge → right**. The pin sits at the card's left; the card scales/expands
177
+ rightward from there. Implement with `transform-origin: left center` + a
178
+ `scaleX`/width-and-opacity transition (~160–200ms, ease-out); the pin stays put and
179
+ the body grows out of it. (Today's `.atip` grows *upward* from centre,
180
+ comments.js:228–232 — this is a different origin: anchor left, expand right.)
181
+ - Default placement is to the **right** of the pin; flip to the left if it would
182
+ overflow the right edge (mirror the viewport clamp in `showTip`, comments.js:455).
183
+ - Hover-intent: small delay in (~150ms) so sweeping the canvas doesn't flash cards;
184
+ hide on mouse-leave unless the cursor moves onto the preview itself.
185
+ - Truncate body to ~3 lines via CSS line-clamp; never render the full thread here.
186
+ - Touch: no hover — tap opens the thread directly.
187
+
188
+ **Today vs. build.**
189
+ - Today: hover preview exists **only for annotations in delivery mode** — the blue
190
+ `.atip` bubble shows the note text (comments.js:451, :228). Normal comment pins in
191
+ review mode have only a native `title` tooltip (comments.js:396).
192
+ - Build: extend hover preview to all pins in review mode, upgraded from a text
193
+ bubble to an author + relative-time + snippet card (no avatar) that grows from the
194
+ pin left→right. The annotation delivery-mode bubble can stay as-is (it's a
195
+ different, simpler affordance) or share the card styling.
196
+
197
+ ---
198
+
199
+ ## 6. Open thread + reply
200
+
201
+ > **Copy this card as-is — the user loves it.** Replicate the layout, spacing, and
202
+ > chrome of the reference exactly: rounded white card, "Comment" header, bold
203
+ > name + grey relative time per message, the accent-coloured `@mention` and
204
+ > name token in body text, and the rounded grey **"Reply"** pill with an inner
205
+ > circular send arrow on its trailing edge. Match it pixel-faithfully, then map the
206
+ > header icons to our semantics below. **Avatars:** a signed-in author
207
+ > renders as their photo or initials; an anonymous one keeps the bold name and a
208
+ > numbered pin. **White only** — no dark-mode
209
+ > variant.
210
+
211
+ **Target.** The full thread card. Header: **"Comment"** title on the left; on the
212
+ right, **four** icon buttons in this order — **⋯**, the **annotation (cat) toggle**,
213
+ **resolve ✓** (circle-check), and **✕** close. Body: each message as bold
214
+ name + grey relative time + per-message **⋯** + the text. Footer: a **reply** compose
215
+ box (collapsed "Reply" pill → expands to the §2–4 toolbar compose on focus).
216
+
217
+ **Header icon mapping (our semantics — not the reference's):**
218
+
219
+ | Icon | Our action |
220
+ |-------------------------|-------------------------------------------------------------------|
221
+ | **⋯** (three dots) | **Delete** the thread (this is our delete affordance, not a menu) |
222
+ | **cat avatar** | **Annotation toggle** — promote/demote the always-on dev note |
223
+ | **✓** (circle-check) | **Resolve** / reopen |
224
+ | **✕** | **Close** the card (browse stays on) |
225
+
226
+ **Interaction detail.**
227
+ - **⋯ = Delete.** It is *not* an overflow menu — clicking it deletes the thread
228
+ (keep the existing confirm, comments.js:568–571). Style it as a three-dot
229
+ glyph, in the header's top-right cluster.
230
+ - **Cat toggle** sits next to resolve, carrying the annotation promotion that's
231
+ inline today (`anno-toggle`, comments.js:561). Same on/off visual it already has
232
+ (greyscale ↔ amber border, comments.js:264–266), just relocated into the header
233
+ row. Keep the title/tooltip copy.
234
+ - **Resolve ✓** toggles resolved (green pin, comments.js:218); resolved threads stay
235
+ listed and reopenable. **✕** closes the card.
236
+ - Each message's **⋯** → delete that message (per-message; today only whole threads
237
+ delete). Lower priority than the header cluster — build last.
238
+ - Reply: collapsed grey **"Reply"** pill with inner send arrow (disabled until text);
239
+ on focus/keypress it expands into the same toolbar compose as §2–4 (auto-grow, ⏎
240
+ sends, Shift+⏎ newline).
241
+ - **Relative time:** show "Just now" / "1 minute ago". Replace the absolute
242
+ `fmt()` (comments.js:611) with a relative formatter (re-tick open cards on an
243
+ interval so "Just now" ages to "1 minute ago").
244
+
245
+ **Today vs. build.**
246
+ - Today: header is `Comment/Annotation · resolved` text + an inline anno-toggle
247
+ avatar (comments.js:533–537); actions are labelled buttons in a footer row
248
+ (Delete / Resolve / Reply, comments.js:540–542); messages are name + absolute time
249
+ + body, no avatar, no per-message menu (comments.js:544–551); reply is a plain
250
+ textarea (comments.js:539).
251
+ - Build: restyle header to the four icon buttons (⋯ delete / cat annotate / ✓ resolve
252
+ / ✕ close); add per-message ⋯ (delete message, last); swap the reply textarea for
253
+ the expanding compose pill; relative timestamps; remove dark mode.
254
+
255
+ ---
256
+
257
+ ## 7. Open questions / decisions before building
258
+
259
+ **Locked for v1:** no avatars · white only · build all at once · **emoji, @mentions,
260
+ and images all deferred** → the compose box has **no toolbar row** for now (just the
261
+ auto-growing field + send arrow). Native OS emoji can still be typed into the field.
262
+ The annotation (cat) affordance is **removed from compose** — you create a plain
263
+ comment, then promote it via the cat icon in the open-thread header (§6).
264
+
265
+ These aren't visible in the image but the build can't proceed cleanly without them:
266
+
267
+ 1. **Avatars / identity — DECIDED.** Users exist (invite-only accounts, an admin
268
+ people table, per-person photo/initials/colour), so comments carry a `by` id
269
+ stamped from the session and resolve to a face via `/__people`.
270
+ Anonymous comments keep the numbered pin.
271
+ 2. **Theme — DECIDED: white only.** Remove the `prefers-color-scheme: dark` block
272
+ (comments.js:282–292). Go all-white for compose/thread chrome.
273
+ 4. **@mention directory.** Where does the autocomplete list come from? There's no
274
+ user list in a static prototype. Options: a hardcoded teammate list per
275
+ prototype, or free-text `@anything` that just styles the token without resolving
276
+ to a real user. **Recommend free-text styled tokens** unless a real list exists.
277
+ 5. **Image upload.** KV stores small JSON thread blobs (comments.js:88); it's not an
278
+ image store. Options: base64-inline small images into the message (bloats KV,
279
+ simplest), upload to R2/an asset endpoint (new worker route), or **drop image
280
+ support for v1** and ship emoji + @ only (the reference author even says *"I don't
281
+ need images"*). **Recommend deferring images.**
282
+ 6. **Persistence shape.** Mentions/rich content change the stored `message` schema
283
+ (comments.js:519). Decide the model once (plain string vs. structured) — it
284
+ touches add, reply, render, and the resolve tooling (`scripts/review.mjs`).
285
+ 7. **Scope.** Build all surfaces at once, or stage it? Suggested order that keeps the
286
+ overlay shippable at each step: **(1)** collapsed→expanded compose + ⏎-to-send +
287
+ auto-grow → **(2)** relative time + header icon buttons + remove dark mode →
288
+ **(3)** hover preview card → **(4)** emoji picker → **(5)** @mentions →
289
+ **(6)** images (if at all).
290
+
291
+ ---
292
+
293
+ ## Invariants to preserve (don't regress these)
294
+
295
+ - **Shadow-DOM isolation** — all UI stays inside `root` (comments.js:208); never
296
+ leak styles to the host page.
297
+ - **No `innerHTML` of user text** — tokenise/`textContent` only (comments.js:549).
298
+ - **Screen contract** — pins stay anchored via `anchorAt`/`pinXY` + `data-gv-screen`
299
+ scoping (comments.js:179, :136); compose chrome must not touch anchoring.
300
+ - **KV-or-localStorage fallback** — every new field must round-trip through both
301
+ `mutate`/`apiCall` and `applyLocal` (comments.js:88–108).
302
+ - **Annotations** — the cat-avatar always-on delivery notes (comments.js:110, :340)
303
+ and their resolve-tooling exemption stay intact; the annotation toggle just moves
304
+ into the thread ⋯ menu.
305
+ - **Self-contained** — no external picker/emoji CDN; everything ships in-file
306
+ (CLAUDE.md prototype rules).
Binary file
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26">
2
+ <!-- The cursor for placing a comment: a white speech bubble outlined in black, its tail
3
+ pointing at the exact spot the pin will land (hotspot 4 23, the tail's tip). Flat by
4
+ design — cursor images are painted by the OS, where filters and shadows are unreliable. -->
5
+ <path d="M7 3 H20 A3 3 0 0 1 23 6 V15 A3 3 0 0 1 20 18 H10.5 L4 23 L6.5 18 A3 3 0 0 1 4 15 V6 A3 3 0 0 1 7 3 Z"
6
+ fill="#ffffff" stroke="#1a1a1a" stroke-width="1.8" stroke-linejoin="round"/>
7
+ </svg>