Skip to content

Releases: xKeCo/helldots

v0.12.1

Choose a tag to compare

@github-actions github-actions released this 09 Sep 06:39

Patch Changes

  • b32177b: Give the text fields a readable surface and move the focus cue off the text.
    #comment-input and .thread-input were borderless and unpadded, so the
    inset 0 -2px 0 underline they took on focus was painted through the
    descenders of the line being typed β€” .thread-input is one line tall. All
    three fields now share the inline editor's rounded, padded surface and light
    their own edge on focus: a blue border plus a soft glow around the same box.

    Also removes the horizontal scrollbar the screenshot strip put on the whole
    thread. Its margin-inline: -4px made the strip wider than .thread-scroll,
    which is overflow-y: auto and so scrolls on both axes.

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 23:32

Minor Changes

  • eb1fd07: Restore visible keyboard focus indicators (WCAG 2.1 AA, 2.4.7 Focus Visible).

    Every control the widget exposes to the keyboard now shows one: the toolbar,
    the status, type and priority pickers, the menu items, the inbox cards and
    filter chips, the marker circles and the screenshot thumbnails take a 2px
    ring, and the borderless text fields take an inset underline.

    They are bound to :focus-visible, so they appear only when focus arrived by
    keyboard β€” the pointer look is unchanged, which is what the earlier revert of
    these rules was protecting.

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 21:49

Minor Changes

  • 072ec69: Editing and deleting are no longer open to everyone

    The widget recorded authorId on every comment and reply but never read it
    back, so the β‹― menu offered Edit and Delete on all of them to whoever was
    looking. It now offers them only on records carrying your own identity.

    A new can(action, target) option overrides that rule for moderators, owner
    roles or read-only viewers. action is one of "edit:comment",
    "delete:comment", "edit:reply" or "delete:reply"; return literal true
    to allow. The same verdict is readable from overlay.can(action, target), so
    a delete button in your own chrome can ask the one rule.

    Only those four actions are gated β€” status, type, priority, tags, reactions
    and replying stay open to everyone. Your own API calls are never refused:
    can governs clicks inside the widget, not overlay.deleteComment(id) from
    your code.

    Behaviour changes only for hosts that set user. Without one, every record
    is written by the same anonymous actor and nothing is hidden.

    This is not authorization β€” HellDots runs in the page. Keep checking
    authorId against the session on your server.

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 26 Aug 17:57

Minor Changes

  • 3886169: Add a marker visibility toggle: an eye button in its own pill beside the
    toolbar hides every comment marker to cut visual noise, and shows them
    again. The preference persists per browser, and the layer re-shows itself
    when the user enters comment mode (button or shortcut) or navigates to a
    comment from the inbox. The icon, tooltip, and accessible name flip
    together.

Patch Changes

  • 110ff07: Drop the hover tooltip from reaction pills. The emoji and the count already say
    what a pill is, so a bubble per pill turned a dense row into a wall of popups.
    The trigger beside the row keeps its tooltip, and the pills keep their
    accessible name and aria-pressed state.
  • 87a0e72: A drag comment now anchors to the region it selected, not to the mouseup
    pixel. The comment is placed at the rectangle's center, and the anchor
    target is the topmost element at that point whose box covers at least 60%
    of the region β€” so a floating panel that happens to sit under the released
    mouse (and later closes) can no longer capture the anchor and leave the
    marker invisible. Plain-click placement is unchanged.
  • 5ffb3a9: Show the full author name in a hover tooltip when the meta row truncates it.
    The name is measured on hover, so the bubble only appears when the ellipsis
    actually hid something β€” short names stay tooltip-free.
  • a4bac45: Resolve anchor-matching ties to the deepest matching element β€” in both the
    selector match and the rescue search. The text fingerprint is truncated to
    64 characters, so in nested DOMs a parent and its child score identically;
    the strict comparison kept the first candidate in document order β€” always
    the ancestor β€” so the most specific match could never win. Ties between
    unrelated elements keep document order, and anchors that resolved uniquely
    before resolve identically now.

v0.9.1

Choose a tag to compare

@github-actions github-actions released this 25 Aug 17:42

Patch Changes

  • 1fb00c8: Load modern-screenshot lazily on the first capture instead of statically.

    Bundlers now split the renderer (~10 KB gzip) into its own chunk that
    downloads when the first capture runs, so apps embedding HellDots no longer
    pay for it in their initial bundle β€” and pages where nobody captures never
    fetch it at all. A failed load surfaces through the existing onError path
    and is retried on the next capture rather than cached.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 17:30

Minor Changes

  • 222d567: Declare modern-screenshot as a peer dependency instead of a direct one.

    The ESM bundle already treated the renderer as external, but package size
    scanners (Bundlephobia, bundlejs) resolve bare imports from dependencies,
    so the published package measured 51.6–54 KB gzip against a 50 KB budget the
    bundle itself meets at 44.7 KB. Both scanners attribute peer dependencies to
    their own package, so the public measurement now matches the gate.

    npm 7+, pnpm 8+ and bun install missing peer dependencies automatically, so
    npm install helldots is unchanged. Yarn users must add
    modern-screenshot alongside.

Patch Changes

  • b395335: Fix a blue focus ring appearing around the inbox panel when a comment is
    opened from a copied link.

    The panel takes focus as it opens so a screen reader lands inside the dialog.
    Opened by a click that is quiet, but a link opens it during page load with no
    pointer interaction behind it, which is enough for Chrome's :focus-visible
    heuristic to paint its default ring. The panel carries tabindex="-1" and is
    never reachable by Tab, so the ring marked nothing anyone could act on.

    Focus still moves into the dialog; only the ring is suppressed. The focus
    rings on the confirm dialog's buttons are untouched.

  • 5f21bb5: Cap the thread header's meta row at 280px so it stops claiming the full width
    of the popover.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 16:28

Minor Changes

  • 4d352a8: Add fastCapture, an opt-in that narrows the screenshot renderer's
    computed-style enumeration to a curated allow-list instead of every property
    the browser exposes. That enumeration is ~91% of a capture's cost and scales
    with element count; the list cuts the dominant phase by about 2.7x, measured
    pixel-identical to a full capture on the pages it was verified against.

    Off by default: a property the list does not name is absent from the image,
    so the trade belongs to the host. See the README for when to turn it on.

  • 4d352a8: Add skipIframeContent, an opt-in that renders embedded documents as blank
    instead of cloning them. A same-origin iframe's cost is invisible from the
    host page β€” the renderer clones the whole embedded document, so a page of
    242 elements can be a capture of 9 245.

    The <iframe> element is kept, so its box and the layout below it stay
    where the page put them. Off by default: on a same-origin frame the content
    you lose is real.

  • 4d352a8: Add captureTimeout, bounding how long one remote asset may hold a capture
    up while the renderer re-fetches the page's images and fonts to inline them.

    The default is unchanged. A dead asset URL stalls a capture for about a
    minute β€” the renderer's own 30 second deadline, paid twice because the number
    drives two waits in sequence, one for the image on the page to load and one
    for the fetch that inlines it β€” and the capture still succeeds with that asset
    replaced by a placeholder. The wait is bounded rather than multiplied: one
    dead asset and ten cost the same.

    Left at the default because a shorter one drops assets that were only slow
    and leaves holes in the image with nothing to say so. Set it if you have
    measured your own page. Only a finite positive number is honoured: 0 means
    "never give up" to the renderer, and Infinity is coerced to no wait at all.

  • 4d352a8: Dragging a region no longer waits for the screenshot. The marker and the
    comment box appear as soon as the mouse is released, and the crop drops into
    a "Capturing…" slot in the attachment strip when its render lands. On a
    heavy page that removes over a second between the gesture and being able to
    type; Send waits for the crop if you get there first.

    Adds capturingScreenshot to both locales.

Patch Changes

  • 4d352a8: Fix captures coming back blank on very long pages. Past the browser's canvas
    ceiling β€” 65 535px in a dimension, and an area cap besides β€” a canvas accepts
    its size, hands out a context, takes every draw call and holds no pixels, so
    every screenshot off it was empty with nothing said about it.

    Renders now fit their scale to what the browser will actually paint, verify
    that the result holds pixels, and retry smaller if it does not. Pages below
    the ceiling are unchanged; past it the capture goes soft rather than blank,
    and a render that cannot be produced at all reports through onError instead
    of attaching an empty image.

  • 4d352a8: Screenshot capture no longer freezes the page. The clone traversal now hands
    the main thread back to the browser on an 8 ms budget, so the page keeps
    painting and accepting input while a render is in flight β€” on heavy pages
    that render could block for over a second. Wall-clock capture time is
    unchanged; what changes is that it is no longer a freeze.

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 19:18

Minor Changes

  • a616711: Four additions for hosts integrating the widget into a real app.

    • onCommentModeChanged(active) β€” comment mode turned on or off, however
      it was flipped. The keyboard shortcut is the reason: it never reaches the
      host, so an app that has to stand down while somebody picks an element had
      no signal at all.
    • onCommentOpened(comment) β€” somebody opened a comment's thread, from
      its marker or from the inbox detail. This is what an unread count is built
      on; it does not fire when the inbox merely re-renders. HellDots stores no
      read state of its own, because whose read it is depends on an identity only
      the host can persist.
    • setUser(user) β€” replaces the identity new comments, replies and
      reactions are attributed to, without rebuilding the widget. For a session
      that resolves after mount, or a user switching account. Nothing already
      written is rewritten; null returns to the anonymous author.
    • exportCommentsCsv() / exportMetricsCsv() now return the CSV text as
      well as downloading it, so the rows can be sent somewhere instead of handed
      to the user as a file. printMetricsReport() still returns nothing β€” what
      it produces is a print dialog.
  • 57e8cda: transformScreenshot β€” swap every image the widget acquires for a string
    of your own, so a ~33 KB base64 data URL per comment does not end up in your
    database.

    Called with (dataUrl, { kind, commentId }) for the automatic viewport
    capture (kind: "context"), drag-crop regions, and file attachments on
    comments and replies (kind: "attachment"). Return the string to store β€”
    typically a URL into your own object storage.

    It runs at two moments: everything on a comment transforms as the comment is
    saved, while a reply attachment transforms when the file is picked, because
    addReply() is synchronous. Either way the record may never arrive β€” an
    abandoned draft, or a box dismissed mid-upload β€” so sweep for unreferenced
    blobs rather than assuming every URL you hand back gets stored.

    Fail-open: a rejection, a throw, or a resolved value that is not a non-empty
    string keeps the original data URL and reports the new
    onError(error, "transform"). Not called for records passed to
    loadComments(), nor for screenshots handed to addReply() directly.

    The comment box's submit button is now disabled while a save is in flight,
    since that save may be waiting on an upload.

  • 318a284: Callbacks now say who caused a change and what moved, and a shared link can
    ask for the comment it points at.

    • meta.origin β€” every callback takes one extra trailing argument, and
      onChange events carry the same fields flattened onto them. "user" is
      somebody acting inside the widget, "host" is your own code calling a
      method. Multi-user apps needed this to stop echoing their own remote writes
      back to the server. Existing handlers that ignore the argument are
      unaffected.
    • meta.from / meta.to / meta.field β€” comment:status-changed now
      carries both ends of the move (so a reopen is told apart from a resolve),
      and comment:updated says which of type, priority or tags it was about.
      field narrows from/to in TypeScript.
    • onCommentRequested(id) β€” fires when a "Copy link" URL points at a
      comment the widget does not hold, once per id. Fetch it, hand it to
      loadComments(), and the inbox opens on it; return a promise and the link
      is retried once it settles. This is what makes loading only the linked
      comment possible. DEFAULT_LINK_PARAM and readCommentLinkParam are also
      exported now, for reading the id before an overlay exists.
    • onReady(overlay) β€” the widget has mounted and every method is safe to
      call. loadComments() before that no longer throws: the data is held and
      applied at mount, though the counts come back as zeroes until then.
    • onError(error, context) β€” failures the widget survives but only the
      console used to hear about: "capture", "storage", "load", "link".
    • setCommentType, setCommentPriority and setCommentTags now no-op when
      the value does not change, matching setCommentStatus. They previously
      wrote to storage and emitted an event for a change that did not happen.

Patch Changes

  • 6993d78: A save that outlives its own comment box no longer lands on a different one.
    The guard after the awaits in _saveCommentNow asked whether a box was
    open rather than whether it was still the same one β€” a window that a host's
    transformScreenshot upload stretches to seconds. Dismissing the box, opening
    a second comment elsewhere and letting the upload resolve wrote the first
    draft onto the second one's anchor and tore the second draft down. The draft
    is now snapshotted before the first await and compared by identity.

    (The same release adds transformScreenshot; a reply attachment sent while
    its upload was in flight could be dropped in the first cut of that feature,
    and is not in any published version.)

v0.6.0

Choose a tag to compare

@xKeCo xKeCo released this 20 Aug 12:39

Minor Changes

  • 599e9f0: Add emoji reactions to comments and replies. One of six fixed reactions
    (πŸ‘ πŸ‘Ž ❀️ πŸŽ‰ πŸ‘€ πŸš€) is added from the emoji button in the action strip β€” the
    same strip the thread popover and every inbox card share β€” and once something
    has been reacted to, a row of pills appears under the comment (below its
    screenshot when there is one) with a trailing button for adding one more.
    Reacting again with the same emoji removes it, and the row disappears with the
    last reaction.

    The action strip is now split in two: status, type and priority on the left,
    and the tools β€” react, copy context, β‹― β€” on the right. Replies carry the same
    pair of controls on their meta line.

    Two new methods, toggleCommentReaction(id, emoji) and
    toggleReplyReaction(commentId, replyId, emoji), plus a reaction:toggled
    event and its onReactionToggled(comment, reply) callback β€” reply is null
    when the reaction is on the root comment.

    user gained an optional id. It is never displayed: it is what a reaction is
    keyed on, so two teammates who share a display name do not share a reaction.
    Without it the name is used, exactly as authorship already does.

    Reactions travel in serializeComments() output as an { emoji: actorKey[] }
    map, null when nobody has reacted, and hostile or stale persisted values are
    scrubbed on load. Costs 2 KB gzip and no new dependency.

  • 1d23150: Persist the host-supplied user identity as authorId on comments and replies.

    user.id already keyed reactions; it is now also stored alongside author on
    everything that user creates, so two teammates who share a display name stay
    distinguishable in the record. The id is opaque to HellDots β€” point it at your
    user table, at a comments-only store, or at nothing. The display name is still
    the only thing rendered, and it travels with the record, so a store holding
    nothing but comments renders every author without a lookup.

    The field is additive and optional: records written before this change load
    unchanged with authorId: null, and no migration is involved. A non-string id
    arriving through loadComments is dropped rather than trusted.

    One identifier now has exactly one spelling. The id is trimmed β€” and never
    truncated β€” in every place it lands: authorId on comments and replies, the
    actor.id of each audit entry, and the key a reaction is stored under. They
    each normalised it differently before, so a padded or long id could arrive in
    three different forms inside one payload.

    HellDots still authenticates nobody. Whatever the host declares in user is
    recorded as-is.

  • d353b7b: Add an append-only audit trail to every comment: who created it, edited its
    text, moved its status or changed its classification, and when. It shows as a
    folded History (n) disclosure in the inbox detail and rides along in
    serializeComments() output as history.

    Resolution time is now derived from that log instead of read off a stored
    figure, so a comment that was resolved, reopened and resolved again reports the
    duration of the resolution currently in force β€” and the superseded ones are
    listed under Previous resolutions in the same disclosure.

    Replies and reactions are deliberately not recorded: a reply already carries
    its own author and timestamp, and reactions are high-frequency signal with no
    audit value. That keeps a typical comment at three to five entries.

    The field is additive and optional β€” a corpus written before this change loads
    unchanged with history: null, and no migration is involved. Entries arriving
    through loadComments are scrubbed: an unknown event type or an unparseable
    timestamp is dropped rather than trusted.

    HellDots still authenticates nobody, so the trail records what the host
    declared in user at the moment of each action. It is attributive, not
    evidential.

  • ba46d13: Add a metrics dashboard and report exports.

    The inbox header gains a Metrics button that swaps the list for a
    dashboard: totals, resolved and reopened counts, average and median resolution
    time, bars per status, type and priority, and a daily distribution. Each bar is
    painted in the same colour that value already carries in its picker. It measures
    whatever the panel is filtered to; overlay.getMetrics() returns the same
    shape over the whole corpus.

    Three exports, from the dashboard or directly:

    • overlay.exportCommentsCsv() β€” one row per comment
    • overlay.exportMetricsCsv() β€” the aggregate figures in section, key, value
    • overlay.printMetricsReport() β€” the browser's print dialog, where "Save as
      PDF" produces a real PDF

    The CSVs are RFC 4180 with a UTF-8 BOM so Excel reads accents correctly, and
    values that a spreadsheet would evaluate as formulas are neutralised. No new
    dependency: the charts are hand-drawn SVG and the PDF is the browser's own, so
    the whole feature costs 4.28 KB gzip.

    Also fixes mountStyles constructing its stylesheet in the calling realm
    rather than the target's, which prevented styles from being adopted into a
    document other than the caller's.

  • 6405f7c: Add an In review state to the comment lifecycle, between In progress and
    Resolved. It is available in the status picker, in the inbox status filter
    and through setCommentStatus(id, "in_review"), and it carries the blue that
    open used to have.

    open moves to an unsaturated off-white grey, so the states somebody actively
    moved a comment into are the ones that stand out. CommentStatus gains
    "in_review"; stored comments need no migration.

Patch Changes

  • 448c0d9: Close the gap under the context block in the inbox detail view. A comment
    with no replies still rendered its replies container, and as a zero-height
    flex item it collected 24px of the column gap β€” most visible with the
    context block collapsed.

  • ae9049d: Fix dropdowns rendering see-through on resolved comments. A resolved card is
    dimmed with opacity, which composites it and everything inside it as a single
    translucent layer β€” so the status, type, priority and ... menus opened from
    one were painted above the context block and still showed it through
    themselves. The dim is now lifted while a dropdown inside the card is open.

    Most visible in the inbox detail view, where the context screenshot sits
    directly behind the menu.

  • d186cbb: Fix the inbox detail header: its prev/next/close buttons reuse the card
    action strip, whose space-between scattered them across the row instead
    of grouping them opposite Back. They now align to the end of the strip,
    which keeps its full width.

  • 494ecbf: Keep dropdowns inside the surface that clips them on the horizontal axis
    too. The status picker leads the action strip, so its menu hung 45px past
    the left edge of the inbox panel and was cut in half; it now aligns to the
    button's left edge when β€” and only when β€” it fits that way.

v0.5.0

Choose a tag to compare

@xKeCo xKeCo released this 14 Aug 15:04

Single-page app support, one event stream for every change, and a pre-release
audit of the screenshot pipeline that turned up four separate ways a capture
disagreed with the page it was taken from.

npm install helldots@0.5.0

Single-page apps

notifyNavigation() re-syncs the widget after a client-side navigation:
comments reclassify against the new pathname, anchors re-resolve against the
new DOM, markers rebuild and the inbox moves onto the new page β€” deep links
and the cross-page handoff included. The new navigate option routes the
widget's own cross-page jumps through your router instead of a full reload,
and autoDetectNavigation: true covers back/forward automatically.

const overlay = createCommentOverlay({
  navigate: (page) => router.push(page),
});
router.afterEach(() => overlay.notifyNavigation());

One subscription for every change

onChange fires for every mutation, typed as a discriminated union on
event.type β€” handy when you sync everything to one endpoint instead of
wiring nine callbacks. The existing callbacks are unchanged and keep firing at
the same moments; this is additive. A handler that throws is now caught and
warned about instead of propagating out of the mutation that already happened.

Screenshots that match the page

A screenshot is not a screen grab β€” the browser exposes no way to rasterize
the painted page from JavaScript, so the capture is a re-render of the DOM,
and anything the re-render gets wrong moves content relative to what you saw.
Four such defects are fixed here:

  • The render was anchored to <body>, where the user-agent's 8px margin
    reappears, so every element in normal flow landed 8px off and the last 8px
    fell outside the canvas.
  • Pages shorter than the viewport came out with a solid black band where the
    render did not reach.
  • A strict style-src Content Security Policy killed the capture outright.
  • A web font served from a cross-origin stylesheet never reached the clone, so
    captured text reflowed into a fallback face β€” and its different metrics made
    drag selections over text come back holding the wrong glyphs.

That last one ships behind a new opt-in option, embedCrossOriginFonts. Most
pages want the cheaper fix instead: add crossorigin to the font <link>, or
self-host it. See Web fonts in screenshots in the README.

Also in this release

  • addReply accepts a comment id as well as the live object, and the new
    clearComments() wipes everything at once β€” the bulk reset for reconciling
    against a backend before a fresh loadComments.
  • The inbox detail's Context section folds away, like the thread popover's.
  • The widget renders under a strict style-src CSP: styles are delivered as
    constructed stylesheets.
  • Dropdowns open upward when there is no room below them.
  • Accessibility: the keyboard contract role="menu" promises, focus handling
    in the lightbox and inbox, and keyboard-operable screenshot thumbnails.
  • Performance: the inbox list reconciles by comment id instead of rebuilding,
    and keeps its scroll position.
  • Internals: overlay.js split into capture-flow, popover-controller and
    marker-engine. No public API change.

Upgrading from 0.4.0

No breaking changes.

Only if you pass a custom strings object: one new key, modifierShift
("Shift"). Nothing was removed. The bundled en and es locales are
complete.


Full list of changes in CHANGELOG.md.