dsh-select-quote 0.3.5 → 0.3.7

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/client.js CHANGED
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let react_jsx_runtime = require("react/jsx-runtime");
9
8
  let react_dom = require("react-dom");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region src/client/dom/composer-host.ts
11
11
  /**
12
12
  * Host DOM contract for the resident composer.
@@ -95,7 +95,6 @@ body {
95
95
  --dsq-font-size: var(--dsh-content-font-size, 14px);
96
96
  --dsq-font-delta: var(--dsh-content-font-delta, 0px);
97
97
  --dsq-bg-raised: var(--dsw-alias-bg-layer-1, #fff);
98
- --dsq-bg-panel: var(--dsw-alias-bg-layer-2, #fff);
99
98
  --dsq-bg-toast: var(--dsw-alias-bg-layer-3, rgba(0, 0, 0, 0.78));
100
99
  --dsq-line: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
101
100
  --dsq-line-divider: var(--dsw-alias-border-l4, rgba(0, 0, 0, 0.12));
@@ -106,10 +105,9 @@ body {
106
105
  --dsq-label-2: var(--dsw-alias-label-secondary, rgba(0, 0, 0, 0.65));
107
106
  --dsq-label-3: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
108
107
  --dsq-link: var(--dsw-alias-link, rgba(0, 0, 0, 0.65));
109
- --dsq-shadow-pop: var(--dsw-elevation-prominent, 0 3px 8px rgba(0, 0, 0, 0.04), 0 0 20px rgba(0, 0, 0, 0.05));
110
- --dsq-shadow-soft: var(--dsw-elevation-soft, 0 1px 2px rgba(0, 0, 0, 0.03), 0 6px 20px rgba(0, 0, 0, 0.06));
111
108
  --dsq-bubble: var(--dsw-specific-bubble, rgba(0, 0, 0, 0.05));
112
- --dsq-menu: var(--dsw-specific-menu, #fff);
109
+ --dsq-menu: var(--dsw-menu-surface-fill, rgba(248, 249, 250, .58));
110
+ --dsq-menu-backdrop: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
113
111
  --dsq-accent-400: var(--dsw-static-blue-400, #1677ff);
114
112
  --dsq-accent: var(--dsw-static-blue-500, #3b82f6);
115
113
  --dsq-accent-tint: var(--dsw-static-blue-50p, #eaf3ff);
@@ -126,23 +124,150 @@ body {
126
124
  ${atomRules}
127
125
 
128
126
  /* Layer 3 — components. Floating selection toolbar. */
127
+
128
+ /* 入场动画位置(2026-10-19 定稿):scale + Y 位移都放在 .dsq_toolbar 容器上。
129
+ 规范层面 transform 不形成 backdrop root(opacity 才会),磨砂不受影响;用户已在
130
+ 真实客户端验证过容器 scale 没问题。若日后换环境再出问题,回退路径:去掉容器上的
131
+ animation,给 .dsq_toolbarInner 加回 dsq-rise-in(关键帧还在)。 */
132
+ @keyframes dsq-toolbar-in {
133
+ from {
134
+ transform: translate(-50%, -100%) translateY(4px) scale(0.96);
135
+ }
136
+ to {
137
+ transform: translate(-50%, -100%) translateY(0) scale(1);
138
+ }
139
+ }
140
+
141
+ /* Same entrance for the below-anchor variant (only the content top uses it). */
142
+ @keyframes dsq-toolbar-in-below {
143
+ from {
144
+ transform: translate(-50%, 0) translateY(-4px) scale(0.96);
145
+ }
146
+ to {
147
+ transform: translate(-50%, 0) translateY(0) scale(1);
148
+ }
149
+ }
150
+
151
+ /* Comment phase: the hint sits above the popover's top-left corner (explicit
152
+ top-left placement comes from placeCommentPop), so the entrance no longer
153
+ owns the centering shift. */
154
+ @keyframes dsq-toolbar-in-popover-top {
155
+ from {
156
+ transform: translateY(4px) scale(0.96);
157
+ }
158
+ to {
159
+ transform: translateY(0) scale(1);
160
+ }
161
+ }
162
+
163
+ /* 内容层的 rise:悬停面板的列表在用;工具条改用容器 scale 后这条在这里待命。 */
164
+ @keyframes dsq-rise-in {
165
+ from {
166
+ transform: translateY(4px);
167
+ }
168
+ to {
169
+ transform: translateY(0);
170
+ }
171
+ }
172
+
173
+ /* 宿主菜单材质(官方 material 图层同款):填充走 --dsw-menu-surface-fill,模糊走
174
+ --dsw-menu-backdrop-filter(恒为 blur(40px) saturate(150%)),亮暗自动翻。
175
+ 画在 z-index:-1 的独立图层上,内容在它之上:模糊只取身后的页面,不会把
176
+ 面板里的文字一起糊掉。工具条、评论气泡、悬停面板三个浮层共用这一条。
177
+ 注意:macOS 上宿主另有一个 --dsw-specific-menu(覆写成 94% 不透明)给它的
178
+ 菜单用;这里按需求用 surface-fill,所以我们的浮层会比宿主自己的菜单更透。 */
179
+ .dsq_toolbar::before,
180
+ .dsq_commentPop::before,
181
+ .dsq_summaryHoverInner::before {
182
+ content: "";
183
+ position: absolute;
184
+ inset: 0;
185
+ z-index: -1;
186
+ border-radius: inherit;
187
+ background: var(--dsq-menu);
188
+ backdrop-filter: var(--dsq-menu-backdrop);
189
+ -webkit-backdrop-filter: var(--dsq-menu-backdrop);
190
+ pointer-events: none;
191
+ }
192
+
193
+ /* 覆层不描边:elevation-prominent 自带的那圈 0 0 0 .5px(--dsw-elevation-stroke-color,
194
+ 默认 border-l4)对磨砂覆层是多余的——边缘自带材质分界,再勾一道反而抢视觉,覆写成
195
+ 更淡的 border-l1。
196
+ 两个坑,改之前先读:① 这两行必须写在三个浮层自己的选择器上。写在 body 上会随继承
197
+ 影响页面里其它依赖默认值的面板(宿主带 [data-menu-material] 的菜单有自己的覆写,
198
+ 不受影响)。② --dsq-shadow 也必须在这儿声明,不能留在 body:自定义属性是在**声明处**
199
+ 展开的,--dsq-shadow 留在 body 上时 var(--dsw-elevation-prominent) 会在 body 就被
200
+ 替换成 border-l4 那一版并继承下来,浮层上的描边覆写根本够不着;跟着声明挪到这里,
201
+ 整条链才在浮层元素上取值。 */
202
+ .dsq_toolbar,
203
+ .dsq_commentPop,
204
+ .dsq_summaryHoverInner {
205
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
206
+ --dsq-shadow: var(--dsw-elevation-prominent, 0 0 0 .5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.04), 0 0 20px rgba(0, 0, 0, 0.05));
207
+ }
208
+
209
+ /* 浮层挂载与层带(2026-10-20 定,改前先读):
210
+ 工具条/评论泡/状态不再 portal 到 body,而是由 dom/floating-layer.ts 挂进对话
211
+ 真正的滚动容器(桌面抽屉布局是 ST7X_W_scrollBody,普通布局是 .icaHSq_scroll)
212
+ 里的一个 absolute host,坐标用**内容坐标**。
213
+
214
+ 为什么不是 body:header(对话标题那一排)是滚动容器之外的非定位元素,任何
215
+ body 级 positioned + z-index 的浮层都天然压过它(这就是 z-index 降到 0 也仍在
216
+ header 之上的原因);composer 座位是 sticky + z-index 7,只有低于它才不会被
217
+ 输入框穿透。
218
+
219
+ 现在把两件事交回宿主布局:
220
+ - 滚动容器负责裁剪:浮层随内容上移,到容器上边缘就被裁掉,自然滑进 header
221
+ 下面,和正文一致;
222
+ - host 的 z-index 固定 6(见 floating-layer.ts):低于 composer 的 7,高于
223
+ 内容里吸顶元素的 6(同 z 时 host 追加在滚动容器末尾,DOM 次序压过它们)。
224
+ host 内部次序:status 3 > toolbar 2 > commentPop 1。
225
+
226
+ 摘要行与批注卡片在内容普通流里,不参与这套层带;悬停面板仍是 body 级 fixed
227
+ (原因见 AnnotationCardView 里 contain:layout 的说明)。 */
228
+
129
229
  .dsq_toolbar {
130
- position: fixed;
131
- z-index: 10060;
132
- transform: translate(-50%, -100%);
230
+ position: absolute;
231
+ z-index: 2;
232
+ /* The layer host is 0×0, so an auto-width absolute child would shrink to
233
+ min-content (Chinese hint text then wraps one character per line).
234
+ max-content keeps the bubble sized by its row regardless of the host. */
235
+ width: max-content;
236
+ max-width: calc(100vw - 24px);
237
+ transform: translate(-50%, var(--dsq-anchor-y, -100%));
133
238
  gap: 2px;
134
239
  padding: 4px;
135
- /* 无边框:--dsq-shadow-soft 自带 0 0 0 .5px 的描边环,由它勾勒轮廓,
136
- 所以这里不要再画 border,否则描边会和环叠成两条线。 */
240
+ /* 无边框:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的描边环,
241
+ 由它勾勒轮廓,所以这里不要再画 border,否则描边会和环叠成两条线。 */
137
242
  border: 0;
138
243
  border-radius: 12px;
139
- background: var(--dsq-bg-panel);
140
- box-shadow: var(--dsq-shadow-soft);
244
+ /* 底色与模糊都在上面的材质图层里,这里不再画背景 */
245
+ box-shadow: var(--dsq-shadow);
141
246
  pointer-events: auto;
142
- /* Mounts once per selection, after the drag ends: soft rise + fade. */
247
+ /* A/B:scale 入场放在容器上(材质图层归它所有)。关键帧自带 translate(-50%,-100%),
248
+ 所以缩放不会破坏居中。若真实客户端里磨砂在放大期间消失,改回内容层方案。 */
143
249
  animation: dsq-toolbar-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
144
250
  }
145
251
 
252
+ /* Hangs below the selection: no room above at the very top of the transcript. */
253
+ .dsq_toolbar[data-anchor='below'] {
254
+ --dsq-anchor-y: 0%;
255
+ animation-name: dsq-toolbar-in-below;
256
+ }
257
+
258
+ /* Comment phase: placed at an explicit top-left, 6px above the comment popover. */
259
+ .dsq_toolbar[data-anchor='popover-top'] {
260
+ transform: none;
261
+ animation-name: dsq-toolbar-in-popover-top;
262
+ }
263
+
264
+ /* 按钮行:只负责布局;入场动画在容器上(见 .dsq_toolbar 的 A/B 说明)。 */
265
+ .dsq_toolbarInner {
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 2px;
269
+ }
270
+
146
271
  .dsq_button {
147
272
  color: var(--dsq-label);
148
273
  font: inherit;
@@ -182,8 +307,8 @@ ${atomRules}
182
307
  }
183
308
 
184
309
  .dsq_status {
185
- position: fixed;
186
- z-index: 10061;
310
+ position: absolute;
311
+ z-index: 3;
187
312
  transform: translate(-50%, 8px);
188
313
  padding: 4px 10px;
189
314
  border-radius: 999px;
@@ -200,7 +325,7 @@ ${atomRules}
200
325
  position: absolute;
201
326
  top: 8px;
202
327
  left: 8px;
203
- z-index: 40;
328
+ z-index: 12;
204
329
  width: 20%;
205
330
  min-width: 148px;
206
331
  max-width: 220px;
@@ -340,7 +465,7 @@ ${atomRules}
340
465
  /* Qoder-style annotation summary (“N 条批注”) inside the composer. */
341
466
  .dsq_summary {
342
467
  position: relative;
343
- z-index: 10040;
468
+ z-index: 13;
344
469
  gap: 10px;
345
470
  width: 100%;
346
471
  max-width: 100%;
@@ -384,7 +509,7 @@ ${atomRules}
384
509
  flex atoms — the open class below turns display back on. */
385
510
  display: none;
386
511
  position: fixed;
387
- z-index: 10050;
512
+ z-index: 14;
388
513
  width: min(320px, calc(100vw - 16px));
389
514
  flex-direction: column;
390
515
  padding: 12px;
@@ -397,38 +522,44 @@ ${atomRules}
397
522
  }
398
523
 
399
524
  /* Must come after .dsq_summaryHover (same class weight otherwise loses to display:none). */
525
+ /* 容器上不做 opacity 过渡、也不做 scale 入场。opacity 那条是规范实锤:祖先一带上
526
+ opacity<1 就成了 backdrop root,::before 采样不到身后的页面,磨砂在整段淡入里
527
+ 消失、结束才糊上——所以 opacity 过渡删掉,只留 place() 里那次 0→1 瞬时翻转
528
+ (「定位前不可见」的守卫)。scale 一并去掉是保守起见:容器彻底静止,采样区域
529
+ 不会逐帧移动;入场感交给内层列表的 rise(内容不是材质的祖先)。 */
400
530
  .dsq_summaryHover.dsq_summaryHoverOpen {
401
531
  display: flex;
402
- transition: opacity 0.12s ease;
403
- animation: dsq-panel-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
404
- }
405
-
406
- .dsq_summaryHover[data-side='bottom'] {
407
- transform-origin: top center;
408
532
  }
409
533
 
410
- .dsq_summaryHover[data-side='top'] {
411
- transform-origin: bottom center;
534
+ .dsq_summaryHoverInner {
535
+ position: relative;
536
+ gap: 4px;
537
+ padding: 4px;
538
+ border-radius: 12px;
539
+ /* 不画 border:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的
540
+ 描边环,再画一遍会叠成两条线。底色与模糊都在材质图层里。 */
541
+ box-shadow: var(--dsq-shadow);
412
542
  }
413
543
 
414
- .dsq_summaryHoverInner {
544
+ /* 滚动只放在列表上:材质图层是绝对定位的,留在会滚动的容器里会跟着内容跑掉,
545
+ 滚到底部时面板顶部就露空。入场 rise 也在这层——内容不是材质图层的祖先,
546
+ 动它不会切断磨砂的采样源。 */
547
+ .dsq_summaryList {
415
548
  gap: 4px;
416
- max-height: min(420px, calc(100vh - 120px));
417
549
  overflow-y: auto;
418
550
  overscroll-behavior: contain;
419
- padding: 4px;
420
- border-radius: 12px;
421
- border: 0.5px solid var(--dsq-line);
422
- /* Frosted panel: the menu token at partial alpha over a blur. */
423
- background: color-mix(in srgb, var(--dsq-bg-raised) 50%, transparent);
424
- backdrop-filter: blur(14px) saturate(1.4);
425
- -webkit-backdrop-filter: blur(14px) saturate(1.4);
426
- box-shadow: var(--dsq-shadow-pop);
551
+ max-height: min(412px, calc(100vh - 128px));
552
+ animation: dsq-rise-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
427
553
  }
428
554
 
555
+ /* 老浏览器没有 backdrop-filter:图层退成不透明底色,否则半透明直接糊在文字上。 */
429
556
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
430
- .dsq_summaryHoverInner {
431
- background: var(--dsq-menu);
557
+ .dsq_toolbar::before,
558
+ .dsq_commentPop::before,
559
+ .dsq_summaryHoverInner::before {
560
+ background: var(--dsq-bg-raised);
561
+ backdrop-filter: none;
562
+ -webkit-backdrop-filter: none;
432
563
  }
433
564
  }
434
565
 
@@ -599,17 +730,19 @@ ${atomRules}
599
730
 
600
731
  /* Inline comment composer under the floating selection toolbar. */
601
732
  .dsq_commentPop {
602
- position: fixed;
603
- z-index: 10050;
604
- transform: translate(-50%, 4px);
733
+ position: absolute;
734
+ z-index: 1;
735
+ /* Vertical offset comes from placeCommentPop (below the selection); only
736
+ the horizontal centering belongs to the transform. */
737
+ transform: translateX(-50%);
605
738
  width: min(320px, calc(100vw - 24px));
606
739
  gap: 8px;
607
740
  padding: 8px;
608
741
  border-radius: 16px;
609
- background: color-mix(in srgb, var(--dsq-bg-raised) 75%, transparent);
610
- backdrop-filter: blur(10px) saturate(1.4);
611
- -webkit-backdrop-filter: blur(14px) saturate(1.4);
612
- box-shadow: var(--dsq-shadow-pop);
742
+ /* 底色与模糊都在材质图层里 */
743
+ box-shadow: var(--dsq-shadow);
744
+ /* The layer host is pointer-events:none; interactive surfaces opt back in. */
745
+ pointer-events: auto;
613
746
  }
614
747
 
615
748
  .dsq_commentInput {
@@ -779,6 +912,7 @@ ${atomRules}
779
912
  .dsq_annMark {
780
913
  --dsq-mark-color: var(--dsq-accent);
781
914
  --dsq-mark-tint: var(--dsq-accent-tint);
915
+ position: relative;
782
916
  background: var(--dsq-mark-tint);
783
917
  text-decoration: underline;
784
918
  text-decoration-style: dashed;
@@ -792,10 +926,17 @@ ${atomRules}
792
926
 
793
927
  /* Ordinal as a superscript corner badge at the END of the marked text, so it
794
928
  never sits between the reader and the sentence. Only the last fragment of a
795
- multi-fragment mark carries the index, so the number appears exactly once. */
929
+ multi-fragment mark carries the index, so the number appears exactly once.
930
+ Absolute so the badge adds no inline advance width: annotating never reflows
931
+ the paragraph or moves following words. Bottom (not top) anchors to the last
932
+ fragment: for a mark spanning several lines the containing block is the union
933
+ of the first and last fragments, so its bottom edge is the last line. */
796
934
  .dsq_annMark[data-dsq-ann-index]::after {
797
935
  content: attr(data-dsq-ann-index);
798
- display: inline-grid;
936
+ position: absolute;
937
+ bottom: 8px;
938
+ left: 100%;
939
+ display: grid;
799
940
  place-items: center;
800
941
  min-width: 15px;
801
942
  height: 15px;
@@ -807,7 +948,7 @@ ${atomRules}
807
948
  font-size: 10px;
808
949
  font-weight: 600;
809
950
  line-height: 1;
810
- vertical-align: super;
951
+ pointer-events: none;
811
952
  }
812
953
 
813
954
  .dsq_annMark:hover {
@@ -821,26 +962,6 @@ ${atomRules}
821
962
  /* Layer 4 — host surface: keyframes, theme flips, and the two rules that reach
822
963
  into product-owned DOM. */
823
964
 
824
- @keyframes dsq-toolbar-in {
825
- from {
826
- opacity: 0;
827
- transform: translate(-50%, -100%) translateY(4px);
828
- }
829
- to {
830
- opacity: 1;
831
- transform: translate(-50%, -100%);
832
- }
833
- }
834
-
835
- @keyframes dsq-panel-in {
836
- from {
837
- transform: scale(0.96);
838
- }
839
- to {
840
- transform: scale(1);
841
- }
842
- }
843
-
844
965
  @keyframes dsq-ann-flash {
845
966
  0% {
846
967
  background: var(--dsq-mark-color);
@@ -882,6 +1003,7 @@ function ensureToolbarStyles() {
882
1003
  }
883
1004
  const styles = {
884
1005
  toolbar: "dsq_toolbar",
1006
+ toolbarInner: "dsq_toolbarInner",
885
1007
  button: "dsq_button",
886
1008
  buttonAddComment: "dsq_buttonAddComment",
887
1009
  divider: "dsq_divider",
@@ -914,6 +1036,7 @@ const styles = {
914
1036
  annEditCancel: "dsq_annEditCancel",
915
1037
  annEditSave: "dsq_annEditSave",
916
1038
  summaryHoverInner: "dsq_summaryHoverInner",
1039
+ summaryList: "dsq_summaryList",
917
1040
  summaryItemButton: "dsq_summaryItemButton",
918
1041
  summaryHoverOpen: "dsq_summaryHoverOpen",
919
1042
  summaryItemActions: "dsq_summaryItemActions",
@@ -1111,6 +1234,12 @@ function createMark(job, withIndex) {
1111
1234
  * selections that cross inline elements or paragraph boundaries never reorder
1112
1235
  * the DOM; fragments of one annotation share the mark id, and the ordinal
1113
1236
  * badge rides on the final fragment (end of the annotation).
1237
+ *
1238
+ * `surroundContents` is load-bearing: `extractContents()` leaves the text
1239
+ * before **and after** the range in the same node, so inserting the mark at
1240
+ * `node.nextSibling` (the old implementation) appended every partial selection
1241
+ * to the end of its parent — a selection starting at offset 0, such as the
1242
+ * sentence after a colon, visibly jumped to the paragraph's end.
1114
1243
  */
1115
1244
  function wrapRange(item, job, from, to) {
1116
1245
  const located = cleanTextRange(item, from, to);
@@ -1120,15 +1249,12 @@ function wrapRange(item, job, from, to) {
1120
1249
  let wrapped = false;
1121
1250
  pieces.forEach((piece, i) => {
1122
1251
  const node = piece.entry.node;
1123
- const parent = node.parentElement;
1124
- if (!parent) return;
1252
+ if (!node.parentElement) return;
1125
1253
  const range = document.createRange();
1126
1254
  range.setStart(node, piece.start - piece.entry.start);
1127
1255
  range.setEnd(node, piece.end - piece.entry.start);
1128
1256
  const mark = createMark(job, i === pieces.length - 1);
1129
- mark.append(range.extractContents());
1130
- parent.insertBefore(mark, node.nextSibling);
1131
- if (node.nodeValue === "") node.remove();
1257
+ range.surroundContents(mark);
1132
1258
  wrapped = true;
1133
1259
  });
1134
1260
  return wrapped;
@@ -1663,20 +1789,24 @@ const GAP = 8;
1663
1789
  const MARGIN = 8;
1664
1790
  /**
1665
1791
  * Place `panel` near `anchor`, flipping vertical side when the preferred side
1666
- * lacks room. Keeps the panel inside the viewport on the horizontal axis.
1792
+ * lacks room. Keeps the panel inside `bounds` (the viewport by default).
1667
1793
  */
1668
- function placePopover(anchor, panel, preferred = "bottom", gap = GAP) {
1669
- const vw = window.innerWidth;
1670
- const vh = window.innerHeight;
1671
- const spaceBelow = vh - anchor.bottom;
1672
- const spaceAbove = anchor.top;
1794
+ function placePopover(anchor, panel, preferred = "bottom", gap = GAP, bounds) {
1795
+ const box = bounds ?? {
1796
+ top: 0,
1797
+ left: 0,
1798
+ right: window.innerWidth,
1799
+ bottom: window.innerHeight
1800
+ };
1801
+ const spaceBelow = box.bottom - anchor.bottom;
1802
+ const spaceAbove = anchor.top - box.top;
1673
1803
  let side = preferred;
1674
1804
  const need = panel.height + gap;
1675
1805
  if (preferred === "bottom" && spaceBelow < need && spaceAbove > spaceBelow) side = "top";
1676
1806
  else if (preferred === "top" && spaceAbove < need && spaceBelow > spaceAbove) side = "bottom";
1677
1807
  return {
1678
- top: side === "bottom" ? Math.min(anchor.bottom + gap, vh - panel.height - MARGIN) : Math.max(anchor.top - panel.height - gap, MARGIN),
1679
- left: Math.min(Math.max(anchor.left, MARGIN), Math.max(MARGIN, vw - panel.width - MARGIN)),
1808
+ top: side === "bottom" ? Math.min(anchor.bottom + gap, box.bottom - panel.height - MARGIN) : Math.max(anchor.top - panel.height - gap, box.top + MARGIN),
1809
+ left: Math.min(Math.max(anchor.left, box.left + MARGIN), Math.max(box.left + MARGIN, box.right - panel.width - MARGIN)),
1680
1810
  side
1681
1811
  };
1682
1812
  }
@@ -1694,78 +1824,32 @@ const SVG_ATTRS = {
1694
1824
  focusable: false
1695
1825
  };
1696
1826
  const SHAPES = {
1697
- copy: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("rect", {
1698
- key: "r",
1827
+ copy: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1699
1828
  width: 14,
1700
1829
  height: 14,
1701
1830
  x: 8,
1702
1831
  y: 8,
1703
1832
  rx: 2,
1704
1833
  ry: 2
1705
- }), (0, react_jsx_runtime.jsx)("path", {
1706
- key: "p",
1707
- d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
1708
- })] }),
1709
- pencil: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("path", {
1710
- key: "p1",
1711
- d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
1712
- }), (0, react_jsx_runtime.jsx)("path", {
1713
- key: "p2",
1714
- d: "m15 5 4 4"
1715
- })] }),
1716
- "trash-2": (0, react_jsx_runtime.jsxs)("g", { children: [
1717
- (0, react_jsx_runtime.jsx)("path", {
1718
- key: "p1",
1719
- d: "M10 11v6"
1720
- }),
1721
- (0, react_jsx_runtime.jsx)("path", {
1722
- key: "p2",
1723
- d: "M14 11v6"
1724
- }),
1725
- (0, react_jsx_runtime.jsx)("path", {
1726
- key: "p3",
1727
- d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
1728
- }),
1729
- (0, react_jsx_runtime.jsx)("path", {
1730
- key: "p4",
1731
- d: "M3 6h18"
1732
- }),
1733
- (0, react_jsx_runtime.jsx)("path", {
1734
- key: "p5",
1735
- d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
1736
- })
1834
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })] }),
1835
+ pencil: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m15 5 4 4" })] }),
1836
+ "trash-2": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
1837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 11v6" }),
1838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M14 11v6" }),
1839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }),
1840
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
1737
1842
  ] }),
1738
- x: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("path", {
1739
- key: "p1",
1740
- d: "M18 6 6 18"
1741
- }), (0, react_jsx_runtime.jsx)("path", {
1742
- key: "p2",
1743
- d: "m6 6 12 12"
1744
- })] }),
1745
- plus: (0, react_jsx_runtime.jsxs)("g", { children: [(0, react_jsx_runtime.jsx)("path", {
1746
- key: "p1",
1747
- d: "M5 12h14"
1748
- }), (0, react_jsx_runtime.jsx)("path", {
1749
- key: "p2",
1750
- d: "M12 5v14"
1751
- })] }),
1752
- "external-link": (0, react_jsx_runtime.jsxs)("g", { children: [
1753
- (0, react_jsx_runtime.jsx)("path", {
1754
- key: "p1",
1755
- d: "M15 3h6v6"
1756
- }),
1757
- (0, react_jsx_runtime.jsx)("path", {
1758
- key: "p2",
1759
- d: "M10 14 21 3"
1760
- }),
1761
- (0, react_jsx_runtime.jsx)("path", {
1762
- key: "p3",
1763
- d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
1764
- })
1843
+ x: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 6 6 18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 6 12 12" })] }),
1844
+ plus: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12h14" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 5v14" })] }),
1845
+ "external-link": /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
1846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M15 3h6v6" }),
1847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 14 21 3" }),
1848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
1765
1849
  ] })
1766
1850
  };
1767
1851
  function LucideIcon({ name, size = 15 }) {
1768
- return (0, react_jsx_runtime.jsx)("svg", {
1852
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1769
1853
  ...SVG_ATTRS,
1770
1854
  width: size,
1771
1855
  height: size,
@@ -1977,23 +2061,23 @@ function AnnotationCardView({ title, text, comment, variant, index, onRemove, on
1977
2061
  useLocale();
1978
2062
  const composer = variant === "composer";
1979
2063
  const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
1980
- return (0, react_jsx_runtime.jsxs)("div", {
2064
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1981
2065
  className: cx(A.box, A.row, styles.card, !composer && styles.tCard),
1982
2066
  role: "group",
1983
2067
  "aria-label": index ? t("card.ariaIndexed", { index }) : t("card.ariaPlain"),
1984
2068
  children: [
1985
- (0, react_jsx_runtime.jsx)("div", {
2069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1986
2070
  className: cx(A.keep, A.gridMid, styles.cardIcon, !composer && styles.tCardIcon),
1987
- "aria-hidden": true,
2071
+ "aria-hidden": "true",
1988
2072
  children: index ? String(index) : "AI"
1989
2073
  }),
1990
- (0, react_jsx_runtime.jsxs)("div", {
2074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1991
2075
  className: cx(A.grow, A.col, styles.cardBody),
1992
- children: [(0, react_jsx_runtime.jsx)("div", {
2076
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1993
2077
  className: cx(A.ell, styles.cardTitle),
1994
2078
  title: text,
1995
2079
  children: title
1996
- }), (0, react_jsx_runtime.jsx)("div", {
2080
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1997
2081
  className: cx(A.ell, styles.cardSubtitle),
1998
2082
  ...comment ? {
1999
2083
  title: comment,
@@ -2002,14 +2086,14 @@ function AnnotationCardView({ title, text, comment, variant, index, onRemove, on
2002
2086
  children: subtitle
2003
2087
  })]
2004
2088
  }),
2005
- composer && onEditComment ? (0, react_jsx_runtime.jsx)("button", {
2089
+ composer && onEditComment ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2006
2090
  type: "button",
2007
2091
  className: cx(A.plain, A.gridMid, styles.cardAction),
2008
2092
  "aria-label": t("card.editComment"),
2009
2093
  onClick: onEditComment,
2010
2094
  children: "✎"
2011
2095
  }) : null,
2012
- composer && onRemove ? (0, react_jsx_runtime.jsx)("button", {
2096
+ composer && onRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2013
2097
  type: "button",
2014
2098
  className: cx(A.plain, A.gridMid, styles.cardClose),
2015
2099
  "aria-label": t("card.remove"),
@@ -2052,7 +2136,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2052
2136
  const { top, left, side } = placePopover(root.getBoundingClientRect(), {
2053
2137
  width: pop.offsetWidth || 300,
2054
2138
  height: pop.offsetHeight || 220
2055
- }, "bottom", 0);
2139
+ }, "top", 0);
2056
2140
  pop.style.top = `${top}px`;
2057
2141
  pop.style.left = `${left}px`;
2058
2142
  pop.style.paddingTop = side === "bottom" ? "12px" : "12px";
@@ -2187,122 +2271,125 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2187
2271
  editDraft,
2188
2272
  onItemEdit
2189
2273
  ]);
2190
- const panel = open ? (0, react_jsx_runtime.jsx)("div", {
2274
+ const panel = open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2191
2275
  ref: popRef,
2192
2276
  className: `${styles.summaryHover} ${styles.summaryHoverOpen}`,
2193
2277
  role: "dialog",
2194
2278
  "aria-label": t("summary.dialogAria", { count }),
2195
2279
  onPointerEnter: openPop,
2196
2280
  onPointerLeave,
2197
- children: (0, react_jsx_runtime.jsx)("div", {
2281
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2198
2282
  className: cx(A.col, styles.summaryHoverInner),
2199
- children: items.map((item, i) => {
2200
- const isEditing = editing === i;
2201
- return (0, react_jsx_runtime.jsxs)("div", {
2202
- className: styles.summaryItem,
2203
- children: [(0, react_jsx_runtime.jsxs)("div", {
2204
- className: cx(A.row, styles.summaryItemHead),
2205
- children: [
2206
- (0, react_jsx_runtime.jsx)("div", {
2207
- className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
2208
- "aria-hidden": true,
2209
- children: String(i + 1)
2210
- }),
2211
- (0, react_jsx_runtime.jsx)("div", {
2212
- className: styles.summaryItemLabel,
2213
- children: t("common.selectedText")
2214
- }),
2215
- !isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
2216
- className: cx(A.row, styles.summaryItemActions),
2217
- children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
2218
- type: "button",
2219
- className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2220
- "aria-label": t("summary.edit", { index: i + 1 }),
2221
- onClick: () => startEdit(i),
2222
- children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
2223
- }) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
2224
- type: "button",
2225
- className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2226
- "aria-label": t("summary.remove", { index: i + 1 }),
2227
- onClick: () => onItemRemove(i, item),
2228
- children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
2229
- }) : null]
2230
- }) : null
2231
- ]
2232
- }), isEditing ? (0, react_jsx_runtime.jsxs)("div", {
2233
- className: styles.summaryItemMain,
2234
- children: [
2235
- (0, react_jsx_runtime.jsx)("div", {
2236
- className: styles.annEditText,
2237
- children: item.text
2238
- }),
2239
- (0, react_jsx_runtime.jsx)("div", {
2240
- className: styles.summaryItemLabel,
2241
- children: t("summary.userComment")
2242
- }),
2243
- (0, react_jsx_runtime.jsx)("textarea", {
2244
- ref: editInputRef,
2245
- className: cx(A.box, styles.annEditInput),
2246
- placeholder: t("common.commentPlaceholder"),
2247
- value: editDraft,
2248
- onChange: (e) => setEditDraft(e.target.value),
2249
- onKeyDown: (e) => {
2250
- if (e.key === "Escape") {
2251
- e.preventDefault();
2252
- setEditing(null);
2253
- return;
2254
- }
2255
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2256
- e.preventDefault();
2257
- commitEdit();
2258
- }
2259
- }
2260
- }),
2261
- (0, react_jsx_runtime.jsxs)("div", {
2262
- className: cx(A.row, styles.annEditActions),
2263
- children: [(0, react_jsx_runtime.jsx)("button", {
2264
- type: "button",
2265
- className: cx(A.plain, styles.annEditCancel),
2266
- onClick: () => setEditing(null),
2267
- children: t("common.cancel")
2268
- }), (0, react_jsx_runtime.jsx)("button", {
2269
- type: "button",
2270
- className: styles.annEditSave,
2271
- onClick: commitEdit,
2272
- children: t("common.save")
2273
- })]
2274
- })
2275
- ]
2276
- }) : (0, react_jsx_runtime.jsx)("div", {
2277
- className: styles.summaryItemMain,
2278
- children: (0, react_jsx_runtime.jsxs)("button", {
2279
- type: "button",
2280
- className: cx(A.plain, styles.summaryItemButton),
2281
- onClick: () => {
2282
- onItemSelect?.(i, item);
2283
- closePop();
2284
- },
2283
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2284
+ className: cx(A.col, styles.summaryList),
2285
+ children: items.map((item, i) => {
2286
+ const isEditing = editing === i;
2287
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2288
+ className: styles.summaryItem,
2289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2290
+ className: cx(A.row, styles.summaryItemHead),
2291
+ children: [
2292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2293
+ className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
2294
+ "aria-hidden": "true",
2295
+ children: String(i + 1)
2296
+ }),
2297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2298
+ className: styles.summaryItemLabel,
2299
+ children: t("common.selectedText")
2300
+ }),
2301
+ !isEditing && (onItemRemove || onItemEdit) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2302
+ className: cx(A.row, styles.summaryItemActions),
2303
+ children: [onItemEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2304
+ type: "button",
2305
+ className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2306
+ "aria-label": t("summary.edit", { index: i + 1 }),
2307
+ onClick: () => startEdit(i),
2308
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
2309
+ }) : null, onItemRemove ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2310
+ type: "button",
2311
+ className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2312
+ "aria-label": t("summary.remove", { index: i + 1 }),
2313
+ onClick: () => onItemRemove(i, item),
2314
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
2315
+ }) : null]
2316
+ }) : null
2317
+ ]
2318
+ }), isEditing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2319
+ className: styles.summaryItemMain,
2285
2320
  children: [
2286
- (0, react_jsx_runtime.jsx)("div", {
2287
- className: styles.summaryItemText,
2288
- children: item.title
2321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2322
+ className: styles.annEditText,
2323
+ children: item.text
2289
2324
  }),
2290
- (0, react_jsx_runtime.jsx)("div", {
2325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2291
2326
  className: styles.summaryItemLabel,
2292
2327
  children: t("summary.userComment")
2293
2328
  }),
2294
- (0, react_jsx_runtime.jsx)("div", {
2295
- className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
2296
- children: item.comment || t("summary.noComment")
2329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2330
+ ref: editInputRef,
2331
+ className: cx(A.box, styles.annEditInput),
2332
+ placeholder: t("common.commentPlaceholder"),
2333
+ value: editDraft,
2334
+ onChange: (e) => setEditDraft(e.target.value),
2335
+ onKeyDown: (e) => {
2336
+ if (e.key === "Escape") {
2337
+ e.preventDefault();
2338
+ setEditing(null);
2339
+ return;
2340
+ }
2341
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2342
+ e.preventDefault();
2343
+ commitEdit();
2344
+ }
2345
+ }
2346
+ }),
2347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2348
+ className: cx(A.row, styles.annEditActions),
2349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2350
+ type: "button",
2351
+ className: cx(A.plain, styles.annEditCancel),
2352
+ onClick: () => setEditing(null),
2353
+ children: t("common.cancel")
2354
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2355
+ type: "button",
2356
+ className: styles.annEditSave,
2357
+ onClick: commitEdit,
2358
+ children: t("common.save")
2359
+ })]
2297
2360
  })
2298
2361
  ]
2299
- })
2300
- })]
2301
- }, item.id ?? `${i}`);
2362
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2363
+ className: styles.summaryItemMain,
2364
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2365
+ type: "button",
2366
+ className: cx(A.plain, styles.summaryItemButton),
2367
+ onClick: () => {
2368
+ onItemSelect?.(i, item);
2369
+ closePop();
2370
+ },
2371
+ children: [
2372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2373
+ className: styles.summaryItemText,
2374
+ children: item.title
2375
+ }),
2376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2377
+ className: styles.summaryItemLabel,
2378
+ children: t("summary.userComment")
2379
+ }),
2380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2381
+ className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
2382
+ children: item.comment || t("summary.noComment")
2383
+ })
2384
+ ]
2385
+ })
2386
+ })]
2387
+ }, item.id ?? `${i}`);
2388
+ })
2302
2389
  })
2303
2390
  })
2304
2391
  }) : null;
2305
- return (0, react_jsx_runtime.jsxs)("div", {
2392
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2306
2393
  ref: rootRef,
2307
2394
  className: cx(A.row, styles.summary),
2308
2395
  tabIndex: 0,
@@ -2315,27 +2402,27 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2315
2402
  onBlur,
2316
2403
  onKeyDown,
2317
2404
  children: [
2318
- (0, react_jsx_runtime.jsx)("div", {
2405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2319
2406
  className: cx(A.keep, A.gridMid, styles.summaryIcon),
2320
- "aria-hidden": true,
2407
+ "aria-hidden": "true",
2321
2408
  children: String(count)
2322
2409
  }),
2323
- (0, react_jsx_runtime.jsxs)("div", {
2410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2324
2411
  className: cx(A.grow, styles.summaryBody),
2325
- children: [(0, react_jsx_runtime.jsx)("div", {
2412
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2326
2413
  className: styles.summaryTitle,
2327
2414
  children: t("summary.title", { count })
2328
- }), (0, react_jsx_runtime.jsx)("div", {
2415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2329
2416
  className: styles.summaryHint,
2330
2417
  children: t("summary.hint")
2331
2418
  })]
2332
2419
  }),
2333
- onRemoveAll ? (0, react_jsx_runtime.jsx)("button", {
2420
+ onRemoveAll ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2334
2421
  type: "button",
2335
2422
  className: cx(A.plain, A.gridMid, styles.cardClose),
2336
2423
  "aria-label": t("summary.removeAll"),
2337
2424
  onClick: onRemoveAll,
2338
- children: (0, react_jsx_runtime.jsx)(LucideIcon, {
2425
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
2339
2426
  name: "x",
2340
2427
  size: 13
2341
2428
  })
@@ -2550,10 +2637,10 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
2550
2637
  sync();
2551
2638
  }, [sync]);
2552
2639
  if (!sessionId || items.length === 0) return null;
2553
- return (0, react_jsx_runtime.jsx)("div", {
2640
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2554
2641
  ref: stackRef,
2555
2642
  className: cx(A.col, styles.cardStack),
2556
- children: (0, react_jsx_runtime.jsx)(AnnotationSummary, {
2643
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationSummary, {
2557
2644
  count: items.length,
2558
2645
  items: items.map((item) => ({
2559
2646
  id: item.id,
@@ -2571,9 +2658,116 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
2571
2658
  /** Alias kept for overlay registration. */
2572
2659
  const QuoteCard = AnnotationPanel;
2573
2660
  //#endregion
2661
+ //#region src/client/dom/floating-layer.ts
2662
+ /**
2663
+ * Mount point for the selection UI: a zero-size host inside the transcript's
2664
+ * real scroll container.
2665
+ *
2666
+ * The host is `position: absolute` in **content coordinates**, so floating UI
2667
+ * scrolls with the selected text and the scroller clips it at its own edges.
2668
+ * That is what keeps the toolbar out of the app header and the composer:
2669
+ * clipping happens in the scroller's stacking context instead of competing
2670
+ * with the header (which is not positioned and therefore loses to any
2671
+ * body-level positioned portal, whatever its z-index).
2672
+ *
2673
+ * Two host layouts exist in the product:
2674
+ * - desktop / overlay composer: `ST7X_W_scrollBody` scrolls; the chat frame is
2675
+ * content-sized and the chat's own `.icaHSq_scroll` is `overflow: visible`;
2676
+ * - regular layout: `.icaHSq_scroll` is the scroller.
2677
+ * Walking up from the selected flow item finds whichever one is live.
2678
+ */
2679
+ const HOST_ATTR = "data-dsq-layer";
2680
+ /** Below the composer seat (sticky, z-index 7); above in-content stickies (6). */
2681
+ const HOST_Z = 6;
2682
+ const layers = /* @__PURE__ */ new WeakMap();
2683
+ /** flow item → layer, so scrolling does not re-walk the ancestor chain. */
2684
+ const itemLayers = /* @__PURE__ */ new WeakMap();
2685
+ let fallback = null;
2686
+ function contentCoords(scroller, left, top) {
2687
+ const rect = scroller.getBoundingClientRect();
2688
+ return {
2689
+ left: left - rect.left - scroller.clientLeft + scroller.scrollLeft,
2690
+ top: top - rect.top - scroller.clientTop + scroller.scrollTop
2691
+ };
2692
+ }
2693
+ function layerFor(scroller) {
2694
+ const cached = layers.get(scroller);
2695
+ if (cached?.host.isConnected) return cached;
2696
+ if (getComputedStyle(scroller).position === "static") scroller.style.position = "relative";
2697
+ let host = scroller.querySelector(`:scope > [${HOST_ATTR}]`);
2698
+ if (!host) {
2699
+ host = document.createElement("div");
2700
+ host.setAttribute(HOST_ATTR, "");
2701
+ host.style.cssText = `position:absolute;left:0;top:0;width:0;height:0;z-index:${HOST_Z};pointer-events:none;`;
2702
+ scroller.appendChild(host);
2703
+ }
2704
+ const layer = {
2705
+ host,
2706
+ scroller,
2707
+ bounds: () => {
2708
+ const rect = scroller.getBoundingClientRect();
2709
+ return new DOMRect(rect.left + scroller.clientLeft, rect.top + scroller.clientTop, scroller.clientWidth, scroller.clientHeight);
2710
+ },
2711
+ toContent: (left, top) => contentCoords(scroller, left, top),
2712
+ fromContent: (left, top) => {
2713
+ const rect = scroller.getBoundingClientRect();
2714
+ return {
2715
+ left: left + rect.left + scroller.clientLeft - scroller.scrollLeft,
2716
+ top: top + rect.top + scroller.clientTop - scroller.scrollTop
2717
+ };
2718
+ }
2719
+ };
2720
+ layers.set(scroller, layer);
2721
+ return layer;
2722
+ }
2723
+ /** Fallback for hosts whose transcript has no scrollable ancestor. */
2724
+ function fallbackLayer() {
2725
+ if (fallback?.host.isConnected) return fallback;
2726
+ const host = document.createElement("div");
2727
+ host.setAttribute(HOST_ATTR, "");
2728
+ host.style.cssText = `position:absolute;left:0;top:0;width:0;height:0;z-index:${HOST_Z};pointer-events:none;`;
2729
+ document.body.appendChild(host);
2730
+ fallback = {
2731
+ host,
2732
+ scroller: document.body,
2733
+ bounds: () => new DOMRect(0, 0, window.innerWidth, window.innerHeight),
2734
+ toContent: (left, top) => ({
2735
+ left,
2736
+ top
2737
+ }),
2738
+ fromContent: (left, top) => ({
2739
+ left,
2740
+ top
2741
+ })
2742
+ };
2743
+ return fallback;
2744
+ }
2745
+ /** Layer for a node inside the transcript (falls back to a body host). */
2746
+ function floatingLayerFor(node) {
2747
+ const item = flowItemOf(node);
2748
+ const cached = item ? itemLayers.get(item) : void 0;
2749
+ if (cached?.host.isConnected) return cached;
2750
+ let el = item;
2751
+ while (el && el !== document.body) {
2752
+ const { overflowY } = getComputedStyle(el);
2753
+ if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
2754
+ const layer = layerFor(el);
2755
+ if (item) itemLayers.set(item, layer);
2756
+ return layer;
2757
+ }
2758
+ el = el.parentElement;
2759
+ }
2760
+ const layer = fallbackLayer();
2761
+ if (item) itemLayers.set(item, layer);
2762
+ return layer;
2763
+ }
2764
+ //#endregion
2574
2765
  //#region src/client/dom/selection.ts
2575
2766
  /** Selection snapshot used by the floating toolbar. */
2576
2767
  const TOOLBAR_OFFSET = 12;
2768
+ const BELOW_GAP = 8;
2769
+ /** Estimated toolbar height; only used to decide above vs below at content top. */
2770
+ const TOOLBAR_HEIGHT = 40;
2577
2771
  const VIEWPORT_MARGIN = 12;
2578
2772
  const LINE_TOP_TOLERANCE = 3;
2579
2773
  /**
@@ -2614,12 +2808,24 @@ function getRangeFirstLineRect(range) {
2614
2808
  const firstTop = Math.min(...rects.map((rect) => rect.top));
2615
2809
  return mergeRects(rects.filter((rect) => Math.abs(rect.top - firstTop) <= LINE_TOP_TOLERANCE));
2616
2810
  }
2617
- function toolbarPosition(rect) {
2811
+ /** Keep the centered toolbar inside the visible horizontal band. */
2812
+ function anchorX(rect, bounds) {
2618
2813
  const center = rect.left + rect.width / 2;
2619
- return {
2620
- left: Math.min(Math.max(center, VIEWPORT_MARGIN), window.innerWidth - VIEWPORT_MARGIN),
2621
- top: Math.max(rect.top - TOOLBAR_OFFSET, VIEWPORT_MARGIN)
2622
- };
2814
+ const min = bounds.left + VIEWPORT_MARGIN;
2815
+ const max = Math.max(min, bounds.right - VIEWPORT_MARGIN);
2816
+ return Math.min(Math.max(center, min), max);
2817
+ }
2818
+ /**
2819
+ * True when the selection's first line still touches the viewport.
2820
+ *
2821
+ * The toolbar anchors to that line, so once it has scrolled fully out of
2822
+ * view there is nothing to anchor to and the snapshot reads as "gone" — the
2823
+ * toolbar hides and comes back when the user scrolls to the selection again
2824
+ * (scroll re-reads the selection). A partially visible line still counts:
2825
+ * `toolbarPosition` parks the toolbar at the viewport edge in that case.
2826
+ */
2827
+ function isFirstLineOnScreen(rect) {
2828
+ return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
2623
2829
  }
2624
2830
  /**
2625
2831
  * Read the live selection when it is a non-empty range inside a conversation
@@ -2637,13 +2843,24 @@ function readSelectionSnapshot(toolbarRoot) {
2637
2843
  if (!text) return null;
2638
2844
  const rect = getRangeFirstLineRect(selection.getRangeAt(0));
2639
2845
  if (!rect) return null;
2640
- const { left, top } = toolbarPosition(rect);
2641
- const source = rangeOffsetsInFlowItem(selection.getRangeAt(0));
2846
+ if (!isFirstLineOnScreen(rect)) return null;
2847
+ const layer = floatingLayerFor(anchorNode);
2848
+ const x = anchorX(rect, layer.bounds());
2849
+ const above = layer.toContent(x, rect.top - TOOLBAR_OFFSET);
2850
+ const below = above.top < TOOLBAR_HEIGHT;
2851
+ const anchor = below ? layer.toContent(x, rect.bottom + BELOW_GAP) : above;
2852
+ const range = selection.getRangeAt(0);
2853
+ const union = range.getBoundingClientRect();
2854
+ const selectionBottom = layer.toContent(0, union.height > 0 ? union.bottom : rect.bottom).top;
2855
+ const source = rangeOffsetsInFlowItem(range);
2642
2856
  const anchorHref = (anchorNode instanceof Element ? anchorNode : anchorNode.parentElement)?.closest("a[href]")?.getAttribute("href") ?? void 0;
2643
2857
  return {
2644
2858
  text,
2645
- left,
2646
- top,
2859
+ left: anchor.left,
2860
+ top: anchor.top,
2861
+ below,
2862
+ selectionBottom,
2863
+ layer,
2647
2864
  ...source ? { source } : {},
2648
2865
  ...anchorHref ? { anchorHref } : {}
2649
2866
  };
@@ -2720,35 +2937,72 @@ function SelectionToolbar({ sessionId }) {
2720
2937
  const [status, setStatus] = (0, react.useState)(null);
2721
2938
  const activeRef = (0, react.useRef)(null);
2722
2939
  activeRef.current = active;
2940
+ /** Annotation saved when the comment step opened; the box only edits it. */
2941
+ const pendingIdRef = (0, react.useRef)(null);
2723
2942
  (0, react.useEffect)(() => {
2724
2943
  ensureToolbarStyles();
2725
2944
  if (sessionId) bindAnnotationSession(sessionId);
2726
2945
  }, [sessionId]);
2727
2946
  const hide = (0, react.useCallback)(() => {
2947
+ pendingIdRef.current = null;
2728
2948
  setActive(null);
2729
2949
  setPhase("idle");
2730
2950
  setComment("");
2731
2951
  setStatus(null);
2732
2952
  }, []);
2953
+ /**
2954
+ * Keep what was typed when the comment popover goes away without an explicit
2955
+ * cancel. The annotation itself was saved when the step opened, so dropping
2956
+ * the text would also lose the only optional part of the interaction.
2957
+ */
2958
+ const flushComment = (0, react.useCallback)(() => {
2959
+ const id = pendingIdRef.current;
2960
+ pendingIdRef.current = null;
2961
+ if (!id) return;
2962
+ const typed = commentRef.current?.value ?? "";
2963
+ if (typed.trim()) updateAnnotationComment(void 0, id, typed);
2964
+ }, []);
2965
+ /** Save the current selection without a comment; returns the store id. */
2966
+ const saveSelection = (0, react.useCallback)(() => {
2967
+ const snapshot = activeRef.current;
2968
+ if (!snapshot) return null;
2969
+ return saveAnnotation(void 0, {
2970
+ text: snapshot.text,
2971
+ sources: snapshot.source ? [{
2972
+ text: snapshot.text,
2973
+ messageId: snapshot.source.messageId,
2974
+ messageKind: snapshot.source.messageKind,
2975
+ startOffset: snapshot.source.startOffset,
2976
+ endOffset: snapshot.source.endOffset
2977
+ }] : void 0
2978
+ }).id;
2979
+ }, []);
2733
2980
  const refresh = (0, react.useCallback)(() => {
2734
2981
  const next = readSelectionSnapshot(toolbarRef.current);
2735
2982
  if (!next) {
2736
2983
  if (phaseRef.current === "idle") setActive(null);
2737
2984
  return;
2738
2985
  }
2739
- setActive((prev) => {
2740
- if (phaseRef.current === "comment" && prev && prev.text !== next.text) {
2741
- setPhase("idle");
2742
- setComment("");
2743
- }
2744
- if (prev && prev.text === next.text && prev.left === next.left && prev.top === next.top) return prev;
2745
- return next;
2746
- });
2747
- }, []);
2986
+ const current = activeRef.current;
2987
+ if (phaseRef.current === "comment" && current && current.text !== next.text) {
2988
+ flushComment();
2989
+ setPhase("idle");
2990
+ setComment("");
2991
+ }
2992
+ setActive((prev) => prev && prev.text === next.text && prev.left === next.left && prev.top === next.top && prev.below === next.below && prev.layer === next.layer ? prev : next);
2993
+ }, [flushComment]);
2748
2994
  const phaseRef = (0, react.useRef)(phase);
2749
2995
  phaseRef.current = phase;
2750
2996
  (0, react.useEffect)(() => {
2751
- const schedule = () => window.setTimeout(refresh, 0);
2997
+ let scheduled = false;
2998
+ const schedule = () => {
2999
+ if (scheduled) return;
3000
+ scheduled = true;
3001
+ window.setTimeout(() => {
3002
+ scheduled = false;
3003
+ refresh();
3004
+ }, 0);
3005
+ };
2752
3006
  let dragging = false;
2753
3007
  const onPointerDown = (event) => {
2754
3008
  const target = event.target;
@@ -2758,6 +3012,7 @@ function SelectionToolbar({ sessionId }) {
2758
3012
  if (inToolbar || inComment) return;
2759
3013
  dragging = true;
2760
3014
  if (phaseRef.current === "comment") {
3015
+ flushComment();
2761
3016
  hide();
2762
3017
  return;
2763
3018
  }
@@ -2779,7 +3034,7 @@ function SelectionToolbar({ sessionId }) {
2779
3034
  const onScroll = (event) => {
2780
3035
  const target = event.target;
2781
3036
  if (target instanceof Node && (toolbarRef.current?.contains(target) === true || commentWrapRef.current?.contains(target) === true)) return;
2782
- hide();
3037
+ schedule();
2783
3038
  };
2784
3039
  document.addEventListener("pointerdown", onPointerDown, true);
2785
3040
  document.addEventListener("pointerup", onPointerUp, true);
@@ -2795,12 +3050,27 @@ function SelectionToolbar({ sessionId }) {
2795
3050
  window.removeEventListener("resize", schedule);
2796
3051
  document.removeEventListener("scroll", onScroll, true);
2797
3052
  };
2798
- }, [hide, refresh]);
3053
+ }, [
3054
+ flushComment,
3055
+ hide,
3056
+ refresh
3057
+ ]);
2799
3058
  const placeCommentPop = (0, react.useCallback)(() => {
3059
+ const snapshot = activeRef.current;
3060
+ const layer = snapshot?.layer;
2800
3061
  const toolbar = toolbarRef.current;
2801
3062
  const pop = commentWrapRef.current;
2802
- if (!toolbar || !pop) return;
2803
- const anchor = toolbar.getBoundingClientRect();
3063
+ if (!snapshot || !layer || !toolbar || !pop) return;
3064
+ const bounds = layer.bounds();
3065
+ const toolbarW = toolbar.offsetWidth;
3066
+ const toolbarH = toolbar.offsetHeight;
3067
+ const home = layer.fromContent(snapshot.left, snapshot.top);
3068
+ const anchor = {
3069
+ left: home.left - toolbarW / 2,
3070
+ top: home.top - (snapshot.below ? 0 : toolbarH),
3071
+ width: toolbarW,
3072
+ height: toolbarH
3073
+ };
2804
3074
  pop.style.display = "flex";
2805
3075
  pop.style.visibility = "hidden";
2806
3076
  const panel = {
@@ -2808,24 +3078,45 @@ function SelectionToolbar({ sessionId }) {
2808
3078
  height: pop.offsetHeight || 180
2809
3079
  };
2810
3080
  pop.style.visibility = "";
2811
- const { top } = placePopover(anchor, panel, "bottom");
3081
+ const gap = 8;
3082
+ const margin = 8;
3083
+ const belowTop = layer.fromContent(0, snapshot.selectionBottom).top + gap;
3084
+ const aboveTop = anchor.top - panel.height - gap;
3085
+ const top = belowTop <= bounds.bottom - margin - panel.height ? belowTop : aboveTop >= bounds.top + margin ? aboveTop : Math.max(bounds.top + margin, Math.min(belowTop, bounds.bottom - margin - panel.height));
2812
3086
  const half = panel.width / 2 + 8;
2813
- const center = Math.min(Math.max(anchor.left + anchor.width / 2, half), Math.max(half, window.innerWidth - half));
2814
- pop.style.top = `${top}px`;
2815
- pop.style.left = `${center}px`;
3087
+ const min = bounds.left + half;
3088
+ const center = Math.min(Math.max(anchor.left + anchor.width / 2, min), Math.max(min, bounds.right - half));
3089
+ const at = layer.toContent(center, top);
3090
+ pop.style.top = `${at.top}px`;
3091
+ pop.style.left = `${at.left}px`;
3092
+ const sideGap = 6;
3093
+ const popLeftView = layer.fromContent(at.left, at.top).left - panel.width / 2;
3094
+ const popTopView = top;
3095
+ let leftView = popLeftView;
3096
+ let topView = popTopView - sideGap - toolbarH;
3097
+ if (topView < bounds.top + margin) topView = popTopView + panel.height + sideGap;
3098
+ leftView = Math.max(bounds.left + margin, Math.min(leftView, bounds.right - margin - toolbarW));
3099
+ topView = Math.max(bounds.top + margin, Math.min(topView, bounds.bottom - margin - toolbarH));
3100
+ const toolbarAt = layer.toContent(leftView, topView);
3101
+ toolbar.style.left = `${toolbarAt.left}px`;
3102
+ toolbar.style.top = `${toolbarAt.top}px`;
2816
3103
  }, []);
3104
+ (0, react.useLayoutEffect)(() => {
3105
+ if (phase === "comment") placeCommentPop();
3106
+ }, [
3107
+ active,
3108
+ phase,
3109
+ placeCommentPop
3110
+ ]);
2817
3111
  (0, react.useEffect)(() => {
2818
- if (phase === "comment") {
2819
- placeCommentPop();
2820
- commentRef.current?.focus();
2821
- }
2822
- }, [phase, placeCommentPop]);
3112
+ if (phase === "comment") commentRef.current?.focus();
3113
+ }, [phase]);
2823
3114
  (0, react.useEffect)(() => {
2824
3115
  const onResize = () => placeCommentPop();
2825
3116
  window.addEventListener("resize", onResize);
2826
3117
  return () => window.removeEventListener("resize", onResize);
2827
3118
  }, [placeCommentPop]);
2828
- (0, react.useEffect)(() => {
3119
+ (0, react.useLayoutEffect)(() => {
2829
3120
  if (phase !== "comment") return;
2830
3121
  const area = commentRef.current;
2831
3122
  if (!area) return;
@@ -2852,12 +3143,18 @@ function SelectionToolbar({ sessionId }) {
2852
3143
  setStatus(t("toolbar.copyFailed"));
2853
3144
  }
2854
3145
  }, [hide]);
3146
+ /**
3147
+ * "Add to task" is the commit: the quote is saved immediately, with or
3148
+ * without a comment. The popover only edits that saved entry, so dismissing
3149
+ * it (outside click, new selection) can never undo the annotation.
3150
+ */
2855
3151
  const openComment = (0, react.useCallback)(() => {
2856
3152
  if (!activeRef.current) return;
3153
+ pendingIdRef.current = saveSelection();
2857
3154
  setPhase("comment");
2858
3155
  setComment("");
2859
3156
  setStatus(null);
2860
- }, []);
3157
+ }, [saveSelection]);
2861
3158
  /**
2862
3159
  * Hand the selected link to the system browser.
2863
3160
  *
@@ -2875,97 +3172,75 @@ function SelectionToolbar({ sessionId }) {
2875
3172
  clearNativeSelection();
2876
3173
  hide();
2877
3174
  }, [hide]);
2878
- /** Quick-add without opening the comment step (double-tap / “Skip comment”). */
2879
- const commitAnnotation = (0, react.useCallback)((body) => {
2880
- const snapshot = activeRef.current;
2881
- if (!snapshot) {
2882
- hide();
2883
- return;
2884
- }
2885
- saveAnnotation(void 0, {
2886
- text: snapshot.text,
2887
- ...body && body.trim() ? { comment: body.trim() } : {},
2888
- sources: snapshot.source ? [{
2889
- text: snapshot.text,
2890
- messageId: snapshot.source.messageId,
2891
- messageKind: snapshot.source.messageKind,
2892
- startOffset: snapshot.source.startOffset,
2893
- endOffset: snapshot.source.endOffset
2894
- }] : void 0
2895
- });
2896
- getAnnotations().length;
3175
+ /** Close the optional comment step; the annotation stays saved either way. */
3176
+ const finishComment = (0, react.useCallback)((keepComment) => {
3177
+ if (keepComment) flushComment();
3178
+ else pendingIdRef.current = null;
2897
3179
  clearNativeSelection();
2898
3180
  hide();
2899
- setStatus(null);
2900
3181
  window.setTimeout(focusComposer, 16);
2901
- }, [hide]);
3182
+ }, [flushComment, hide]);
2902
3183
  if (!active) return null;
2903
3184
  const toolbarStyle = {
2904
3185
  left: active.left,
2905
3186
  top: active.top
2906
3187
  };
2907
3188
  const link = linkTarget(active.text, active.anchorHref);
2908
- return (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
2909
- (0, react_jsx_runtime.jsx)("div", {
3189
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3190
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2910
3191
  ref: toolbarRef,
2911
3192
  className: cx(A.row, styles.toolbar),
2912
- style: toolbarStyle,
3193
+ style: phase === "comment" ? void 0 : toolbarStyle,
2913
3194
  role: "toolbar",
2914
3195
  "aria-label": t("toolbar.aria"),
2915
3196
  "data-dsq-toolbar": "true",
3197
+ "data-anchor": phase === "comment" ? "popover-top" : active.below ? "below" : "above",
2916
3198
  onPointerDown: handleToolbarPointerDown,
2917
- children: phase === "comment" ? (0, react_jsx_runtime.jsx)("div", {
2918
- style: {
2919
- display: "inline-flex",
2920
- alignItems: "center",
2921
- gap: 2
2922
- },
2923
- children: (0, react_jsx_runtime.jsx)("span", {
3199
+ children: phase === "comment" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3200
+ className: cx(A.inlineRow, styles.toolbarInner),
3201
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2924
3202
  style: {
2925
3203
  padding: "0 8px",
2926
3204
  fontSize: 13,
2927
- opacity: .8
3205
+ opacity: .8,
3206
+ whiteSpace: "nowrap"
2928
3207
  },
2929
3208
  children: t("toolbar.commentHint")
2930
3209
  })
2931
- }) : (0, react_jsx_runtime.jsxs)("div", {
2932
- style: {
2933
- display: "inline-flex",
2934
- alignItems: "center",
2935
- gap: 2
2936
- },
3210
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3211
+ className: cx(A.inlineRow, styles.toolbarInner),
2937
3212
  children: [
2938
- link ? (0, react_jsx_runtime.jsx)("button", {
3213
+ link ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2939
3214
  type: "button",
2940
3215
  className: cx(A.plain, A.inlineRow, styles.button),
2941
3216
  onClick: handleOpenLink,
2942
- children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
3217
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
2943
3218
  name: "external-link",
2944
3219
  size: 14
2945
3220
  }), t("toolbar.openLink")]
2946
3221
  }) : null,
2947
- link ? (0, react_jsx_runtime.jsx)("span", {
3222
+ link ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2948
3223
  className: styles.divider,
2949
- "aria-hidden": true
3224
+ "aria-hidden": "true"
2950
3225
  }) : null,
2951
- (0, react_jsx_runtime.jsx)("button", {
3226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2952
3227
  type: "button",
2953
3228
  className: cx(A.plain, A.inlineRow, styles.button),
2954
3229
  onClick: () => void handleCopy(),
2955
- children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
3230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
2956
3231
  name: "copy",
2957
3232
  size: 14
2958
3233
  }), t("toolbar.copy")]
2959
3234
  }),
2960
- (0, react_jsx_runtime.jsx)("span", {
3235
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2961
3236
  className: styles.divider,
2962
- "aria-hidden": true
3237
+ "aria-hidden": "true"
2963
3238
  }),
2964
- (0, react_jsx_runtime.jsx)("button", {
3239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2965
3240
  type: "button",
2966
3241
  className: cx(A.plain, A.inlineRow, styles.button),
2967
3242
  onClick: openComment,
2968
- children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
3243
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
2969
3244
  name: "plus",
2970
3245
  size: 14
2971
3246
  }), t("toolbar.addToTask")]
@@ -2973,14 +3248,13 @@ function SelectionToolbar({ sessionId }) {
2973
3248
  ]
2974
3249
  })
2975
3250
  }),
2976
- phase === "comment" ? (0, react_jsx_runtime.jsxs)("div", {
3251
+ phase === "comment" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2977
3252
  ref: commentWrapRef,
2978
3253
  className: cx(A.col, styles.commentPop),
2979
- style: void 0,
2980
3254
  role: "dialog",
2981
3255
  "aria-label": t("toolbar.commentDialogAria"),
2982
3256
  onPointerDown: handleToolbarPointerDown,
2983
- children: [(0, react_jsx_runtime.jsx)("textarea", {
3257
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2984
3258
  ref: commentRef,
2985
3259
  className: cx(A.box, styles.commentInput),
2986
3260
  placeholder: t("common.commentPlaceholder"),
@@ -2989,37 +3263,38 @@ function SelectionToolbar({ sessionId }) {
2989
3263
  onKeyDown: (e) => {
2990
3264
  if (e.key === "Escape") {
2991
3265
  e.preventDefault();
3266
+ pendingIdRef.current = null;
2992
3267
  hide();
2993
3268
  return;
2994
3269
  }
2995
3270
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2996
3271
  e.preventDefault();
2997
- commitAnnotation(comment);
3272
+ finishComment(true);
2998
3273
  }
2999
3274
  }
3000
- }), (0, react_jsx_runtime.jsxs)("div", {
3275
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3001
3276
  className: styles.commentActions,
3002
- children: [(0, react_jsx_runtime.jsx)("button", {
3277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3003
3278
  type: "button",
3004
3279
  className: cx(A.plain, A.inlineRow, styles.button),
3005
- onClick: () => commitAnnotation(void 0),
3280
+ onClick: () => finishComment(false),
3006
3281
  children: t("toolbar.skipComment")
3007
- }), (0, react_jsx_runtime.jsx)("button", {
3282
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3008
3283
  type: "button",
3009
3284
  className: cx(A.plain, A.inlineRow, styles.button, styles.buttonAddComment),
3010
3285
  disabled: !comment.trim(),
3011
- onClick: () => commitAnnotation(comment),
3286
+ onClick: () => finishComment(true),
3012
3287
  children: t("toolbar.addComment")
3013
3288
  })]
3014
3289
  })]
3015
3290
  }) : null,
3016
- status ? (0, react_jsx_runtime.jsx)("div", {
3291
+ status ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3017
3292
  className: styles.status,
3018
3293
  style: toolbarStyle,
3019
3294
  role: "status",
3020
3295
  children: status
3021
3296
  }) : null
3022
- ] }), document.body);
3297
+ ] }), active.layer.host);
3023
3298
  }
3024
3299
  //#endregion
3025
3300
  //#region src/client/dom/decorateAnnotationDirectives.ts
@@ -3149,11 +3424,11 @@ function TranscriptQuoteCard({ node }) {
3149
3424
  scrollToAnnotation(index + 1, text, markIdOf(index));
3150
3425
  }, [markIdOf]);
3151
3426
  if (quotes.length === 0) return null;
3152
- return (0, react_jsx_runtime.jsx)("div", {
3427
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3153
3428
  ref: stackRef,
3154
3429
  className: styles.tCardStack,
3155
3430
  "data-dsq-deco": "true",
3156
- children: quotes.map((quote, index) => (0, react_jsx_runtime.jsx)("div", {
3431
+ children: quotes.map((quote, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3157
3432
  className: cx(A.plain, styles.tCardButton),
3158
3433
  role: "button",
3159
3434
  tabIndex: 0,
@@ -3164,7 +3439,7 @@ function TranscriptQuoteCard({ node }) {
3164
3439
  event.preventDefault();
3165
3440
  onSelect(index, quote.text);
3166
3441
  },
3167
- children: (0, react_jsx_runtime.jsx)(AnnotationCardView, {
3442
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnnotationCardView, {
3168
3443
  variant: "transcript",
3169
3444
  title: quote.title,
3170
3445
  text: quote.text,
@@ -3211,12 +3486,12 @@ function UserMessageDisplay({ node, renderMessageImages }) {
3211
3486
  const quotes = scanned.annotations;
3212
3487
  const images = contentImages(node.data?.content);
3213
3488
  if (!text && images.length === 0 && quotes.length === 0) return null;
3214
- return (0, react_jsx_runtime.jsx)("div", {
3489
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3215
3490
  className: cx(A.colEnd, styles.userRow),
3216
- children: (0, react_jsx_runtime.jsxs)("div", {
3491
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3217
3492
  className: cx(A.colEnd, styles.userStack),
3218
3493
  children: [
3219
- images.length > 0 && renderMessageImages !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
3494
+ images.length > 0 && renderMessageImages !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3220
3495
  className: cx(A.colEnd, styles.userImages),
3221
3496
  "data-message-attachments": true,
3222
3497
  children: renderMessageImages({
@@ -3224,12 +3499,12 @@ function UserMessageDisplay({ node, renderMessageImages }) {
3224
3499
  align: "end",
3225
3500
  compact: images.length > 1
3226
3501
  })
3227
- }, "images") : null,
3228
- quotes.length > 0 ? (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }, "quotes") : null,
3229
- text ? (0, react_jsx_runtime.jsx)("div", {
3502
+ }) : null,
3503
+ quotes.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }) : null,
3504
+ text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3230
3505
  className: styles.userBubble,
3231
3506
  children: text
3232
- }, "bubble") : null
3507
+ }) : null
3233
3508
  ]
3234
3509
  })
3235
3510
  });
@@ -3281,8 +3556,8 @@ function warnQuota() {
3281
3556
  console.warn("dsh-select-quote: annotation persistence disabled (storage write failed)");
3282
3557
  }
3283
3558
  /**
3284
- * Direct localStorage backend (the documented fallback). Writes are wrapped so
3285
- * a quota failure degrades to memory-only instead of breaking the composer.
3559
+ * Direct localStorage backend. Writes are wrapped so a quota failure degrades
3560
+ * to memory-only instead of breaking the composer.
3286
3561
  */
3287
3562
  function createStoragePersistence(storage) {
3288
3563
  let degraded = false;
@@ -3305,45 +3580,6 @@ function createStoragePersistence(storage) {
3305
3580
  prune: (now) => pruneSessions(storage, now)
3306
3581
  };
3307
3582
  }
3308
- /**
3309
- * Host store backend: the same `defineStore` the composer draft uses, so the
3310
- * write path, per-session keying and failure semantics are the host's.
3311
- */
3312
- function createHostStorePersistence(defineStore, storage) {
3313
- const handle = defineStore({
3314
- init: () => ({
3315
- v: PAYLOAD_VERSION,
3316
- updatedAt: 0,
3317
- items: []
3318
- }),
3319
- persist: ANNOTATION_KEY_PREFIX,
3320
- actions: { replace(draft, items, now) {
3321
- draft.items = [...items];
3322
- draft.updatedAt = now;
3323
- } }
3324
- });
3325
- const instances = /* @__PURE__ */ new Map();
3326
- const instanceOf = (sessionId) => {
3327
- const cached = instances.get(sessionId);
3328
- if (cached) return cached;
3329
- const instance = handle.create(sessionId);
3330
- instances.set(sessionId, instance);
3331
- return instance;
3332
- };
3333
- return {
3334
- load: (sessionId) => itemsOf(instanceOf(sessionId).getSnapshot()),
3335
- save: (sessionId, items) => instanceOf(sessionId).actions.replace(items, Date.now()),
3336
- clear: (sessionId) => {
3337
- instanceOf(sessionId).clearPersisted();
3338
- instances.delete(sessionId);
3339
- },
3340
- prune: (now) => {
3341
- const removed = pruneSessions(storage, now);
3342
- for (const sessionId of removed) instances.delete(sessionId);
3343
- return removed;
3344
- }
3345
- };
3346
- }
3347
3583
  /** Retention: newest `maxSessions` within `maxAgeMs`, by payload timestamp. */
3348
3584
  function pruneSessions(storage, now = Date.now(), options = {}) {
3349
3585
  const maxSessions = options.maxSessions ?? 20;
@@ -3383,27 +3619,20 @@ function createNoopPersistence() {
3383
3619
  //#endregion
3384
3620
  //#region src/client/state/annotation-storage.ts
3385
3621
  /**
3386
- * Backend selection for annotation persistence.
3622
+ * Environment probe for annotation persistence.
3387
3623
  *
3388
- * Kept apart from `annotation-persistence.ts` because reaching the host's
3389
- * client-store needs a guarded `require`, and that is the one thing the pure
3390
- * module cannot do — verify-bundle imports the pure module straight into Node,
3391
- * where a `declare` statement is not valid JavaScript.
3624
+ * Kept apart from `annotation-persistence.ts` because this is the only module
3625
+ * that reads `globalThis`: the pure layer stays importable in Node, where
3626
+ * verify-bundle replays the whole cycle against a Map-backed storage.
3392
3627
  *
3393
- * Preference order: the host store (same engine and keying as the composer
3394
- * draft), plain localStorage, and a no-op when the environment has no storage
3395
- * at all.
3628
+ * The backend is plain localStorage — the same per-origin store the composer
3629
+ * draft lives in, under this plugin's own key prefix. Requiring the host's
3630
+ * `@deepseek-ai/dsh-client-store` instead would buy nothing: its persistence
3631
+ * backend is localStorage with the same key shape and the same whole-value
3632
+ * payload, while depending on a Harness Client package that the plugin spec
3633
+ * forbids loading as a module (it changes without notice, and a throwing
3634
+ * backend selection would take the whole slot entry down with it).
3396
3635
  */
3397
- /** The host package is a platform seed word; older hosts may not carry it. */
3398
- const HOST_STORE_MODULE = "@deepseek-ai/dsh-client-store";
3399
- function hostDefineStore() {
3400
- try {
3401
- const loaded = require(HOST_STORE_MODULE);
3402
- return typeof loaded?.defineStore === "function" ? loaded.defineStore : null;
3403
- } catch {
3404
- return null;
3405
- }
3406
- }
3407
3636
  function browserStorage() {
3408
3637
  try {
3409
3638
  const storage = globalThis.localStorage;
@@ -3427,10 +3656,7 @@ function browserStorage() {
3427
3656
  }
3428
3657
  function createAnnotationPersistence() {
3429
3658
  const storage = browserStorage();
3430
- const defineStore = hostDefineStore();
3431
- if (storage && defineStore) return createHostStorePersistence(defineStore, storage);
3432
- if (storage) return createStoragePersistence(storage);
3433
- return createNoopPersistence();
3659
+ return storage ? createStoragePersistence(storage) : createNoopPersistence();
3434
3660
  }
3435
3661
  //#endregion
3436
3662
  //#region src/client/index.tsx