@sobree/review 0.1.9 → 0.1.10
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/dist/index.js +45 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,41 +2,8 @@ import './index.css';var A = Object.defineProperty;
|
|
|
2
2
|
var C = (s, t, e) => t in s ? A(s, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : s[t] = e;
|
|
3
3
|
var a = (s, t, e) => C(s, typeof t != "symbol" ? t + "" : t, e);
|
|
4
4
|
import { getFloatingCorner as R } from "@sobree/core";
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
// 0 muted blue
|
|
8
|
-
"#2ca02c",
|
|
9
|
-
// 1 green
|
|
10
|
-
"#9467bd",
|
|
11
|
-
// 2 purple
|
|
12
|
-
"#8c564b",
|
|
13
|
-
// 3 brown
|
|
14
|
-
"#e377c2",
|
|
15
|
-
// 4 pink
|
|
16
|
-
"#17becf",
|
|
17
|
-
// 5 teal
|
|
18
|
-
"#bcbd22",
|
|
19
|
-
// 6 olive
|
|
20
|
-
"#ff7f0e"
|
|
21
|
-
// 7 orange
|
|
22
|
-
];
|
|
23
|
-
function I(s) {
|
|
24
|
-
if (!s) return 0;
|
|
25
|
-
let t = 2166136261;
|
|
26
|
-
for (let e = 0; e < s.length; e++)
|
|
27
|
-
t ^= s.charCodeAt(e), t = Math.imul(t, 16777619);
|
|
28
|
-
return Math.abs(t) % y.length;
|
|
29
|
-
}
|
|
30
|
-
function p(s) {
|
|
31
|
-
const t = I(s);
|
|
32
|
-
return `var(--sobree-author-${t}, ${y[t]})`;
|
|
33
|
-
}
|
|
34
|
-
const v = (s) => `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${s}</svg>`, _ = v('<path d="M3 8.5l3.2 3.2L13 5"/>'), T = v('<path d="M4 4l8 8M12 4l-8 8"/>'), S = v(
|
|
35
|
-
'<circle cx="8" cy="8" r="5.5"/><path d="M5.5 8l1.8 1.8L10.8 6"/>'
|
|
36
|
-
), E = v(
|
|
37
|
-
'<path d="M3.5 8a4.5 4.5 0 1 1 1.3 3.2"/><path d="M3.2 5v3h3"/>'
|
|
38
|
-
), x = 220;
|
|
39
|
-
class L {
|
|
5
|
+
const v = (s) => `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${s}</svg>`, I = v('<path d="M3 8.5l3.2 3.2L13 5"/>'), _ = v('<path d="M4 4l8 8M12 4l-8 8"/>'), T = v('<circle cx="8" cy="8" r="5.5"/><path d="M5.5 8l1.8 1.8L10.8 6"/>'), S = v('<path d="M3.5 8a4.5 4.5 0 1 1 1.3 3.2"/><path d="M3.2 5v3h3"/>'), E = 220;
|
|
6
|
+
class x {
|
|
40
7
|
constructor(t, e) {
|
|
41
8
|
a(this, "editor");
|
|
42
9
|
a(this, "stackRoot");
|
|
@@ -55,8 +22,8 @@ class L {
|
|
|
55
22
|
buildPopover() {
|
|
56
23
|
const t = document.createElement("div");
|
|
57
24
|
return t.className = "sobree-review-actions", t.setAttribute("role", "toolbar"), t.append(
|
|
58
|
-
this.actionButton("Accept change",
|
|
59
|
-
this.actionButton("Reject change",
|
|
25
|
+
this.actionButton("Accept change", I, "accept"),
|
|
26
|
+
this.actionButton("Reject change", _, "reject")
|
|
60
27
|
), t.addEventListener("mouseenter", () => this.cancelHide()), t.addEventListener("mouseleave", () => this.scheduleHide()), t;
|
|
61
28
|
}
|
|
62
29
|
actionButton(t, e, o) {
|
|
@@ -93,7 +60,7 @@ class L {
|
|
|
93
60
|
this.cancelHide(), this.target = e, this.label(e), this.position(t);
|
|
94
61
|
}
|
|
95
62
|
scheduleHide() {
|
|
96
|
-
this.cancelHide(), this.hideTimer = setTimeout(() => this.hide(),
|
|
63
|
+
this.cancelHide(), this.hideTimer = setTimeout(() => this.hide(), E);
|
|
97
64
|
}
|
|
98
65
|
cancelHide() {
|
|
99
66
|
this.hideTimer && (clearTimeout(this.hideTimer), this.hideTimer = null);
|
|
@@ -183,6 +150,35 @@ function k(s, t) {
|
|
|
183
150
|
const e = document.createRange();
|
|
184
151
|
return e.selectNodeContents(s), e.setEndBefore(t), e.toString().length;
|
|
185
152
|
}
|
|
153
|
+
const y = [
|
|
154
|
+
"#1f77b4",
|
|
155
|
+
// 0 muted blue
|
|
156
|
+
"#2ca02c",
|
|
157
|
+
// 1 green
|
|
158
|
+
"#9467bd",
|
|
159
|
+
// 2 purple
|
|
160
|
+
"#8c564b",
|
|
161
|
+
// 3 brown
|
|
162
|
+
"#e377c2",
|
|
163
|
+
// 4 pink
|
|
164
|
+
"#17becf",
|
|
165
|
+
// 5 teal
|
|
166
|
+
"#bcbd22",
|
|
167
|
+
// 6 olive
|
|
168
|
+
"#ff7f0e"
|
|
169
|
+
// 7 orange
|
|
170
|
+
];
|
|
171
|
+
function L(s) {
|
|
172
|
+
if (!s) return 0;
|
|
173
|
+
let t = 2166136261;
|
|
174
|
+
for (let e = 0; e < s.length; e++)
|
|
175
|
+
t ^= s.charCodeAt(e), t = Math.imul(t, 16777619);
|
|
176
|
+
return Math.abs(t) % y.length;
|
|
177
|
+
}
|
|
178
|
+
function p(s) {
|
|
179
|
+
const t = L(s);
|
|
180
|
+
return `var(--sobree-author-${t}, ${y[t]})`;
|
|
181
|
+
}
|
|
186
182
|
const O = 700;
|
|
187
183
|
class N {
|
|
188
184
|
constructor(t) {
|
|
@@ -218,16 +214,11 @@ class N {
|
|
|
218
214
|
e.textContent = `${r} ${n}${c}`;
|
|
219
215
|
}
|
|
220
216
|
const i = t[this.cursorIndex];
|
|
221
|
-
i && e && e.style.setProperty(
|
|
222
|
-
"--sobree-review-dock-accent",
|
|
223
|
-
p(i.author)
|
|
224
|
-
);
|
|
217
|
+
i && e && e.style.setProperty("--sobree-review-dock-accent", p(i.author));
|
|
225
218
|
}
|
|
226
219
|
// ---- handlers ----
|
|
227
220
|
handleClick(t) {
|
|
228
|
-
const e = t.target.closest(
|
|
229
|
-
"button[data-action]"
|
|
230
|
-
);
|
|
221
|
+
const e = t.target.closest("button[data-action]");
|
|
231
222
|
if (!e) return;
|
|
232
223
|
switch (t.preventDefault(), e.dataset.action) {
|
|
233
224
|
case "prev":
|
|
@@ -338,7 +329,7 @@ class q {
|
|
|
338
329
|
/** Pending debounce handles — `null` when no refresh is queued. */
|
|
339
330
|
a(this, "rafId", null);
|
|
340
331
|
a(this, "timerId", null);
|
|
341
|
-
this.editor = t.editor, this.stackRoot = t.sobree.stackRoot, this.showComments = e.showComments ?? !0, this.revisionActions = new
|
|
332
|
+
this.editor = t.editor, this.stackRoot = t.sobree.stackRoot, this.showComments = e.showComments ?? !0, this.revisionActions = new x(this.editor, this.stackRoot), this.dock = e.showDock ?? !0 ? new N({
|
|
342
333
|
host: t.host,
|
|
343
334
|
editor: this.editor,
|
|
344
335
|
stackRoot: this.stackRoot,
|
|
@@ -382,7 +373,9 @@ class q {
|
|
|
382
373
|
for (const e of this.unsubs) e();
|
|
383
374
|
this.rafId !== null && cancelAnimationFrame(this.rafId), this.timerId !== null && clearTimeout(this.timerId), this.revisionActions.destroy(), (t = this.dock) == null || t.destroy();
|
|
384
375
|
for (const e of Array.from(
|
|
385
|
-
this.stackRoot.querySelectorAll(
|
|
376
|
+
this.stackRoot.querySelectorAll(
|
|
377
|
+
"ins[data-revision-author], del[data-revision-author]"
|
|
378
|
+
)
|
|
386
379
|
))
|
|
387
380
|
e.style.removeProperty("--author-color");
|
|
388
381
|
for (const e of Array.from(
|
|
@@ -404,21 +397,14 @@ function B(s) {
|
|
|
404
397
|
"ins[data-revision-author], del[data-revision-author]"
|
|
405
398
|
);
|
|
406
399
|
for (const r of Array.from(t))
|
|
407
|
-
r.style.setProperty(
|
|
408
|
-
|
|
409
|
-
p(r.dataset.revisionAuthor)
|
|
410
|
-
);
|
|
411
|
-
const e = s.querySelectorAll(
|
|
412
|
-
"[data-block-revision]"
|
|
413
|
-
);
|
|
400
|
+
r.style.setProperty("--author-color", p(r.dataset.revisionAuthor));
|
|
401
|
+
const e = s.querySelectorAll("[data-block-revision]");
|
|
414
402
|
for (const r of Array.from(e))
|
|
415
403
|
r.style.setProperty(
|
|
416
404
|
"--sobree-block-revision-color",
|
|
417
405
|
p(r.dataset.blockRevisionAuthor)
|
|
418
406
|
);
|
|
419
|
-
const o = s.querySelectorAll(
|
|
420
|
-
"span.sobree-revision-format"
|
|
421
|
-
);
|
|
407
|
+
const o = s.querySelectorAll("span.sobree-revision-format");
|
|
422
408
|
for (const r of Array.from(o))
|
|
423
409
|
r.style.setProperty(
|
|
424
410
|
"--sobree-format-revision-color",
|
|
@@ -485,7 +471,7 @@ function W(s, t, e) {
|
|
|
485
471
|
const n = document.createElement("button");
|
|
486
472
|
n.type = "button", n.className = "sobree-review-comment__action";
|
|
487
473
|
const c = !!s.done;
|
|
488
|
-
n.title = c ? "Reopen comment" : "Resolve comment", n.setAttribute("aria-label", n.title), n.innerHTML = c ?
|
|
474
|
+
n.title = c ? "Reopen comment" : "Resolve comment", n.setAttribute("aria-label", n.title), n.innerHTML = c ? S : T, n.addEventListener("click", () => {
|
|
489
475
|
const l = c ? e.reopenComment(s.id) : e.resolveComment(s.id);
|
|
490
476
|
l.ok || console.warn("[review] resolve/reopen failed:", l.error);
|
|
491
477
|
}), r.appendChild(n), o.appendChild(r);
|
|
@@ -509,7 +495,7 @@ function G(s, t) {
|
|
|
509
495
|
return e;
|
|
510
496
|
}
|
|
511
497
|
export {
|
|
512
|
-
|
|
498
|
+
L as authorSlot,
|
|
513
499
|
p as colorForAuthor,
|
|
514
500
|
X as review
|
|
515
501
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/authorColor.ts","../src/icons.ts","../src/actions.ts","../src/dock.ts","../src/index.ts"],"sourcesContent":["/**\n * Hash an author name to one of 8 palette *slots*, returning a CSS\n * value that references the matching `@sobree/core` design token.\n * Same author → same slot across the document.\n *\n * Returns `var(--sobree-author-N, #hex)` — a token reference with a\n * hard-coded hex fallback, so it works with or without\n * `@sobree/core/tokens.css` loaded, and consumers can re-theme any\n * slot by overriding `--sobree-author-N`.\n *\n * (Moved out of `@sobree/core` when the review display became a\n * plugin — core keeps only the neutral semantic marks.)\n */\n\nconst FALLBACK_PALETTE = [\n \"#1f77b4\", // 0 muted blue\n \"#2ca02c\", // 1 green\n \"#9467bd\", // 2 purple\n \"#8c564b\", // 3 brown\n \"#e377c2\", // 4 pink\n \"#17becf\", // 5 teal\n \"#bcbd22\", // 6 olive\n \"#ff7f0e\", // 7 orange\n];\n\n/** Hash `author` to a palette slot index 0..7. Deterministic. */\nexport function authorSlot(author: string | undefined): number {\n if (!author) return 0;\n let h = 2166136261;\n for (let i = 0; i < author.length; i++) {\n h ^= author.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return Math.abs(h) % FALLBACK_PALETTE.length;\n}\n\n/** CSS colour value for `author` — a `var(--sobree-author-N, #hex)` ref. */\nexport function colorForAuthor(author: string | undefined): string {\n const slot = authorSlot(author);\n return `var(--sobree-author-${slot}, ${FALLBACK_PALETTE[slot]})`;\n}\n","/**\n * Minimal inline SVG icons for the review action buttons. 16×16,\n * `currentColor` stroke — they inherit the button's text colour, so\n * theming is just setting `color`.\n */\n\nconst SVG = (paths: string): string =>\n `<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" ` +\n `stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" ` +\n `stroke-linejoin=\"round\" aria-hidden=\"true\">${paths}</svg>`;\n\n/** Checkmark — accept. */\nexport const ICON_ACCEPT = SVG(`<path d=\"M3 8.5l3.2 3.2L13 5\"/>`);\n\n/** Cross — reject. */\nexport const ICON_REJECT = SVG(`<path d=\"M4 4l8 8M12 4l-8 8\"/>`);\n\n/** Check-in-circle — resolve a comment. */\nexport const ICON_RESOLVE = SVG(\n `<circle cx=\"8\" cy=\"8\" r=\"5.5\"/><path d=\"M5.5 8l1.8 1.8L10.8 6\"/>`,\n);\n\n/** Counter-clockwise arrow — reopen a resolved comment. */\nexport const ICON_REOPEN = SVG(\n `<path d=\"M3.5 8a4.5 4.5 0 1 1 1.3 3.2\"/><path d=\"M3.2 5v3h3\"/>`,\n);\n","/**\n * Accept / reject controls for tracked-change marks.\n *\n * A shared floating popover (one per controller) appears above a\n * revision mark on hover, offering ✓ accept and ✗ reject. The popover\n * is appended to `document.body` and `position: fixed` — so it stays\n * UI-sized regardless of the document zoom, and isn't clipped by the\n * viewport.\n *\n * The accept/reject *range* comes from `editor.getRevisions()` — core\n * coalesces contiguous same-author runs into logical `RevisionSpan`s\n * and hands back exact, versioned ranges. The plugin only has to\n * figure out *which* span the hovered mark belongs to (one offset\n * lookup), never to construct the range itself.\n *\n * Spans are fetched *live* on every hover rather than cached: the walk\n * is O(runs) and only runs on pointer-over, and a fresh fetch carries\n * the current block versions — so the range never fails optimistic\n * locking against a doc that moved since the last paginate.\n */\n\nimport type { Editor, RevisionSpan } from \"@sobree/core\";\nimport { ICON_ACCEPT, ICON_REJECT } from \"./icons\";\n\n/** How long to keep the popover alive after the pointer leaves the\n * mark, so the user can travel into the popover itself. */\nconst HIDE_DELAY_MS = 220;\n\nexport class RevisionActions {\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly popover: HTMLElement;\n private readonly onOver: (e: Event) => void;\n private readonly onOut: (e: Event) => void;\n private hideTimer: ReturnType<typeof setTimeout> | null = null;\n /** The revision span the popover currently targets. */\n private target: RevisionSpan | null = null;\n\n constructor(editor: Editor, stackRoot: HTMLElement) {\n this.editor = editor;\n this.stackRoot = stackRoot;\n this.popover = this.buildPopover();\n document.body.appendChild(this.popover);\n\n this.onOver = (e) => this.handleOver(e);\n this.onOut = (e) => this.handleOut(e);\n this.stackRoot.addEventListener(\"mouseover\", this.onOver);\n this.stackRoot.addEventListener(\"mouseout\", this.onOut);\n }\n\n destroy(): void {\n this.stackRoot.removeEventListener(\"mouseover\", this.onOver);\n this.stackRoot.removeEventListener(\"mouseout\", this.onOut);\n if (this.hideTimer) clearTimeout(this.hideTimer);\n this.popover.remove();\n }\n\n // ---- popover element ----\n\n private buildPopover(): HTMLElement {\n const pop = document.createElement(\"div\");\n pop.className = \"sobree-review-actions\";\n pop.setAttribute(\"role\", \"toolbar\");\n pop.append(\n this.actionButton(\"Accept change\", ICON_ACCEPT, \"accept\"),\n this.actionButton(\"Reject change\", ICON_REJECT, \"reject\"),\n );\n pop.addEventListener(\"mouseenter\", () => this.cancelHide());\n pop.addEventListener(\"mouseleave\", () => this.scheduleHide());\n return pop;\n }\n\n private actionButton(\n label: string,\n svg: string,\n kind: \"accept\" | \"reject\",\n ): HTMLElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = `sobree-review-actions__btn is-${kind}`;\n btn.title = label; // native hover tooltip (\"alt text\")\n btn.setAttribute(\"aria-label\", label);\n btn.innerHTML = svg;\n btn.addEventListener(\"click\", () => this.run(kind));\n return btn;\n }\n\n // ---- hover handling ----\n\n private handleOver(e: Event): void {\n // Priority order — checked from most specific to least:\n // 1. Inline ins/del (`.sobree-revision`) — wraps a tracked run.\n // 2. Format-change (`.sobree-revision-format`) — wraps a run\n // whose properties were tracked-changed.\n // 3. Paragraph-mark (`[data-block-revision]`) — the whole\n // paragraph element when its mark is tracked.\n // Specificity matters because the wrappers nest: an inserted +\n // format-changed run is wrapped in BOTH; the inline `ins`/`del`\n // wins because accepting it covers both the insertion and any\n // format changes inside it.\n const target = e.target as HTMLElement | null;\n if (!target) return;\n const inline = target.closest<HTMLElement>(\".sobree-revision\");\n if (inline) {\n const span = this.resolveInlineSpan(inline);\n if (span) {\n this.openOn(inline, span);\n }\n return;\n }\n const formatEl = target.closest<HTMLElement>(\".sobree-revision-format\");\n if (formatEl) {\n const span = this.resolveFormatSpan(formatEl);\n if (span) {\n this.openOn(formatEl, span);\n }\n return;\n }\n const paraEl = target.closest<HTMLElement>(\"[data-block-revision]\");\n if (paraEl) {\n const span = this.resolveParagraphSpan(paraEl);\n if (span) {\n this.openOn(paraEl, span);\n }\n }\n }\n\n private handleOut(e: Event): void {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (\n target.closest(\".sobree-revision\") ||\n target.closest(\".sobree-revision-format\") ||\n target.closest(\"[data-block-revision]\")\n ) {\n this.scheduleHide();\n }\n }\n\n private openOn(mark: HTMLElement, span: RevisionSpan): void {\n this.cancelHide();\n this.target = span;\n this.label(span);\n this.position(mark);\n }\n\n private scheduleHide(): void {\n this.cancelHide();\n this.hideTimer = setTimeout(() => this.hide(), HIDE_DELAY_MS);\n }\n\n private cancelHide(): void {\n if (this.hideTimer) {\n clearTimeout(this.hideTimer);\n this.hideTimer = null;\n }\n }\n\n /** Tooltip wording follows the span's level + revision kind(s). */\n private label(span: RevisionSpan): void {\n let noun: string;\n if (span.level === \"format\") {\n noun = \"format change\";\n } else if (span.level === \"paragraph\") {\n noun = span.kinds[0] === \"del\" ? \"paragraph deletion\" : \"paragraph insertion\";\n } else {\n noun =\n span.kinds.length > 1\n ? \"replacement\"\n : span.kinds[0] === \"del\"\n ? \"deletion\"\n : \"insertion\";\n }\n const [accept, reject] = Array.from(\n this.popover.querySelectorAll<HTMLElement>(\".sobree-review-actions__btn\"),\n );\n if (accept) {\n accept.title = `Accept ${noun}`;\n accept.setAttribute(\"aria-label\", accept.title);\n }\n if (reject) {\n reject.title = `Reject ${noun}`;\n reject.setAttribute(\"aria-label\", reject.title);\n }\n }\n\n private position(mark: HTMLElement): void {\n const r = mark.getBoundingClientRect();\n this.popover.classList.add(\"is-visible\");\n const popW = this.popover.offsetWidth || 64;\n let left = r.left + r.width / 2 - popW / 2;\n left = Math.max(4, Math.min(left, window.innerWidth - popW - 4));\n const top = Math.max(4, r.top - this.popover.offsetHeight - 6);\n this.popover.style.left = `${left}px`;\n this.popover.style.top = `${top}px`;\n }\n\n private hide(): void {\n this.popover.classList.remove(\"is-visible\");\n this.target = null;\n }\n\n // ---- accept / reject ----\n\n /**\n * Dispatch the accept/reject to the right editor method based on the\n * span's `level`. Inline → `acceptRevision` / `rejectRevision`,\n * paragraph → the paragraph variant (takes a `BlockRef`, not a\n * range), format → the format variant (range over the format-changed\n * runs). The popover doesn't track which one fired; the level on the\n * cached `target` span is the source of truth.\n */\n private run(kind: \"accept\" | \"reject\"): void {\n const span = this.target;\n if (!span) return;\n let result;\n if (span.level === \"paragraph\") {\n const blockRef = span.range.from.block;\n result =\n kind === \"accept\"\n ? this.editor.acceptParagraphRevision(blockRef)\n : this.editor.rejectParagraphRevision(blockRef);\n } else if (span.level === \"format\") {\n result =\n kind === \"accept\"\n ? this.editor.acceptFormatRevision(span.range)\n : this.editor.rejectFormatRevision(span.range);\n } else {\n result =\n kind === \"accept\"\n ? this.editor.acceptRevision(span.range)\n : this.editor.rejectRevision(span.range);\n }\n if (!result.ok) {\n console.warn(`[review] ${kind} revision failed:`, result.error);\n }\n this.hide();\n }\n\n /**\n * Map a hovered inline `ins`/`del` mark to its `RevisionSpan`. We\n * compute the mark's character offset within its block, then pick the\n * inline-level span whose range covers it.\n */\n private resolveInlineSpan(mark: HTMLElement): RevisionSpan | null {\n const block = mark.closest<HTMLElement>(\"[data-block-id]\");\n const blockId = block?.dataset.blockId;\n if (!block || !blockId) return null;\n const offset = textLengthBefore(block, mark);\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"inline\" && span.level !== undefined) continue;\n if (\n span.range.from.block.id === blockId &&\n span.range.from.offset <= offset &&\n offset < span.range.to.offset\n ) {\n return span;\n }\n }\n return null;\n }\n\n /**\n * Map a hovered format-changed run wrapper to its `RevisionSpan`.\n * Same offset-lookup as inline, but we accept the `format` level.\n */\n private resolveFormatSpan(mark: HTMLElement): RevisionSpan | null {\n const block = mark.closest<HTMLElement>(\"[data-block-id]\");\n const blockId = block?.dataset.blockId;\n if (!block || !blockId) return null;\n const offset = textLengthBefore(block, mark);\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"format\") continue;\n if (\n span.range.from.block.id === blockId &&\n span.range.from.offset <= offset &&\n offset < span.range.to.offset\n ) {\n return span;\n }\n }\n return null;\n }\n\n /**\n * Map a paragraph element with `data-block-revision` to its\n * paragraph-level `RevisionSpan`. The whole block is the target;\n * no offset math needed — just match by block id.\n */\n private resolveParagraphSpan(blockEl: HTMLElement): RevisionSpan | null {\n const blockId = blockEl.dataset.blockId;\n if (!blockId) return null;\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"paragraph\") continue;\n if (span.range.from.block.id === blockId) return span;\n }\n return null;\n }\n}\n\n/** Character count of `block`'s text content before `el` starts. */\nfunction textLengthBefore(block: HTMLElement, el: HTMLElement): number {\n const range = document.createRange();\n range.selectNodeContents(block);\n range.setEndBefore(el);\n return range.toString().length;\n}\n","/**\n * Top-level review dock — the \"there are unresolved tracked changes\n * in this doc\" UI. A small horizontal pill anchored to one corner of\n * the rendering area via core's shared floating-corner stack.\n *\n * ┌────────────────────────────────────────────────────────┐\n * │ ⚑ 5 changes · 2 authors │ ◀ ▶ │ ✓ All │ ✗ All │\n * └────────────────────────────────────────────────────────┘\n *\n * Visibility:\n * - Hidden when `editor.getRevisions().length === 0` (clean doc).\n * - Shown otherwise; auto-updates on every editor `change` /\n * `paginate` via the controller's existing refresh path.\n *\n * Navigation:\n * - Maintains a `cursorIndex` over `getRevisions()` results.\n * - ◀ / ▶ moves the cursor, scrolls the corresponding DOM mark\n * into view (smooth), and triggers a CSS pulse on the mark via\n * the `.is-flashing` class for `FLASH_MS`.\n * - The cursor is clamped on every refresh, so accepting the\n * current revision advances naturally.\n *\n * The dock is intentionally separate from the toolbar pill in\n * `@sobree/block-tools`. The pill is about the *mode flag* (toggles\n * authoring behaviour, lives in a per-block toolbar). The dock is\n * about *unresolved revisions* (auto-shows when they exist, lives\n * floating regardless of selection). A reviewer opening a `.docx`\n * full of someone else's tracked changes shouldn't have to discover\n * a per-block toolbar to act on them.\n */\n\nimport { getFloatingCorner, type Editor, type FloatingCornerPlacement } from \"@sobree/core\";\nimport { colorForAuthor } from \"./authorColor\";\n\n/** How long the flash pulse runs on a navigated-to mark, in ms. */\nconst FLASH_MS = 700;\n\nexport interface ReviewDockOptions {\n /** Element to anchor against. Typically `ctx.host` (rendering area). */\n host: HTMLElement;\n /** Editor for `getRevisions` + `accept/rejectAllRevisions` + DOM lookup. */\n editor: Editor;\n /** Stack root — where we look for revision marks to scroll/flash. */\n stackRoot: HTMLElement;\n /** Which corner to dock in. Defaults to `top-right`. */\n placement?: FloatingCornerPlacement;\n}\n\nexport class ReviewDock {\n private readonly host: HTMLElement;\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly placement: FloatingCornerPlacement;\n private readonly root: HTMLElement;\n private cursorIndex = 0;\n private flashTimer: ReturnType<typeof setTimeout> | null = null;\n private flashTarget: HTMLElement | null = null;\n\n constructor(opts: ReviewDockOptions) {\n this.host = opts.host;\n this.editor = opts.editor;\n this.stackRoot = opts.stackRoot;\n this.placement = opts.placement ?? \"top-right\";\n\n this.root = document.createElement(\"div\");\n this.root.className = \"sobree-review-dock\";\n this.root.dataset.placement = this.placement;\n this.root.setAttribute(\"role\", \"toolbar\");\n this.root.setAttribute(\"aria-label\", \"Review tracked changes\");\n this.root.hidden = true;\n this.root.innerHTML = this.buildHtml();\n\n getFloatingCorner(this.host, this.placement).appendChild(this.root);\n\n this.root.addEventListener(\"click\", (e) => this.handleClick(e));\n }\n\n destroy(): void {\n if (this.flashTimer) clearTimeout(this.flashTimer);\n this.clearFlash();\n this.root.remove();\n }\n\n /**\n * Re-render with the current revision count + author summary. Called\n * by the controller from its rAF/timer-debounced refresh, so every\n * `change` and `paginate` event keeps the dock in sync without us\n * registering our own listeners.\n */\n refresh(): void {\n const spans = this.editor.getRevisions();\n const summary = this.root.querySelector<HTMLElement>(\".sobree-review-dock__summary\");\n\n if (spans.length === 0) {\n // Empty state — dock stays visible with an explicit \"nothing to\n // do here\" message, no action buttons. Gives the user a clear\n // signal that accept-all/reject-all are intentionally not\n // available because there's nothing to act on, rather than a\n // disappeared dock + ambiguous state. The `.is-empty` class\n // hides the divider + buttons + nav arrows via CSS.\n this.root.hidden = false;\n this.root.classList.add(\"is-empty\");\n this.cursorIndex = 0;\n if (summary) {\n summary.textContent = \"No changes to be tracked\";\n // Reset the accent so the empty-state pill isn't tinted by a\n // stale author colour from before the last accept-all.\n summary.style.removeProperty(\"--sobree-review-dock-accent\");\n }\n return;\n }\n\n this.root.hidden = false;\n this.root.classList.remove(\"is-empty\");\n // Clamp cursor to current count (a previous index may now be out\n // of range because the user accepted/rejected something).\n if (this.cursorIndex >= spans.length) this.cursorIndex = spans.length - 1;\n if (this.cursorIndex < 0) this.cursorIndex = 0;\n\n const authors = new Set(spans.map((s) => s.author ?? \"\"));\n const count = spans.length;\n if (summary) {\n const noun = count === 1 ? \"change\" : \"changes\";\n const authorPart =\n authors.size > 1 ? ` · ${authors.size} authors` : \"\";\n summary.textContent = `${count} ${noun}${authorPart}`;\n }\n // Reflect the current author of the cursor span in the summary's\n // accent — same colour the marks use.\n const current = spans[this.cursorIndex];\n if (current && summary) {\n summary.style.setProperty(\n \"--sobree-review-dock-accent\",\n colorForAuthor(current.author),\n );\n }\n }\n\n // ---- handlers ----\n\n private handleClick(e: MouseEvent): void {\n const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(\n \"button[data-action]\",\n );\n if (!btn) return;\n e.preventDefault();\n const action = btn.dataset.action;\n switch (action) {\n case \"prev\":\n this.navigate(-1);\n break;\n case \"next\":\n this.navigate(1);\n break;\n case \"accept-all\": {\n const r = this.editor.acceptAllRevisions();\n if (!r.ok) console.warn(\"[review] acceptAllRevisions failed:\", r.error);\n break;\n }\n case \"reject-all\": {\n const r = this.editor.rejectAllRevisions();\n if (!r.ok) console.warn(\"[review] rejectAllRevisions failed:\", r.error);\n break;\n }\n }\n }\n\n private navigate(delta: 1 | -1): void {\n const spans = this.editor.getRevisions();\n if (spans.length === 0) return;\n // Wrap around — common UX expectation for \"next\" past the last\n // item and \"prev\" before the first.\n this.cursorIndex =\n (this.cursorIndex + delta + spans.length) % spans.length;\n const span = spans[this.cursorIndex];\n if (!span) return;\n const mark = this.findMarkForSpan(span);\n if (mark) this.scrollIntoViewAndFlash(mark);\n this.refresh();\n }\n\n /**\n * Find a DOM element matching the given revision span. We need this\n * to scroll to. The element type depends on the span's level:\n * - inline → an `<ins>` / `<del>` element inside the right block\n * whose character range covers the span.\n * - paragraph → the `<p data-block-revision>` element for the block.\n * - format → a `span.sobree-revision-format` inside the right block.\n *\n * Returns `null` if the mark isn't currently rendered (e.g. the\n * span is on a not-yet-paginated block, or the doc just changed).\n */\n private findMarkForSpan(span: ReturnType<Editor[\"getRevisions\"]>[number]): HTMLElement | null {\n const blockId = span.range.from.block.id;\n const block = this.stackRoot.querySelector<HTMLElement>(\n `[data-block-id=\"${cssEscape(blockId)}\"]`,\n );\n if (!block) return null;\n if (span.level === \"paragraph\") return block;\n const selector =\n span.level === \"format\"\n ? \"span.sobree-revision-format\"\n : \"ins[data-revision-author], del[data-revision-author]\";\n // First matching descendant — good enough; refinement by exact\n // character range would need text-node walking which the popover\n // doesn't bother with either.\n return block.querySelector<HTMLElement>(selector);\n }\n\n private scrollIntoViewAndFlash(mark: HTMLElement): void {\n // Centred so the user can scan context around the revision.\n // `behavior: \"smooth\"` is honoured by all modern browsers and\n // respects CSS transforms (our viewport zoom is one).\n mark.scrollIntoView({ behavior: \"smooth\", block: \"center\", inline: \"nearest\" });\n this.flash(mark);\n }\n\n private flash(mark: HTMLElement): void {\n // Clear any in-flight flash so a rapid prev/next sequence pulses\n // the most recent target only, not a stale one.\n this.clearFlash();\n mark.classList.add(\"is-flashing\");\n this.flashTarget = mark;\n this.flashTimer = setTimeout(() => this.clearFlash(), FLASH_MS);\n }\n\n private clearFlash(): void {\n if (this.flashTimer) {\n clearTimeout(this.flashTimer);\n this.flashTimer = null;\n }\n if (this.flashTarget) {\n this.flashTarget.classList.remove(\"is-flashing\");\n this.flashTarget = null;\n }\n }\n\n // ---- markup ----\n\n private buildHtml(): string {\n // Keep the markup minimal; CSS owns the visuals. Buttons use SVG\n // icons inline so the plugin has no external icon dependency.\n return `\n <span class=\"sobree-review-dock__summary\" aria-live=\"polite\"></span>\n <span class=\"sobree-review-dock__divider\"></span>\n <button type=\"button\" class=\"sobree-review-dock__btn\" data-action=\"prev\"\n title=\"Previous change\" aria-label=\"Previous change\">\n ${ICON_PREV}\n </button>\n <button type=\"button\" class=\"sobree-review-dock__btn\" data-action=\"next\"\n title=\"Next change\" aria-label=\"Next change\">\n ${ICON_NEXT}\n </button>\n <span class=\"sobree-review-dock__divider\"></span>\n <button type=\"button\" class=\"sobree-review-dock__btn is-accept\"\n data-action=\"accept-all\"\n title=\"Accept all changes\" aria-label=\"Accept all changes\">\n ${ICON_CHECK} <span class=\"sobree-review-dock__btn-label\">All</span>\n </button>\n <button type=\"button\" class=\"sobree-review-dock__btn is-reject\"\n data-action=\"reject-all\"\n title=\"Reject all changes\" aria-label=\"Reject all changes\">\n ${ICON_CROSS} <span class=\"sobree-review-dock__btn-label\">All</span>\n </button>\n `;\n }\n}\n\nconst ICON_PREV = `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"15 18 9 12 15 6\"/></svg>`;\nconst ICON_NEXT = `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>`;\nconst ICON_CHECK = `<svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"20 6 9 17 4 12\"/></svg>`;\nconst ICON_CROSS = `<svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/><line x1=\"6\" y1=\"18\" x2=\"18\" y2=\"6\"/></svg>`;\n\n/**\n * Minimal CSS.escape fallback for environments without it (jsdom,\n * older browsers). Block IDs are alphanumeric + underscore in\n * practice, so a tight regex suffices.\n */\nfunction cssEscape(s: string): string {\n if (typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\") {\n return CSS.escape(s);\n }\n return s.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\\\${ch}`);\n}\n","/**\n * `@sobree/review` — tracked-changes & comments review surface.\n *\n * `@sobree/core` renders the *semantic* marks (`<ins>` / `<del>` /\n * comment-range highlight) in neutral styling — visible without any\n * plugin, so an imported docx never silently reads wrong. This plugin\n * layers the *review surface* on top:\n *\n * - per-author colour on the inline marks\n * - post-it comment cards in the right-margin sidebar, threaded and\n * vertically aligned to the text they annotate\n * - (accept/reject/resolve actions — wired in a later step)\n *\n * It owns no document state: it reacts to `paginate` events, reads the\n * AST via `editor.getDocument()`, and decorates the already-rendered\n * DOM. Removing the plugin removes the surface, not the data.\n *\n * Recommended usage:\n *\n * import { review } from \"@sobree/review\";\n * createSobree(host, { plugins: [review()] });\n */\n\nimport \"./review.css\";\nimport type {\n Block,\n Comment,\n FloatingCornerPlacement,\n InlineRun,\n PluginContext,\n SobreePlugin,\n SobreeUnsubscribe,\n Editor,\n} from \"@sobree/core\";\nimport { colorForAuthor } from \"./authorColor\";\nimport { RevisionActions } from \"./actions\";\nimport { ReviewDock } from \"./dock\";\nimport { ICON_RESOLVE, ICON_REOPEN } from \"./icons\";\n\nexport { colorForAuthor, authorSlot } from \"./authorColor\";\n\nexport interface ReviewOptions {\n /**\n * Show the per-page comment sidebar. When false, the plugin still\n * colours the inline marks per author but renders no cards. Default\n * true.\n */\n showComments?: boolean;\n /**\n * Show the top-level review dock — a floating pill with count +\n * prev/next + accept-all/reject-all. Auto-shows when revisions\n * exist, auto-hides when the doc is clean. Default true.\n */\n showDock?: boolean;\n /**\n * Which corner of the rendering area the dock pins to. Defaults to\n * `top-right`. Stacks cleanly with other plugins' floating UIs in\n * the same corner (zoom-controls, etc.) via core's shared\n * `getFloatingCorner` utility.\n */\n dockPlacement?: FloatingCornerPlacement;\n}\n\n/** Plugin factory — hand to `createSobree({ plugins: [review()] })`. */\nexport function review(opts: ReviewOptions = {}): SobreePlugin {\n return {\n name: \"review\",\n setup(ctx: PluginContext) {\n const controller = new ReviewController(ctx, opts);\n return { destroy: () => controller.destroy() };\n },\n };\n}\n\n/** Vertical gap between two stacked comment cards (px, pre-transform). */\nconst CARD_GAP = 8;\n\nclass ReviewController {\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly showComments: boolean;\n private readonly unsubs: SobreeUnsubscribe[] = [];\n private readonly revisionActions: RevisionActions;\n private readonly dock: ReviewDock | null;\n /** Pending debounce handles — `null` when no refresh is queued. */\n private rafId: number | null = null;\n private timerId: ReturnType<typeof setTimeout> | null = null;\n\n constructor(ctx: PluginContext, opts: ReviewOptions) {\n this.editor = ctx.editor;\n this.stackRoot = ctx.sobree.stackRoot;\n this.showComments = opts.showComments ?? true;\n // Hover-popover accept/reject for tracked-change marks. It fetches\n // `editor.getRevisions()` live on each hover, so the range it\n // mutates always carries current block versions.\n this.revisionActions = new RevisionActions(this.editor, this.stackRoot);\n // Top-level review dock — count + prev/next + accept-all/reject-all.\n // Anchored via core's shared floating-corner stack so it cohabits\n // cleanly with any other dock the embedder mounts in the same\n // corner (zoom-controls, etc.).\n this.dock =\n (opts.showDock ?? true)\n ? new ReviewDock({\n host: ctx.host,\n editor: this.editor,\n stackRoot: this.stackRoot,\n ...(opts.dockPlacement !== undefined\n ? { placement: opts.dockPlacement }\n : {}),\n })\n : null;\n // `paginate` is the \"DOM re-laid-out\" signal — it fires after every\n // content change → repaginate cycle, so card *positions* are fresh.\n // `change` additionally catches mutations that don't repaginate —\n // notably comment resolve/reopen, which only touches document\n // metadata — so the cards re-render to reflect the new state.\n // Both funnel through the debounced `schedule()`, which coalesces a\n // paired change+paginate into one refresh.\n this.unsubs.push(ctx.sobree.on(\"paginate\", () => this.schedule()));\n this.unsubs.push(ctx.sobree.on(\"change\", () => this.schedule()));\n this.schedule();\n }\n\n /**\n * Coalesce a burst of events into a single refresh.\n *\n * We arm an animation frame *and* a timer, and the first to fire\n * wins (cancelling the other). The rAF gives frame-aligned layout\n * reads when the tab is visible; the timer is the safety net —\n * `requestAnimationFrame` callbacks are paused entirely in a hidden\n * tab, so an rAF-only debounce would wedge permanently (`pending`\n * never clears) and the review surface would go dark. Timers keep\n * running (throttled) in background tabs, so the timer guarantees\n * the surface still updates and can never get stuck.\n */\n private schedule(): void {\n if (this.rafId !== null || this.timerId !== null) return;\n const run = (): void => {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n try {\n this.refresh();\n } catch (err) {\n console.error(\"[review] refresh failed:\", err);\n }\n };\n this.rafId = requestAnimationFrame(run);\n this.timerId = setTimeout(run, 100);\n }\n\n private refresh(): void {\n colourMarks(this.stackRoot);\n if (this.showComments) {\n const comments = this.editor.getDocument().comments ?? {};\n renderComments(this.stackRoot, comments, this.editor);\n }\n // Dock reads `editor.getRevisions()` itself; refresh paints the\n // count + author summary and auto-shows/hides based on the result.\n this.dock?.refresh();\n }\n\n destroy(): void {\n for (const u of this.unsubs) u();\n if (this.rafId !== null) cancelAnimationFrame(this.rafId);\n if (this.timerId !== null) clearTimeout(this.timerId);\n this.revisionActions.destroy();\n this.dock?.destroy();\n // Leave core's neutral marks intact; just clear what we added.\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\"ins[data-revision-author], del[data-revision-author]\"),\n )) {\n el.style.removeProperty(\"--author-color\");\n }\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\"[data-block-revision]\"),\n )) {\n el.style.removeProperty(\"--sobree-block-revision-color\");\n }\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\"span.sobree-revision-format\"),\n )) {\n el.style.removeProperty(\"--sobree-format-revision-color\");\n }\n for (const slot of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\".paper-comments\"),\n )) {\n slot.replaceChildren();\n slot.classList.add(\"is-empty\");\n }\n }\n}\n\n// ---------- inline marks ----------\n\n/** Apply per-author colour to every tracked-change mark in `root`. */\nfunction colourMarks(root: HTMLElement): void {\n // Inline ins/del runs.\n const marks = root.querySelectorAll<HTMLElement>(\n \"ins[data-revision-author], del[data-revision-author]\",\n );\n for (const mark of Array.from(marks)) {\n mark.style.setProperty(\n \"--author-color\",\n colorForAuthor(mark.dataset.revisionAuthor),\n );\n }\n // Paragraph-mark revisions — `data-block-revision=\"ins\"|\"del\"` lives\n // on the paragraph element; the `::after` pseudo in core reads\n // `--sobree-block-revision-color`, which we set per-author here.\n const blockMarks = root.querySelectorAll<HTMLElement>(\n \"[data-block-revision]\",\n );\n for (const mark of Array.from(blockMarks)) {\n mark.style.setProperty(\n \"--sobree-block-revision-color\",\n colorForAuthor(mark.dataset.blockRevisionAuthor),\n );\n }\n // Format-change revisions — the wrapping `<span.sobree-revision-format>`\n // reads `--sobree-format-revision-color` to colour the dashed\n // underline core ships as the neutral visual hint.\n const formatMarks = root.querySelectorAll<HTMLElement>(\n \"span.sobree-revision-format\",\n );\n for (const mark of Array.from(formatMarks)) {\n mark.style.setProperty(\n \"--sobree-format-revision-color\",\n colorForAuthor(mark.dataset.revisionFormatAuthor),\n );\n }\n}\n\n// ---------- comment cards ----------\n\n/**\n * Build the post-it comment cards for every paper and place them in\n * the per-paper `.paper-comments` sidebar slot, vertically aligned to\n * the comment ranges they annotate.\n */\nfunction renderComments(\n root: HTMLElement,\n comments: Record<number, Comment>,\n editor: Editor,\n): void {\n // parent → replies index, for threading.\n const repliesByParent = new Map<number, Comment[]>();\n for (const c of Object.values(comments)) {\n // `replyToId` is absent on a top-level comment — but a YDoc\n // round-trip can materialise the missing field as `null`, so\n // treat `null` and `undefined` alike (`== null`).\n if (c.replyToId == null) continue;\n const list = repliesByParent.get(c.replyToId) ?? [];\n list.push(c);\n repliesByParent.set(c.replyToId, list);\n }\n for (const list of repliesByParent.values()) list.sort((a, b) => a.id - b.id);\n\n for (const row of Array.from(root.querySelectorAll<HTMLElement>(\".paper-row\"))) {\n const paper = row.querySelector<HTMLElement>(\".paper\");\n const slot = row.querySelector<HTMLElement>(\".paper-comments\");\n if (!paper || !slot) continue;\n slot.replaceChildren();\n\n // Collect (commentId, anchorTopPx) for top-level comments whose\n // range starts on this paper, in document order.\n const placements: { id: number; top: number }[] = [];\n const seen = new Set<number>();\n const anchors = paper.querySelectorAll<HTMLElement>(\".sobree-comment-range\");\n for (const anchor of Array.from(anchors)) {\n const raw = anchor.dataset.commentIds ?? \"\";\n for (const part of raw.split(\",\")) {\n const id = Number(part.trim());\n if (!Number.isFinite(id) || seen.has(id)) continue;\n const c = comments[id];\n // Skip replies (they ride their parent card). `!= null` so a\n // YDoc-materialised `null` still counts as \"top-level\".\n if (!c || c.replyToId != null) continue;\n seen.add(id);\n placements.push({ id, top: offsetWithin(anchor, paper) });\n }\n }\n if (placements.length === 0) {\n slot.classList.add(\"is-empty\");\n continue;\n }\n slot.classList.remove(\"is-empty\");\n\n // Place cards top-down, pushing later cards past earlier ones so\n // threads never overlap (the classic comment-margin layout).\n placements.sort((a, b) => a.top - b.top);\n let cursor = 0;\n for (const { id, top } of placements) {\n const card = buildThreadCard(comments[id]!, repliesByParent, editor);\n card.style.top = `${Math.max(top, cursor)}px`;\n slot.appendChild(card);\n cursor = Math.max(top, cursor) + card.offsetHeight + CARD_GAP;\n }\n }\n}\n\n/** Build one post-it: the top-level comment + its reply thread. */\nfunction buildThreadCard(\n root: Comment,\n repliesByParent: Map<number, Comment[]>,\n editor: Editor,\n): HTMLElement {\n const card = document.createElement(\"div\");\n card.className = \"sobree-review-card\";\n card.dataset.commentId = String(root.id);\n\n const emit = (c: Comment, depth: number): void => {\n card.appendChild(buildCommentEl(c, depth, editor));\n for (const reply of repliesByParent.get(c.id) ?? []) emit(reply, depth + 1);\n };\n emit(root, 0);\n return card;\n}\n\n/** One comment within a thread card. */\nfunction buildCommentEl(c: Comment, depth: number, editor: Editor): HTMLElement {\n const el = document.createElement(\"article\");\n el.className = \"sobree-review-comment\";\n if (depth > 0) el.classList.add(\"is-reply\");\n if (c.done) el.classList.add(\"is-resolved\");\n el.dataset.commentId = String(c.id);\n el.id = `sobree-comment-${c.id}`;\n\n const header = document.createElement(\"header\");\n header.className = \"sobree-review-comment__header\";\n const author = document.createElement(\"span\");\n author.className = \"sobree-review-comment__author\";\n author.textContent = c.author ?? \"Anonymous\";\n author.style.color = colorForAuthor(c.author);\n header.appendChild(author);\n if (c.date) {\n const time = document.createElement(\"time\");\n time.className = \"sobree-review-comment__date\";\n time.dateTime = c.date;\n time.textContent = c.date.slice(0, 10);\n header.appendChild(time);\n }\n if (c.done) {\n const badge = document.createElement(\"span\");\n badge.className = \"sobree-review-comment__status\";\n badge.textContent = \"✓ Resolved\";\n header.appendChild(badge);\n }\n // Resolve / reopen toggle — flips `Comment.done` via the editor.\n const toggle = document.createElement(\"button\");\n toggle.type = \"button\";\n toggle.className = \"sobree-review-comment__action\";\n const reopening = !!c.done;\n toggle.title = reopening ? \"Reopen comment\" : \"Resolve comment\";\n toggle.setAttribute(\"aria-label\", toggle.title);\n toggle.innerHTML = reopening ? ICON_REOPEN : ICON_RESOLVE;\n toggle.addEventListener(\"click\", () => {\n const result = reopening\n ? editor.reopenComment(c.id)\n : editor.resolveComment(c.id);\n if (!result.ok) {\n console.warn(\"[review] resolve/reopen failed:\", result.error);\n }\n });\n header.appendChild(toggle);\n el.appendChild(header);\n\n const body = document.createElement(\"div\");\n body.className = \"sobree-review-comment__body\";\n body.textContent = flattenBlocks(c.body);\n el.appendChild(body);\n return el;\n}\n\n/** Flatten a comment body (`Block[]`) to plain text — comment bodies\n * are short, so rich formatting is dropped for now. */\nfunction flattenBlocks(blocks: Block[]): string {\n const parts: string[] = [];\n for (const b of blocks) {\n if (b.kind !== \"paragraph\") continue;\n parts.push(b.runs.map(inlineText).join(\"\"));\n }\n return parts.join(\"\\n\");\n}\n\nfunction inlineText(run: InlineRun): string {\n if (run.kind === \"text\") return run.text;\n if (run.kind === \"hyperlink\") return run.children.map(inlineText).join(\"\");\n if (run.kind === \"tab\") return \"\\t\";\n return \"\";\n}\n\n/** Vertical offset of `el` relative to `ancestor`, in layout px\n * (transform-independent — uses the offsetTop chain, not rects). */\nfunction offsetWithin(el: HTMLElement, ancestor: HTMLElement): number {\n let top = 0;\n let node: HTMLElement | null = el;\n while (node && node !== ancestor) {\n top += node.offsetTop;\n node = node.offsetParent as HTMLElement | null;\n }\n return top;\n}\n"],"names":["FALLBACK_PALETTE","authorSlot","author","h","i","colorForAuthor","slot","SVG","paths","ICON_ACCEPT","ICON_REJECT","ICON_RESOLVE","ICON_REOPEN","HIDE_DELAY_MS","RevisionActions","editor","stackRoot","__publicField","e","pop","label","svg","kind","btn","target","inline","span","formatEl","paraEl","mark","noun","accept","reject","r","popW","left","top","result","blockRef","block","blockId","offset","textLengthBefore","blockEl","el","range","FLASH_MS","ReviewDock","opts","getFloatingCorner","spans","summary","authors","s","count","authorPart","current","delta","cssEscape","selector","ICON_PREV","ICON_NEXT","ICON_CHECK","ICON_CROSS","ch","review","ctx","controller","ReviewController","CARD_GAP","run","err","colourMarks","comments","renderComments","_a","u","root","marks","blockMarks","formatMarks","repliesByParent","c","list","a","b","row","paper","placements","seen","anchors","anchor","raw","part","id","offsetWithin","cursor","card","buildThreadCard","emit","depth","buildCommentEl","reply","header","time","badge","toggle","reopening","body","flattenBlocks","blocks","parts","inlineText","ancestor","node"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/icons.ts","../src/actions.ts","../src/authorColor.ts","../src/dock.ts","../src/index.ts"],"sourcesContent":["/**\n * Minimal inline SVG icons for the review action buttons. 16×16,\n * `currentColor` stroke — they inherit the button's text colour, so\n * theming is just setting `color`.\n */\n\nconst SVG = (paths: string): string =>\n `<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">${paths}</svg>`;\n\n/** Checkmark — accept. */\nexport const ICON_ACCEPT = SVG(`<path d=\"M3 8.5l3.2 3.2L13 5\"/>`);\n\n/** Cross — reject. */\nexport const ICON_REJECT = SVG(`<path d=\"M4 4l8 8M12 4l-8 8\"/>`);\n\n/** Check-in-circle — resolve a comment. */\nexport const ICON_RESOLVE = SVG(`<circle cx=\"8\" cy=\"8\" r=\"5.5\"/><path d=\"M5.5 8l1.8 1.8L10.8 6\"/>`);\n\n/** Counter-clockwise arrow — reopen a resolved comment. */\nexport const ICON_REOPEN = SVG(`<path d=\"M3.5 8a4.5 4.5 0 1 1 1.3 3.2\"/><path d=\"M3.2 5v3h3\"/>`);\n","/**\n * Accept / reject controls for tracked-change marks.\n *\n * A shared floating popover (one per controller) appears above a\n * revision mark on hover, offering ✓ accept and ✗ reject. The popover\n * is appended to `document.body` and `position: fixed` — so it stays\n * UI-sized regardless of the document zoom, and isn't clipped by the\n * viewport.\n *\n * The accept/reject *range* comes from `editor.getRevisions()` — core\n * coalesces contiguous same-author runs into logical `RevisionSpan`s\n * and hands back exact, versioned ranges. The plugin only has to\n * figure out *which* span the hovered mark belongs to (one offset\n * lookup), never to construct the range itself.\n *\n * Spans are fetched *live* on every hover rather than cached: the walk\n * is O(runs) and only runs on pointer-over, and a fresh fetch carries\n * the current block versions — so the range never fails optimistic\n * locking against a doc that moved since the last paginate.\n */\n\nimport type { EditResult, Editor, RevisionSpan } from \"@sobree/core\";\nimport { ICON_ACCEPT, ICON_REJECT } from \"./icons\";\n\n/** How long to keep the popover alive after the pointer leaves the\n * mark, so the user can travel into the popover itself. */\nconst HIDE_DELAY_MS = 220;\n\nexport class RevisionActions {\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly popover: HTMLElement;\n private readonly onOver: (e: Event) => void;\n private readonly onOut: (e: Event) => void;\n private hideTimer: ReturnType<typeof setTimeout> | null = null;\n /** The revision span the popover currently targets. */\n private target: RevisionSpan | null = null;\n\n constructor(editor: Editor, stackRoot: HTMLElement) {\n this.editor = editor;\n this.stackRoot = stackRoot;\n this.popover = this.buildPopover();\n document.body.appendChild(this.popover);\n\n this.onOver = (e) => this.handleOver(e);\n this.onOut = (e) => this.handleOut(e);\n this.stackRoot.addEventListener(\"mouseover\", this.onOver);\n this.stackRoot.addEventListener(\"mouseout\", this.onOut);\n }\n\n destroy(): void {\n this.stackRoot.removeEventListener(\"mouseover\", this.onOver);\n this.stackRoot.removeEventListener(\"mouseout\", this.onOut);\n if (this.hideTimer) clearTimeout(this.hideTimer);\n this.popover.remove();\n }\n\n // ---- popover element ----\n\n private buildPopover(): HTMLElement {\n const pop = document.createElement(\"div\");\n pop.className = \"sobree-review-actions\";\n pop.setAttribute(\"role\", \"toolbar\");\n pop.append(\n this.actionButton(\"Accept change\", ICON_ACCEPT, \"accept\"),\n this.actionButton(\"Reject change\", ICON_REJECT, \"reject\"),\n );\n pop.addEventListener(\"mouseenter\", () => this.cancelHide());\n pop.addEventListener(\"mouseleave\", () => this.scheduleHide());\n return pop;\n }\n\n private actionButton(label: string, svg: string, kind: \"accept\" | \"reject\"): HTMLElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.className = `sobree-review-actions__btn is-${kind}`;\n btn.title = label; // native hover tooltip (\"alt text\")\n btn.setAttribute(\"aria-label\", label);\n btn.innerHTML = svg;\n btn.addEventListener(\"click\", () => this.run(kind));\n return btn;\n }\n\n // ---- hover handling ----\n\n private handleOver(e: Event): void {\n // Priority order — checked from most specific to least:\n // 1. Inline ins/del (`.sobree-revision`) — wraps a tracked run.\n // 2. Format-change (`.sobree-revision-format`) — wraps a run\n // whose properties were tracked-changed.\n // 3. Paragraph-mark (`[data-block-revision]`) — the whole\n // paragraph element when its mark is tracked.\n // Specificity matters because the wrappers nest: an inserted +\n // format-changed run is wrapped in BOTH; the inline `ins`/`del`\n // wins because accepting it covers both the insertion and any\n // format changes inside it.\n const target = e.target as HTMLElement | null;\n if (!target) return;\n const inline = target.closest<HTMLElement>(\".sobree-revision\");\n if (inline) {\n const span = this.resolveInlineSpan(inline);\n if (span) {\n this.openOn(inline, span);\n }\n return;\n }\n const formatEl = target.closest<HTMLElement>(\".sobree-revision-format\");\n if (formatEl) {\n const span = this.resolveFormatSpan(formatEl);\n if (span) {\n this.openOn(formatEl, span);\n }\n return;\n }\n const paraEl = target.closest<HTMLElement>(\"[data-block-revision]\");\n if (paraEl) {\n const span = this.resolveParagraphSpan(paraEl);\n if (span) {\n this.openOn(paraEl, span);\n }\n }\n }\n\n private handleOut(e: Event): void {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (\n target.closest(\".sobree-revision\") ||\n target.closest(\".sobree-revision-format\") ||\n target.closest(\"[data-block-revision]\")\n ) {\n this.scheduleHide();\n }\n }\n\n private openOn(mark: HTMLElement, span: RevisionSpan): void {\n this.cancelHide();\n this.target = span;\n this.label(span);\n this.position(mark);\n }\n\n private scheduleHide(): void {\n this.cancelHide();\n this.hideTimer = setTimeout(() => this.hide(), HIDE_DELAY_MS);\n }\n\n private cancelHide(): void {\n if (this.hideTimer) {\n clearTimeout(this.hideTimer);\n this.hideTimer = null;\n }\n }\n\n /** Tooltip wording follows the span's level + revision kind(s). */\n private label(span: RevisionSpan): void {\n let noun: string;\n if (span.level === \"format\") {\n noun = \"format change\";\n } else if (span.level === \"paragraph\") {\n noun = span.kinds[0] === \"del\" ? \"paragraph deletion\" : \"paragraph insertion\";\n } else {\n noun =\n span.kinds.length > 1 ? \"replacement\" : span.kinds[0] === \"del\" ? \"deletion\" : \"insertion\";\n }\n const [accept, reject] = Array.from(\n this.popover.querySelectorAll<HTMLElement>(\".sobree-review-actions__btn\"),\n );\n if (accept) {\n accept.title = `Accept ${noun}`;\n accept.setAttribute(\"aria-label\", accept.title);\n }\n if (reject) {\n reject.title = `Reject ${noun}`;\n reject.setAttribute(\"aria-label\", reject.title);\n }\n }\n\n private position(mark: HTMLElement): void {\n const r = mark.getBoundingClientRect();\n this.popover.classList.add(\"is-visible\");\n const popW = this.popover.offsetWidth || 64;\n let left = r.left + r.width / 2 - popW / 2;\n left = Math.max(4, Math.min(left, window.innerWidth - popW - 4));\n const top = Math.max(4, r.top - this.popover.offsetHeight - 6);\n this.popover.style.left = `${left}px`;\n this.popover.style.top = `${top}px`;\n }\n\n private hide(): void {\n this.popover.classList.remove(\"is-visible\");\n this.target = null;\n }\n\n // ---- accept / reject ----\n\n /**\n * Dispatch the accept/reject to the right editor method based on the\n * span's `level`. Inline → `acceptRevision` / `rejectRevision`,\n * paragraph → the paragraph variant (takes a `BlockRef`, not a\n * range), format → the format variant (range over the format-changed\n * runs). The popover doesn't track which one fired; the level on the\n * cached `target` span is the source of truth.\n */\n private run(kind: \"accept\" | \"reject\"): void {\n const span = this.target;\n if (!span) return;\n let result: EditResult<void>;\n if (span.level === \"paragraph\") {\n const blockRef = span.range.from.block;\n result =\n kind === \"accept\"\n ? this.editor.acceptParagraphRevision(blockRef)\n : this.editor.rejectParagraphRevision(blockRef);\n } else if (span.level === \"format\") {\n result =\n kind === \"accept\"\n ? this.editor.acceptFormatRevision(span.range)\n : this.editor.rejectFormatRevision(span.range);\n } else {\n result =\n kind === \"accept\"\n ? this.editor.acceptRevision(span.range)\n : this.editor.rejectRevision(span.range);\n }\n if (!result.ok) {\n console.warn(`[review] ${kind} revision failed:`, result.error);\n }\n this.hide();\n }\n\n /**\n * Map a hovered inline `ins`/`del` mark to its `RevisionSpan`. We\n * compute the mark's character offset within its block, then pick the\n * inline-level span whose range covers it.\n */\n private resolveInlineSpan(mark: HTMLElement): RevisionSpan | null {\n const block = mark.closest<HTMLElement>(\"[data-block-id]\");\n const blockId = block?.dataset.blockId;\n if (!block || !blockId) return null;\n const offset = textLengthBefore(block, mark);\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"inline\" && span.level !== undefined) continue;\n if (\n span.range.from.block.id === blockId &&\n span.range.from.offset <= offset &&\n offset < span.range.to.offset\n ) {\n return span;\n }\n }\n return null;\n }\n\n /**\n * Map a hovered format-changed run wrapper to its `RevisionSpan`.\n * Same offset-lookup as inline, but we accept the `format` level.\n */\n private resolveFormatSpan(mark: HTMLElement): RevisionSpan | null {\n const block = mark.closest<HTMLElement>(\"[data-block-id]\");\n const blockId = block?.dataset.blockId;\n if (!block || !blockId) return null;\n const offset = textLengthBefore(block, mark);\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"format\") continue;\n if (\n span.range.from.block.id === blockId &&\n span.range.from.offset <= offset &&\n offset < span.range.to.offset\n ) {\n return span;\n }\n }\n return null;\n }\n\n /**\n * Map a paragraph element with `data-block-revision` to its\n * paragraph-level `RevisionSpan`. The whole block is the target;\n * no offset math needed — just match by block id.\n */\n private resolveParagraphSpan(blockEl: HTMLElement): RevisionSpan | null {\n const blockId = blockEl.dataset.blockId;\n if (!blockId) return null;\n for (const span of this.editor.getRevisions()) {\n if (span.level !== \"paragraph\") continue;\n if (span.range.from.block.id === blockId) return span;\n }\n return null;\n }\n}\n\n/** Character count of `block`'s text content before `el` starts. */\nfunction textLengthBefore(block: HTMLElement, el: HTMLElement): number {\n const range = document.createRange();\n range.selectNodeContents(block);\n range.setEndBefore(el);\n return range.toString().length;\n}\n","/**\n * Hash an author name to one of 8 palette *slots*, returning a CSS\n * value that references the matching `@sobree/core` design token.\n * Same author → same slot across the document.\n *\n * Returns `var(--sobree-author-N, #hex)` — a token reference with a\n * hard-coded hex fallback, so it works with or without\n * `@sobree/core/tokens.css` loaded, and consumers can re-theme any\n * slot by overriding `--sobree-author-N`.\n *\n * (Moved out of `@sobree/core` when the review display became a\n * plugin — core keeps only the neutral semantic marks.)\n */\n\nconst FALLBACK_PALETTE = [\n \"#1f77b4\", // 0 muted blue\n \"#2ca02c\", // 1 green\n \"#9467bd\", // 2 purple\n \"#8c564b\", // 3 brown\n \"#e377c2\", // 4 pink\n \"#17becf\", // 5 teal\n \"#bcbd22\", // 6 olive\n \"#ff7f0e\", // 7 orange\n];\n\n/** Hash `author` to a palette slot index 0..7. Deterministic. */\nexport function authorSlot(author: string | undefined): number {\n if (!author) return 0;\n let h = 2166136261;\n for (let i = 0; i < author.length; i++) {\n h ^= author.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return Math.abs(h) % FALLBACK_PALETTE.length;\n}\n\n/** CSS colour value for `author` — a `var(--sobree-author-N, #hex)` ref. */\nexport function colorForAuthor(author: string | undefined): string {\n const slot = authorSlot(author);\n return `var(--sobree-author-${slot}, ${FALLBACK_PALETTE[slot]})`;\n}\n","/**\n * Top-level review dock — the \"there are unresolved tracked changes\n * in this doc\" UI. A small horizontal pill anchored to one corner of\n * the rendering area via core's shared floating-corner stack.\n *\n * ┌────────────────────────────────────────────────────────┐\n * │ ⚑ 5 changes · 2 authors │ ◀ ▶ │ ✓ All │ ✗ All │\n * └────────────────────────────────────────────────────────┘\n *\n * Visibility:\n * - Hidden when `editor.getRevisions().length === 0` (clean doc).\n * - Shown otherwise; auto-updates on every editor `change` /\n * `paginate` via the controller's existing refresh path.\n *\n * Navigation:\n * - Maintains a `cursorIndex` over `getRevisions()` results.\n * - ◀ / ▶ moves the cursor, scrolls the corresponding DOM mark\n * into view (smooth), and triggers a CSS pulse on the mark via\n * the `.is-flashing` class for `FLASH_MS`.\n * - The cursor is clamped on every refresh, so accepting the\n * current revision advances naturally.\n *\n * The dock is intentionally separate from the toolbar pill in\n * `@sobree/block-tools`. The pill is about the *mode flag* (toggles\n * authoring behaviour, lives in a per-block toolbar). The dock is\n * about *unresolved revisions* (auto-shows when they exist, lives\n * floating regardless of selection). A reviewer opening a `.docx`\n * full of someone else's tracked changes shouldn't have to discover\n * a per-block toolbar to act on them.\n */\n\nimport { type Editor, type FloatingCornerPlacement, getFloatingCorner } from \"@sobree/core\";\nimport { colorForAuthor } from \"./authorColor\";\n\n/** How long the flash pulse runs on a navigated-to mark, in ms. */\nconst FLASH_MS = 700;\n\nexport interface ReviewDockOptions {\n /** Element to anchor against. Typically `ctx.host` (rendering area). */\n host: HTMLElement;\n /** Editor for `getRevisions` + `accept/rejectAllRevisions` + DOM lookup. */\n editor: Editor;\n /** Stack root — where we look for revision marks to scroll/flash. */\n stackRoot: HTMLElement;\n /** Which corner to dock in. Defaults to `top-right`. */\n placement?: FloatingCornerPlacement;\n}\n\nexport class ReviewDock {\n private readonly host: HTMLElement;\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly placement: FloatingCornerPlacement;\n private readonly root: HTMLElement;\n private cursorIndex = 0;\n private flashTimer: ReturnType<typeof setTimeout> | null = null;\n private flashTarget: HTMLElement | null = null;\n\n constructor(opts: ReviewDockOptions) {\n this.host = opts.host;\n this.editor = opts.editor;\n this.stackRoot = opts.stackRoot;\n this.placement = opts.placement ?? \"top-right\";\n\n this.root = document.createElement(\"div\");\n this.root.className = \"sobree-review-dock\";\n this.root.dataset.placement = this.placement;\n this.root.setAttribute(\"role\", \"toolbar\");\n this.root.setAttribute(\"aria-label\", \"Review tracked changes\");\n this.root.hidden = true;\n this.root.innerHTML = this.buildHtml();\n\n getFloatingCorner(this.host, this.placement).appendChild(this.root);\n\n this.root.addEventListener(\"click\", (e) => this.handleClick(e));\n }\n\n destroy(): void {\n if (this.flashTimer) clearTimeout(this.flashTimer);\n this.clearFlash();\n this.root.remove();\n }\n\n /**\n * Re-render with the current revision count + author summary. Called\n * by the controller from its rAF/timer-debounced refresh, so every\n * `change` and `paginate` event keeps the dock in sync without us\n * registering our own listeners.\n */\n refresh(): void {\n const spans = this.editor.getRevisions();\n const summary = this.root.querySelector<HTMLElement>(\".sobree-review-dock__summary\");\n\n if (spans.length === 0) {\n // Empty state — dock stays visible with an explicit \"nothing to\n // do here\" message, no action buttons. Gives the user a clear\n // signal that accept-all/reject-all are intentionally not\n // available because there's nothing to act on, rather than a\n // disappeared dock + ambiguous state. The `.is-empty` class\n // hides the divider + buttons + nav arrows via CSS.\n this.root.hidden = false;\n this.root.classList.add(\"is-empty\");\n this.cursorIndex = 0;\n if (summary) {\n summary.textContent = \"No changes to be tracked\";\n // Reset the accent so the empty-state pill isn't tinted by a\n // stale author colour from before the last accept-all.\n summary.style.removeProperty(\"--sobree-review-dock-accent\");\n }\n return;\n }\n\n this.root.hidden = false;\n this.root.classList.remove(\"is-empty\");\n // Clamp cursor to current count (a previous index may now be out\n // of range because the user accepted/rejected something).\n if (this.cursorIndex >= spans.length) this.cursorIndex = spans.length - 1;\n if (this.cursorIndex < 0) this.cursorIndex = 0;\n\n const authors = new Set(spans.map((s) => s.author ?? \"\"));\n const count = spans.length;\n if (summary) {\n const noun = count === 1 ? \"change\" : \"changes\";\n const authorPart = authors.size > 1 ? ` · ${authors.size} authors` : \"\";\n summary.textContent = `${count} ${noun}${authorPart}`;\n }\n // Reflect the current author of the cursor span in the summary's\n // accent — same colour the marks use.\n const current = spans[this.cursorIndex];\n if (current && summary) {\n summary.style.setProperty(\"--sobree-review-dock-accent\", colorForAuthor(current.author));\n }\n }\n\n // ---- handlers ----\n\n private handleClick(e: MouseEvent): void {\n const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(\"button[data-action]\");\n if (!btn) return;\n e.preventDefault();\n const action = btn.dataset.action;\n switch (action) {\n case \"prev\":\n this.navigate(-1);\n break;\n case \"next\":\n this.navigate(1);\n break;\n case \"accept-all\": {\n const r = this.editor.acceptAllRevisions();\n if (!r.ok) console.warn(\"[review] acceptAllRevisions failed:\", r.error);\n break;\n }\n case \"reject-all\": {\n const r = this.editor.rejectAllRevisions();\n if (!r.ok) console.warn(\"[review] rejectAllRevisions failed:\", r.error);\n break;\n }\n }\n }\n\n private navigate(delta: 1 | -1): void {\n const spans = this.editor.getRevisions();\n if (spans.length === 0) return;\n // Wrap around — common UX expectation for \"next\" past the last\n // item and \"prev\" before the first.\n this.cursorIndex = (this.cursorIndex + delta + spans.length) % spans.length;\n const span = spans[this.cursorIndex];\n if (!span) return;\n const mark = this.findMarkForSpan(span);\n if (mark) this.scrollIntoViewAndFlash(mark);\n this.refresh();\n }\n\n /**\n * Find a DOM element matching the given revision span. We need this\n * to scroll to. The element type depends on the span's level:\n * - inline → an `<ins>` / `<del>` element inside the right block\n * whose character range covers the span.\n * - paragraph → the `<p data-block-revision>` element for the block.\n * - format → a `span.sobree-revision-format` inside the right block.\n *\n * Returns `null` if the mark isn't currently rendered (e.g. the\n * span is on a not-yet-paginated block, or the doc just changed).\n */\n private findMarkForSpan(span: ReturnType<Editor[\"getRevisions\"]>[number]): HTMLElement | null {\n const blockId = span.range.from.block.id;\n const block = this.stackRoot.querySelector<HTMLElement>(\n `[data-block-id=\"${cssEscape(blockId)}\"]`,\n );\n if (!block) return null;\n if (span.level === \"paragraph\") return block;\n const selector =\n span.level === \"format\"\n ? \"span.sobree-revision-format\"\n : \"ins[data-revision-author], del[data-revision-author]\";\n // First matching descendant — good enough; refinement by exact\n // character range would need text-node walking which the popover\n // doesn't bother with either.\n return block.querySelector<HTMLElement>(selector);\n }\n\n private scrollIntoViewAndFlash(mark: HTMLElement): void {\n // Centred so the user can scan context around the revision.\n // `behavior: \"smooth\"` is honoured by all modern browsers and\n // respects CSS transforms (our viewport zoom is one).\n mark.scrollIntoView({ behavior: \"smooth\", block: \"center\", inline: \"nearest\" });\n this.flash(mark);\n }\n\n private flash(mark: HTMLElement): void {\n // Clear any in-flight flash so a rapid prev/next sequence pulses\n // the most recent target only, not a stale one.\n this.clearFlash();\n mark.classList.add(\"is-flashing\");\n this.flashTarget = mark;\n this.flashTimer = setTimeout(() => this.clearFlash(), FLASH_MS);\n }\n\n private clearFlash(): void {\n if (this.flashTimer) {\n clearTimeout(this.flashTimer);\n this.flashTimer = null;\n }\n if (this.flashTarget) {\n this.flashTarget.classList.remove(\"is-flashing\");\n this.flashTarget = null;\n }\n }\n\n // ---- markup ----\n\n private buildHtml(): string {\n // Keep the markup minimal; CSS owns the visuals. Buttons use SVG\n // icons inline so the plugin has no external icon dependency.\n return `\n <span class=\"sobree-review-dock__summary\" aria-live=\"polite\"></span>\n <span class=\"sobree-review-dock__divider\"></span>\n <button type=\"button\" class=\"sobree-review-dock__btn\" data-action=\"prev\"\n title=\"Previous change\" aria-label=\"Previous change\">\n ${ICON_PREV}\n </button>\n <button type=\"button\" class=\"sobree-review-dock__btn\" data-action=\"next\"\n title=\"Next change\" aria-label=\"Next change\">\n ${ICON_NEXT}\n </button>\n <span class=\"sobree-review-dock__divider\"></span>\n <button type=\"button\" class=\"sobree-review-dock__btn is-accept\"\n data-action=\"accept-all\"\n title=\"Accept all changes\" aria-label=\"Accept all changes\">\n ${ICON_CHECK} <span class=\"sobree-review-dock__btn-label\">All</span>\n </button>\n <button type=\"button\" class=\"sobree-review-dock__btn is-reject\"\n data-action=\"reject-all\"\n title=\"Reject all changes\" aria-label=\"Reject all changes\">\n ${ICON_CROSS} <span class=\"sobree-review-dock__btn-label\">All</span>\n </button>\n `;\n }\n}\n\nconst ICON_PREV = `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"15 18 9 12 15 6\"/></svg>`;\nconst ICON_NEXT = `<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"9 18 15 12 9 6\"/></svg>`;\nconst ICON_CHECK = `<svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"20 6 9 17 4 12\"/></svg>`;\nconst ICON_CROSS = `<svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/><line x1=\"6\" y1=\"18\" x2=\"18\" y2=\"6\"/></svg>`;\n\n/**\n * Minimal CSS.escape fallback for environments without it (jsdom,\n * older browsers). Block IDs are alphanumeric + underscore in\n * practice, so a tight regex suffices.\n */\nfunction cssEscape(s: string): string {\n if (typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\") {\n return CSS.escape(s);\n }\n return s.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\\\${ch}`);\n}\n","/**\n * `@sobree/review` — tracked-changes & comments review surface.\n *\n * `@sobree/core` renders the *semantic* marks (`<ins>` / `<del>` /\n * comment-range highlight) in neutral styling — visible without any\n * plugin, so an imported docx never silently reads wrong. This plugin\n * layers the *review surface* on top:\n *\n * - per-author colour on the inline marks\n * - post-it comment cards in the right-margin sidebar, threaded and\n * vertically aligned to the text they annotate\n * - (accept/reject/resolve actions — wired in a later step)\n *\n * It owns no document state: it reacts to `paginate` events, reads the\n * AST via `editor.getDocument()`, and decorates the already-rendered\n * DOM. Removing the plugin removes the surface, not the data.\n *\n * Recommended usage:\n *\n * import { review } from \"@sobree/review\";\n * createSobree(host, { plugins: [review()] });\n */\n\nimport \"./review.css\";\nimport type {\n Block,\n Comment,\n Editor,\n FloatingCornerPlacement,\n InlineRun,\n PluginContext,\n SobreePlugin,\n SobreeUnsubscribe,\n} from \"@sobree/core\";\nimport { RevisionActions } from \"./actions\";\nimport { colorForAuthor } from \"./authorColor\";\nimport { ReviewDock } from \"./dock\";\nimport { ICON_REOPEN, ICON_RESOLVE } from \"./icons\";\n\nexport { colorForAuthor, authorSlot } from \"./authorColor\";\n\nexport interface ReviewOptions {\n /**\n * Show the per-page comment sidebar. When false, the plugin still\n * colours the inline marks per author but renders no cards. Default\n * true.\n */\n showComments?: boolean;\n /**\n * Show the top-level review dock — a floating pill with count +\n * prev/next + accept-all/reject-all. Auto-shows when revisions\n * exist, auto-hides when the doc is clean. Default true.\n */\n showDock?: boolean;\n /**\n * Which corner of the rendering area the dock pins to. Defaults to\n * `top-right`. Stacks cleanly with other plugins' floating UIs in\n * the same corner (zoom-controls, etc.) via core's shared\n * `getFloatingCorner` utility.\n */\n dockPlacement?: FloatingCornerPlacement;\n}\n\n/** Plugin factory — hand to `createSobree({ plugins: [review()] })`. */\nexport function review(opts: ReviewOptions = {}): SobreePlugin {\n return {\n name: \"review\",\n setup(ctx: PluginContext) {\n const controller = new ReviewController(ctx, opts);\n return { destroy: () => controller.destroy() };\n },\n };\n}\n\n/** Vertical gap between two stacked comment cards (px, pre-transform). */\nconst CARD_GAP = 8;\n\nclass ReviewController {\n private readonly editor: Editor;\n private readonly stackRoot: HTMLElement;\n private readonly showComments: boolean;\n private readonly unsubs: SobreeUnsubscribe[] = [];\n private readonly revisionActions: RevisionActions;\n private readonly dock: ReviewDock | null;\n /** Pending debounce handles — `null` when no refresh is queued. */\n private rafId: number | null = null;\n private timerId: ReturnType<typeof setTimeout> | null = null;\n\n constructor(ctx: PluginContext, opts: ReviewOptions) {\n this.editor = ctx.editor;\n this.stackRoot = ctx.sobree.stackRoot;\n this.showComments = opts.showComments ?? true;\n // Hover-popover accept/reject for tracked-change marks. It fetches\n // `editor.getRevisions()` live on each hover, so the range it\n // mutates always carries current block versions.\n this.revisionActions = new RevisionActions(this.editor, this.stackRoot);\n // Top-level review dock — count + prev/next + accept-all/reject-all.\n // Anchored via core's shared floating-corner stack so it cohabits\n // cleanly with any other dock the embedder mounts in the same\n // corner (zoom-controls, etc.).\n this.dock =\n (opts.showDock ?? true)\n ? new ReviewDock({\n host: ctx.host,\n editor: this.editor,\n stackRoot: this.stackRoot,\n ...(opts.dockPlacement !== undefined ? { placement: opts.dockPlacement } : {}),\n })\n : null;\n // `paginate` is the \"DOM re-laid-out\" signal — it fires after every\n // content change → repaginate cycle, so card *positions* are fresh.\n // `change` additionally catches mutations that don't repaginate —\n // notably comment resolve/reopen, which only touches document\n // metadata — so the cards re-render to reflect the new state.\n // Both funnel through the debounced `schedule()`, which coalesces a\n // paired change+paginate into one refresh.\n this.unsubs.push(ctx.sobree.on(\"paginate\", () => this.schedule()));\n this.unsubs.push(ctx.sobree.on(\"change\", () => this.schedule()));\n this.schedule();\n }\n\n /**\n * Coalesce a burst of events into a single refresh.\n *\n * We arm an animation frame *and* a timer, and the first to fire\n * wins (cancelling the other). The rAF gives frame-aligned layout\n * reads when the tab is visible; the timer is the safety net —\n * `requestAnimationFrame` callbacks are paused entirely in a hidden\n * tab, so an rAF-only debounce would wedge permanently (`pending`\n * never clears) and the review surface would go dark. Timers keep\n * running (throttled) in background tabs, so the timer guarantees\n * the surface still updates and can never get stuck.\n */\n private schedule(): void {\n if (this.rafId !== null || this.timerId !== null) return;\n const run = (): void => {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n this.timerId = null;\n }\n try {\n this.refresh();\n } catch (err) {\n console.error(\"[review] refresh failed:\", err);\n }\n };\n this.rafId = requestAnimationFrame(run);\n this.timerId = setTimeout(run, 100);\n }\n\n private refresh(): void {\n colourMarks(this.stackRoot);\n if (this.showComments) {\n const comments = this.editor.getDocument().comments ?? {};\n renderComments(this.stackRoot, comments, this.editor);\n }\n // Dock reads `editor.getRevisions()` itself; refresh paints the\n // count + author summary and auto-shows/hides based on the result.\n this.dock?.refresh();\n }\n\n destroy(): void {\n for (const u of this.unsubs) u();\n if (this.rafId !== null) cancelAnimationFrame(this.rafId);\n if (this.timerId !== null) clearTimeout(this.timerId);\n this.revisionActions.destroy();\n this.dock?.destroy();\n // Leave core's neutral marks intact; just clear what we added.\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\n \"ins[data-revision-author], del[data-revision-author]\",\n ),\n )) {\n el.style.removeProperty(\"--author-color\");\n }\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\"[data-block-revision]\"),\n )) {\n el.style.removeProperty(\"--sobree-block-revision-color\");\n }\n for (const el of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\"span.sobree-revision-format\"),\n )) {\n el.style.removeProperty(\"--sobree-format-revision-color\");\n }\n for (const slot of Array.from(\n this.stackRoot.querySelectorAll<HTMLElement>(\".paper-comments\"),\n )) {\n slot.replaceChildren();\n slot.classList.add(\"is-empty\");\n }\n }\n}\n\n// ---------- inline marks ----------\n\n/** Apply per-author colour to every tracked-change mark in `root`. */\nfunction colourMarks(root: HTMLElement): void {\n // Inline ins/del runs.\n const marks = root.querySelectorAll<HTMLElement>(\n \"ins[data-revision-author], del[data-revision-author]\",\n );\n for (const mark of Array.from(marks)) {\n mark.style.setProperty(\"--author-color\", colorForAuthor(mark.dataset.revisionAuthor));\n }\n // Paragraph-mark revisions — `data-block-revision=\"ins\"|\"del\"` lives\n // on the paragraph element; the `::after` pseudo in core reads\n // `--sobree-block-revision-color`, which we set per-author here.\n const blockMarks = root.querySelectorAll<HTMLElement>(\"[data-block-revision]\");\n for (const mark of Array.from(blockMarks)) {\n mark.style.setProperty(\n \"--sobree-block-revision-color\",\n colorForAuthor(mark.dataset.blockRevisionAuthor),\n );\n }\n // Format-change revisions — the wrapping `<span.sobree-revision-format>`\n // reads `--sobree-format-revision-color` to colour the dashed\n // underline core ships as the neutral visual hint.\n const formatMarks = root.querySelectorAll<HTMLElement>(\"span.sobree-revision-format\");\n for (const mark of Array.from(formatMarks)) {\n mark.style.setProperty(\n \"--sobree-format-revision-color\",\n colorForAuthor(mark.dataset.revisionFormatAuthor),\n );\n }\n}\n\n// ---------- comment cards ----------\n\n/**\n * Build the post-it comment cards for every paper and place them in\n * the per-paper `.paper-comments` sidebar slot, vertically aligned to\n * the comment ranges they annotate.\n */\nfunction renderComments(\n root: HTMLElement,\n comments: Record<number, Comment>,\n editor: Editor,\n): void {\n // parent → replies index, for threading.\n const repliesByParent = new Map<number, Comment[]>();\n for (const c of Object.values(comments)) {\n // `replyToId` is absent on a top-level comment — but a YDoc\n // round-trip can materialise the missing field as `null`, so\n // treat `null` and `undefined` alike (`== null`).\n if (c.replyToId == null) continue;\n const list = repliesByParent.get(c.replyToId) ?? [];\n list.push(c);\n repliesByParent.set(c.replyToId, list);\n }\n for (const list of repliesByParent.values()) list.sort((a, b) => a.id - b.id);\n\n for (const row of Array.from(root.querySelectorAll<HTMLElement>(\".paper-row\"))) {\n const paper = row.querySelector<HTMLElement>(\".paper\");\n const slot = row.querySelector<HTMLElement>(\".paper-comments\");\n if (!paper || !slot) continue;\n slot.replaceChildren();\n\n // Collect (commentId, anchorTopPx) for top-level comments whose\n // range starts on this paper, in document order.\n const placements: { id: number; top: number }[] = [];\n const seen = new Set<number>();\n const anchors = paper.querySelectorAll<HTMLElement>(\".sobree-comment-range\");\n for (const anchor of Array.from(anchors)) {\n const raw = anchor.dataset.commentIds ?? \"\";\n for (const part of raw.split(\",\")) {\n const id = Number(part.trim());\n if (!Number.isFinite(id) || seen.has(id)) continue;\n const c = comments[id];\n // Skip replies (they ride their parent card). `!= null` so a\n // YDoc-materialised `null` still counts as \"top-level\".\n if (!c || c.replyToId != null) continue;\n seen.add(id);\n placements.push({ id, top: offsetWithin(anchor, paper) });\n }\n }\n if (placements.length === 0) {\n slot.classList.add(\"is-empty\");\n continue;\n }\n slot.classList.remove(\"is-empty\");\n\n // Place cards top-down, pushing later cards past earlier ones so\n // threads never overlap (the classic comment-margin layout).\n placements.sort((a, b) => a.top - b.top);\n let cursor = 0;\n for (const { id, top } of placements) {\n const card = buildThreadCard(comments[id]!, repliesByParent, editor);\n card.style.top = `${Math.max(top, cursor)}px`;\n slot.appendChild(card);\n cursor = Math.max(top, cursor) + card.offsetHeight + CARD_GAP;\n }\n }\n}\n\n/** Build one post-it: the top-level comment + its reply thread. */\nfunction buildThreadCard(\n root: Comment,\n repliesByParent: Map<number, Comment[]>,\n editor: Editor,\n): HTMLElement {\n const card = document.createElement(\"div\");\n card.className = \"sobree-review-card\";\n card.dataset.commentId = String(root.id);\n\n const emit = (c: Comment, depth: number): void => {\n card.appendChild(buildCommentEl(c, depth, editor));\n for (const reply of repliesByParent.get(c.id) ?? []) emit(reply, depth + 1);\n };\n emit(root, 0);\n return card;\n}\n\n/** One comment within a thread card. */\nfunction buildCommentEl(c: Comment, depth: number, editor: Editor): HTMLElement {\n const el = document.createElement(\"article\");\n el.className = \"sobree-review-comment\";\n if (depth > 0) el.classList.add(\"is-reply\");\n if (c.done) el.classList.add(\"is-resolved\");\n el.dataset.commentId = String(c.id);\n el.id = `sobree-comment-${c.id}`;\n\n const header = document.createElement(\"header\");\n header.className = \"sobree-review-comment__header\";\n const author = document.createElement(\"span\");\n author.className = \"sobree-review-comment__author\";\n author.textContent = c.author ?? \"Anonymous\";\n author.style.color = colorForAuthor(c.author);\n header.appendChild(author);\n if (c.date) {\n const time = document.createElement(\"time\");\n time.className = \"sobree-review-comment__date\";\n time.dateTime = c.date;\n time.textContent = c.date.slice(0, 10);\n header.appendChild(time);\n }\n if (c.done) {\n const badge = document.createElement(\"span\");\n badge.className = \"sobree-review-comment__status\";\n badge.textContent = \"✓ Resolved\";\n header.appendChild(badge);\n }\n // Resolve / reopen toggle — flips `Comment.done` via the editor.\n const toggle = document.createElement(\"button\");\n toggle.type = \"button\";\n toggle.className = \"sobree-review-comment__action\";\n const reopening = !!c.done;\n toggle.title = reopening ? \"Reopen comment\" : \"Resolve comment\";\n toggle.setAttribute(\"aria-label\", toggle.title);\n toggle.innerHTML = reopening ? ICON_REOPEN : ICON_RESOLVE;\n toggle.addEventListener(\"click\", () => {\n const result = reopening ? editor.reopenComment(c.id) : editor.resolveComment(c.id);\n if (!result.ok) {\n console.warn(\"[review] resolve/reopen failed:\", result.error);\n }\n });\n header.appendChild(toggle);\n el.appendChild(header);\n\n const body = document.createElement(\"div\");\n body.className = \"sobree-review-comment__body\";\n body.textContent = flattenBlocks(c.body);\n el.appendChild(body);\n return el;\n}\n\n/** Flatten a comment body (`Block[]`) to plain text — comment bodies\n * are short, so rich formatting is dropped for now. */\nfunction flattenBlocks(blocks: Block[]): string {\n const parts: string[] = [];\n for (const b of blocks) {\n if (b.kind !== \"paragraph\") continue;\n parts.push(b.runs.map(inlineText).join(\"\"));\n }\n return parts.join(\"\\n\");\n}\n\nfunction inlineText(run: InlineRun): string {\n if (run.kind === \"text\") return run.text;\n if (run.kind === \"hyperlink\") return run.children.map(inlineText).join(\"\");\n if (run.kind === \"tab\") return \"\\t\";\n return \"\";\n}\n\n/** Vertical offset of `el` relative to `ancestor`, in layout px\n * (transform-independent — uses the offsetTop chain, not rects). */\nfunction offsetWithin(el: HTMLElement, ancestor: HTMLElement): number {\n let top = 0;\n let node: HTMLElement | null = el;\n while (node && node !== ancestor) {\n top += node.offsetTop;\n node = node.offsetParent as HTMLElement | null;\n }\n return top;\n}\n"],"names":["SVG","paths","ICON_ACCEPT","ICON_REJECT","ICON_RESOLVE","ICON_REOPEN","HIDE_DELAY_MS","RevisionActions","editor","stackRoot","__publicField","e","pop","label","svg","kind","btn","target","inline","span","formatEl","paraEl","mark","noun","accept","reject","r","popW","left","top","result","blockRef","block","blockId","offset","textLengthBefore","blockEl","el","range","FALLBACK_PALETTE","authorSlot","author","h","i","colorForAuthor","slot","FLASH_MS","ReviewDock","opts","getFloatingCorner","spans","summary","authors","s","count","authorPart","current","delta","cssEscape","selector","ICON_PREV","ICON_NEXT","ICON_CHECK","ICON_CROSS","ch","review","ctx","controller","ReviewController","CARD_GAP","run","err","colourMarks","comments","renderComments","_a","u","root","marks","blockMarks","formatMarks","repliesByParent","c","list","a","b","row","paper","placements","seen","anchors","anchor","raw","part","id","offsetWithin","cursor","card","buildThreadCard","emit","depth","buildCommentEl","reply","header","time","badge","toggle","reopening","body","flattenBlocks","blocks","parts","inlineText","ancestor","node"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sobree/review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@sobree/core": "0.1.
|
|
43
|
+
"@sobree/core": "0.1.10"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --noEmit",
|