/** * CommentGutter — Word-style comment bubbles beside the page. * * The converter renders a commented range inline (`span.comment-highlight[data-comment-id]` * around the runs, `a.comment-marker` at the reference), and `DocxSession.ListComments` is the * truth about the threads themselves. This module joins the two: for every thread root it finds * the highlight in the live DOM, places a bubble in a column beside the sheet at the highlight's * vertical position (stacking bubbles downward when they would overlap, the way Word's markup * area does), and draws a leader line from the highlight to the bubble. * * Nothing here is a second source of state. The bubbles are re-derived from `listComments()` * plus the DOM on every layout pass, and every action (reply, resolve, edit, delete, post a new * comment) goes through the editor's session commands, then re-lays out. * * The gutter lives INSIDE the editor's scrolling container so bubbles scroll with the page; the * host is responsible for reserving horizontal room for it (the ribbon pads the surface). */ import type { CommentListEntry } from "./types.js"; /** What the gutter needs from its editor — the comment commands plus the DOM it overlays. */ export interface CommentGutterHost { /** The element the document renders into; the gutter is appended to it. */ readonly container: HTMLElement; /** Author for comments and replies posted from the gutter. */ readonly commentAuthor: string; listComments(): CommentListEntry[]; /** Post a comment on `target` (captured at draft time — the live selection is long gone once * the user has typed into the bubble). Returns the created thread root, or null. */ addComment(markdown: string, author: string, target?: CommentTarget): CommentListEntry | null; addCommentReply(parentAnchorId: string, markdown: string, author: string): boolean; updateComment(anchorId: string, markdown: string): boolean; removeComment(anchorId: string): boolean; setCommentResolved(anchorId: string, resolved: boolean): boolean; /** The block that would receive a new comment, and the selection span inside it. */ commentTarget(): CommentTarget | null; } export interface CommentGutterOptions { /** Column width in px. Default 248. */ width?: number; /** Vertical gap between stacked bubbles. Default 8. */ gap?: number; /** Called after every layout with the number of visible (unresolved) threads. */ onChange?: (info: { threads: number; open: number; active: string | null; }) => void; } /** * What a comment is about to be attached to: a block and the selection span in it (null = the * whole paragraph). `anchor` and `text` record which paragraph, with what content, the target * was captured on, so a post that comes after the block was re-rendered or edited can find the * live node and tell whether the span still means the same characters. */ export interface CommentTarget { block: HTMLElement; span: { start: number; length: number; } | null; anchor?: string; text?: string; } /** * Every comment hanging off `rootAnchorId`, transitively — replies to replies included — deepest * first, so removing them in order never leaves an orphan behind. The root itself is not listed. */ export declare function threadMembers(comments: readonly CommentListEntry[], rootAnchorId: string): string[]; export declare class CommentGutter { readonly element: HTMLElement; private readonly host; private readonly options; private readonly leaders; private frame; private mutations; private resize; private activeId; private draft; private draftTarget; private draftAnchorTop; private editing; private replying; private expanded; private visible; private disposed; private readonly onContainerClick; constructor(host: CommentGutterHost, options?: CommentGutterOptions); /** Re-derive and re-position every bubble on the next animation frame. */ schedule(): void; /** Synchronous layout (tests, and callers that need the DOM settled now). */ layout(): void; /** Re-append the gutter's elements after the container was emptied by a mount. */ reattach(): void; /** Show or hide the markup area (Word's "Show Comments"). */ setVisible(visible: boolean): void; get isVisible(): boolean; /** The active thread root's anchor id, if any. */ get active(): string | null; /** Activate a thread: its bubble lifts, its highlight darkens, and (optionally) it scrolls * into view. Pass null to clear. */ setActive(id: string | null, opts?: { scrollBubble?: boolean; scrollAnchor?: boolean; }): void; /** Thread roots in document order — what Previous/Next step through. */ threadsInOrder(): CommentListEntry[]; /** Activate the thread after (or before) the active one, wrapping around. */ step(direction: 1 | -1): CommentListEntry | null; /** * Start a new comment on the current selection: a draft bubble appears beside it with a * focused textarea. Posting goes through the editor; cancelling removes the draft. Returns * false when nothing is selected/focused to comment on. */ beginDraft(): boolean; /** Remove the draft bubble, if any. */ cancelDraft(): void; get hasDraft(): boolean; dispose(): void; /** The first highlight (else marker) carrying the thread's numeric id, in the live DOM. */ private anchorFor; private markHighlights; private bubbleKey; private buildBubble; private buildEntry; } /** The gutter's stylesheet — injected once per document by the editor. Scoped to the classes * this module emits, so it is safe on a host page. Colours derive from a per-author hue so a * reviewer's highlight and bubble read as one. */ export declare const COMMENT_GUTTER_CSS = "\n.docx-comment-gutter {\n position: absolute; top: 0; right: 0; bottom: 0; z-index: 4;\n pointer-events: none; font: 12.5px/1.4 \"Inter\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n}\n.docx-comment-gutter[data-empty] { display: none; }\n.docx-comment-leaders { position: absolute; top: 0; left: 0; z-index: 3; pointer-events: none; overflow: visible; }\n.docx-comment-leader { fill: none; stroke: hsl(var(--docx-comment-hue, 200) 45% 62%); stroke-width: 1; stroke-dasharray: 3 3; opacity: .55; }\n.docx-comment-leader-active { stroke-dasharray: none; opacity: .95; stroke-width: 1.5; }\n.docx-comment-bubble {\n position: absolute; left: 12px; right: 8px; pointer-events: auto;\n padding: 9px 10px 8px; border: 1px solid hsl(var(--docx-comment-hue, 200) 40% 78%);\n border-left: 3px solid hsl(var(--docx-comment-hue, 200) 55% 55%);\n border-radius: 8px; background: #fff; color: #1e293b;\n box-shadow: 0 1px 2px rgba(15, 23, 42, .06), 0 3px 8px rgba(15, 23, 42, .05);\n transition: top .18s cubic-bezier(.4,0,.2,1), box-shadow .15s ease, transform .15s ease;\n}\n.docx-comment-bubble[data-active] {\n border-color: hsl(var(--docx-comment-hue, 200) 55% 55%);\n box-shadow: 0 2px 6px rgba(15, 23, 42, .08), 0 10px 22px rgba(15, 23, 42, .1);\n transform: translateX(-4px); z-index: 2;\n}\n.docx-comment-bubble[data-resolved] { opacity: .72; background: #f8fafc; }\n.docx-comment-bubble[data-collapsed] .docx-comment-text { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; color: #64748b; }\n.docx-comment-bubble[data-collapsed] .docx-comment-reply, .docx-comment-bubble[data-collapsed] .docx-comment-bar { display: none; }\n.docx-comment-bubble[data-orphan] { border-style: dashed; }\n.docx-comment-bubble[data-draft] { border-style: solid; }\n.docx-comment-entry + .docx-comment-entry { margin-top: 8px; padding-top: 8px; border-top: 1px solid #f1f5f9; }\n.docx-comment-reply { margin-left: 10px; }\n.docx-comment-head { display: flex; align-items: center; gap: 6px; min-height: 20px; }\n.docx-comment-avatar {\n display: inline-grid; place-items: center; flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;\n background: hsl(var(--docx-comment-hue, 200) 55% 45%); color: #fff; font-size: 9px; font-weight: 700; letter-spacing: .02em;\n}\n.docx-comment-author { font-weight: 600; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.docx-comment-date { color: #94a3b8; font-size: 10.5px; white-space: nowrap; margin-left: auto; }\n.docx-comment-badge { padding: 1px 6px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }\n.docx-comment-menu { display: none; gap: 2px; margin-left: 4px; }\n.docx-comment-bubble:hover .docx-comment-menu, .docx-comment-bubble[data-active] .docx-comment-menu { display: inline-flex; }\n.docx-comment-menu button {\n padding: 0 4px; min-height: 18px; border: 0; border-radius: 4px; background: none; color: #94a3b8; font-size: 12px; line-height: 1; cursor: pointer;\n}\n.docx-comment-menu button:hover { background: #f1f5f9; color: #1e293b; }\n.docx-comment-text { margin: 5px 0 0; white-space: pre-wrap; word-wrap: break-word; color: #334155; }\n.docx-comment-bar { display: flex; gap: 6px; margin-top: 8px; }\n.docx-comment-bar button, .docx-comment-actions button {\n min-height: 24px; padding: 2px 9px; border: 1px solid #e2e8f0; border-radius: 6px; background: #fff; color: #1e293b;\n font: inherit; font-size: 11.5px; font-weight: 500; cursor: pointer;\n}\n.docx-comment-bar button:hover, .docx-comment-actions button:hover { background: #f1f5f9; }\n.docx-comment-actions { display: flex; gap: 6px; margin-top: 6px; }\n.docx-comment-actions .docx-comment-primary { background: #0f766e; border-color: #0f766e; color: #fff; }\n.docx-comment-actions .docx-comment-primary:hover { background: #0d9488; }\n.docx-comment-input {\n display: block; width: 100%; margin-top: 6px; padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 6px;\n background: #fff; color: #1e293b; font: inherit; font-size: 12.5px; line-height: 1.4; resize: vertical; box-sizing: border-box;\n}\n.docx-comment-input:focus { outline: 2px solid #0f766e; outline-offset: 1px; border-color: #0f766e; }\n/* The commented range itself. The converter's stylesheet paints a flat yellow; the editor\n tints by author and lifts the active thread the way Word does. */\nspan.comment-highlight[data-comment-id] {\n background: hsl(var(--docx-comment-hue, 45) 90% 88%);\n border-bottom: 2px solid hsl(var(--docx-comment-hue, 45) 70% 55%);\n cursor: pointer;\n}\nspan.comment-highlight[data-comment-id][data-comment-resolved] { background: transparent; border-bottom-color: #cbd5e1; }\nspan.comment-highlight.docx-comment-active { background: hsl(var(--docx-comment-hue, 45) 90% 78%); }\n/* The [n] reference marker is engine chrome, not document text: hide it, keep the anchor.\n (Qualified by the editor's roots to outrank the converter's own a.comment-marker rule,\n which is injected later in the document.) */\n.docx-body-flow a.comment-marker, [data-hf-band] a.comment-marker, .page-box a.comment-marker { display: none; }\n[data-comments-hidden] span.comment-highlight[data-comment-id] { background: transparent; border-bottom-color: transparent; }\n"; //# sourceMappingURL=editor-comments.d.ts.map