CI's node coverage run (vite.config.ts, 'measure utility + server-side logic
only') counts every .ts under the include globs via all-files, but the Tiptap
NodeView builds live ProseMirror DOM and only runs in the browser editor — it is
exercised by the client project's browser tests, not the node run. Left in, it
showed 0% and dragged global functions (78.68%) and branches (78.48%) below the
80% gate.
Exclude it alongside the .svelte / browser-only UI files this config already
measures around. Restores the gate: statements 88.82%, branches 82.3%,
functions 87.27%, lines 89.77% (server project, verified locally).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the clean-agent review of PR #717:
- C1: the hidden pencil was opacity-0 only, which still hit-tests; its 44px box
overhangs adjacent text, so a click in the gap between two mentions could land
on the invisible button and spuriously open the dropdown (AC-8 hole). Add
pointer-events-none while hidden, re-enabled with the opacity reveal on
hover/focus.
- C2/N1: editor.setEditable() emits "update", not a ProseMirror transaction, so
the NodeView's 'transaction' listener missed a mid-session disable flip (stale
aria-disabled/tabindex; the comment was wrong). Listen on 'update' instead —
which also skips selection-only changes, so it fires far less often.
- N2: track the node across update() so the pencil opens with the live
displayName (hardening; relink only swaps personId today).
Tests: structural guard that the hidden pencil is pointer-events-none + reveals,
and a mid-session disable-flip test (fixture gains an onReady setDisabled hook).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Passes editingDisplayName into MentionDropdown; the persistent aria-live region
announces person_mention_editing_announce({displayName}) on re-edit open and
falls back to the prompt/empty/count copy once the user edits or results arrive.
Routed through the SAME sr-only region as the result count — no second live
region (avoids the double-announce bug Leonie S-2 fixed). Fresh-@ passes an
empty editingDisplayName, so its announcements are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AC-7: disabled editor → pencil is disabled + aria-disabled + tabindex -1, and
neither keyboard nor pointer activation mounts a dropdown (WCAG 2.1.1, not just
pointer-events-none).
- AC-8: plain text shows no pencil/dropdown; two adjacent mentions each keep one
pencil with no spurious gap pencil and no auto-open; a doc-start mention still
renders its pencil.
- Security: an oversized stored displayName clips the search query to 100 chars
while the preserved node text stays full-length; re-link sources personId
solely from the picked Person (p-anna), never the reflected/clipped text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Locks in the single-owner controller guarantees: pencil→pencil, fresh-@→pencil
and pencil→fresh-@ all leave exactly one dropdown open; the request-token bump
on open discards a superseded open's in-flight fetch (open A → open B → A
resolves, deterministic, no sleeps). Plus a #380 AC-1 regression guard that the
fresh-@ path still inserts the typed text as displayName after the controller
refactor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a visible × dismiss control to MentionDropdown (shared by the fresh-@ and
re-edit paths) and, for the re-edit path which has no Tiptap suggestion plugin
to forward keys, focuses the search input on open and handles its own keyboard:
Escape dismisses (AC-4), Arrow/Enter reuse the exported selection logic so the
dropdown is navigable on its own (AC-9 parity with the fresh-@ dropdown).
Both close paths (Escape + ×) leave the mention node attrs + text byte-identical
(AC-4) — close() never touches the document. Controller wires ondismiss=close
(+refocus editor) and focusOnMount only for the re-edit open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hosts each mention as a Tiptap NodeView (mentionNodeView.ts) that renders the
@displayName token (textContent — never innerHTML) plus a contenteditable=false
pencil button in a fixed-width slot, revealed on whole-token hover and keyboard
focus (instant opacity swap, no reflow). Activating the pencil (click or Enter/
Space) opens the single mention dropdown via the controller, anchored at the
token and pre-filled with the stored displayName.
commitRelink swaps ONLY personId in place via setNodeMarkup, sourcing the id
solely from the selected Person — the stored displayName is preserved by
construction (AC-3), even after the search input is edited (AC-5, the #380 AC-1
invariant). renderHTML/renderText stay for serialization + clipboard.
AC-1/AC-2/AC-3/AC-5 + serializer round-trip covered by browser tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls mountedDropdown / requestId / debouncedSearch / dropdownState ownership
out of Tiptap's suggestion.render() closure into one createMentionController().
render() becomes a thin adapter: onStart→open, onUpdate→update, onExit→close.
This is the single-owner structure #628 needs for the AC-6 single-dropdown
invariant — the upcoming pencil re-edit affordance opens via the same
controller.open() instead of racing the suggestion plugin over module state.
open() now also bumps the request token so an open-A→open-B sequence discards
A's in-flight fetch (preserved increment-on-open semantics). No behaviour
change for the fresh-@ path — existing browser suite is the regression guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>