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/README.md +414 -24
- package/dist/helldots.esm.js +6 -5
- package/dist/helldots.esm.js.map +4 -4
- package/dist/helldots.umd.js +13 -12
- package/dist/index.d.ts +450 -36
- package/package.json +4 -2
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 →
|
|
98
|
-
*
|
|
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
|
|
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
|
|
108
|
-
* exactly the same moments; subscribe either way,
|
|
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
|
-
| {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
/**
|
|
168
|
-
|
|
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?: (
|
|
458
|
+
onReplyAdded?: (
|
|
459
|
+
comment: SerializedComment,
|
|
460
|
+
reply: CommentReply,
|
|
461
|
+
meta: ChangeMeta
|
|
462
|
+
) => void;
|
|
200
463
|
/** Fired after deleteReply removes a reply. */
|
|
201
|
-
onReplyDeleted?: (
|
|
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?: (
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 →
|
|
266
|
-
*
|
|
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
|
|
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.
|
|
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",
|