@erdemtuna/doc-review 0.11.0 → 0.12.0

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/lib/sdk.js CHANGED
@@ -56,6 +56,9 @@ let disposed = false;
56
56
  let viewObserver = null;
57
57
  let viewChangeTimer = null;
58
58
  let lastObservedView = null;
59
+ let reviewTheme = null;
60
+ let themeRevision = 0;
61
+ let themeWarnings = 0;
59
62
  const blockTargets = new Map();
60
63
  const blockMarkers = new Map();
61
64
  /** True when the page's own scripts rewrote the DOM before any user edit. */
@@ -70,22 +73,115 @@ const shadow = host.attachShadow({ mode: "open" });
70
73
  shadow.innerHTML = `
71
74
  <style>
72
75
  :host { all: initial; }
76
+ :host(:not([data-review-theme])) { visibility: hidden !important; }
77
+ :host(:not([data-review-theme])) * { visibility: hidden !important; pointer-events: none !important; }
78
+ /* REVIEW_THEME_SHADOW_START */
79
+ :host([data-review-theme="light"]) {
80
+ color-scheme: light;
81
+ --review-background: #F5F2EA;
82
+ --review-foreground: #292E2B;
83
+ --review-card: #FFFDF7;
84
+ --review-card-foreground: #292E2B;
85
+ --review-popover: #FFFDF7;
86
+ --review-popover-foreground: #292E2B;
87
+ --review-primary: #17685F;
88
+ --review-primary-foreground: #FFFDF7;
89
+ --review-secondary: #ECEDE5;
90
+ --review-secondary-foreground: #626B65;
91
+ --review-muted: #ECEDE5;
92
+ --review-muted-foreground: #626B65;
93
+ --review-accent: #E1EEEA;
94
+ --review-accent-foreground: #17685F;
95
+ --review-destructive: #B43C48;
96
+ --review-destructive-foreground: #FFFDF7;
97
+ --review-border: #D8D8CC;
98
+ --review-input: #7C877D;
99
+ --review-ring: #17685F;
100
+ --review-review-added: #EFF3E2;
101
+ --review-review-added-foreground: #4D6B24;
102
+ --review-review-added-border: #82945E;
103
+ --review-review-removed: #FCECEF;
104
+ --review-review-removed-foreground: #B43C48;
105
+ --review-review-removed-border: #C88791;
106
+ --review-review-modified: #FFF1D9;
107
+ --review-review-modified-foreground: #915B13;
108
+ --review-review-modified-border: #B99157;
109
+ --review-review-count-added: #4D6B24;
110
+ --review-review-count-removed: #B43C48;
111
+ --review-review-count-modified: #915B13;
112
+ --review-review-insert: #DCE7C5;
113
+ --review-review-delete: #FAE2E7;
114
+ --review-review-overlay: #191F1D80;
115
+ --review-review-shadow-color: #191F1D26;
116
+ --review-annotation-foreground: #75470E;
117
+ --review-annotation-background: #FFF1D9;
118
+ --review-annotation-active: #F5D6A3;
119
+ --review-annotation-border: #915B13;
120
+ --review-annotation-tint: #915B131A;
121
+ --review-annotation-active-tint: #915B132E;
122
+ --review-halo-light: #FFFDF7;
123
+ --review-halo-dark: #191F1D;
124
+ }
125
+ :host([data-review-theme="dark"]) {
126
+ color-scheme: dark;
127
+ --review-background: #191F1D;
128
+ --review-foreground: #EEEFE6;
129
+ --review-card: #222A26;
130
+ --review-card-foreground: #EEEFE6;
131
+ --review-popover: #222A26;
132
+ --review-popover-foreground: #EEEFE6;
133
+ --review-primary: #85C7B8;
134
+ --review-primary-foreground: #102F29;
135
+ --review-secondary: #303A33;
136
+ --review-secondary-foreground: #AFB9AF;
137
+ --review-muted: #303A33;
138
+ --review-muted-foreground: #AFB9AF;
139
+ --review-accent: #293F37;
140
+ --review-accent-foreground: #85C7B8;
141
+ --review-destructive: #F2A2AB;
142
+ --review-destructive-foreground: #40272C;
143
+ --review-border: #3C4841;
144
+ --review-input: #77877B;
145
+ --review-ring: #85C7B8;
146
+ --review-review-added: #2B3522;
147
+ --review-review-added-foreground: #BBCD87;
148
+ --review-review-added-border: #6F8347;
149
+ --review-review-removed: #40272C;
150
+ --review-review-removed-foreground: #F2A2AB;
151
+ --review-review-removed-border: #A96572;
152
+ --review-review-modified: #3D3020;
153
+ --review-review-modified-foreground: #E7BB72;
154
+ --review-review-modified-border: #9C7A46;
155
+ --review-review-count-added: #BBCD87;
156
+ --review-review-count-removed: #F2A2AB;
157
+ --review-review-count-modified: #E7BB72;
158
+ --review-review-insert: #3E4D2D;
159
+ --review-review-delete: #633944;
160
+ --review-review-overlay: #191F1DB3;
161
+ --review-review-shadow-color: #00000066;
162
+ --review-annotation-foreground: #F1CC8E;
163
+ --review-annotation-background: #3D3020;
164
+ --review-annotation-active: #594325;
165
+ --review-annotation-border: #E7BB72;
166
+ --review-annotation-tint: #E7BB721A;
167
+ --review-annotation-active-tint: #E7BB722E;
168
+ --review-halo-light: #FFFDF7;
169
+ --review-halo-dark: #191F1D;
170
+ }
171
+ /* REVIEW_THEME_SHADOW_END */
73
172
  .box { position: fixed; pointer-events: none; z-index: 2147483646; border-radius: 3px; display: none; }
74
- .outline { border: 1px dashed #96938c; animation: outline-in 90ms ease-out; }
173
+ .outline { border: 1px dashed var(--review-input); animation: outline-in 90ms ease-out; }
75
174
  @keyframes outline-in { from { opacity: .2; } to { opacity: 1; } }
76
- .active { border: 2px solid #b46a00; box-shadow: 0 0 0 1px #fff9; }
175
+ .active { border: 2px solid var(--review-annotation-border); }
77
176
  .block-marker { position: fixed; pointer-events: none; z-index: 2147483644;
78
- box-sizing: border-box; border-left: 3px solid #a96100; background: #d58b0012; }
79
- .block-marker[data-active="true"] { border-left-width: 6px; background: #d58b0026; }
80
- .block-marker[data-dark="true"] { border-left-color: #ffcb66; background: #ffcb6617; }
81
- .block-marker[data-dark="true"][data-active="true"] { background: #ffcb6630; }
177
+ box-sizing: border-box; border-left: 3px solid var(--review-annotation-border); background: var(--review-annotation-tint); }
178
+ .block-marker[data-active="true"] { border-left-width: 6px; background: var(--review-annotation-active-tint); }
82
179
  .block-badge { position: fixed; z-index: 2147483647; pointer-events: auto;
83
180
  height: 24px; min-width: 32px; box-sizing: border-box; padding: 2px 6px;
84
- border: 1px solid #965900; border-radius: 5px; background: #fff3d3; color: #663b00;
181
+ border: 1px solid var(--review-annotation-border); border-radius: 5px; background: var(--review-annotation-background); color: var(--review-annotation-foreground);
85
182
  font: 600 12px/18px system-ui, sans-serif; cursor: pointer; }
86
- .block-badge[data-dark="true"] { background: #493517; color: #ffe0a0; border-color: #ffcb66; }
87
183
  .block-badge[aria-pressed="true"] { border-width: 2px; text-decoration: underline; }
88
- .block-badge:focus-visible { outline: 3px solid #718cff; outline-offset: 2px; }
184
+ .block-badge:focus-visible { outline: 3px solid var(--review-ring); outline-offset: 2px; }
89
185
  .chips {
90
186
  position: fixed; z-index: 2147483647; display: none; gap: 4px;
91
187
  pointer-events: auto;
@@ -93,60 +189,66 @@ shadow.innerHTML = `
93
189
  .chip {
94
190
  width: 23px; height: 23px; display: flex;
95
191
  align-items: center; justify-content: center; padding: 0;
96
- border: 1px solid #e4e2db; border-radius: 50%; background: #fff; color: #6b6862;
192
+ border: 1px solid var(--review-input); border-radius: 50%; background: var(--review-card); color: var(--review-muted-foreground);
97
193
  font: 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
98
- cursor: pointer; box-shadow: 0 2px 8px rgba(27,26,22,.14);
194
+ cursor: pointer;
99
195
  }
100
- .chip:hover { background: #1b1a16; color: #fff; border-color: #1b1a16; }
101
- .chip.danger { color: #b23b2e; }
102
- .chip.danger:hover { background: #b23b2e; color: #fff; border-color: #b23b2e; }
103
- .chips .chip, .mover { opacity: .6; transition: opacity 100ms ease; }
104
- .chips .chip:hover, .chips .chip:focus-visible, .mover:hover, .mover:focus-visible { opacity: 1; }
196
+ .chip:hover { background: var(--review-accent); color: var(--review-accent-foreground); border-color: var(--review-primary); }
197
+ .chip.danger { color: var(--review-destructive); }
198
+ .chip.danger:hover { background: var(--review-destructive); color: var(--review-destructive-foreground); border-color: var(--review-destructive); }
199
+ .chips .chip, .mover { transition: background-color 100ms ease; }
200
+ button:disabled { background: var(--review-muted); color: var(--review-muted-foreground); cursor: not-allowed; }
105
201
  .grip {
106
202
  position: fixed; z-index: 2147483647; width: 13px; height: 13px;
107
- display: none; border: 1px solid #1b1a16; border-radius: 3px;
108
- background: #fff; cursor: nwse-resize; pointer-events: auto;
109
- box-shadow: 0 1px 4px rgba(27,26,22,.2);
203
+ display: none; border: 1px solid var(--review-primary); border-radius: 3px;
204
+ background: var(--review-card); cursor: nwse-resize; pointer-events: auto;
110
205
  }
111
206
  .hint {
112
207
  position: fixed; z-index: 2147483647; display: none; padding: 3px 7px;
113
- border-radius: 5px; background: #1b1a16; color: #fbfaf7; white-space: nowrap;
208
+ border-radius: 5px; background: var(--review-card); color: var(--review-foreground); white-space: nowrap;
114
209
  font: 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
115
210
  pointer-events: none;
116
211
  }
117
212
  .linkbox {
118
213
  position: fixed; z-index: 2147483647; display: none; gap: 4px; align-items: center;
119
- padding: 5px; border: 1px solid #e4e2db; border-radius: 9px; background: #fff;
120
- box-shadow: 0 4px 16px rgba(27,26,22,.16); pointer-events: auto;
214
+ padding: 5px; border: 1px solid var(--review-input); border-radius: 9px; background: var(--review-card);
215
+ pointer-events: auto;
121
216
  }
122
217
  .linkbox input {
123
218
  width: 224px; padding: 4px 7px; border: none; outline: none; background: none;
124
- font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #1b1a16;
219
+ font: 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--review-foreground);
125
220
  }
126
- .linkbox input::placeholder { color: #a29f95; }
221
+ .linkbox input::placeholder { color: var(--review-muted-foreground); opacity: 1; }
127
222
  .mover {
128
223
  position: fixed; z-index: 2147483647; width: 18px; height: 24px;
129
224
  display: none; align-items: center; justify-content: center;
130
- border: 1px solid #e4e2db; border-radius: 6px; background: #fff; color: #a29f95;
225
+ border: 1px solid var(--review-input); border-radius: 6px; background: var(--review-card); color: var(--review-muted-foreground);
131
226
  font: 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
132
227
  cursor: grab; pointer-events: auto; user-select: none;
133
- box-shadow: 0 2px 8px rgba(27,26,22,.14);
134
228
  }
135
- .mover:hover { color: #1b1a16; border-color: #c2beb4; }
229
+ .mover:hover { color: var(--review-accent-foreground); background: var(--review-accent); border-color: var(--review-primary); }
136
230
  .mover:active { cursor: grabbing; }
137
231
  .dropline {
138
232
  position: fixed; z-index: 2147483646; height: 0; display: none;
139
- border-top: 2px solid #1b1a16; border-radius: 1px; pointer-events: none;
233
+ border-top: 2px solid var(--review-primary); border-radius: 1px; pointer-events: none;
140
234
  }
141
235
  .comment-action {
142
236
  position: fixed; z-index: 2147483647; width: 30px; height: 30px;
143
237
  display: none; align-items: center; justify-content: center; padding: 0;
144
- border: 1px solid #d9d5c9; border-radius: 999px; background: #1b1a16; color: #fff;
145
- cursor: pointer; pointer-events: auto; box-shadow: 0 5px 16px rgba(27,26,22,.24);
238
+ border: 1px solid var(--review-primary); border-radius: 999px; background: var(--review-primary); color: var(--review-primary-foreground);
239
+ cursor: pointer; pointer-events: auto;
240
+ }
241
+ .comment-action:hover { background: var(--review-accent); color: var(--review-accent-foreground); }
242
+ button:focus-visible, input:focus-visible, .mover:focus-visible, .grip:focus-visible {
243
+ outline: 3px solid var(--review-ring); outline-offset: 2px;
244
+ }
245
+ .box, .block-marker, .block-badge, .chip, .grip, .hint, .linkbox, .mover, .dropline, .comment-action {
246
+ box-shadow: 0 0 0 1px var(--review-halo-light), 0 0 0 2px var(--review-halo-dark), 0 3px 10px var(--review-review-shadow-color);
146
247
  }
147
- .comment-action:hover, .comment-action:focus-visible { background: #34312b; outline: 3px solid rgba(90,99,216,.32); }
148
248
  .selection-cues { position: fixed; inset: 0; pointer-events: none; z-index: 2147483645; }
149
- .selection-cue { position: fixed; border-radius: 2px; background: rgba(245,196,0,.2); }
249
+ .selection-cue { position: fixed; border-radius: 2px; background: var(--review-annotation-tint);
250
+ box-shadow: inset 0 -2px var(--review-annotation-border), 0 1px var(--review-halo-light), 0 2px var(--review-halo-dark); }
251
+ ::selection { background: var(--review-annotation-active); color: var(--review-annotation-foreground); }
150
252
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
151
253
  </style>
152
254
  <div class="box outline" id="outline"></div>
@@ -187,15 +289,6 @@ const mountOverlay = () => {
187
289
  els.commentAction = shadow.getElementById("commentAction");
188
290
  els.blockAnnotations = shadow.getElementById("blockAnnotations");
189
291
  };
190
- function darkSurface(element) {
191
- for (let probe = element; probe; probe = probe.parentElement) {
192
- const color = getComputedStyle(probe).backgroundColor.match(/[\d.]+/g)?.map(Number);
193
- if (color?.length >= 3 && (color.length < 4 || color[3] >= .5)) {
194
- return color[0] * .2126 + color[1] * .7152 + color[2] * .0722 < 128;
195
- }
196
- }
197
- return matchMedia("(prefers-color-scheme: dark)").matches;
198
- }
199
292
  function positionBlockBadge(badge, rect) {
200
293
  const width = badge.offsetWidth || 36;
201
294
  const height = 24;
@@ -259,9 +352,7 @@ function renderBlockAnnotations() {
259
352
  }
260
353
  const { marker, badge } = entry;
261
354
  const active = ids.includes(activeCommentId);
262
- const dark = darkSurface(element);
263
355
  marker.dataset.active = String(active);
264
- marker.dataset.dark = badge.dataset.dark = String(dark);
265
356
  badge.setAttribute("aria-pressed", String(active));
266
357
  const label = commentTargetFor(element)?.label || element.tagName.toLowerCase();
267
358
  const name = `${ids.length} block comment${ids.length === 1 ? "" : "s"} on ${label}${ids.length > 1 ? "; activate to open next comment" : ""}`;
@@ -618,7 +709,7 @@ function geometryWatchSignature() {
618
709
  blocks: [...groupCommentTargets(blockTargets).keys()].map((element) => {
619
710
  const rect = element.getBoundingClientRect();
620
711
  const clip = effectiveClipRect({ kind: "element", element });
621
- return [rect.left, rect.top, rect.width, rect.height, clip, darkSurface(element)];
712
+ return [rect.left, rect.top, rect.width, rect.height, clip];
622
713
  }),
623
714
  });
624
715
  }
@@ -1282,7 +1373,7 @@ function setElementTarget(container) {
1282
1373
  }
1283
1374
  function openPendingCompose() {
1284
1375
  hoverIntent?.cancel();
1285
- if (composing || disposed)
1376
+ if (composing || disposed || !themeRevision)
1286
1377
  return false;
1287
1378
  const selection = document.getSelection();
1288
1379
  if (selection && !selection.isCollapsed && selection.rangeCount) {
@@ -1485,7 +1576,6 @@ function activate(id, scroll) {
1485
1576
  for (const mark of marks)
1486
1577
  mark.classList.add("eh-active");
1487
1578
  place(els.activeBox, target);
1488
- els.activeBox.style.borderColor = target && darkSurface(target) ? "#ffcb66" : "#a96100";
1489
1579
  renderBlockAnnotations();
1490
1580
  const rects = marks.length
1491
1581
  ? marks.map((mark) => rectData(mark.getBoundingClientRect()))
@@ -1531,18 +1621,54 @@ function activate(id, scroll) {
1531
1621
  }
1532
1622
  }
1533
1623
  // -------------------------------------------------------------------- boot
1624
+ /* REVIEW_THEME_DOCUMENT_START */
1625
+ const REVIEW_DOCUMENT_COLORS = {
1626
+ light: `
1627
+ mark[${MARK_ATTR}] { background: #FFF1D9; color: #75470E; text-decoration: underline #915B13; }
1628
+ mark[${MARK_ATTR}]:hover, mark[${MARK_ATTR}].eh-active { background: #F5D6A3; }
1629
+ mark[${MARK_ATTR}].eh-active { outline: 2px solid #915B13; outline-offset: 1px; }
1630
+ ::selection { background: #F5D6A3; color: #75470E; }
1631
+ `,
1632
+ dark: `
1633
+ mark[${MARK_ATTR}] { background: #3D3020; color: #F1CC8E; text-decoration: underline #E7BB72; }
1634
+ mark[${MARK_ATTR}]:hover, mark[${MARK_ATTR}].eh-active { background: #594325; }
1635
+ mark[${MARK_ATTR}].eh-active { outline: 2px solid #E7BB72; outline-offset: 1px; }
1636
+ ::selection { background: #594325; color: #F1CC8E; }
1637
+ `,
1638
+ };
1639
+ /* REVIEW_THEME_DOCUMENT_END */
1534
1640
  function boot() {
1535
1641
  mountOverlay();
1536
1642
  const style = document.createElement("style");
1537
1643
  style.setAttribute("data-eh-sdk", "");
1538
- style.textContent = `
1539
- mark[${MARK_ATTR}] { background: rgba(245,196,0,.32); border-radius: 2px; color: inherit; cursor: pointer; }
1540
- mark[${MARK_ATTR}]:hover { background: rgba(243,176,0,.5); }
1541
- mark[${MARK_ATTR}].eh-active { background: rgba(255,180,0,.55); }
1644
+ const documentStyle = `
1645
+ mark[${MARK_ATTR}] { background: transparent; border-radius: 2px; color: inherit; cursor: pointer; }
1542
1646
  body[contenteditable]:focus { outline: none; }
1543
- ::selection { background: rgba(245,196,0,.42); }
1544
1647
  `;
1648
+ style.textContent = documentStyle;
1545
1649
  document.head.appendChild(style);
1650
+ const applyReviewTheme = (message) => {
1651
+ const allowedFields = ["type", "theme", "themeRevision", "capability", "generation", "pageKey"];
1652
+ const malformed = !["light", "dark"].includes(message.theme) ||
1653
+ !Number.isSafeInteger(message.themeRevision) || message.themeRevision <= 0 ||
1654
+ Object.keys(message).some((field) => !allowedFields.includes(field));
1655
+ const conflicting = message.themeRevision === themeRevision && message.theme !== reviewTheme;
1656
+ if (malformed || conflicting) {
1657
+ if (themeWarnings++ < 3)
1658
+ console.warn("[doc-review-frame] Rejected invalid theme command.");
1659
+ return;
1660
+ }
1661
+ if (message.themeRevision < themeRevision)
1662
+ return;
1663
+ if (message.themeRevision > themeRevision) {
1664
+ // Only review-owned nodes change: keep the document, controls, focus and mode intact.
1665
+ style.textContent = documentStyle + REVIEW_DOCUMENT_COLORS[message.theme];
1666
+ host.dataset.reviewTheme = message.theme;
1667
+ reviewTheme = message.theme;
1668
+ themeRevision = message.themeRevision;
1669
+ }
1670
+ post("eh:themeApplied", { theme: reviewTheme, themeRevision });
1671
+ };
1546
1672
  modeController = keepBodyInReviewMode(document.body, "view");
1547
1673
  baseline = serialize();
1548
1674
  bootSnapshot = baseline;
@@ -1581,7 +1707,7 @@ function boot() {
1581
1707
  const modified = event.metaKey || event.ctrlKey;
1582
1708
  const href = navigationHref(event.target);
1583
1709
  const mark = event.target.closest && event.target.closest(`mark[${MARK_ATTR}]`);
1584
- if (mark) {
1710
+ if (mark && themeRevision) {
1585
1711
  event.preventDefault();
1586
1712
  post("eh:activate", { id: mark.getAttribute(MARK_ATTR) });
1587
1713
  return;
@@ -1701,7 +1827,7 @@ function boot() {
1701
1827
  clearPending();
1702
1828
  };
1703
1829
  hoverIntent = createHoverIntent((event) => {
1704
- if (disposed || pointerSelecting || composing || composeOpen || selectionIsActive())
1830
+ if (disposed || !themeRevision || pointerSelecting || composing || composeOpen || selectionIsActive())
1705
1831
  return;
1706
1832
  if (!event) {
1707
1833
  clearHover();
@@ -1734,7 +1860,7 @@ function boot() {
1734
1860
  showHint(reviewMode === "edit" && interactive ? "⌘-click to open" : reviewMode === "edit" && draggable ? "Drag to move" : "", event.clientX, event.clientY);
1735
1861
  });
1736
1862
  const updateHover = (event) => {
1737
- if (disposed || resizing || moving || pointerSelecting || composing || composeOpen ||
1863
+ if (disposed || !themeRevision || resizing || moving || pointerSelecting || composing || composeOpen ||
1738
1864
  commentOpenRequestGeneration || selectionIsActive()) {
1739
1865
  hoverIntent.cancel();
1740
1866
  return;
@@ -1987,6 +2113,30 @@ function boot() {
1987
2113
  document.body.focus({ preventScroll: true });
1988
2114
  }
1989
2115
  }
2116
+ function positionLinkbox() {
2117
+ if (!linkState)
2118
+ return false;
2119
+ const { range, anchor, blockEl, startNode, endNode } = linkState;
2120
+ if (!startNode.isConnected || !endNode.isConnected || (anchor && !anchor.isConnected) ||
2121
+ (blockEl && !blockEl.isConnected) || !document.body.contains(range.commonAncestorContainer)) {
2122
+ closeLinkbox(false);
2123
+ return false;
2124
+ }
2125
+ const rects = visibleRects([...range.getClientRects()].map(rectData), {
2126
+ kind: "element", element: anchor || blockEl || startNode.parentElement,
2127
+ });
2128
+ if (!rects.length) {
2129
+ closeLinkbox(false);
2130
+ return false;
2131
+ }
2132
+ const r = rects[0];
2133
+ const width = els.linkbox.offsetWidth || 320;
2134
+ const height = els.linkbox.offsetHeight || 36;
2135
+ const below = r.bottom + height + 8 < window.innerHeight;
2136
+ els.linkbox.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - width - 8))}px`;
2137
+ els.linkbox.style.top = `${Math.max(8, Math.min(below ? r.bottom + 8 : r.top - height - 8, window.innerHeight - height - 8))}px`;
2138
+ return true;
2139
+ }
1990
2140
  function openLinkbox() {
1991
2141
  const sel = document.getSelection();
1992
2142
  if (!sel || !sel.rangeCount)
@@ -2010,18 +2160,18 @@ function boot() {
2010
2160
  anchor,
2011
2161
  blockEl: target ? target.el : null,
2012
2162
  label: target ? target.label : "Document body",
2163
+ startNode: range.startContainer,
2164
+ endNode: range.endContainer,
2013
2165
  };
2014
2166
  if (linkState.blockEl)
2015
2167
  captureOriginal(linkState.blockEl);
2016
2168
  // The compose card and the link popup fight over the same selection.
2017
2169
  clearPending();
2018
2170
  post("eh:dismiss", {});
2019
- const r = range.getBoundingClientRect();
2020
- const below = r.bottom + 44 < window.innerHeight;
2021
2171
  els.linkbox.style.display = "flex";
2022
- els.linkbox.style.left = `${Math.max(8, Math.min(r.left, window.innerWidth - 320))}px`;
2023
- els.linkbox.style.top = `${below ? r.bottom + 8 : Math.max(8, r.top - 44)}px`;
2024
2172
  els.linkRemove.style.display = anchor ? "" : "none";
2173
+ if (!positionLinkbox())
2174
+ return false;
2025
2175
  els.linkInput.value = anchor ? anchor.getAttribute("href") || "" : "";
2026
2176
  els.linkInput.focus();
2027
2177
  els.linkInput.select();
@@ -2476,9 +2626,10 @@ function boot() {
2476
2626
  scheduleTargetGeometry();
2477
2627
  if (activeCommentId)
2478
2628
  activate(activeCommentId, false);
2479
- // The popup is viewport-fixed; scrolled away from its text it just lies.
2629
+ // Recovery notices can resize the frame. Re-anchor without discarding the
2630
+ // open editor's draft or focus; only a missing/invisible target invalidates it.
2480
2631
  if (linkState)
2481
- closeLinkbox(false);
2632
+ positionLinkbox();
2482
2633
  };
2483
2634
  // getBoundingClientRect on every scroll event forces layout mid-scroll;
2484
2635
  // one reposition per frame is plenty.
@@ -2514,6 +2665,9 @@ function boot() {
2514
2665
  if (!matchesFrameMessage(msg))
2515
2666
  return;
2516
2667
  switch (msg.type) {
2668
+ case "eh:setTheme":
2669
+ applyReviewTheme(msg);
2670
+ break;
2517
2671
  case "eh:anchors":
2518
2672
  reanchor(msg.comments || []);
2519
2673
  break;