helldots 0.6.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 +229 -25
- package/dist/helldots.esm.js +6 -6
- package/dist/helldots.esm.js.map +3 -3
- package/dist/helldots.umd.js +12 -12
- package/dist/index.d.ts +255 -34
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -191,42 +191,104 @@ export interface SerializedComment {
|
|
|
191
191
|
reactions: Record<string, string[]> | null;
|
|
192
192
|
}
|
|
193
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
|
+
|
|
194
252
|
/**
|
|
195
253
|
* Everything that can change, as one discriminated union. Switch on `type`
|
|
196
254
|
* and TypeScript narrows the rest of the fields for you.
|
|
197
255
|
*
|
|
198
256
|
* The ten specific callbacks below carry exactly the same events at
|
|
199
|
-
* exactly the same moments; subscribe either way,
|
|
257
|
+
* exactly the same moments, with the same metadata; subscribe either way,
|
|
258
|
+
* or both.
|
|
200
259
|
*/
|
|
201
260
|
export type ChangeEvent =
|
|
202
|
-
| { type: "comment:created"; comment: SerializedComment }
|
|
203
|
-
| { type: "comment:edited"; comment: SerializedComment }
|
|
204
|
-
| { type: "comment:deleted"; id: CommentId }
|
|
205
|
-
| {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
| ({
|
|
210
272
|
type: "reply:added";
|
|
211
273
|
comment: SerializedComment;
|
|
212
274
|
reply: CommentReply;
|
|
213
|
-
}
|
|
214
|
-
| {
|
|
275
|
+
} & ChangeMeta)
|
|
276
|
+
| ({
|
|
215
277
|
type: "reply:deleted";
|
|
216
278
|
comment: SerializedComment;
|
|
217
279
|
reply: CommentReply;
|
|
218
|
-
}
|
|
219
|
-
| {
|
|
280
|
+
} & ChangeMeta)
|
|
281
|
+
| ({
|
|
220
282
|
type: "reply:edited";
|
|
221
283
|
comment: SerializedComment;
|
|
222
284
|
reply: CommentReply;
|
|
223
|
-
}
|
|
285
|
+
} & ChangeMeta)
|
|
224
286
|
/** `reply` is null when the reaction is on the root comment. */
|
|
225
|
-
| {
|
|
287
|
+
| ({
|
|
226
288
|
type: "reaction:toggled";
|
|
227
289
|
comment: SerializedComment;
|
|
228
290
|
reply: CommentReply | null;
|
|
229
|
-
};
|
|
291
|
+
} & ChangeMeta);
|
|
230
292
|
|
|
231
293
|
export interface CommentOverlayOptions {
|
|
232
294
|
shortcutKey?: string;
|
|
@@ -293,39 +355,155 @@ export interface CommentOverlayOptions {
|
|
|
293
355
|
* `notifyNavigation()` call from the router's hook. Default: false.
|
|
294
356
|
*/
|
|
295
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;
|
|
296
444
|
/**
|
|
297
445
|
* Single subscription point: fires for every change, alongside whichever
|
|
298
446
|
* specific callback below carries the same event. Handy when a host syncs
|
|
299
447
|
* everything to one endpoint instead of wiring nine functions. A handler
|
|
300
448
|
* that throws is caught and warned about — it never rolls back the
|
|
301
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`.
|
|
302
453
|
*/
|
|
303
454
|
onChange?: (event: ChangeEvent) => void;
|
|
304
455
|
/** Fired after a new comment is saved. */
|
|
305
|
-
onCommentCreated?: (comment: SerializedComment) => void;
|
|
456
|
+
onCommentCreated?: (comment: SerializedComment, meta: ChangeMeta) => void;
|
|
306
457
|
/** Fired after a reply is added to any comment. */
|
|
307
|
-
onReplyAdded?: (
|
|
458
|
+
onReplyAdded?: (
|
|
459
|
+
comment: SerializedComment,
|
|
460
|
+
reply: CommentReply,
|
|
461
|
+
meta: ChangeMeta
|
|
462
|
+
) => void;
|
|
308
463
|
/** Fired after deleteReply removes a reply. */
|
|
309
|
-
onReplyDeleted?: (
|
|
464
|
+
onReplyDeleted?: (
|
|
465
|
+
comment: SerializedComment,
|
|
466
|
+
reply: CommentReply,
|
|
467
|
+
meta: ChangeMeta
|
|
468
|
+
) => void;
|
|
310
469
|
/** Fired after editComment rewrites a comment's text. */
|
|
311
|
-
onCommentEdited?: (comment: SerializedComment) => void;
|
|
470
|
+
onCommentEdited?: (comment: SerializedComment, meta: ChangeMeta) => void;
|
|
312
471
|
/** Fired after editReply rewrites a reply's text. */
|
|
313
|
-
onReplyEdited?: (
|
|
314
|
-
|
|
315
|
-
|
|
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;
|
|
316
483
|
/** Fired after deleteComment removes a comment. */
|
|
317
|
-
onCommentDeleted?: (id: CommentId) => void;
|
|
318
|
-
/**
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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;
|
|
322
499
|
/**
|
|
323
500
|
* Fired after a reaction is added to or removed from a comment or a reply.
|
|
324
501
|
* `reply` is null when the reaction is on the root comment.
|
|
325
502
|
*/
|
|
326
503
|
onReactionToggled?: (
|
|
327
504
|
comment: SerializedComment,
|
|
328
|
-
reply: CommentReply | null
|
|
505
|
+
reply: CommentReply | null,
|
|
506
|
+
meta: ChangeMeta
|
|
329
507
|
) => void;
|
|
330
508
|
}
|
|
331
509
|
|
|
@@ -451,6 +629,12 @@ export declare class CommentOverlay {
|
|
|
451
629
|
/** The shareable URL for a comment, or null when the id is unknown. */
|
|
452
630
|
commentLink(id: CommentId): string | null;
|
|
453
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
|
+
*/
|
|
454
638
|
loadComments(data: SerializedComment[]): {
|
|
455
639
|
anchored: number;
|
|
456
640
|
orphaned: number;
|
|
@@ -481,21 +665,40 @@ export declare class CommentOverlay {
|
|
|
481
665
|
*/
|
|
482
666
|
getMetrics(): CommentMetrics;
|
|
483
667
|
/**
|
|
484
|
-
* Downloads the corpus as CSV, one row per comment
|
|
485
|
-
* automatic context capture stay out. Defaults
|
|
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.
|
|
486
675
|
*/
|
|
487
|
-
exportCommentsCsv(comments?: SerializedComment[]):
|
|
676
|
+
exportCommentsCsv(comments?: SerializedComment[]): string;
|
|
488
677
|
/**
|
|
489
678
|
* Downloads the aggregate figures as CSV in long format — `section, key,
|
|
490
|
-
* value` — so the column count does not change with the corpus.
|
|
679
|
+
* value` — so the column count does not change with the corpus. Returns
|
|
680
|
+
* the same text, for the same reason as above.
|
|
491
681
|
*/
|
|
492
|
-
exportMetricsCsv(comments?: SerializedComment[]):
|
|
682
|
+
exportMetricsCsv(comments?: SerializedComment[]): string;
|
|
493
683
|
/**
|
|
494
684
|
* Opens the browser's print dialog on a report of the figures, which is
|
|
495
685
|
* where "save as PDF" lives. The report is built in its own document, so
|
|
496
686
|
* what prints is the report rather than the host page.
|
|
497
687
|
*/
|
|
498
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;
|
|
499
702
|
setCommentStatus(id: CommentId, status: CommentStatus): boolean;
|
|
500
703
|
setCommentType(id: CommentId, type: CommentType | null): boolean;
|
|
501
704
|
setCommentPriority(id: CommentId, priority: CommentPriority | null): boolean;
|
|
@@ -530,4 +733,22 @@ export declare function createCommentOverlay(
|
|
|
530
733
|
options: CommentOverlayOptions & { autoInit: false }
|
|
531
734
|
): () => CommentOverlay;
|
|
532
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
|
+
|
|
533
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",
|