From 14d3f9f943de800738ae5af502dfb1c9f192befb Mon Sep 17 00:00:00 2001 From: Vishal Sachdev Date: Sun, 26 Jul 2026 12:52:25 +0530 Subject: [PATCH] fix(editor): render visible highlight + popover for Flag marks Flag from the selection menu wrote a proofFlagged mark to the doc but createDecorations skipped 'flagged' entirely (case fell into the authored/approved continue), so flagging produced zero visual feedback even though STYLES.flagged already existed unused. Comment/Suggest were unaffected because their kinds are handled in the switch. - marks.ts: give 'flagged' its own decoration case (mark-flagged class, dusty-rose left border + tint) and a rose box-shadow-only glow for new flags so the forwards-filled green glow doesn't override the flag tint. - mark-popover.ts: clicking a flagged span now shows a 'Flagged by ...' card (note or quote) with Remove flag / Close, instead of an empty Suggestion card with dead Apply/Reject. Co-Authored-By: Claude Fable 5 --- src/editor/plugins/mark-popover.ts | 43 ++++++++++++++++++++++++++++++ src/editor/plugins/marks.ts | 16 ++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/editor/plugins/mark-popover.ts b/src/editor/plugins/mark-popover.ts index 7bb4e581..562deb9d 100644 --- a/src/editor/plugins/mark-popover.ts +++ b/src/editor/plugins/mark-popover.ts @@ -1215,6 +1215,10 @@ class MarkPopoverController { } private renderSuggestion(mark: Mark): void { + if (mark.kind === 'flagged') { + this.renderFlag(mark); + return; + } this.popover.innerHTML = ''; const header = document.createElement('div'); @@ -1286,6 +1290,45 @@ class MarkPopoverController { this.popover.appendChild(actions); } + private renderFlag(mark: Mark): void { + this.popover.innerHTML = ''; + + const header = document.createElement('div'); + header.className = 'mark-popover-header'; + header.textContent = `Flagged by ${getActorName(mark.by)}`; + + const body = document.createElement('div'); + body.className = 'mark-popover-body'; + const note = (mark.data as { note?: string } | undefined)?.note; + body.textContent = note || mark.quote || ''; + + const actions = document.createElement('div'); + actions.className = 'mark-popover-actions'; + + if (canCommentInRuntime()) { + const removeButton = document.createElement('button'); + removeButton.type = 'button'; + removeButton.textContent = 'Remove flag'; + installTouchSafeButton(removeButton, () => { + deleteMark(this.view, mark.id); + this.close(); + }); + actions.appendChild(removeButton); + } + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.textContent = 'Close'; + installTouchSafeButton(closeButton, () => { + this.close(); + }); + actions.appendChild(closeButton); + + this.popover.appendChild(header); + this.popover.appendChild(body); + this.popover.appendChild(actions); + } + private cacheActionRange(): void { const { from, to } = this.view.state.selection; if (from === to) return; diff --git a/src/editor/plugins/marks.ts b/src/editor/plugins/marks.ts index 910604cb..1356af9b 100644 --- a/src/editor/plugins/marks.ts +++ b/src/editor/plugins/marks.ts @@ -3110,9 +3110,14 @@ function createDecorations( switch (mark.kind) { case 'authored': case 'approved': - case 'flagged': continue; + case 'flagged': { + style = STYLES.flagged; + cssClass = `mark-flagged ${isActive ? 'mark-active' : ''}`; + break; + } + case 'comment': { const data = mark.data as CommentData; if (data?.resolved) continue; @@ -3223,6 +3228,15 @@ function injectGlowStyles(): void { 100% { box-shadow: none; } } + /* Flag glow (dusty rose, box-shadow only so the flag background persists) */ + .mark-flagged.proof-mark-new { + animation-name: proof-flag-glow; + } + @keyframes proof-flag-glow { + 0% { box-shadow: 0 0 8px rgba(252, 165, 165, 0.8); } + 100% { box-shadow: none; } + } + /* Refresh transition animations */ .proof-refreshing { opacity: 0.3; transition: opacity 200ms ease-out; } .proof-refreshed { opacity: 1; transition: opacity 300ms ease-in; }