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/README.md +17 -6
- package/icon.svg +9 -0
- package/lib/client.js +652 -426
- package/locale/en.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -2
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-
|
|
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:
|
|
131
|
-
z-index:
|
|
132
|
-
|
|
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-
|
|
136
|
-
|
|
240
|
+
/* 无边框:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的描边环,
|
|
241
|
+
由它勾勒轮廓,所以这里不要再画 border,否则描边会和环叠成两条线。 */
|
|
137
242
|
border: 0;
|
|
138
243
|
border-radius: 12px;
|
|
139
|
-
|
|
140
|
-
box-shadow: var(--dsq-shadow
|
|
244
|
+
/* 底色与模糊都在上面的材质图层里,这里不再画背景 */
|
|
245
|
+
box-shadow: var(--dsq-shadow);
|
|
141
246
|
pointer-events: auto;
|
|
142
|
-
/*
|
|
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:
|
|
186
|
-
z-index:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
.
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
.
|
|
431
|
-
|
|
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:
|
|
603
|
-
z-index:
|
|
604
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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,
|
|
1679
|
-
left: Math.min(Math.max(anchor.left, MARGIN), Math.max(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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
d: "
|
|
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
|
-
}, "
|
|
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:
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
className:
|
|
2205
|
-
children: [
|
|
2206
|
-
(
|
|
2207
|
-
|
|
2208
|
-
"
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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.
|
|
2288
|
-
children: item.
|
|
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)("
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
|
|
2641
|
-
const
|
|
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
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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
|
|
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
|
|
2814
|
-
|
|
2815
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
/**
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
|
|
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)(
|
|
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
|
-
|
|
2919
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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
|
-
] }),
|
|
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
|
-
}
|
|
3228
|
-
quotes.length > 0 ? (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }
|
|
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
|
-
}
|
|
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
|
|
3285
|
-
*
|
|
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
|
-
*
|
|
3622
|
+
* Environment probe for annotation persistence.
|
|
3387
3623
|
*
|
|
3388
|
-
* Kept apart from `annotation-persistence.ts` because
|
|
3389
|
-
*
|
|
3390
|
-
*
|
|
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
|
-
*
|
|
3394
|
-
* draft
|
|
3395
|
-
*
|
|
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
|
-
|
|
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
|