helldots 0.5.0 → 0.6.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/README.md CHANGED
@@ -141,6 +141,59 @@ Three ways out, cheapest first:
141
141
  - **Leave it.** Captures of such a page stay misaligned where text is
142
142
  concerned; everything else about them is correct.
143
143
 
144
+ ## Identity
145
+
146
+ HellDots authenticates nobody. It takes whoever your app says is signed in
147
+ and records that:
148
+
149
+ ```js
150
+ createCommentOverlay({
151
+ user: { name: currentUser.fullName, id: currentUser.id },
152
+ });
153
+ ```
154
+
155
+ `name` is the display name — it is what appears on every comment and reply.
156
+ `id` is optional, never rendered, and persisted as `authorId` on everything
157
+ that user creates. Pass it whenever two people on your team can share a
158
+ display name: without it they are indistinguishable in the record, and they
159
+ share one reaction.
160
+
161
+ ```js
162
+ overlay.serializeComments()[0];
163
+ // { author: "Ana Pérez", authorId: "u_42", ... }
164
+ ```
165
+
166
+ Both fields ride along in `serializeComments()` output, on comments and on
167
+ replies alike. **The display name travels with the record**, so a store that
168
+ holds nothing but comments — a database of its own, with no users table —
169
+ renders every author and every audit entry without a single lookup back into
170
+ your app. The id is opaque to HellDots: point it at your user table, at a
171
+ comments-only store, or at nothing. `authorId` is `null` when you pass no
172
+ `id`, and on records written before it existed — the field is additive, so no
173
+ stored corpus needs migrating.
174
+
175
+ What the denormalised name costs: a rename does not travel backwards. Old
176
+ comments keep the name that was current when they were written, which is what
177
+ an audit trail should do, and the id is what lets you reconcile if you want
178
+ the current one.
179
+
180
+ With no `id` at all, two people sharing a display name are one author. If your
181
+ app has no accounts, mint the id yourself — you control the key, the lifetime
182
+ and the consent story, which HellDots cannot:
183
+
184
+ ```js
185
+ const KEY = "my-app-anon-id";
186
+ let id = localStorage.getItem(KEY);
187
+ if (!id) localStorage.setItem(KEY, (id = crypto.randomUUID()));
188
+ createCommentOverlay({ user: { name: typedName, id } });
189
+ ```
190
+
191
+ Bear in mind what that identifies: a browser profile, not a person.
192
+
193
+ Whatever you declare here is taken at face value and stored as-is. The record
194
+ says what your application asserted about who acted; verifying that claim is
195
+ your backend’s job, and `onChange` carries every mutation to it.
196
+
144
197
  ## Triage
145
198
 
146
199
  Comments carry an optional type, priority and free-form tags. All three start
@@ -151,6 +204,12 @@ neutral: the person reporting can classify, or not.
151
204
  | `type` | `bug`, `suggestion`, `question`, `improvement`, or `null` |
152
205
  | `priority` | `high`, `medium`, `low`, or `null` |
153
206
  | `tags` | any strings — trimmed, lowercased and de-duplicated |
207
+ | `status` | `open`, `in_progress`, `in_review`, `resolved` |
208
+
209
+ The status is the one field that is never neutral: every comment starts `open`
210
+ and moves through the lifecycle in any order. `open` is the only state painted
211
+ in an unsaturated off-white, so the three states somebody actually moved a
212
+ comment into are the ones that stand out.
154
213
 
155
214
  The inbox filters on all of them, combined with page and status. Resolved
156
215
  comments show how long they took, measured from creation to resolution.
@@ -165,6 +224,129 @@ overlay.setCommentStatus(id, "resolved"); // stamps the resolution time
165
224
  Passing `null` to `setCommentType` or `setCommentPriority` returns the field to
166
225
  its neutral state. Reopening a resolved comment clears its resolution time.
167
226
 
227
+ ### Reactions
228
+
229
+ Comments and replies take one of six reactions — 👍 👎 ❤️ 🎉 👀 🚀 — so a team
230
+ can agree, flag "watching this" or mark something shipped without adding a
231
+ reply. The set is fixed: a searchable picker would need an emoji dataset
232
+ larger than the whole widget.
233
+
234
+ The emoji button in a comment's action strip (or on a reply's meta line) is
235
+ where a reaction starts. Once there is one, a row of pills sits under the
236
+ comment — below its screenshot when it has one — and carries its own button for
237
+ adding another. Nothing is shown there until somebody reacts.
238
+
239
+ ```js
240
+ overlay.toggleCommentReaction(id, "👍");
241
+ overlay.toggleReplyReaction(commentId, replyId, "🎉");
242
+ ```
243
+
244
+ Both toggle: reacting again with the same emoji removes it. A reaction is
245
+ stored against `user.id` when you pass one, and against `user.name`
246
+ otherwise — so give HellDots an `id` if two people on your team can share a
247
+ display name:
248
+
249
+ ```js
250
+ createCommentOverlay({ user: { name: currentUser.name, id: currentUser.id } });
251
+ ```
252
+
253
+ Reactions ride along in `serializeComments()` output as `reactions`, an
254
+ `{ emoji: actorKey[] }` map that is `null` when nobody has reacted. The pills
255
+ show counts, never who reacted: the stored keys are your ids, and they stay
256
+ out of the UI.
257
+
258
+ ### Audit trail
259
+
260
+ Every comment carries an append-only log of what happened to it — who created
261
+ it, edited its text, moved its status or changed its classification, and when.
262
+ It shows up as a folded `History (n)` disclosure in the inbox detail, next to
263
+ the context block.
264
+
265
+ ```js
266
+ overlay.serializeComments()[0].history;
267
+ // [
268
+ // { type: "created", at: "…", actor: { id: "u_42", name: "Ana Pérez" } },
269
+ // { type: "status", at: "…", actor: {…}, from: "open", to: "resolved" },
270
+ // { type: "classified", at: "…", actor: {…}, field: "type", from: null, to: "bug" },
271
+ // ]
272
+ ```
273
+
274
+ Replies and reactions are deliberately **not** in it. A reply already carries
275
+ its own author and timestamp and is visible in the thread; reactions are
276
+ high-frequency signal with no audit value. That bound is what keeps the log at
277
+ three to five entries per comment — a hundred comments’ worth of history costs
278
+ about what two automatic screenshots cost.
279
+
280
+ Resolution time is derived from this log rather than stored beside it, so a
281
+ comment that was resolved, reopened and resolved again reports the duration of
282
+ the resolution currently in force, and the superseded ones are listed under
283
+ **Previous resolutions** in the same disclosure.
284
+
285
+ Two things worth knowing before you rely on it:
286
+
287
+ - **It is attributive, not evidential.** HellDots authenticates nobody. The log
288
+ records the `user` your app declared at the moment of the action, so it says
289
+ what your application asserted about who acted — not a verified fact. Verify
290
+ on your own backend if you need the stronger claim; `onChange` carries every
291
+ mutation to it.
292
+ - **Timestamps come from the acting client’s clock.** Merge corpora written on
293
+ machines whose clocks disagree and an entry can predate the comment it
294
+ belongs to. Durations are clamped at zero rather than rendered negative.
295
+
296
+ A corpus written before the log existed loads unchanged with `history: null`,
297
+ and its comments render no disclosure — additive, so nothing needs migrating.
298
+
299
+ ## Metrics and reports
300
+
301
+ The inbox header carries a **Metrics** button. It swaps the list for a
302
+ dashboard: totals, how many were resolved and how many came back, average and
303
+ median resolution time, bars per status, type and priority, and a daily
304
+ distribution. Each bar carries the colour its own picker uses, so a chip and
305
+ its bar read as the same thing.
306
+
307
+ The dashboard measures **what the panel is currently filtered to** — the
308
+ filter summary sits right above the figures, so they answer "what am I looking
309
+ at". For the unfiltered aggregate, ask the overlay:
310
+
311
+ ```js
312
+ overlay.getMetrics();
313
+ // {
314
+ // total: 42,
315
+ // byStatus: { open: 12, in_progress: 4, in_review: 2, resolved: 24 },
316
+ // byType: { bug: 18, suggestion: 9, question: 3, improvement: 4, unset: 8 },
317
+ // byPriority: { high: 7, medium: 15, low: 6, unset: 14 },
318
+ // overTime: [{ date: "2026-08-18", count: 5 }, …],
319
+ // resolution: { resolvedCount: 24, reopenedCount: 3,
320
+ // averageMs: 9000000, medianMs: 5400000 },
321
+ // }
322
+ ```
323
+
324
+ Every bucket is present even when empty, so you can index it without guarding.
325
+ `overTime` lists only the days that saw activity — filling the gaps would put
326
+ a year of empty buckets between two comments twelve months apart.
327
+
328
+ ### Exporting
329
+
330
+ Three buttons at the foot of the dashboard, and the same three as methods:
331
+
332
+ ```js
333
+ overlay.exportCommentsCsv(); // helldots-comments.csv — one row per comment
334
+ overlay.exportMetricsCsv(); // helldots-metrics.csv — section, key, value
335
+ overlay.printMetricsReport(); // the browser's print dialog → Save as PDF
336
+ ```
337
+
338
+ The CSVs are RFC 4180 with a UTF-8 BOM, so Excel opens them without turning
339
+ every accent into mojibake, and a value that would otherwise be evaluated as a
340
+ formula is neutralised on the way out. Headers are the internal field names
341
+ rather than translated labels: the file is an interchange format, and a column
342
+ whose spelling follows the widget's locale cannot be joined against anything.
343
+ Screenshots stay out — a 33 KB base64 string in a spreadsheet cell is not data.
344
+
345
+ The PDF is the browser's. HellDots builds the report in its own document and
346
+ asks that document to print, so "Save as PDF" in the dialog gives you a real
347
+ one at no cost in bundle size — the lightest PDF library measured 133 KB gzip
348
+ against a 50 KB budget. What prints is the report, not the page behind it.
349
+
168
350
  ## Handing a comment to a coding agent
169
351
 
170
352
  Every comment has a **copy** button that puts a plain-text context block on the
@@ -194,7 +376,7 @@ OS: iOS 17.2
194
376
 
195
377
  | Option | Type | Default | |
196
378
  | ----------------------- | -------------------------------- | ------------------- | ----------------------------------------------------------------- |
197
- | `user` | `{ name: string }` | `"Anonymous"` | Author of new comments and replies |
379
+ | `user` | `{ name: string, id?: string }` | `"Anonymous"` | Author of new comments and replies; `id` persists as `authorId` |
198
380
  | `persistence` | `"localStorage"` \| `"none"` | `"none"` | Auto save/restore, or handle it yourself via callbacks |
199
381
  | `autoScreenshot` | `boolean` | `true` | Capture a screenshot and environment snapshot per comment |
200
382
  | `embedCrossOriginFonts` | `boolean` | `false` | Fetch unreadable stylesheets so their web fonts reach the capture |
@@ -217,6 +399,7 @@ createCommentOverlay({
217
399
  // "comment:created" | "comment:edited" | "comment:deleted"
218
400
  // "comment:status-changed" | "comment:updated" | "comment:anchor-lost"
219
401
  // "reply:added" | "reply:deleted" | "reply:edited"
402
+ // "reaction:toggled"
220
403
  api.post("/helldots-events", event);
221
404
  },
222
405
  });
@@ -227,17 +410,18 @@ TypeScript narrows the payload. The specific callbacks below carry the same
227
410
  events at the same moments — subscribe either way, or both. A handler that
228
411
  throws is caught and warned about, never rolling back the change.
229
412
 
230
- | Callback | Fires when |
231
- | --------------------------------- | -------------------------------------------------- |
232
- | `onCommentCreated(comment)` | A new comment is saved |
233
- | `onReplyAdded(comment, reply)` | A reply is added to any comment |
234
- | `onReplyDeleted(comment, reply)` | A reply is removed |
235
- | `onCommentEdited(comment)` | A comment's text is rewritten |
236
- | `onReplyEdited(comment, reply)` | A reply's text is rewritten |
237
- | `onCommentStatusChanged(comment)` | Status moves between open / in progress / resolved |
238
- | `onCommentUpdated(comment)` | Type, priority or tags change |
239
- | `onCommentDeleted(id)` | A comment is removed |
240
- | `onAnchorLost(comment)` | A comment could not be re-anchored on load |
413
+ | Callback | Fires when |
414
+ | ----------------------------------- | -------------------------------------------------------------- |
415
+ | `onCommentCreated(comment)` | A new comment is saved |
416
+ | `onReplyAdded(comment, reply)` | A reply is added to any comment |
417
+ | `onReplyDeleted(comment, reply)` | A reply is removed |
418
+ | `onCommentEdited(comment)` | A comment's text is rewritten |
419
+ | `onReplyEdited(comment, reply)` | A reply's text is rewritten |
420
+ | `onCommentStatusChanged(comment)` | Status moves along the lifecycle |
421
+ | `onCommentUpdated(comment)` | Type, priority or tags change |
422
+ | `onCommentDeleted(id)` | A comment is removed |
423
+ | `onAnchorLost(comment)` | A comment could not be re-anchored on load |
424
+ | `onReactionToggled(comment, reply)` | A reaction is added or removed (`reply` is `null` at the root) |
241
425
 
242
426
  ## API
243
427
 
@@ -261,6 +445,8 @@ overlay.setCommentStatus(id, status); // → boolean
261
445
  overlay.setCommentType(id, type); // → boolean
262
446
  overlay.setCommentPriority(id, priority); // → boolean
263
447
  overlay.setCommentTags(id, tags); // → boolean
448
+ overlay.toggleCommentReaction(id, emoji); // → boolean
449
+ overlay.toggleReplyReaction(commentId, replyId, emoji); // → boolean
264
450
  overlay.cleanup(); // remove the widget entirely
265
451
  ```
266
452