helldots 0.5.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface CommentAnchor {
24
24
  export type AnchorState = "anchored" | "orphaned" | "inactive";
25
25
 
26
26
  /** RF09 — comment lifecycle state. */
27
- export type CommentStatus = "open" | "in_progress" | "resolved";
27
+ export type CommentStatus = "open" | "in_progress" | "in_review" | "resolved";
28
28
 
29
29
  /** RF3 — comment category. `null` means deliberately unclassified. */
30
30
  export type CommentType = "bug" | "suggestion" | "question" | "improvement";
@@ -32,6 +32,72 @@ export type CommentType = "bug" | "suggestion" | "question" | "improvement";
32
32
  /** RF4 — comment priority. `null` means deliberately unprioritised. */
33
33
  export type CommentPriority = "high" | "medium" | "low";
34
34
 
35
+ /**
36
+ * Aggregate figures over a corpus of comments. Every bucket is present even
37
+ * when empty, so a consumer can index it without guarding — an absent key and
38
+ * a zero would otherwise be indistinguishable.
39
+ */
40
+ export interface CommentMetrics {
41
+ total: number;
42
+ byStatus: Record<CommentStatus, number>;
43
+ /** `unset` holds the comments left deliberately unclassified. */
44
+ byType: Record<CommentType | "unset", number>;
45
+ /** `unset` holds the comments left deliberately unprioritised. */
46
+ byPriority: Record<CommentPriority | "unset", number>;
47
+ /**
48
+ * Comments created per day, oldest first, in `YYYY-MM-DD`. Only days that
49
+ * saw activity get an entry: filling the gaps would put a year of empty
50
+ * buckets between two comments twelve months apart.
51
+ */
52
+ overTime: Array<{ date: string; count: number }>;
53
+ resolution: {
54
+ resolvedCount: number;
55
+ /** Comments that were resolved, reopened and resolved again. */
56
+ reopenedCount: number;
57
+ /** Mean and median of the resolution currently in force, or null when nothing is resolved. */
58
+ averageMs: number | null;
59
+ medianMs: number | null;
60
+ };
61
+ }
62
+
63
+ /** Who performed an audited action, as the host declared them at the time. */
64
+ export interface AuditActor {
65
+ /** From `user.id`, when the host supplies one. Never rendered. */
66
+ id?: string;
67
+ /** The display name at the time of the action. */
68
+ name: string;
69
+ }
70
+
71
+ /**
72
+ * The four actions worth auditing. Replies carry their own author and
73
+ * timestamp already, and reactions are high-frequency signal with no audit
74
+ * value — neither produces an entry.
75
+ */
76
+ export type AuditEventType = "created" | "edited" | "status" | "classified";
77
+
78
+ /** One entry of a comment's append-only audit trail. */
79
+ export interface AuditEvent {
80
+ type: AuditEventType;
81
+ /**
82
+ * ISO timestamp, from the acting client's clock. Merge corpora written on
83
+ * machines whose clocks disagree and an entry can predate the comment it
84
+ * belongs to; durations derived from these are clamped at zero rather than
85
+ * rendered negative.
86
+ */
87
+ at: string;
88
+ actor: AuditActor;
89
+ /** "classified" only: which field moved. */
90
+ field?: "type" | "priority" | "tags";
91
+ /**
92
+ * Both ends of the transition, for "status" and for "classified" on type or
93
+ * priority. `null` is a value, not an absence — it is how type and priority
94
+ * read when deliberately unset. Absent for "created", "edited", and for a
95
+ * tag change, which is a list with no two-value transition.
96
+ */
97
+ from?: string | null;
98
+ to?: string | null;
99
+ }
100
+
35
101
  /** RF2 — environment snapshot taken when the comment was created. */
36
102
  export interface CommentContext {
37
103
  version: 1;
@@ -72,8 +138,31 @@ export interface SerializedComment {
72
138
  anchor: CommentAnchor | null;
73
139
  /** location.pathname where the comment was created. */
74
140
  page: string;
141
+ /**
142
+ * Append-only audit trail: who created, edited, moved or reclassified this
143
+ * comment, and when. Optional and absent by default — records written
144
+ * before it existed simply have none, so no migration is involved, and
145
+ * `null` rather than `[]` keeps an untouched corpus free of extra bytes.
146
+ *
147
+ * Every resolution the comment has had is derivable from the `status`
148
+ * entries, which is why no separate resolution history is stored.
149
+ */
150
+ history?: AuditEvent[] | null;
75
151
  replies: CommentReply[];
76
152
  author: string;
153
+ /**
154
+ * Stable identifier of the author, from the `user.id` the host supplied at
155
+ * creation. Optional and absent by default: records written before it
156
+ * existed simply have none, so no migration is involved.
157
+ *
158
+ * Never rendered — `author` is the display name and stays what any UI
159
+ * shows — it travels with the record, so a store that holds nothing but
160
+ * comments renders every author without a lookup. The id is opaque to
161
+ * HellDots: whether it points into the host's user table, into a
162
+ * comments-only database, or nowhere at all is the host's business. What it
163
+ * buys is telling two teammates who share a display name apart.
164
+ */
165
+ authorId?: string | null;
77
166
  createdAt: string;
78
167
  screenshots: string[];
79
168
  status: CommentStatus;
@@ -94,42 +183,112 @@ export interface SerializedComment {
94
183
  /** RF1 — automatic viewport capture (JPEG data-URL). */
95
184
  contextScreenshot: string | null;
96
185
  /**
97
- * RF6 — emoji → authors who reacted. Reserved, not implemented yet.
98
- * Optional and absent by default, so shipping it later needs no migration.
186
+ * RF6 — emoji → the actor keys that reacted (see the `user` option). Always
187
+ * one of the six emoji in the fixed set; anything else is dropped on load.
188
+ * Null when nobody has reacted, so an untouched corpus carries no extra
189
+ * payload.
99
190
  */
100
- reactions?: Record<string, string[]>;
191
+ reactions: Record<string, string[]> | null;
101
192
  }
102
193
 
194
+ /**
195
+ * Who caused a change.
196
+ *
197
+ * `"user"` is somebody acting inside the widget — a marker, the thread
198
+ * popover, the inbox. `"host"` is the app's own code calling a method.
199
+ *
200
+ * The distinction exists for multi-user apps. Applying a change that arrived
201
+ * over a socket means calling the same method the UI calls, so without this
202
+ * every host has to wrap its own writes in a flag to avoid echoing them
203
+ * straight back to the server. `if (meta.origin === "host") return;` is that
204
+ * flag, and it is now the library's job to provide it.
205
+ */
206
+ export type ChangeOrigin = "user" | "host";
207
+
208
+ /** Which part of the widget a reported failure came from. */
209
+ export type ErrorContext =
210
+ /** A screenshot render failed; the comment saves without one. */
211
+ | "capture"
212
+ /** localStorage could not be written; this browser's copy now diverges. */
213
+ | "storage"
214
+ /** A record handed to loadComments was malformed and was skipped. */
215
+ | "load"
216
+ /** An `onCommentRequested` handler threw or rejected. */
217
+ | "link"
218
+ /** A `transformScreenshot` handler failed; the data URL was kept. */
219
+ | "transform";
220
+
221
+ /**
222
+ * What every change carries, whatever its type. Delivered as one trailing
223
+ * argument to the specific callbacks, and flattened onto the `onChange`
224
+ * event next to `comment`/`reply`/`id`.
225
+ */
226
+ export interface ChangeMeta {
227
+ origin: ChangeOrigin;
228
+ }
229
+
230
+ /** `comment:status-changed` — both ends of the move along the lifecycle. */
231
+ export interface StatusChangeMeta extends ChangeMeta {
232
+ from: CommentStatus;
233
+ to: CommentStatus;
234
+ }
235
+
236
+ /**
237
+ * `comment:updated` — which of the three fields moved, and both ends of the
238
+ * move. Discriminated on `field`, so narrowing gives you correctly typed
239
+ * `from`/`to` for each.
240
+ */
241
+ export type UpdateMeta = ChangeMeta &
242
+ (
243
+ | { field: "type"; from: CommentType | null; to: CommentType | null }
244
+ | {
245
+ field: "priority";
246
+ from: CommentPriority | null;
247
+ to: CommentPriority | null;
248
+ }
249
+ | { field: "tags"; from: string[]; to: string[] }
250
+ );
251
+
103
252
  /**
104
253
  * Everything that can change, as one discriminated union. Switch on `type`
105
254
  * and TypeScript narrows the rest of the fields for you.
106
255
  *
107
- * The nine specific callbacks below carry exactly the same events at
108
- * exactly the same moments; subscribe either way, or both.
256
+ * The ten specific callbacks below carry exactly the same events at
257
+ * exactly the same moments, with the same metadata; subscribe either way,
258
+ * or both.
109
259
  */
110
260
  export type ChangeEvent =
111
- | { type: "comment:created"; comment: SerializedComment }
112
- | { type: "comment:edited"; comment: SerializedComment }
113
- | { type: "comment:deleted"; id: CommentId }
114
- | { type: "comment:status-changed"; comment: SerializedComment }
115
- /** Type, priority or tags changed. */
116
- | { type: "comment:updated"; comment: SerializedComment }
117
- | { type: "comment:anchor-lost"; comment: SerializedComment }
118
- | {
261
+ | ({ type: "comment:created"; comment: SerializedComment } & ChangeMeta)
262
+ | ({ type: "comment:edited"; comment: SerializedComment } & ChangeMeta)
263
+ | ({ type: "comment:deleted"; id: CommentId } & ChangeMeta)
264
+ | ({
265
+ type: "comment:status-changed";
266
+ comment: SerializedComment;
267
+ } & StatusChangeMeta)
268
+ /** Type, priority or tags changed; `field` says which. */
269
+ | ({ type: "comment:updated"; comment: SerializedComment } & UpdateMeta)
270
+ | ({ type: "comment:anchor-lost"; comment: SerializedComment } & ChangeMeta)
271
+ | ({
119
272
  type: "reply:added";
120
273
  comment: SerializedComment;
121
274
  reply: CommentReply;
122
- }
123
- | {
275
+ } & ChangeMeta)
276
+ | ({
124
277
  type: "reply:deleted";
125
278
  comment: SerializedComment;
126
279
  reply: CommentReply;
127
- }
128
- | {
280
+ } & ChangeMeta)
281
+ | ({
129
282
  type: "reply:edited";
130
283
  comment: SerializedComment;
131
284
  reply: CommentReply;
132
- };
285
+ } & ChangeMeta)
286
+ /** `reply` is null when the reaction is on the root comment. */
287
+ | ({
288
+ type: "reaction:toggled";
289
+ comment: SerializedComment;
290
+ reply: CommentReply | null;
291
+ } & ChangeMeta);
133
292
 
134
293
  export interface CommentOverlayOptions {
135
294
  shortcutKey?: string;
@@ -164,8 +323,19 @@ export interface CommentOverlayOptions {
164
323
  * font or adding `crossorigin` to the `<link>`.
165
324
  */
166
325
  embedCrossOriginFonts?: boolean;
167
- /** Identity used as the author of new comments and replies. */
168
- user?: { name: string };
326
+ /**
327
+ * Identity used as the author of new comments and replies.
328
+ *
329
+ * `name` is what gets displayed. `id` is optional and never rendered: it
330
+ * is persisted as `authorId` on the comments and replies this user creates,
331
+ * and it is what a reaction is keyed on — so two teammates who share a
332
+ * display name are still told apart. Without it the name is used, and
333
+ * without any `user` at all every actor collapses into one.
334
+ *
335
+ * HellDots authenticates nobody: whatever the host declares here is taken
336
+ * at face value and recorded as-is.
337
+ */
338
+ user?: { name: string; id?: string };
169
339
  /**
170
340
  * Query parameter carrying a comment id in "Copy link" URLs, and read back
171
341
  * on startup to open that comment. Default: "helldotsComment". Override it
@@ -185,42 +355,184 @@ export interface CommentOverlayOptions {
185
355
  * `notifyNavigation()` call from the router's hook. Default: false.
186
356
  */
187
357
  autoDetectNavigation?: boolean;
358
+ /**
359
+ * Called once the widget has mounted and every method on it is safe to
360
+ * drive. Receives the instance, because when the document is already
361
+ * parsed the mount happens inside the constructor — before
362
+ * `createCommentOverlay()` has returned anything to assign.
363
+ *
364
+ * This is the right place to `loadComments()`: a call made earlier is held
365
+ * and replayed here, but it can only return zeroes until then.
366
+ */
367
+ onReady?: (overlay: CommentOverlay) => void;
368
+ /**
369
+ * Called when something the widget survived nonetheless went wrong: a
370
+ * screenshot that failed to render, a localStorage write that could not be
371
+ * made, a malformed record skipped on load, a rejected
372
+ * `onCommentRequested`. Each of these already warns on the console and the
373
+ * widget carries on regardless — this is how they reach the host's own
374
+ * logging instead of only a devtools panel nobody has open.
375
+ */
376
+ onError?: (error: unknown, context: ErrorContext) => void;
377
+ /**
378
+ * Called when a "Copy link" URL points at a comment the widget does not
379
+ * hold — once per id, not once per attempt.
380
+ *
381
+ * This is what makes lazy loading work: fetch that one comment and hand it
382
+ * to `loadComments()`, and the inbox opens on it. Return a promise and the
383
+ * link is retried once it settles. Without a handler the inbox still opens
384
+ * and says the comment was not found.
385
+ */
386
+ onCommentRequested?: (id: CommentId) => void | Promise<unknown>;
387
+ /**
388
+ * Called for every image the widget acquires — the automatic viewport
389
+ * capture, a drag-crop region, and anything attached through the file
390
+ * picker. Return the string to store in its place, typically a URL into
391
+ * your own object storage.
392
+ *
393
+ * Without it, a ~33KB base64 data URL travels inside every comment: into
394
+ * localStorage, where it is the first thing shed under quota pressure, and
395
+ * into whatever your backend persists.
396
+ *
397
+ * Everything on a comment is transformed as the comment is saved; an
398
+ * attachment on a *reply* is transformed when the file is picked, because
399
+ * `addReply()` is synchronous. Either way the record may never arrive —
400
+ * an abandoned reply draft, or a comment box dismissed while its upload is
401
+ * in flight — so treat a URL you hand back as an upload, not as a record,
402
+ * and sweep for unreferenced blobs.
403
+ *
404
+ * `kind` separates the automatic capture ("context" — disposable and
405
+ * regenerable) from something a person chose to include ("attachment"), so
406
+ * the two can go to different buckets with different retention. It does
407
+ * not distinguish the two moments above: both arrive as "attachment".
408
+ * `commentId` is the comment the image will belong to — for an attachment
409
+ * on a reply, the parent comment.
410
+ *
411
+ * Fail-open: a rejection, a throw, or a resolved value that is not a
412
+ * non-empty string leaves the original data URL in place and reports
413
+ * `onError(error, "transform")`. Losing the user's comment would be worse
414
+ * than sending you a large one.
415
+ *
416
+ * Not called for records passed to `loadComments()`, nor for screenshots
417
+ * you hand to `addReply()` yourself — in both cases the strings are
418
+ * already yours.
419
+ */
420
+ transformScreenshot?: (
421
+ dataUrl: string,
422
+ info: { kind: "context" | "attachment"; commentId: CommentId }
423
+ ) => Promise<string>;
424
+ /**
425
+ * Called whenever comment mode turns on or off, however it was flipped —
426
+ * the toolbar button, the keyboard shortcut, the inbox empty state, or the
427
+ * automatic switch-off after a comment is saved.
428
+ *
429
+ * The shortcut is the reason this exists: the host never sees that
430
+ * keystroke, so an app that needs to stand down while the user is picking
431
+ * an element — pause a carousel, disable its own drag-and-drop, dim a
432
+ * layer — has no other way to know.
433
+ */
434
+ onCommentModeChanged?: (active: boolean) => void;
435
+ /**
436
+ * Called when somebody opens a comment's full thread, from its marker or
437
+ * from the inbox detail — the only two places the replies are readable.
438
+ *
439
+ * This is what an unread count is built on. HellDots keeps no read state
440
+ * of its own: whose "read" it is depends on an identity only the host can
441
+ * persist.
442
+ */
443
+ onCommentOpened?: (comment: SerializedComment) => void;
188
444
  /**
189
445
  * Single subscription point: fires for every change, alongside whichever
190
446
  * specific callback below carries the same event. Handy when a host syncs
191
447
  * everything to one endpoint instead of wiring nine functions. A handler
192
448
  * that throws is caught and warned about — it never rolls back the
193
449
  * mutation that emitted it.
450
+ *
451
+ * The event carries `origin` (and, where there is one, the transition that
452
+ * caused it) flattened onto it — see `ChangeMeta`.
194
453
  */
195
454
  onChange?: (event: ChangeEvent) => void;
196
455
  /** Fired after a new comment is saved. */
197
- onCommentCreated?: (comment: SerializedComment) => void;
456
+ onCommentCreated?: (comment: SerializedComment, meta: ChangeMeta) => void;
198
457
  /** Fired after a reply is added to any comment. */
199
- onReplyAdded?: (comment: SerializedComment, reply: CommentReply) => void;
458
+ onReplyAdded?: (
459
+ comment: SerializedComment,
460
+ reply: CommentReply,
461
+ meta: ChangeMeta
462
+ ) => void;
200
463
  /** Fired after deleteReply removes a reply. */
201
- onReplyDeleted?: (comment: SerializedComment, reply: CommentReply) => void;
464
+ onReplyDeleted?: (
465
+ comment: SerializedComment,
466
+ reply: CommentReply,
467
+ meta: ChangeMeta
468
+ ) => void;
202
469
  /** Fired after editComment rewrites a comment's text. */
203
- onCommentEdited?: (comment: SerializedComment) => void;
470
+ onCommentEdited?: (comment: SerializedComment, meta: ChangeMeta) => void;
204
471
  /** Fired after editReply rewrites a reply's text. */
205
- onReplyEdited?: (comment: SerializedComment, reply: CommentReply) => void;
206
- /** Fired for each comment that could not be re-anchored by loadComments. */
207
- onAnchorLost?: (comment: SerializedComment) => void;
472
+ onReplyEdited?: (
473
+ comment: SerializedComment,
474
+ reply: CommentReply,
475
+ meta: ChangeMeta
476
+ ) => void;
477
+ /**
478
+ * Fired for each comment that could not be re-anchored — by loadComments,
479
+ * and again by every notifyNavigation that lands somewhere its element
480
+ * does not exist. Those repeats always carry `origin: "host"`.
481
+ */
482
+ onAnchorLost?: (comment: SerializedComment, meta: ChangeMeta) => void;
208
483
  /** Fired after deleteComment removes a comment. */
209
- onCommentDeleted?: (id: CommentId) => void;
210
- /** Fired after setCommentStatus changes a comment's lifecycle state. */
211
- onCommentStatusChanged?: (comment: SerializedComment) => void;
212
- /** Fired after type, priority or tags change on any comment. */
213
- onCommentUpdated?: (comment: SerializedComment) => void;
484
+ onCommentDeleted?: (id: CommentId, meta: ChangeMeta) => void;
485
+ /**
486
+ * Fired after setCommentStatus changes a comment's lifecycle state.
487
+ * `meta.from`/`meta.to` are both ends of the move, so "reopened" and
488
+ * "resolved" are told apart without diffing against a previous copy.
489
+ */
490
+ onCommentStatusChanged?: (
491
+ comment: SerializedComment,
492
+ meta: StatusChangeMeta
493
+ ) => void;
494
+ /**
495
+ * Fired after type, priority or tags change on any comment. `meta.field`
496
+ * says which one moved, and narrows `meta.from`/`meta.to` with it.
497
+ */
498
+ onCommentUpdated?: (comment: SerializedComment, meta: UpdateMeta) => void;
499
+ /**
500
+ * Fired after a reaction is added to or removed from a comment or a reply.
501
+ * `reply` is null when the reaction is on the root comment.
502
+ */
503
+ onReactionToggled?: (
504
+ comment: SerializedComment,
505
+ reply: CommentReply | null,
506
+ meta: ChangeMeta
507
+ ) => void;
214
508
  }
215
509
 
216
510
  export interface CommentReply {
217
511
  id: CommentId;
218
512
  text: string;
219
513
  author: string;
514
+ /**
515
+ * Stable identifier of the author, from the `user.id` the host supplied at
516
+ * creation. Optional and absent by default: records written before it
517
+ * existed simply have none, so no migration is involved.
518
+ *
519
+ * Never rendered — `author` is the display name and stays what any UI
520
+ * shows — it travels with the record, so a store that holds nothing but
521
+ * comments renders every author without a lookup. The id is opaque to
522
+ * HellDots: whether it points into the host's user table, into a
523
+ * comments-only database, or nowhere at all is the host's business. What it
524
+ * buys is telling two teammates who share a display name apart.
525
+ */
526
+ authorId?: string | null;
220
527
  timestamp: string;
221
528
  screenshots?: string[];
222
529
  /** ISO timestamp of the last edit; null when never edited. */
223
530
  editedAt?: string | null;
531
+ /**
532
+ * RF6 — emoji → the actor keys that reacted. Same shape and same rules as a
533
+ * comment's; null when nobody has reacted.
534
+ */
535
+ reactions?: Record<string, string[]> | null;
224
536
  }
225
537
 
226
538
  export interface Comment {
@@ -240,8 +552,31 @@ export interface Comment {
240
552
  target?: HTMLElement | null;
241
553
  /** location.pathname where the comment was created. */
242
554
  page: string;
555
+ /**
556
+ * Append-only audit trail: who created, edited, moved or reclassified this
557
+ * comment, and when. Optional and absent by default — records written
558
+ * before it existed simply have none, so no migration is involved, and
559
+ * `null` rather than `[]` keeps an untouched corpus free of extra bytes.
560
+ *
561
+ * Every resolution the comment has had is derivable from the `status`
562
+ * entries, which is why no separate resolution history is stored.
563
+ */
564
+ history?: AuditEvent[] | null;
243
565
  replies: CommentReply[];
244
566
  author: string;
567
+ /**
568
+ * Stable identifier of the author, from the `user.id` the host supplied at
569
+ * creation. Optional and absent by default: records written before it
570
+ * existed simply have none, so no migration is involved.
571
+ *
572
+ * Never rendered — `author` is the display name and stays what any UI
573
+ * shows — it travels with the record, so a store that holds nothing but
574
+ * comments renders every author without a lookup. The id is opaque to
575
+ * HellDots: whether it points into the host's user table, into a
576
+ * comments-only database, or nowhere at all is the host's business. What it
577
+ * buys is telling two teammates who share a display name apart.
578
+ */
579
+ authorId?: string | null;
245
580
  createdAt: string;
246
581
  screenshots: string[];
247
582
  status: CommentStatus;
@@ -262,10 +597,12 @@ export interface Comment {
262
597
  /** RF1 — automatic viewport capture (JPEG data-URL). */
263
598
  contextScreenshot: string | null;
264
599
  /**
265
- * RF6 — emoji → authors who reacted. Reserved, not implemented yet.
266
- * Optional and absent by default, so shipping it later needs no migration.
600
+ * RF6 — emoji → the actor keys that reacted (see the `user` option). Always
601
+ * one of the six emoji in the fixed set; anything else is dropped on load.
602
+ * Null when nobody has reacted, so an untouched corpus carries no extra
603
+ * payload.
267
604
  */
268
- reactions?: Record<string, string[]>;
605
+ reactions: Record<string, string[]> | null;
269
606
  }
270
607
 
271
608
  export declare class CommentOverlay {
@@ -292,6 +629,12 @@ export declare class CommentOverlay {
292
629
  /** The shareable URL for a comment, or null when the id is unknown. */
293
630
  commentLink(id: CommentId): string | null;
294
631
  serializeComments(): SerializedComment[];
632
+ /**
633
+ * Called before the widget has mounted — possible when a fetch resolves
634
+ * while the document is still parsing — the data is held and applied at
635
+ * mount, and the counts come back as zeroes because nothing has been
636
+ * resolved yet. Load from `onReady` when the counts matter.
637
+ */
295
638
  loadComments(data: SerializedComment[]): {
296
639
  anchored: number;
297
640
  orphaned: number;
@@ -315,10 +658,63 @@ export declare class CommentOverlay {
315
658
  * backend before loadComments — fires no per-comment callbacks.
316
659
  */
317
660
  clearComments(): void;
661
+ /**
662
+ * Aggregate figures over every comment the widget holds. Unfiltered: the
663
+ * dashboard inside the inbox measures whatever that panel is showing, but a
664
+ * host has no notion of those filters.
665
+ */
666
+ getMetrics(): CommentMetrics;
667
+ /**
668
+ * Downloads the corpus as CSV, one row per comment, and returns the same
669
+ * text. Screenshots and the automatic context capture stay out. Defaults
670
+ * to every comment.
671
+ *
672
+ * The return value is for a host that wanted to POST those rows somewhere
673
+ * or attach them to a message: a browser download is a dead end, and
674
+ * building the CSV a second time is the only alternative.
675
+ */
676
+ exportCommentsCsv(comments?: SerializedComment[]): string;
677
+ /**
678
+ * Downloads the aggregate figures as CSV in long format — `section, key,
679
+ * value` — so the column count does not change with the corpus. Returns
680
+ * the same text, for the same reason as above.
681
+ */
682
+ exportMetricsCsv(comments?: SerializedComment[]): string;
683
+ /**
684
+ * Opens the browser's print dialog on a report of the figures, which is
685
+ * where "save as PDF" lives. The report is built in its own document, so
686
+ * what prints is the report rather than the host page.
687
+ */
688
+ printMetricsReport(comments?: SerializedComment[], scope?: string): void;
689
+ /**
690
+ * Replaces the identity new comments, replies and reactions are attributed
691
+ * to. Everything already recorded keeps the author it was written with.
692
+ *
693
+ * For the common case where identity resolves after the widget mounts, or
694
+ * where the user switches account or workspace — the alternative was
695
+ * `cleanup()` and a rebuild, which throws away every loaded comment and
696
+ * whatever panel was open. `null` returns to the anonymous author.
697
+ *
698
+ * Returns false, changing nothing, for anything that is neither null nor
699
+ * an object with a non-blank `name`.
700
+ */
701
+ setUser(user: { name: string; id?: string } | null): boolean;
318
702
  setCommentStatus(id: CommentId, status: CommentStatus): boolean;
319
703
  setCommentType(id: CommentId, type: CommentType | null): boolean;
320
704
  setCommentPriority(id: CommentId, priority: CommentPriority | null): boolean;
321
705
  setCommentTags(id: CommentId, tags: string[]): boolean;
706
+ /**
707
+ * RF6 — flips the current actor's reaction on a comment: present, it is
708
+ * removed; absent, it is added. The actor is `user.id ?? user.name`.
709
+ * Returns false when the id or the emoji is unknown.
710
+ */
711
+ toggleCommentReaction(id: CommentId, emoji: string): boolean;
712
+ /** Same contract as toggleCommentReaction, one level down. */
713
+ toggleReplyReaction(
714
+ commentId: CommentId,
715
+ replyId: CommentId,
716
+ emoji: string
717
+ ): boolean;
322
718
  cleanup(): void;
323
719
  }
324
720
 
@@ -337,4 +733,22 @@ export declare function createCommentOverlay(
337
733
  options: CommentOverlayOptions & { autoInit: false }
338
734
  ): () => CommentOverlay;
339
735
 
736
+ /**
737
+ * Default name of the query parameter carrying a comment id in "Copy link"
738
+ * URLs. Exported so a host reading the id itself — before the widget is up,
739
+ * to fetch just that comment — does not have to hardcode a second copy of
740
+ * it. Override it per instance with the `linkParam` option.
741
+ */
742
+ export declare const DEFAULT_LINK_PARAM: string;
743
+
744
+ /**
745
+ * The comment id the given URL asks for, or null. Pass the same `param` the
746
+ * widget was configured with; both default to `DEFAULT_LINK_PARAM`. A
747
+ * malformed URL yields null rather than throwing.
748
+ */
749
+ export declare function readCommentLinkParam(
750
+ param?: string,
751
+ href?: string
752
+ ): string | null;
753
+
340
754
  export default createCommentOverlay;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helldots",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Drop-in comment overlay for web apps — click anywhere to leave a comment anchored to that element, with an automatic screenshot and environment capture",
5
5
  "keywords": [
6
6
  "comments",
@@ -53,9 +53,11 @@
53
53
  "build": "node scripts/build.mjs",
54
54
  "size": "node scripts/check-size.mjs",
55
55
  "verify": "npm run lint && npm run typecheck && npm run format:check && npm test && npm run build && npm run size",
56
+ "check:commit": "node scripts/check-commit-msg.mjs",
56
57
  "prepublishOnly": "npm run verify",
57
58
  "changeset": "changeset",
58
- "release": "changeset version"
59
+ "release": "changeset version",
60
+ "prepare": "node scripts/install-hooks.mjs"
59
61
  },
60
62
  "devDependencies": {
61
63
  "@changesets/cli": "^2.31.0",