dsh-select-quote 0.3.1 → 0.3.3
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 +23 -9
- package/lib/client.js +600 -358
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -51,60 +51,122 @@ function lastEditable() {
|
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
53
|
//#region src/client/styles.ts
|
|
54
|
+
/**
|
|
55
|
+
* Every style the plugin ships, in four layers.
|
|
56
|
+
*
|
|
57
|
+
* 1. tokens — product design tokens, aliased once with their fallbacks
|
|
58
|
+
* 2. atoms — shared shapes (`.dsq_a_*`), no numbers, no colors
|
|
59
|
+
* 3. components — one block per UI piece; everything measurable lives here
|
|
60
|
+
* 4. host — rules that reach into product DOM, theme flips, keyframes
|
|
61
|
+
*
|
|
62
|
+
* Injection order is the cascade order, so a component rule always wins over an
|
|
63
|
+
* atom on the same element — which is what lets atoms stay shape-only and the
|
|
64
|
+
* component blocks stay the single source of numbers.
|
|
65
|
+
*/
|
|
66
|
+
/** Layer 2 — atoms. Shape only: adding a number here is a layering violation. */
|
|
67
|
+
const ATOMS = {
|
|
68
|
+
row: "display: flex; align-items: center;",
|
|
69
|
+
inlineRow: "display: inline-flex; align-items: center;",
|
|
70
|
+
col: "display: flex; flex-direction: column;",
|
|
71
|
+
colEnd: "display: flex; flex-direction: column; align-items: flex-end;",
|
|
72
|
+
gridMid: "display: grid; place-items: center;",
|
|
73
|
+
ell: "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;",
|
|
74
|
+
plain: "appearance: none; border: none; background: transparent; cursor: pointer;",
|
|
75
|
+
keep: "flex: none;",
|
|
76
|
+
grow: "flex: 1; min-width: 0;",
|
|
77
|
+
box: "box-sizing: border-box;"
|
|
78
|
+
};
|
|
79
|
+
const toKebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
80
|
+
const atomRules = Object.entries(ATOMS).map(([name, declarations]) => `.dsq_a_${toKebab(name)} { ${declarations} }`).join("\n");
|
|
81
|
+
/** Atom class names, for `cx(styles.card, A.row)`. */
|
|
82
|
+
const A = Object.fromEntries(Object.keys(ATOMS).map((name) => [name, `dsq_a_${toKebab(name)}`]));
|
|
83
|
+
/** Join class parts, dropping the falsy ones so a variant can be `cond && A.x`. */
|
|
84
|
+
function cx(...parts) {
|
|
85
|
+
return parts.filter(Boolean).join(" ");
|
|
86
|
+
}
|
|
54
87
|
const CSS = `
|
|
88
|
+
/* Layer 1 — tokens. Every product token this plugin reads is aliased here once,
|
|
89
|
+
with the fallback it used to repeat inline at each use site. Scoped to \`body\`,
|
|
90
|
+
not \`:root\`: the product flips its palette on body[data-ds-dark-theme], so an
|
|
91
|
+
alias computed on the root element would freeze the light values and inherit
|
|
92
|
+
them into the dark page. */
|
|
93
|
+
body {
|
|
94
|
+
--dsq-chat-width: var(--dsh-chat-content-width, 748px);
|
|
95
|
+
--dsq-font-size: var(--dsh-content-font-size, 14px);
|
|
96
|
+
--dsq-font-delta: var(--dsh-content-font-delta, 0px);
|
|
97
|
+
--dsq-bg-raised: var(--dsw-alias-bg-layer-1, #fff);
|
|
98
|
+
--dsq-bg-panel: var(--dsw-alias-bg-layer-2, #fff);
|
|
99
|
+
--dsq-bg-toast: var(--dsw-alias-bg-layer-3, rgba(0, 0, 0, 0.78));
|
|
100
|
+
--dsq-line: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
101
|
+
--dsq-line-divider: var(--dsw-alias-border-l4, rgba(0, 0, 0, 0.12));
|
|
102
|
+
--dsq-focus-ring: var(--dsw-alias-button-primary-fill, #1677ff);
|
|
103
|
+
--dsq-fill-hover: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
104
|
+
--dsq-label: var(--dsw-alias-label-primary, rgba(0, 0, 0, 0.88));
|
|
105
|
+
--dsq-label-inverted: var(--dsw-alias-label-primary-inverted, #fff);
|
|
106
|
+
--dsq-label-2: var(--dsw-alias-label-secondary, rgba(0, 0, 0, 0.65));
|
|
107
|
+
--dsq-label-3: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
|
|
108
|
+
--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
|
+
--dsq-bubble: var(--dsw-specific-bubble, rgba(0, 0, 0, 0.05));
|
|
112
|
+
--dsq-menu: var(--dsw-specific-menu, #fff);
|
|
113
|
+
--dsq-accent-400: var(--dsw-static-blue-400, #1677ff);
|
|
114
|
+
--dsq-accent: var(--dsw-static-blue-500, #3b82f6);
|
|
115
|
+
--dsq-accent-tint: var(--dsw-static-blue-50p, #eaf3ff);
|
|
116
|
+
--dsq-accent-tint-hover: var(--dsw-static-blue-75, #e5f0ff);
|
|
117
|
+
--dsq-accent-tint-dark: var(--dsw-static-blue-950, #172554);
|
|
118
|
+
--dsq-neutral-50: var(--dsw-static-neutral-50, #fafafa);
|
|
119
|
+
--dsq-neutral-100: var(--dsw-static-neutral-100, #f5f5f5);
|
|
120
|
+
--dsq-neutral-800: var(--dsw-static-neutral-800, #292929);
|
|
121
|
+
--dsq-neutral-850: var(--dsw-static-neutral-850, #212123);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Layer 2 — atoms (generated from ATOMS above; the class names and the rules
|
|
125
|
+
cannot drift apart because both come from the same table). */
|
|
126
|
+
${atomRules}
|
|
127
|
+
|
|
128
|
+
/* Layer 3 — components. Floating selection toolbar. */
|
|
55
129
|
.dsq_toolbar {
|
|
56
130
|
position: fixed;
|
|
57
131
|
z-index: 10060;
|
|
58
132
|
transform: translate(-50%, -100%);
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: center;
|
|
61
133
|
gap: 2px;
|
|
62
134
|
padding: 4px;
|
|
63
|
-
/* 无边框:--
|
|
135
|
+
/* 无边框:--dsq-shadow-soft 自带 0 0 0 .5px 的描边环,由它勾勒轮廓,
|
|
64
136
|
所以这里不要再画 border,否则描边会和环叠成两条线。 */
|
|
65
137
|
border: 0;
|
|
66
|
-
border-radius:
|
|
67
|
-
background: var(--
|
|
68
|
-
box-shadow: var(--
|
|
138
|
+
border-radius: 12px;
|
|
139
|
+
background: var(--dsq-bg-panel);
|
|
140
|
+
box-shadow: var(--dsq-shadow-soft);
|
|
69
141
|
pointer-events: auto;
|
|
70
142
|
/* Mounts once per selection, after the drag ends: soft rise + fade. */
|
|
71
143
|
animation: dsq-toolbar-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
72
144
|
}
|
|
73
145
|
|
|
74
|
-
@keyframes dsq-toolbar-in {
|
|
75
|
-
from {
|
|
76
|
-
opacity: 0;
|
|
77
|
-
transform: translate(-50%, -100%) translateY(4px);
|
|
78
|
-
}
|
|
79
|
-
to {
|
|
80
|
-
opacity: 1;
|
|
81
|
-
transform: translate(-50%, -100%);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
146
|
.dsq_button {
|
|
86
|
-
|
|
87
|
-
border: none;
|
|
88
|
-
background: transparent;
|
|
89
|
-
color: var(--dsw-alias-label-primary, rgba(0, 0, 0, 0.88));
|
|
147
|
+
color: var(--dsq-label);
|
|
90
148
|
font: inherit;
|
|
91
|
-
font-size:
|
|
149
|
+
font-size: 12px;
|
|
92
150
|
line-height: 20px;
|
|
93
|
-
|
|
94
|
-
border-radius: 999px;
|
|
151
|
+
border-radius: 8px;
|
|
95
152
|
padding: 4px 10px;
|
|
96
|
-
display: inline-flex;
|
|
97
|
-
align-items: center;
|
|
98
153
|
gap: 6px;
|
|
99
154
|
white-space: nowrap;
|
|
100
155
|
}
|
|
101
|
-
|
|
102
156
|
.dsq_button:hover {
|
|
103
|
-
background: var(--
|
|
157
|
+
background: var(--dsq-fill-hover);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.dsq_buttonAddComment {
|
|
161
|
+
background: var(--dsq-accent);
|
|
162
|
+
color:#fff;
|
|
163
|
+
}
|
|
164
|
+
.dsq_buttonAddComment:hover {
|
|
165
|
+
background: var(--dsq-accent-400);
|
|
104
166
|
}
|
|
105
167
|
|
|
106
168
|
.dsq_button:focus-visible {
|
|
107
|
-
outline: 2px solid var(--
|
|
169
|
+
outline: 2px solid var(--dsq-focus-ring);
|
|
108
170
|
outline-offset: 1px;
|
|
109
171
|
}
|
|
110
172
|
|
|
@@ -115,7 +177,7 @@ const CSS = `
|
|
|
115
177
|
.dsq_divider {
|
|
116
178
|
width: 1px;
|
|
117
179
|
height: 14px;
|
|
118
|
-
background: var(--
|
|
180
|
+
background: var(--dsq-line-divider);
|
|
119
181
|
margin: 0 2px;
|
|
120
182
|
}
|
|
121
183
|
|
|
@@ -125,8 +187,8 @@ const CSS = `
|
|
|
125
187
|
transform: translate(-50%, 8px);
|
|
126
188
|
padding: 4px 10px;
|
|
127
189
|
border-radius: 999px;
|
|
128
|
-
background: var(--
|
|
129
|
-
color: var(--
|
|
190
|
+
background: var(--dsq-bg-toast);
|
|
191
|
+
color: var(--dsq-label-inverted);
|
|
130
192
|
font-size: 12px;
|
|
131
193
|
line-height: 18px;
|
|
132
194
|
pointer-events: none;
|
|
@@ -142,23 +204,20 @@ const CSS = `
|
|
|
142
204
|
width: 20%;
|
|
143
205
|
min-width: 148px;
|
|
144
206
|
max-width: 220px;
|
|
145
|
-
display: flex;
|
|
146
|
-
flex-direction: column;
|
|
147
207
|
align-items: stretch;
|
|
148
208
|
gap: 6px;
|
|
149
209
|
pointer-events: auto;
|
|
150
210
|
}
|
|
151
211
|
|
|
152
|
-
/* One
|
|
153
|
-
|
|
154
|
-
|
|
212
|
+
/* One annotation card, shared by the composer and the transcript: the base
|
|
213
|
+
carries the shape, and \`.dsq_tCard\` / \`.dsq_tCardIcon\` override only the
|
|
214
|
+
numbers where the transcript really differs.
|
|
215
|
+
Geometry mirrors the product's own file card (\`.nyYjTG_file\` in
|
|
216
|
+
ui-deliverables): hairline border, neutral fill, 18px radius, fixed 72px row. */
|
|
155
217
|
.dsq_card {
|
|
156
|
-
--dsq-card-fill: var(--
|
|
157
|
-
--dsq-card-hover: var(--
|
|
158
|
-
box-sizing: border-box;
|
|
218
|
+
--dsq-card-fill: var(--dsq-neutral-50);
|
|
219
|
+
--dsq-card-hover: var(--dsq-neutral-100);
|
|
159
220
|
flex: 0 0 auto;
|
|
160
|
-
display: flex;
|
|
161
|
-
align-items: center;
|
|
162
221
|
gap: 10px;
|
|
163
222
|
width: fit-content;
|
|
164
223
|
max-width: calc(25% - 5px);
|
|
@@ -166,10 +225,10 @@ const CSS = `
|
|
|
166
225
|
height: 64px;
|
|
167
226
|
margin: 0;
|
|
168
227
|
padding: 10px;
|
|
169
|
-
border: 0.5px solid var(--
|
|
228
|
+
border: 0.5px solid var(--dsq-line);
|
|
170
229
|
border-radius: 18px;
|
|
171
230
|
background: var(--dsq-card-fill);
|
|
172
|
-
color: var(--
|
|
231
|
+
color: var(--dsq-label);
|
|
173
232
|
transition: background-color 0.12s;
|
|
174
233
|
position: relative;
|
|
175
234
|
overflow: hidden;
|
|
@@ -185,15 +244,12 @@ const CSS = `
|
|
|
185
244
|
quarter of the input, so 48px would leave no room for the quote text. */
|
|
186
245
|
.dsq_cardIcon {
|
|
187
246
|
z-index: 2;
|
|
188
|
-
flex: none;
|
|
189
247
|
width: 36px;
|
|
190
248
|
height: 36px;
|
|
191
|
-
border: 0.5px solid var(--
|
|
249
|
+
border: 0.5px solid var(--dsq-line);
|
|
192
250
|
border-radius: 12px;
|
|
193
|
-
display: grid;
|
|
194
|
-
place-items: center;
|
|
195
251
|
background: var(--dsq-card-fill);
|
|
196
|
-
color: var(--
|
|
252
|
+
color: var(--dsq-link);
|
|
197
253
|
font-size: 12px;
|
|
198
254
|
font-weight: 600;
|
|
199
255
|
letter-spacing: -0.02em;
|
|
@@ -201,13 +257,13 @@ const CSS = `
|
|
|
201
257
|
|
|
202
258
|
.dsq_cardBody {
|
|
203
259
|
z-index: 2;
|
|
204
|
-
flex: 1;
|
|
205
|
-
min-width: 0;
|
|
206
|
-
display: flex;
|
|
207
|
-
flex-direction: column;
|
|
208
260
|
justify-content: center;
|
|
209
261
|
gap: 2px;
|
|
210
|
-
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Keeps the ellipsised title clear of the corner remove button, which only the
|
|
265
|
+
composer stack renders. */
|
|
266
|
+
.dsq_cardStack .dsq_cardBody {
|
|
211
267
|
padding-right: 14px;
|
|
212
268
|
}
|
|
213
269
|
|
|
@@ -215,19 +271,13 @@ const CSS = `
|
|
|
215
271
|
font-size: 14px;
|
|
216
272
|
font-weight: 500;
|
|
217
273
|
line-height: 22px;
|
|
218
|
-
color: var(--
|
|
219
|
-
overflow: hidden;
|
|
220
|
-
text-overflow: ellipsis;
|
|
221
|
-
white-space: nowrap;
|
|
274
|
+
color: var(--dsq-label);
|
|
222
275
|
}
|
|
223
276
|
|
|
224
277
|
.dsq_cardSubtitle {
|
|
225
278
|
font-size: 12px;
|
|
226
279
|
line-height: 18px;
|
|
227
|
-
color: var(--
|
|
228
|
-
overflow: hidden;
|
|
229
|
-
text-overflow: ellipsis;
|
|
230
|
-
white-space: nowrap;
|
|
280
|
+
color: var(--dsq-label-3);
|
|
231
281
|
}
|
|
232
282
|
|
|
233
283
|
/* Corner remove button: hidden until the card is hovered or focused. */
|
|
@@ -236,16 +286,10 @@ const CSS = `
|
|
|
236
286
|
top: 4px;
|
|
237
287
|
right: 4px;
|
|
238
288
|
z-index: 3;
|
|
239
|
-
appearance: none;
|
|
240
|
-
border: none;
|
|
241
|
-
background: transparent;
|
|
242
289
|
width: 20px;
|
|
243
290
|
height: 20px;
|
|
244
291
|
border-radius: 6px;
|
|
245
|
-
|
|
246
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
|
|
247
|
-
display: grid;
|
|
248
|
-
place-items: center;
|
|
292
|
+
color: var(--dsq-label-3);
|
|
249
293
|
font-size: 14px;
|
|
250
294
|
line-height: 1;
|
|
251
295
|
opacity: 0;
|
|
@@ -262,8 +306,8 @@ const CSS = `
|
|
|
262
306
|
}
|
|
263
307
|
|
|
264
308
|
.dsq_cardClose:hover {
|
|
265
|
-
background: var(--
|
|
266
|
-
color: var(--
|
|
309
|
+
background: var(--dsq-fill-hover);
|
|
310
|
+
color: var(--dsq-label);
|
|
267
311
|
}
|
|
268
312
|
|
|
269
313
|
/* Secondary corner action (edit comment). */
|
|
@@ -272,16 +316,10 @@ const CSS = `
|
|
|
272
316
|
top: 4px;
|
|
273
317
|
right: 28px;
|
|
274
318
|
z-index: 3;
|
|
275
|
-
appearance: none;
|
|
276
|
-
border: none;
|
|
277
|
-
background: transparent;
|
|
278
319
|
width: 20px;
|
|
279
320
|
height: 20px;
|
|
280
321
|
border-radius: 6px;
|
|
281
|
-
|
|
282
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
|
|
283
|
-
display: grid;
|
|
284
|
-
place-items: center;
|
|
322
|
+
color: var(--dsq-label-3);
|
|
285
323
|
font-size: 12px;
|
|
286
324
|
line-height: 1;
|
|
287
325
|
opacity: 0;
|
|
@@ -296,42 +334,35 @@ const CSS = `
|
|
|
296
334
|
}
|
|
297
335
|
|
|
298
336
|
.dsq_cardAction:hover {
|
|
299
|
-
background: var(--
|
|
337
|
+
background: var(--dsq-fill-hover);
|
|
300
338
|
}
|
|
301
339
|
|
|
302
340
|
/* Qoder-style annotation summary (“N 条批注”) inside the composer. */
|
|
303
341
|
.dsq_summary {
|
|
304
342
|
position: relative;
|
|
305
343
|
z-index: 10040;
|
|
306
|
-
display: flex;
|
|
307
|
-
align-items: center;
|
|
308
344
|
gap: 10px;
|
|
309
345
|
width: 100%;
|
|
310
346
|
max-width: 100%;
|
|
311
347
|
padding: 8px 10px;
|
|
312
|
-
border: 0.5px solid var(--
|
|
348
|
+
border: 0.5px solid var(--dsq-line);
|
|
313
349
|
border-radius: 18px;
|
|
314
|
-
background: var(--
|
|
350
|
+
background: var(--dsq-neutral-50);
|
|
315
351
|
pointer-events: auto;
|
|
316
352
|
user-select: none;
|
|
317
353
|
}
|
|
318
354
|
|
|
319
355
|
.dsq_summaryIcon {
|
|
320
|
-
flex: none;
|
|
321
356
|
width: 32px;
|
|
322
357
|
height: 32px;
|
|
323
358
|
border-radius: 10px;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.05));
|
|
327
|
-
color: var(--dsw-alias-link, rgba(0, 0, 0, 0.65));
|
|
359
|
+
background: var(--dsq-fill-hover);
|
|
360
|
+
color: var(--dsq-link);
|
|
328
361
|
font-size: 13px;
|
|
329
362
|
font-weight: 600;
|
|
330
363
|
}
|
|
331
364
|
|
|
332
365
|
.dsq_summaryBody {
|
|
333
|
-
flex: 1;
|
|
334
|
-
min-width: 0;
|
|
335
366
|
position: relative;
|
|
336
367
|
}
|
|
337
368
|
|
|
@@ -339,16 +370,18 @@ const CSS = `
|
|
|
339
370
|
font-size: 14px;
|
|
340
371
|
font-weight: 500;
|
|
341
372
|
line-height: 20px;
|
|
342
|
-
color: var(--
|
|
373
|
+
color: var(--dsq-label);
|
|
343
374
|
}
|
|
344
375
|
|
|
345
376
|
.dsq_summaryHint {
|
|
346
377
|
font-size: 12px;
|
|
347
378
|
line-height: 18px;
|
|
348
|
-
color: var(--
|
|
379
|
+
color: var(--dsq-label-3);
|
|
349
380
|
}
|
|
350
381
|
|
|
351
382
|
.dsq_summaryHover {
|
|
383
|
+
/* \`display: none\` here is the closed state, so this rule must not take the
|
|
384
|
+
flex atoms — the open class below turns display back on. */
|
|
352
385
|
display: none;
|
|
353
386
|
position: fixed;
|
|
354
387
|
z-index: 10050;
|
|
@@ -378,63 +411,47 @@ const CSS = `
|
|
|
378
411
|
transform-origin: bottom center;
|
|
379
412
|
}
|
|
380
413
|
|
|
381
|
-
@keyframes dsq-panel-in {
|
|
382
|
-
from {
|
|
383
|
-
transform: scale(0.96);
|
|
384
|
-
}
|
|
385
|
-
to {
|
|
386
|
-
transform: scale(1);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
414
|
.dsq_summaryHoverInner {
|
|
391
|
-
display: flex;
|
|
392
|
-
flex-direction: column;
|
|
393
415
|
gap: 4px;
|
|
394
416
|
max-height: min(420px, calc(100vh - 120px));
|
|
395
417
|
overflow-y: auto;
|
|
396
418
|
overscroll-behavior: contain;
|
|
397
|
-
padding:
|
|
398
|
-
border-radius:
|
|
399
|
-
border: 0.5px solid var(--
|
|
419
|
+
padding: 4px;
|
|
420
|
+
border-radius: 12px;
|
|
421
|
+
border: 0.5px solid var(--dsq-line);
|
|
400
422
|
/* Frosted panel: the menu token at partial alpha over a blur. */
|
|
401
|
-
background: color-mix(in srgb, var(--
|
|
423
|
+
background: color-mix(in srgb, var(--dsq-bg-raised) 50%, transparent);
|
|
402
424
|
backdrop-filter: blur(14px) saturate(1.4);
|
|
403
425
|
-webkit-backdrop-filter: blur(14px) saturate(1.4);
|
|
404
|
-
box-shadow: var(--
|
|
426
|
+
box-shadow: var(--dsq-shadow-pop);
|
|
405
427
|
}
|
|
406
428
|
|
|
407
429
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
408
430
|
.dsq_summaryHoverInner {
|
|
409
|
-
background: var(--
|
|
431
|
+
background: var(--dsq-menu);
|
|
410
432
|
}
|
|
411
433
|
}
|
|
412
434
|
|
|
413
435
|
.dsq_summaryItem {
|
|
414
436
|
display: block;
|
|
415
437
|
padding: 8px 10px;
|
|
416
|
-
border-radius:
|
|
438
|
+
border-radius: 8px;
|
|
417
439
|
transition: background-color 0.12s;
|
|
418
440
|
}
|
|
419
441
|
|
|
420
442
|
.dsq_summaryItem:hover {
|
|
421
|
-
background: var(--
|
|
443
|
+
background: color-mix(in srgb, var(--dsq-fill-hover) 50%, transparent) ;
|
|
422
444
|
}
|
|
423
445
|
|
|
424
446
|
.dsq_summaryItemHead {
|
|
425
|
-
display: flex;
|
|
426
|
-
align-items: center;
|
|
427
447
|
gap: 6px;
|
|
428
448
|
}
|
|
429
449
|
|
|
430
450
|
.dsq_summaryItemBadge {
|
|
431
|
-
flex: none;
|
|
432
|
-
display: grid;
|
|
433
|
-
place-items: center;
|
|
434
451
|
width: 18px;
|
|
435
452
|
height: 18px;
|
|
436
453
|
border-radius: 999px;
|
|
437
|
-
background: var(--
|
|
454
|
+
background: var(--dsq-accent);
|
|
438
455
|
color: #fff;
|
|
439
456
|
font-size: 11px;
|
|
440
457
|
font-weight: 600;
|
|
@@ -442,8 +459,6 @@ const CSS = `
|
|
|
442
459
|
}
|
|
443
460
|
|
|
444
461
|
.dsq_summaryItemActions {
|
|
445
|
-
display: flex;
|
|
446
|
-
align-items: center;
|
|
447
462
|
gap: 2px;
|
|
448
463
|
margin-left: auto;
|
|
449
464
|
opacity: 0.5;
|
|
@@ -456,21 +471,15 @@ const CSS = `
|
|
|
456
471
|
}
|
|
457
472
|
|
|
458
473
|
.dsq_summaryItemAction {
|
|
459
|
-
appearance: none;
|
|
460
|
-
border: none;
|
|
461
|
-
background: transparent;
|
|
462
474
|
width: 26px;
|
|
463
475
|
height: 26px;
|
|
464
476
|
border-radius: 7px;
|
|
465
|
-
|
|
466
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
|
|
467
|
-
display: grid;
|
|
468
|
-
place-items: center;
|
|
477
|
+
color: var(--dsq-label-3);
|
|
469
478
|
}
|
|
470
479
|
|
|
471
480
|
.dsq_summaryItemAction:hover {
|
|
472
|
-
background: var(--
|
|
473
|
-
color: var(--
|
|
481
|
+
background: var(--dsq-fill-hover);
|
|
482
|
+
color: var(--dsq-label);
|
|
474
483
|
}
|
|
475
484
|
|
|
476
485
|
.dsq_summaryItemMain {
|
|
@@ -478,14 +487,10 @@ const CSS = `
|
|
|
478
487
|
}
|
|
479
488
|
|
|
480
489
|
.dsq_summaryItemButton {
|
|
481
|
-
appearance: none;
|
|
482
490
|
display: block;
|
|
483
491
|
width: 100%;
|
|
484
492
|
text-align: left;
|
|
485
493
|
font: inherit;
|
|
486
|
-
border: none;
|
|
487
|
-
background: transparent;
|
|
488
|
-
cursor: pointer;
|
|
489
494
|
border-radius: 8px;
|
|
490
495
|
padding: 2px 0 0;
|
|
491
496
|
}
|
|
@@ -501,13 +506,13 @@ const CSS = `
|
|
|
501
506
|
.dsq_summaryItemLabel {
|
|
502
507
|
font-size: 11px;
|
|
503
508
|
line-height: 16px;
|
|
504
|
-
color: var(--
|
|
509
|
+
color: var(--dsq-label-3);
|
|
505
510
|
}
|
|
506
511
|
|
|
507
512
|
.dsq_summaryItemText {
|
|
508
513
|
font-size: 13px;
|
|
509
514
|
line-height: 18px;
|
|
510
|
-
color: var(--
|
|
515
|
+
color: var(--dsq-label);
|
|
511
516
|
white-space: pre-wrap;
|
|
512
517
|
overflow-wrap: anywhere;
|
|
513
518
|
display: -webkit-box;
|
|
@@ -517,14 +522,14 @@ const CSS = `
|
|
|
517
522
|
}
|
|
518
523
|
|
|
519
524
|
.dsq_summaryItemTextEmpty {
|
|
520
|
-
color: var(--
|
|
525
|
+
color: var(--dsq-label-3);
|
|
521
526
|
}
|
|
522
527
|
|
|
523
528
|
/* Inline edit form rendered inside the hover panel item (no extra layer). */
|
|
524
529
|
.dsq_annEditText {
|
|
525
530
|
font-size: 13px;
|
|
526
531
|
line-height: 20px;
|
|
527
|
-
color: var(--
|
|
532
|
+
color: var(--dsq-label);
|
|
528
533
|
white-space: pre-wrap;
|
|
529
534
|
overflow-wrap: anywhere;
|
|
530
535
|
max-height: 132px;
|
|
@@ -533,7 +538,6 @@ const CSS = `
|
|
|
533
538
|
}
|
|
534
539
|
|
|
535
540
|
.dsq_annEditInput {
|
|
536
|
-
box-sizing: border-box;
|
|
537
541
|
width: 100%;
|
|
538
542
|
min-height: 72px;
|
|
539
543
|
resize: vertical;
|
|
@@ -543,54 +547,49 @@ const CSS = `
|
|
|
543
547
|
font: inherit;
|
|
544
548
|
font-size: 13px;
|
|
545
549
|
line-height: 20px;
|
|
546
|
-
color: var(--
|
|
547
|
-
background: var(--
|
|
550
|
+
color: var(--dsq-label);
|
|
551
|
+
background: var(--dsq-fill-hover);
|
|
548
552
|
outline: none;
|
|
549
553
|
}
|
|
550
554
|
|
|
551
555
|
.dsq_annEditInput::placeholder {
|
|
552
|
-
color: var(--
|
|
556
|
+
color: var(--dsq-label-3);
|
|
553
557
|
}
|
|
554
558
|
|
|
555
559
|
.dsq_annEditActions {
|
|
556
|
-
display: flex;
|
|
557
560
|
justify-content: flex-end;
|
|
558
|
-
align-items: center;
|
|
559
561
|
gap: 8px;
|
|
562
|
+
margin-top: 4px;
|
|
560
563
|
}
|
|
561
564
|
|
|
562
565
|
.dsq_annEditCancel {
|
|
563
|
-
|
|
564
|
-
border: none;
|
|
565
|
-
background: transparent;
|
|
566
|
-
color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, 0.65));
|
|
566
|
+
color: var(--dsq-label-2);
|
|
567
567
|
font: inherit;
|
|
568
568
|
font-size: 13px;
|
|
569
|
-
padding:
|
|
570
|
-
border-radius:
|
|
571
|
-
cursor: pointer;
|
|
569
|
+
padding: 4px 12px;
|
|
570
|
+
border-radius: 10px;
|
|
572
571
|
}
|
|
573
572
|
|
|
574
573
|
.dsq_annEditCancel:hover {
|
|
575
|
-
background: var(--
|
|
574
|
+
background: var(--dsq-fill-hover);
|
|
576
575
|
}
|
|
577
576
|
|
|
578
577
|
.dsq_annEditSave {
|
|
579
578
|
appearance: none;
|
|
580
579
|
border: none;
|
|
581
|
-
background: var(--
|
|
580
|
+
background: var(--dsq-accent);
|
|
582
581
|
color: #fff;
|
|
583
582
|
font: inherit;
|
|
584
583
|
font-size: 13px;
|
|
585
584
|
font-weight: 500;
|
|
586
|
-
padding:
|
|
587
|
-
border-radius:
|
|
585
|
+
padding: 4px 12px;
|
|
586
|
+
border-radius: 10px;
|
|
588
587
|
cursor: pointer;
|
|
589
588
|
transition: filter 0.12s;
|
|
590
589
|
}
|
|
591
590
|
|
|
592
591
|
.dsq_annEditSave:hover {
|
|
593
|
-
|
|
592
|
+
background: var(--dsq-accent-400);
|
|
594
593
|
}
|
|
595
594
|
|
|
596
595
|
.dsq_annEditSave:disabled {
|
|
@@ -604,36 +603,32 @@ const CSS = `
|
|
|
604
603
|
z-index: 10050;
|
|
605
604
|
transform: translate(-50%, 4px);
|
|
606
605
|
width: min(320px, calc(100vw - 24px));
|
|
607
|
-
display: flex;
|
|
608
|
-
flex-direction: column;
|
|
609
606
|
gap: 8px;
|
|
610
|
-
padding:
|
|
611
|
-
border:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
backdrop-filter: blur(14px) saturate(1.4);
|
|
607
|
+
padding: 8px;
|
|
608
|
+
border-radius: 16px;
|
|
609
|
+
background: color-mix(in srgb, var(--dsq-bg-raised) 75%, transparent);
|
|
610
|
+
backdrop-filter: blur(10px) saturate(1.4);
|
|
615
611
|
-webkit-backdrop-filter: blur(14px) saturate(1.4);
|
|
616
|
-
box-shadow: var(--
|
|
612
|
+
box-shadow: var(--dsq-shadow-pop);
|
|
617
613
|
}
|
|
618
614
|
|
|
619
615
|
.dsq_commentInput {
|
|
620
|
-
box-sizing: border-box;
|
|
621
616
|
width: 100%;
|
|
622
617
|
min-height: 64px;
|
|
623
618
|
resize: none;
|
|
624
619
|
border: none;
|
|
625
|
-
border-radius:
|
|
620
|
+
border-radius: 8px;
|
|
626
621
|
padding: 8px 10px;
|
|
627
622
|
font: inherit;
|
|
628
623
|
font-size: 13px;
|
|
629
624
|
line-height: 20px;
|
|
630
|
-
color: var(--
|
|
631
|
-
background: var(--
|
|
625
|
+
color: var(--dsq-label);
|
|
626
|
+
background: var(--dsq-fill-hover);
|
|
632
627
|
outline: none;
|
|
633
628
|
}
|
|
634
629
|
|
|
635
630
|
.dsq_commentInput::placeholder {
|
|
636
|
-
color: var(--
|
|
631
|
+
color: var(--dsq-label-3);
|
|
637
632
|
}
|
|
638
633
|
|
|
639
634
|
.dsq_commentActions {
|
|
@@ -659,104 +654,38 @@ const CSS = `
|
|
|
659
654
|
margin-right: 0;
|
|
660
655
|
}
|
|
661
656
|
|
|
657
|
+
/* Transcript-only differences from the shared card. Unlike the composer
|
|
658
|
+
summary (one collapsed row per message) each history card shows its own
|
|
659
|
+
comment, so the fixed height and single-line clamps are lifted on \`.dsq_tCardButton\`. */
|
|
662
660
|
.dsq_tCard {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
box-sizing: border-box;
|
|
666
|
-
display: flex;
|
|
667
|
-
align-items: center;
|
|
668
|
-
gap: 10px;
|
|
669
|
-
width: fit-content;
|
|
670
|
-
max-width: min(calc(var(--dsh-chat-content-width, 748px) * 0.72), 520px);
|
|
671
|
-
min-width: 0;
|
|
672
|
-
height: 64px;
|
|
673
|
-
margin: 0 0 0 auto;
|
|
674
|
-
padding: 10px;
|
|
675
|
-
border: 0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
676
|
-
border-radius: 18px;
|
|
677
|
-
background: var(--dsq-card-fill);
|
|
678
|
-
color: var(--dsw-alias-label-primary, rgba(0, 0, 0, 0.88));
|
|
679
|
-
transition: background-color 0.12s;
|
|
680
|
-
position: relative;
|
|
681
|
-
overflow: hidden;
|
|
682
|
-
user-select: none;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
.dsq_tCard:hover {
|
|
686
|
-
background: var(--dsq-card-hover);
|
|
661
|
+
max-width: min(calc(var(--dsq-chat-width) * 0.72), 520px);
|
|
662
|
+
margin-left: auto;
|
|
687
663
|
}
|
|
688
664
|
|
|
689
665
|
.dsq_tCardIcon {
|
|
690
|
-
z-index: 2;
|
|
691
|
-
flex: none;
|
|
692
666
|
width: 40px;
|
|
693
667
|
height: 40px;
|
|
694
|
-
border: 0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
695
|
-
border-radius: 12px;
|
|
696
|
-
display: grid;
|
|
697
|
-
place-items: center;
|
|
698
|
-
background: var(--dsq-card-fill);
|
|
699
|
-
color: var(--dsw-alias-link, rgba(0, 0, 0, 0.65));
|
|
700
|
-
font-size: 14px;
|
|
701
|
-
font-weight: 600;
|
|
702
|
-
letter-spacing: -0.02em;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
.dsq_tCardBody {
|
|
706
|
-
z-index: 2;
|
|
707
|
-
flex: 1;
|
|
708
|
-
min-width: 0;
|
|
709
|
-
display: flex;
|
|
710
|
-
flex-direction: column;
|
|
711
|
-
justify-content: center;
|
|
712
|
-
gap: 2px;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.dsq_tCardTitle {
|
|
716
668
|
font-size: 14px;
|
|
717
|
-
font-weight: 500;
|
|
718
|
-
line-height: 22px;
|
|
719
|
-
color: var(--dsw-alias-label-primary, rgba(0, 0, 0, 0.88));
|
|
720
|
-
overflow: hidden;
|
|
721
|
-
text-overflow: ellipsis;
|
|
722
|
-
white-space: nowrap;
|
|
723
669
|
}
|
|
724
670
|
|
|
725
|
-
.dsq_tCardSubtitle {
|
|
726
|
-
font-size: 12px;
|
|
727
|
-
line-height: 18px;
|
|
728
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
|
|
729
|
-
overflow: hidden;
|
|
730
|
-
text-overflow: ellipsis;
|
|
731
|
-
white-space: nowrap;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/* Transcript card wrapper: the card itself is a click target that scrolls back
|
|
735
|
-
to the marked text. Unlike the composer summary (one collapsed row per
|
|
736
|
-
message) each history card shows its own comment, so the fixed 64px height
|
|
737
|
-
and the single-line clamps are lifted here. */
|
|
738
671
|
.dsq_tCardButton {
|
|
739
|
-
appearance: none;
|
|
740
|
-
display: block;
|
|
741
672
|
/* Compact cards: several fit in one row before wrapping. */
|
|
673
|
+
display: block;
|
|
742
674
|
flex: 0 1 auto;
|
|
743
675
|
width: 230px;
|
|
744
676
|
max-width: 100%;
|
|
745
677
|
padding: 0;
|
|
746
|
-
border: none;
|
|
747
|
-
background: transparent;
|
|
748
678
|
font: inherit;
|
|
749
679
|
text-align: left;
|
|
750
|
-
cursor: pointer;
|
|
751
680
|
border-radius: 14px;
|
|
752
681
|
}
|
|
753
682
|
|
|
754
683
|
.dsq_tCardButton:focus-visible {
|
|
755
|
-
outline: 2px solid var(--
|
|
684
|
+
outline: 2px solid var(--dsq-accent);
|
|
756
685
|
outline-offset: 2px;
|
|
757
686
|
}
|
|
758
687
|
|
|
759
|
-
.dsq_tCardButton .
|
|
688
|
+
.dsq_tCardButton .dsq_card {
|
|
760
689
|
height: auto;
|
|
761
690
|
min-height: 0;
|
|
762
691
|
align-items: flex-start;
|
|
@@ -766,14 +695,14 @@ const CSS = `
|
|
|
766
695
|
border-radius: 14px;
|
|
767
696
|
}
|
|
768
697
|
|
|
769
|
-
.dsq_tCardButton .
|
|
698
|
+
.dsq_tCardButton .dsq_cardIcon {
|
|
770
699
|
width: 22px;
|
|
771
700
|
height: 22px;
|
|
772
701
|
border-radius: 8px;
|
|
773
702
|
font-size: 11px;
|
|
774
703
|
}
|
|
775
704
|
|
|
776
|
-
.dsq_tCardButton .
|
|
705
|
+
.dsq_tCardButton .dsq_cardTitle {
|
|
777
706
|
display: -webkit-box;
|
|
778
707
|
-webkit-line-clamp: 2;
|
|
779
708
|
-webkit-box-orient: vertical;
|
|
@@ -784,7 +713,7 @@ const CSS = `
|
|
|
784
713
|
line-height: 18px;
|
|
785
714
|
}
|
|
786
715
|
|
|
787
|
-
.dsq_tCardButton .
|
|
716
|
+
.dsq_tCardButton .dsq_cardSubtitle {
|
|
788
717
|
display: -webkit-box;
|
|
789
718
|
-webkit-line-clamp: 3;
|
|
790
719
|
-webkit-box-orient: vertical;
|
|
@@ -798,24 +727,18 @@ const CSS = `
|
|
|
798
727
|
|
|
799
728
|
/* A comment is the user's own words — give it more presence than the neutral
|
|
800
729
|
"no comment" placeholder. */
|
|
801
|
-
.
|
|
802
|
-
color: var(--
|
|
730
|
+
.dsq_cardSubtitle[data-dsq-has-comment='true'] {
|
|
731
|
+
color: var(--dsq-label-2);
|
|
803
732
|
}
|
|
804
733
|
|
|
805
734
|
/* Replacement user bubble (quote block stripped from display). */
|
|
806
735
|
.dsq_userRow {
|
|
807
|
-
display: flex;
|
|
808
|
-
flex-direction: column;
|
|
809
|
-
align-items: flex-end;
|
|
810
736
|
gap: 6px;
|
|
811
737
|
}
|
|
812
738
|
|
|
813
739
|
.dsq_userStack {
|
|
814
740
|
min-width: 0;
|
|
815
|
-
max-width: min(calc(var(--
|
|
816
|
-
display: flex;
|
|
817
|
-
flex-direction: column;
|
|
818
|
-
align-items: flex-end;
|
|
741
|
+
max-width: min(calc(var(--dsq-chat-width) * 0.702), 82%);
|
|
819
742
|
gap: 8px;
|
|
820
743
|
}
|
|
821
744
|
|
|
@@ -823,48 +746,28 @@ const CSS = `
|
|
|
823
746
|
max-width: 100%;
|
|
824
747
|
padding: 10px 16px;
|
|
825
748
|
border-radius: 22px;
|
|
826
|
-
background: var(--
|
|
827
|
-
color: var(--
|
|
828
|
-
font-size: var(--
|
|
829
|
-
line-height: calc(22px + var(--
|
|
749
|
+
background: var(--dsq-bubble);
|
|
750
|
+
color: var(--dsq-label);
|
|
751
|
+
font-size: var(--dsq-font-size);
|
|
752
|
+
line-height: calc(22px + var(--dsq-font-delta));
|
|
830
753
|
white-space: pre-wrap;
|
|
831
754
|
word-break: break-word;
|
|
832
755
|
}
|
|
833
756
|
|
|
834
|
-
/* Reserve room for the floating quote cards inside the composer card, so the
|
|
835
|
-
attachment rail and the editor flow below them instead of being covered. */
|
|
836
|
-
.dsq_cardPad {
|
|
837
|
-
padding-top: var(--dsq-quote-pad, 88px) !important;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
757
|
/* Durable message images of a replacement user bubble. */
|
|
841
758
|
.dsq_userImages {
|
|
842
|
-
display: flex;
|
|
843
|
-
flex-direction: column;
|
|
844
|
-
align-items: flex-end;
|
|
845
|
-
gap: 8px;
|
|
846
759
|
max-width: 100%;
|
|
847
760
|
}
|
|
848
761
|
|
|
849
|
-
/* Dark theme: the product flips its palette on body[data-ds-dark-theme], and
|
|
850
|
-
every --dsw-alias-* token follows it. Only the card fill is ours. */
|
|
851
|
-
body[data-ds-dark-theme] .dsq_card,
|
|
852
|
-
body[data-ds-dark-theme] .dsq_tCard {
|
|
853
|
-
--dsq-card-fill: var(--dsw-static-neutral-850, #212123);
|
|
854
|
-
--dsq-card-hover: var(--dsw-static-neutral-800, #292929);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
762
|
/* Model-written :dsh-annotation{index="N"} chips. */
|
|
858
763
|
.dsq_annDirective {
|
|
859
764
|
appearance: none;
|
|
860
|
-
display: inline-flex;
|
|
861
|
-
align-items: center;
|
|
862
765
|
margin: 0 2px;
|
|
863
766
|
padding: 0 6px;
|
|
864
|
-
border: 0.5px solid var(--
|
|
767
|
+
border: 0.5px solid var(--dsq-line);
|
|
865
768
|
border-radius: 6px;
|
|
866
|
-
background: var(--
|
|
867
|
-
color: var(--
|
|
769
|
+
background: var(--dsq-fill-hover);
|
|
770
|
+
color: var(--dsq-link);
|
|
868
771
|
font: inherit;
|
|
869
772
|
font-size: 12px;
|
|
870
773
|
line-height: 18px;
|
|
@@ -872,14 +775,10 @@ body[data-ds-dark-theme] .dsq_tCard {
|
|
|
872
775
|
vertical-align: baseline;
|
|
873
776
|
}
|
|
874
777
|
|
|
875
|
-
.dsq_annDirective:hover {
|
|
876
|
-
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08));
|
|
877
|
-
}
|
|
878
|
-
|
|
879
778
|
/* Inline annotation mark in message bodies: highlight underline + ordinal badge. */
|
|
880
779
|
.dsq_annMark {
|
|
881
|
-
--dsq-mark-color: var(--
|
|
882
|
-
--dsq-mark-tint: var(--
|
|
780
|
+
--dsq-mark-color: var(--dsq-accent);
|
|
781
|
+
--dsq-mark-tint: var(--dsq-accent-tint);
|
|
883
782
|
background: var(--dsq-mark-tint);
|
|
884
783
|
text-decoration: underline;
|
|
885
784
|
text-decoration-style: dashed;
|
|
@@ -912,13 +811,36 @@ body[data-ds-dark-theme] .dsq_tCard {
|
|
|
912
811
|
}
|
|
913
812
|
|
|
914
813
|
.dsq_annMark:hover {
|
|
915
|
-
background: var(--
|
|
814
|
+
background: var(--dsq-accent-tint-hover);
|
|
916
815
|
}
|
|
917
816
|
|
|
918
817
|
.dsq_annMarkFlash {
|
|
919
818
|
animation: dsq-ann-flash 1.4s ease-out;
|
|
920
819
|
}
|
|
921
820
|
|
|
821
|
+
/* Layer 4 — host surface: keyframes, theme flips, and the two rules that reach
|
|
822
|
+
into product-owned DOM. */
|
|
823
|
+
|
|
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
|
+
|
|
922
844
|
@keyframes dsq-ann-flash {
|
|
923
845
|
0% {
|
|
924
846
|
background: var(--dsq-mark-color);
|
|
@@ -930,8 +852,22 @@ body[data-ds-dark-theme] .dsq_tCard {
|
|
|
930
852
|
}
|
|
931
853
|
}
|
|
932
854
|
|
|
855
|
+
/* The product flips its palette on body[data-ds-dark-theme], and every
|
|
856
|
+
--dsq-* alias follows it. Only values we own ourselves need restating — the
|
|
857
|
+
transcript card carries .dsq_card too, so one selector covers both. */
|
|
858
|
+
body[data-ds-dark-theme] .dsq_card {
|
|
859
|
+
--dsq-card-fill: var(--dsq-neutral-850);
|
|
860
|
+
--dsq-card-hover: var(--dsq-neutral-800);
|
|
861
|
+
}
|
|
862
|
+
|
|
933
863
|
body[data-ds-dark-theme] .dsq_annMark {
|
|
934
|
-
--dsq-mark-tint: var(--
|
|
864
|
+
--dsq-mark-tint: var(--dsq-accent-tint-dark);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/* Reserve room for the floating quote cards inside the composer card, so the
|
|
868
|
+
attachment rail and the editor flow below them instead of being covered. */
|
|
869
|
+
.dsq_cardPad {
|
|
870
|
+
padding-top: var(--dsq-quote-pad, 88px) !important;
|
|
935
871
|
}
|
|
936
872
|
`;
|
|
937
873
|
const TAG_ID = "dsh-select-quote/toolbar.css";
|
|
@@ -947,6 +883,7 @@ function ensureToolbarStyles() {
|
|
|
947
883
|
const styles = {
|
|
948
884
|
toolbar: "dsq_toolbar",
|
|
949
885
|
button: "dsq_button",
|
|
886
|
+
buttonAddComment: "dsq_buttonAddComment",
|
|
950
887
|
divider: "dsq_divider",
|
|
951
888
|
status: "dsq_status",
|
|
952
889
|
cardStack: "dsq_cardStack",
|
|
@@ -992,9 +929,6 @@ const styles = {
|
|
|
992
929
|
tCardButton: "dsq_tCardButton",
|
|
993
930
|
tCard: "dsq_tCard",
|
|
994
931
|
tCardIcon: "dsq_tCardIcon",
|
|
995
|
-
tCardBody: "dsq_tCardBody",
|
|
996
|
-
tCardTitle: "dsq_tCardTitle",
|
|
997
|
-
tCardSubtitle: "dsq_tCardSubtitle",
|
|
998
932
|
userRow: "dsq_userRow",
|
|
999
933
|
userStack: "dsq_userStack",
|
|
1000
934
|
userImages: "dsq_userImages",
|
|
@@ -1041,9 +975,17 @@ function collectCleanText(root) {
|
|
|
1041
975
|
return entries;
|
|
1042
976
|
}
|
|
1043
977
|
function flowItemOf(node) {
|
|
1044
|
-
const item = (node instanceof Element ? node : node
|
|
978
|
+
const item = (node instanceof Element ? node : node?.parentElement)?.closest(FLOW_ITEM);
|
|
1045
979
|
return item instanceof HTMLElement ? item : null;
|
|
1046
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* True when the node lives inside a conversation message. Selections in the
|
|
983
|
+
* sidebar, settings, model picker or any other app chrome are not quotes, so
|
|
984
|
+
* the toolbar must stay away from them.
|
|
985
|
+
*/
|
|
986
|
+
function isInsideTranscript(node) {
|
|
987
|
+
return flowItemOf(node) !== null;
|
|
988
|
+
}
|
|
1047
989
|
function pointOffset(entries, container, offset) {
|
|
1048
990
|
for (const entry of entries) if (entry.node === container) return entry.start + Math.min(offset, entry.end - entry.start);
|
|
1049
991
|
if (!(container instanceof Element)) return null;
|
|
@@ -1383,22 +1325,6 @@ function findElementByText(text) {
|
|
|
1383
1325
|
}
|
|
1384
1326
|
return best;
|
|
1385
1327
|
}
|
|
1386
|
-
//#endregion
|
|
1387
|
-
//#region src/client/runtime.ts
|
|
1388
|
-
/**
|
|
1389
|
-
* Shared runtime handles across slot scopes.
|
|
1390
|
-
* The selection toolbar mounts at root (`shell.overlay`) while the annotation
|
|
1391
|
-
* panel is session-scoped — pending annotations use one shared store key so
|
|
1392
|
-
* both sides always read the same list.
|
|
1393
|
-
*/
|
|
1394
|
-
const runtime = {};
|
|
1395
|
-
function setRuntimeSession(sessionId) {
|
|
1396
|
-
if (sessionId) runtime.sessionId = sessionId;
|
|
1397
|
-
}
|
|
1398
|
-
/** Store partition for pending annotations (single active composer). */
|
|
1399
|
-
function annotationStoreKey() {
|
|
1400
|
-
return "pending";
|
|
1401
|
-
}
|
|
1402
1328
|
const DRAFT_MARKER_RE = /\u200B/g;
|
|
1403
1329
|
/** Plain text from Chat user-node content blocks (string or block array). */
|
|
1404
1330
|
function contentBlocksToText(content) {
|
|
@@ -1591,11 +1517,27 @@ function scanAnnotatedMessage(text) {
|
|
|
1591
1517
|
};
|
|
1592
1518
|
}
|
|
1593
1519
|
//#endregion
|
|
1520
|
+
//#region src/client/runtime.ts
|
|
1521
|
+
/**
|
|
1522
|
+
* Shared runtime handles across slot scopes.
|
|
1523
|
+
*
|
|
1524
|
+
* The selection toolbar mounts at root (`shell.overlay`) while the annotation
|
|
1525
|
+
* panel is session-scoped, so both sides read one partition keyed by the
|
|
1526
|
+
* session the host minted for the active composer.
|
|
1527
|
+
*/
|
|
1528
|
+
const runtime = {};
|
|
1529
|
+
function setRuntimeSession(sessionId) {
|
|
1530
|
+
if (sessionId) runtime.sessionId = sessionId;
|
|
1531
|
+
}
|
|
1532
|
+
//#endregion
|
|
1594
1533
|
//#region src/client/state/annotation-store.ts
|
|
1595
1534
|
/** Per-session pending selection annotations (composer-side store). */
|
|
1596
1535
|
const EMPTY$1 = [];
|
|
1536
|
+
/** Bucket for annotations added before a session id is known (never persisted). */
|
|
1537
|
+
const MEMORY_KEY = "pending";
|
|
1597
1538
|
const bySession = /* @__PURE__ */ new Map();
|
|
1598
1539
|
const listeners = /* @__PURE__ */ new Set();
|
|
1540
|
+
let persistence = null;
|
|
1599
1541
|
function notify() {
|
|
1600
1542
|
for (const listener of [...listeners]) listener();
|
|
1601
1543
|
}
|
|
@@ -1605,12 +1547,25 @@ function subscribeAnnotations(listener) {
|
|
|
1605
1547
|
listeners.delete(listener);
|
|
1606
1548
|
};
|
|
1607
1549
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1550
|
+
/**
|
|
1551
|
+
* Store partition: the live session when one is bound, otherwise a memory-only
|
|
1552
|
+
* bucket. The host always mints a session before the composer accepts text, so
|
|
1553
|
+
* the bucket is the "cannot happen" path and never reaches storage.
|
|
1554
|
+
*/
|
|
1555
|
+
function partition(sessionId) {
|
|
1556
|
+
return sessionId ?? runtime.sessionId ?? MEMORY_KEY;
|
|
1557
|
+
}
|
|
1558
|
+
/** Attach the persistence port (null detaches it). */
|
|
1559
|
+
function setAnnotationPersistence(port) {
|
|
1560
|
+
persistence = port;
|
|
1561
|
+
}
|
|
1562
|
+
function persist(partitionKey) {
|
|
1563
|
+
if (!persistence || partitionKey === MEMORY_KEY) return;
|
|
1564
|
+
persistence.save(partitionKey, bySession.get(partitionKey) ?? EMPTY$1);
|
|
1610
1565
|
}
|
|
1611
1566
|
/** Append one selection; an identical text+comment pair is reused. */
|
|
1612
1567
|
function saveAnnotation(sessionId, input) {
|
|
1613
|
-
const k =
|
|
1568
|
+
const k = partition(sessionId);
|
|
1614
1569
|
const comment = input.comment?.trim() ?? "";
|
|
1615
1570
|
const current = bySession.get(k) ?? EMPTY$1;
|
|
1616
1571
|
const duplicate = current.find((item) => item.text === input.text && (item.comment ?? "") === comment);
|
|
@@ -1622,14 +1577,15 @@ function saveAnnotation(sessionId, input) {
|
|
|
1622
1577
|
sources: input.sources?.length ? input.sources : [{ text: input.text }]
|
|
1623
1578
|
};
|
|
1624
1579
|
bySession.set(k, [...current, annotation]);
|
|
1580
|
+
persist(k);
|
|
1625
1581
|
notify();
|
|
1626
1582
|
return annotation;
|
|
1627
1583
|
}
|
|
1628
1584
|
function getAnnotations(sessionId) {
|
|
1629
|
-
return bySession.get(
|
|
1585
|
+
return bySession.get(partition(sessionId)) ?? EMPTY$1;
|
|
1630
1586
|
}
|
|
1631
1587
|
function updateAnnotationComment(sessionId, id, comment) {
|
|
1632
|
-
const k =
|
|
1588
|
+
const k = partition(sessionId);
|
|
1633
1589
|
const current = bySession.get(k);
|
|
1634
1590
|
if (!current) return;
|
|
1635
1591
|
const next = current.map((item) => {
|
|
@@ -1644,20 +1600,59 @@ function updateAnnotationComment(sessionId, id, comment) {
|
|
|
1644
1600
|
};
|
|
1645
1601
|
});
|
|
1646
1602
|
bySession.set(k, next);
|
|
1603
|
+
persist(k);
|
|
1647
1604
|
notify();
|
|
1648
1605
|
}
|
|
1649
1606
|
function removeAnnotation(sessionId, id) {
|
|
1650
|
-
const k =
|
|
1607
|
+
const k = partition(sessionId);
|
|
1651
1608
|
const current = bySession.get(k);
|
|
1652
1609
|
if (!current) return;
|
|
1653
1610
|
const next = current.filter((item) => item.id !== id);
|
|
1654
1611
|
if (next.length === current.length) return;
|
|
1655
1612
|
if (next.length === 0) bySession.delete(k);
|
|
1656
1613
|
else bySession.set(k, next);
|
|
1614
|
+
if (next.length === 0 && k !== MEMORY_KEY) persistence?.clear(k);
|
|
1615
|
+
else persist(k);
|
|
1657
1616
|
notify();
|
|
1658
1617
|
}
|
|
1659
1618
|
function clearAnnotations(sessionId) {
|
|
1660
|
-
|
|
1619
|
+
const k = partition(sessionId);
|
|
1620
|
+
if (!bySession.delete(k)) return;
|
|
1621
|
+
if (k !== MEMORY_KEY) persistence?.clear(k);
|
|
1622
|
+
notify();
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Bind the active session and adopt whatever it left unsent.
|
|
1626
|
+
*
|
|
1627
|
+
* Called from render-phase bridges, so adoption is deferred to a microtask:
|
|
1628
|
+
* the resulting notify() must not land inside another component's render.
|
|
1629
|
+
* Re-binding the same session is a no-op.
|
|
1630
|
+
*/
|
|
1631
|
+
function bindAnnotationSession(sessionId) {
|
|
1632
|
+
if (!sessionId || sessionId === runtime.sessionId) return;
|
|
1633
|
+
setRuntimeSession(sessionId);
|
|
1634
|
+
queueMicrotask(() => adoptPersistedAnnotations(sessionId));
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Adopt persisted annotations for a session.
|
|
1638
|
+
*
|
|
1639
|
+
* In-memory entries win on conflict — they can only be newer, because anything
|
|
1640
|
+
* added after the bind is persisted immediately. Annotations parked in the
|
|
1641
|
+
* memory-only bucket (added before the bind) move over as well.
|
|
1642
|
+
*/
|
|
1643
|
+
function adoptPersistedAnnotations(sessionId) {
|
|
1644
|
+
if (!persistence) return;
|
|
1645
|
+
const stored = persistence.load(sessionId);
|
|
1646
|
+
const current = bySession.get(sessionId) ?? EMPTY$1;
|
|
1647
|
+
const orphaned = bySession.get(MEMORY_KEY) ?? EMPTY$1;
|
|
1648
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1649
|
+
for (const item of stored) merged.set(item.id, item);
|
|
1650
|
+
for (const item of [...current, ...orphaned]) merged.set(item.id, item);
|
|
1651
|
+
const next = [...merged.values()];
|
|
1652
|
+
if (next.length === current.length && next.every((item, i) => item.id === current[i]?.id) && orphaned.length === 0) return;
|
|
1653
|
+
bySession.set(sessionId, next);
|
|
1654
|
+
if (orphaned.length > 0) bySession.delete(MEMORY_KEY);
|
|
1655
|
+
notify();
|
|
1661
1656
|
}
|
|
1662
1657
|
function annotationTitle(annotation) {
|
|
1663
1658
|
return previewTitle(annotation.text);
|
|
@@ -1966,23 +1961,23 @@ function AnnotationCardView({ title, text, comment, variant, index, onRemove, on
|
|
|
1966
1961
|
const composer = variant === "composer";
|
|
1967
1962
|
const subtitle = comment ? composer ? t("card.commentSubtitle", { comment }) : comment : composer ? t("common.selectedText") : t("summary.noComment");
|
|
1968
1963
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1969
|
-
className:
|
|
1964
|
+
className: cx(A.box, A.row, styles.card, !composer && styles.tCard),
|
|
1970
1965
|
role: "group",
|
|
1971
1966
|
"aria-label": index ? t("card.ariaIndexed", { index }) : t("card.ariaPlain"),
|
|
1972
1967
|
children: [
|
|
1973
1968
|
(0, react_jsx_runtime.jsx)("div", {
|
|
1974
|
-
className:
|
|
1969
|
+
className: cx(A.keep, A.gridMid, styles.cardIcon, !composer && styles.tCardIcon),
|
|
1975
1970
|
"aria-hidden": true,
|
|
1976
1971
|
children: index ? String(index) : "AI"
|
|
1977
1972
|
}),
|
|
1978
1973
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
1979
|
-
className:
|
|
1974
|
+
className: cx(A.grow, A.col, styles.cardBody),
|
|
1980
1975
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
1981
|
-
className:
|
|
1976
|
+
className: cx(A.ell, styles.cardTitle),
|
|
1982
1977
|
title: text,
|
|
1983
1978
|
children: title
|
|
1984
1979
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
1985
|
-
className:
|
|
1980
|
+
className: cx(A.ell, styles.cardSubtitle),
|
|
1986
1981
|
...comment ? {
|
|
1987
1982
|
title: comment,
|
|
1988
1983
|
"data-dsq-has-comment": "true"
|
|
@@ -1992,14 +1987,14 @@ function AnnotationCardView({ title, text, comment, variant, index, onRemove, on
|
|
|
1992
1987
|
}),
|
|
1993
1988
|
composer && onEditComment ? (0, react_jsx_runtime.jsx)("button", {
|
|
1994
1989
|
type: "button",
|
|
1995
|
-
className: styles.cardAction,
|
|
1990
|
+
className: cx(A.plain, A.gridMid, styles.cardAction),
|
|
1996
1991
|
"aria-label": t("card.editComment"),
|
|
1997
1992
|
onClick: onEditComment,
|
|
1998
1993
|
children: "✎"
|
|
1999
1994
|
}) : null,
|
|
2000
1995
|
composer && onRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
2001
1996
|
type: "button",
|
|
2002
|
-
className: styles.cardClose,
|
|
1997
|
+
className: cx(A.plain, A.gridMid, styles.cardClose),
|
|
2003
1998
|
"aria-label": t("card.remove"),
|
|
2004
1999
|
onClick: onRemove,
|
|
2005
2000
|
children: "×"
|
|
@@ -2183,16 +2178,16 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2183
2178
|
onPointerEnter: openPop,
|
|
2184
2179
|
onPointerLeave,
|
|
2185
2180
|
children: (0, react_jsx_runtime.jsx)("div", {
|
|
2186
|
-
className: styles.summaryHoverInner,
|
|
2181
|
+
className: cx(A.col, styles.summaryHoverInner),
|
|
2187
2182
|
children: items.map((item, i) => {
|
|
2188
2183
|
const isEditing = editing === i;
|
|
2189
2184
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2190
2185
|
className: styles.summaryItem,
|
|
2191
2186
|
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
2192
|
-
className: styles.summaryItemHead,
|
|
2187
|
+
className: cx(A.row, styles.summaryItemHead),
|
|
2193
2188
|
children: [
|
|
2194
2189
|
(0, react_jsx_runtime.jsx)("div", {
|
|
2195
|
-
className: styles.summaryItemBadge,
|
|
2190
|
+
className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
|
|
2196
2191
|
"aria-hidden": true,
|
|
2197
2192
|
children: String(i + 1)
|
|
2198
2193
|
}),
|
|
@@ -2201,16 +2196,16 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2201
2196
|
children: t("common.selectedText")
|
|
2202
2197
|
}),
|
|
2203
2198
|
!isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
|
|
2204
|
-
className: styles.summaryItemActions,
|
|
2199
|
+
className: cx(A.row, styles.summaryItemActions),
|
|
2205
2200
|
children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
|
|
2206
2201
|
type: "button",
|
|
2207
|
-
className: styles.summaryItemAction,
|
|
2202
|
+
className: cx(A.plain, A.gridMid, styles.summaryItemAction),
|
|
2208
2203
|
"aria-label": t("summary.edit", { index: i + 1 }),
|
|
2209
2204
|
onClick: () => startEdit(i),
|
|
2210
2205
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
|
|
2211
2206
|
}) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
|
|
2212
2207
|
type: "button",
|
|
2213
|
-
className: styles.summaryItemAction,
|
|
2208
|
+
className: cx(A.plain, A.gridMid, styles.summaryItemAction),
|
|
2214
2209
|
"aria-label": t("summary.remove", { index: i + 1 }),
|
|
2215
2210
|
onClick: () => onItemRemove(i, item),
|
|
2216
2211
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
|
|
@@ -2230,7 +2225,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2230
2225
|
}),
|
|
2231
2226
|
(0, react_jsx_runtime.jsx)("textarea", {
|
|
2232
2227
|
ref: editInputRef,
|
|
2233
|
-
className: styles.annEditInput,
|
|
2228
|
+
className: cx(A.box, styles.annEditInput),
|
|
2234
2229
|
placeholder: t("common.commentPlaceholder"),
|
|
2235
2230
|
value: editDraft,
|
|
2236
2231
|
onChange: (e) => setEditDraft(e.target.value),
|
|
@@ -2247,10 +2242,10 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2247
2242
|
}
|
|
2248
2243
|
}),
|
|
2249
2244
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
2250
|
-
className: styles.annEditActions,
|
|
2245
|
+
className: cx(A.row, styles.annEditActions),
|
|
2251
2246
|
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
2252
2247
|
type: "button",
|
|
2253
|
-
className: styles.annEditCancel,
|
|
2248
|
+
className: cx(A.plain, styles.annEditCancel),
|
|
2254
2249
|
onClick: () => setEditing(null),
|
|
2255
2250
|
children: t("common.cancel")
|
|
2256
2251
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -2265,7 +2260,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2265
2260
|
className: styles.summaryItemMain,
|
|
2266
2261
|
children: (0, react_jsx_runtime.jsxs)("button", {
|
|
2267
2262
|
type: "button",
|
|
2268
|
-
className: styles.summaryItemButton,
|
|
2263
|
+
className: cx(A.plain, styles.summaryItemButton),
|
|
2269
2264
|
onClick: () => {
|
|
2270
2265
|
onItemSelect?.(i, item);
|
|
2271
2266
|
closePop();
|
|
@@ -2292,7 +2287,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2292
2287
|
}) : null;
|
|
2293
2288
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2294
2289
|
ref: rootRef,
|
|
2295
|
-
className: styles.summary,
|
|
2290
|
+
className: cx(A.row, styles.summary),
|
|
2296
2291
|
tabIndex: 0,
|
|
2297
2292
|
"data-selection-annotation-summary": "true",
|
|
2298
2293
|
"aria-label": t("summary.aria", { count }),
|
|
@@ -2304,12 +2299,12 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2304
2299
|
onKeyDown,
|
|
2305
2300
|
children: [
|
|
2306
2301
|
(0, react_jsx_runtime.jsx)("div", {
|
|
2307
|
-
className: styles.summaryIcon,
|
|
2302
|
+
className: cx(A.keep, A.gridMid, styles.summaryIcon),
|
|
2308
2303
|
"aria-hidden": true,
|
|
2309
2304
|
children: String(count)
|
|
2310
2305
|
}),
|
|
2311
2306
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
2312
|
-
className: styles.summaryBody,
|
|
2307
|
+
className: cx(A.grow, styles.summaryBody),
|
|
2313
2308
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
2314
2309
|
className: styles.summaryTitle,
|
|
2315
2310
|
children: t("summary.title", { count })
|
|
@@ -2320,7 +2315,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
2320
2315
|
}),
|
|
2321
2316
|
onRemoveAll ? (0, react_jsx_runtime.jsx)("button", {
|
|
2322
2317
|
type: "button",
|
|
2323
|
-
className: styles.cardClose,
|
|
2318
|
+
className: cx(A.plain, A.gridMid, styles.cardClose),
|
|
2324
2319
|
"aria-label": t("summary.removeAll"),
|
|
2325
2320
|
onClick: onRemoveAll,
|
|
2326
2321
|
children: (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
@@ -2413,6 +2408,31 @@ function useSendIntercept({ itemCount, draftRef, setDraft, onFolded }) {
|
|
|
2413
2408
|
//#region src/client/composer/AnnotationPanel.tsx
|
|
2414
2409
|
const EMPTY = [];
|
|
2415
2410
|
/**
|
|
2411
|
+
* True while an IME composition is in flight.
|
|
2412
|
+
*
|
|
2413
|
+
* Composition events bubble, so a capture listener on the document sees them
|
|
2414
|
+
* wherever the composer lives. Anything that writes the draft must check this:
|
|
2415
|
+
* replacing editor content mid-composition drops the composing text.
|
|
2416
|
+
*/
|
|
2417
|
+
function useComposing() {
|
|
2418
|
+
const composing = (0, react.useRef)(false);
|
|
2419
|
+
(0, react.useEffect)(() => {
|
|
2420
|
+
const onStart = () => {
|
|
2421
|
+
composing.current = true;
|
|
2422
|
+
};
|
|
2423
|
+
const onEnd = () => {
|
|
2424
|
+
composing.current = false;
|
|
2425
|
+
};
|
|
2426
|
+
document.addEventListener("compositionstart", onStart, true);
|
|
2427
|
+
document.addEventListener("compositionend", onEnd, true);
|
|
2428
|
+
return () => {
|
|
2429
|
+
document.removeEventListener("compositionstart", onStart, true);
|
|
2430
|
+
document.removeEventListener("compositionend", onEnd, true);
|
|
2431
|
+
};
|
|
2432
|
+
}, []);
|
|
2433
|
+
return composing;
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2416
2436
|
* Composer annotation summary (`conversation.input.overlay`).
|
|
2417
2437
|
*
|
|
2418
2438
|
* Pending selections never enter the editor as text: the summary card is the
|
|
@@ -2426,6 +2446,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2426
2446
|
const draftRef = (0, react.useRef)(draft);
|
|
2427
2447
|
const actionsRef = (0, react.useRef)(inputActions);
|
|
2428
2448
|
const sessionRef = (0, react.useRef)(sessionId);
|
|
2449
|
+
const composing = useComposing();
|
|
2429
2450
|
draftRef.current = draft;
|
|
2430
2451
|
actionsRef.current = inputActions;
|
|
2431
2452
|
sessionRef.current = sessionId;
|
|
@@ -2436,7 +2457,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2436
2457
|
setItems(getAnnotations());
|
|
2437
2458
|
}, []);
|
|
2438
2459
|
(0, react.useEffect)(() => {
|
|
2439
|
-
|
|
2460
|
+
bindAnnotationSession(sessionId);
|
|
2440
2461
|
}, [sessionId]);
|
|
2441
2462
|
(0, react.useEffect)(() => {
|
|
2442
2463
|
ensureToolbarStyles();
|
|
@@ -2472,6 +2493,19 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2472
2493
|
setDraft,
|
|
2473
2494
|
onFolded: () => setItems(EMPTY)
|
|
2474
2495
|
});
|
|
2496
|
+
(0, react.useEffect)(() => {
|
|
2497
|
+
if (composing.current) return;
|
|
2498
|
+
if (items.length === 0) {
|
|
2499
|
+
if (draft.includes("")) setDraft(stripDraftMarker(draft));
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (!draft.includes("") && draft.trim() === "") setDraft(withDraftMarker(draft));
|
|
2503
|
+
}, [
|
|
2504
|
+
items,
|
|
2505
|
+
draft,
|
|
2506
|
+
setDraft,
|
|
2507
|
+
composing
|
|
2508
|
+
]);
|
|
2475
2509
|
const onRemoveAll = (0, react.useCallback)(() => {
|
|
2476
2510
|
clearAnnotations();
|
|
2477
2511
|
setItems(EMPTY);
|
|
@@ -2501,7 +2535,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
2501
2535
|
if (!sessionId || items.length === 0) return null;
|
|
2502
2536
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
2503
2537
|
ref: stackRef,
|
|
2504
|
-
className: styles.cardStack,
|
|
2538
|
+
className: cx(A.col, styles.cardStack),
|
|
2505
2539
|
children: (0, react_jsx_runtime.jsx)(AnnotationSummary, {
|
|
2506
2540
|
count: items.length,
|
|
2507
2541
|
items: items.map((item) => ({
|
|
@@ -2571,7 +2605,8 @@ function toolbarPosition(rect) {
|
|
|
2571
2605
|
};
|
|
2572
2606
|
}
|
|
2573
2607
|
/**
|
|
2574
|
-
* Read the live selection when it is a non-empty range
|
|
2608
|
+
* Read the live selection when it is a non-empty range inside a conversation
|
|
2609
|
+
* message and outside the composer / our own chrome.
|
|
2575
2610
|
*/
|
|
2576
2611
|
function readSelectionSnapshot(toolbarRoot) {
|
|
2577
2612
|
const selection = window.getSelection();
|
|
@@ -2580,6 +2615,7 @@ function readSelectionSnapshot(toolbarRoot) {
|
|
|
2580
2615
|
if (!anchorNode || !focusNode) return null;
|
|
2581
2616
|
if (isInsideComposerInput(anchorNode) || isInsideComposerInput(focusNode)) return null;
|
|
2582
2617
|
if (toolbarRoot?.contains(anchorNode) || toolbarRoot?.contains(focusNode)) return null;
|
|
2618
|
+
if (!isInsideTranscript(anchorNode) || !isInsideTranscript(focusNode)) return null;
|
|
2583
2619
|
const text = selection.toString().replace(/\r\n/g, "\n").trim();
|
|
2584
2620
|
if (!text) return null;
|
|
2585
2621
|
const rect = getRangeFirstLineRect(selection.getRangeAt(0));
|
|
@@ -2628,7 +2664,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2628
2664
|
activeRef.current = active;
|
|
2629
2665
|
(0, react.useEffect)(() => {
|
|
2630
2666
|
ensureToolbarStyles();
|
|
2631
|
-
if (sessionId)
|
|
2667
|
+
if (sessionId) bindAnnotationSession(sessionId);
|
|
2632
2668
|
}, [sessionId]);
|
|
2633
2669
|
const hide = (0, react.useCallback)(() => {
|
|
2634
2670
|
setActive(null);
|
|
@@ -2791,7 +2827,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2791
2827
|
return (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
|
|
2792
2828
|
(0, react_jsx_runtime.jsx)("div", {
|
|
2793
2829
|
ref: toolbarRef,
|
|
2794
|
-
className: styles.toolbar,
|
|
2830
|
+
className: cx(A.row, styles.toolbar),
|
|
2795
2831
|
style: toolbarStyle,
|
|
2796
2832
|
role: "toolbar",
|
|
2797
2833
|
"aria-label": t("toolbar.aria"),
|
|
@@ -2820,7 +2856,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2820
2856
|
children: [
|
|
2821
2857
|
(0, react_jsx_runtime.jsx)("button", {
|
|
2822
2858
|
type: "button",
|
|
2823
|
-
className: styles.button,
|
|
2859
|
+
className: cx(A.plain, A.inlineRow, styles.button),
|
|
2824
2860
|
onClick: () => void handleCopy(),
|
|
2825
2861
|
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2826
2862
|
name: "copy",
|
|
@@ -2833,7 +2869,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2833
2869
|
}),
|
|
2834
2870
|
(0, react_jsx_runtime.jsx)("button", {
|
|
2835
2871
|
type: "button",
|
|
2836
|
-
className: styles.button,
|
|
2872
|
+
className: cx(A.plain, A.inlineRow, styles.button),
|
|
2837
2873
|
onClick: openComment,
|
|
2838
2874
|
children: [(0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
2839
2875
|
name: "plus",
|
|
@@ -2845,14 +2881,14 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2845
2881
|
}),
|
|
2846
2882
|
phase === "comment" ? (0, react_jsx_runtime.jsxs)("div", {
|
|
2847
2883
|
ref: commentWrapRef,
|
|
2848
|
-
className: styles.commentPop,
|
|
2884
|
+
className: cx(A.col, styles.commentPop),
|
|
2849
2885
|
style: void 0,
|
|
2850
2886
|
role: "dialog",
|
|
2851
2887
|
"aria-label": t("toolbar.commentDialogAria"),
|
|
2852
2888
|
onPointerDown: handleToolbarPointerDown,
|
|
2853
2889
|
children: [(0, react_jsx_runtime.jsx)("textarea", {
|
|
2854
2890
|
ref: commentRef,
|
|
2855
|
-
className: styles.commentInput,
|
|
2891
|
+
className: cx(A.box, styles.commentInput),
|
|
2856
2892
|
placeholder: t("common.commentPlaceholder"),
|
|
2857
2893
|
value: comment,
|
|
2858
2894
|
onChange: (e) => setComment(e.target.value),
|
|
@@ -2871,12 +2907,12 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2871
2907
|
className: styles.commentActions,
|
|
2872
2908
|
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
2873
2909
|
type: "button",
|
|
2874
|
-
className: styles.button,
|
|
2910
|
+
className: cx(A.plain, A.inlineRow, styles.button),
|
|
2875
2911
|
onClick: () => commitAnnotation(void 0),
|
|
2876
2912
|
children: t("toolbar.skipComment")
|
|
2877
2913
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
2878
2914
|
type: "button",
|
|
2879
|
-
className: styles.button,
|
|
2915
|
+
className: cx(A.plain, A.inlineRow, styles.button, styles.buttonAddComment),
|
|
2880
2916
|
disabled: !comment.trim(),
|
|
2881
2917
|
onClick: () => commitAnnotation(comment),
|
|
2882
2918
|
children: t("toolbar.addComment")
|
|
@@ -2898,6 +2934,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
2898
2934
|
* Display-only decoration — durable message text is never rewritten.
|
|
2899
2935
|
*/
|
|
2900
2936
|
const DECO = "dsq-ann-directive";
|
|
2937
|
+
const DECO_KEY = "dsqAnnDirective";
|
|
2901
2938
|
function decorateTextNode(node) {
|
|
2902
2939
|
const value = node.nodeValue;
|
|
2903
2940
|
if (!value || !value.includes(":dsh-annotation{")) return;
|
|
@@ -2916,9 +2953,9 @@ function decorateTextNode(node) {
|
|
|
2916
2953
|
const index = Number(m[1]);
|
|
2917
2954
|
const chip = document.createElement("button");
|
|
2918
2955
|
chip.type = "button";
|
|
2919
|
-
chip.className =
|
|
2920
|
-
chip.dataset[DECO] = String(index);
|
|
2956
|
+
chip.className = cx(styles.annDirective, A.inlineRow);
|
|
2921
2957
|
chip.setAttribute("data-dsq-deco", "true");
|
|
2958
|
+
chip.dataset[DECO_KEY] = String(index);
|
|
2922
2959
|
chip.textContent = t("chip.annotation", { index });
|
|
2923
2960
|
chip.addEventListener("click", () => {
|
|
2924
2961
|
const mark = findAnnotationMarkBefore(index, chip);
|
|
@@ -2956,7 +2993,7 @@ function refreshDirectiveChips(root) {
|
|
|
2956
2993
|
const el = root ?? (typeof document === "undefined" ? void 0 : document.body);
|
|
2957
2994
|
if (!el) return;
|
|
2958
2995
|
for (const chip of el.querySelectorAll(`[data-${DECO}]`)) {
|
|
2959
|
-
const index = Number(chip.dataset[
|
|
2996
|
+
const index = Number(chip.dataset[DECO_KEY]);
|
|
2960
2997
|
if (Number.isFinite(index)) chip.textContent = t("chip.annotation", { index });
|
|
2961
2998
|
}
|
|
2962
2999
|
}
|
|
@@ -3017,7 +3054,7 @@ function TranscriptQuoteCard({ node }) {
|
|
|
3017
3054
|
className: styles.tCardStack,
|
|
3018
3055
|
"data-dsq-deco": "true",
|
|
3019
3056
|
children: quotes.map((quote, index) => (0, react_jsx_runtime.jsx)("div", {
|
|
3020
|
-
className: styles.tCardButton,
|
|
3057
|
+
className: cx(A.plain, styles.tCardButton),
|
|
3021
3058
|
role: "button",
|
|
3022
3059
|
tabIndex: 0,
|
|
3023
3060
|
"aria-label": quote.title,
|
|
@@ -3054,7 +3091,8 @@ function contentImages(content) {
|
|
|
3054
3091
|
* Replacement for the built-in `user` Chat node view.
|
|
3055
3092
|
*
|
|
3056
3093
|
* Strips the `<response-annotations>` protocol block from the bubble **and**
|
|
3057
|
-
* renders one card per annotation right
|
|
3094
|
+
* renders one card per annotation right above it — the same order the composer
|
|
3095
|
+
* shows them in, so the sent message reads like the draft did.
|
|
3058
3096
|
*
|
|
3059
3097
|
* Why the cards live here instead of on a `select-quote` Chat node:
|
|
3060
3098
|
* `conversation.chat.node` is gated by the shipped `TURN_PROCESS_INDEPENDENT_KINDS`
|
|
@@ -3074,12 +3112,12 @@ function UserMessageDisplay({ node, renderMessageImages }) {
|
|
|
3074
3112
|
const images = contentImages(node.data?.content);
|
|
3075
3113
|
if (!text && images.length === 0 && quotes.length === 0) return null;
|
|
3076
3114
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
3077
|
-
className: styles.userRow,
|
|
3115
|
+
className: cx(A.colEnd, styles.userRow),
|
|
3078
3116
|
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
3079
|
-
className: styles.userStack,
|
|
3117
|
+
className: cx(A.colEnd, styles.userStack),
|
|
3080
3118
|
children: [
|
|
3081
3119
|
images.length > 0 && renderMessageImages !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
3082
|
-
className: styles.userImages,
|
|
3120
|
+
className: cx(A.colEnd, styles.userImages),
|
|
3083
3121
|
"data-message-attachments": true,
|
|
3084
3122
|
children: renderMessageImages({
|
|
3085
3123
|
images,
|
|
@@ -3087,16 +3125,214 @@ function UserMessageDisplay({ node, renderMessageImages }) {
|
|
|
3087
3125
|
compact: images.length > 1
|
|
3088
3126
|
})
|
|
3089
3127
|
}, "images") : null,
|
|
3128
|
+
quotes.length > 0 ? (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }, "quotes") : null,
|
|
3090
3129
|
text ? (0, react_jsx_runtime.jsx)("div", {
|
|
3091
3130
|
className: styles.userBubble,
|
|
3092
3131
|
children: text
|
|
3093
|
-
}, "bubble") : null
|
|
3094
|
-
quotes.length > 0 ? (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, { node: { data: { quotes } } }, "quotes") : null
|
|
3132
|
+
}, "bubble") : null
|
|
3095
3133
|
]
|
|
3096
3134
|
})
|
|
3097
3135
|
});
|
|
3098
3136
|
}
|
|
3099
3137
|
//#endregion
|
|
3138
|
+
//#region src/client/state/annotation-persistence.ts
|
|
3139
|
+
/** localStorage prefix; a session id is appended as `.<sessionId>`. */
|
|
3140
|
+
const ANNOTATION_KEY_PREFIX = "dsh.select-quote.annotations";
|
|
3141
|
+
const PAYLOAD_VERSION = 1;
|
|
3142
|
+
function keyForSession(sessionId) {
|
|
3143
|
+
return `${ANNOTATION_KEY_PREFIX}.${sessionId}`;
|
|
3144
|
+
}
|
|
3145
|
+
function sessionIdOfKey(key) {
|
|
3146
|
+
const prefix = `${ANNOTATION_KEY_PREFIX}.`;
|
|
3147
|
+
return key.startsWith(prefix) ? key.slice(prefix.length) : null;
|
|
3148
|
+
}
|
|
3149
|
+
function isAnnotation(value) {
|
|
3150
|
+
if (!value || typeof value !== "object") return false;
|
|
3151
|
+
const item = value;
|
|
3152
|
+
return typeof item.id === "string" && typeof item.text === "string" && Array.isArray(item.sources);
|
|
3153
|
+
}
|
|
3154
|
+
/** Tolerant read: a corrupt or foreign payload degrades to "nothing stored". */
|
|
3155
|
+
function itemsOf(value) {
|
|
3156
|
+
if (!value || typeof value !== "object") return [];
|
|
3157
|
+
const payload = value;
|
|
3158
|
+
if (!Array.isArray(payload.items)) return [];
|
|
3159
|
+
return payload.items.filter(isAnnotation);
|
|
3160
|
+
}
|
|
3161
|
+
function encode(items, now) {
|
|
3162
|
+
return JSON.stringify({
|
|
3163
|
+
v: PAYLOAD_VERSION,
|
|
3164
|
+
updatedAt: now,
|
|
3165
|
+
items
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
function decode(raw) {
|
|
3169
|
+
if (raw === null) return [];
|
|
3170
|
+
try {
|
|
3171
|
+
return itemsOf(JSON.parse(raw));
|
|
3172
|
+
} catch {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
/** One warn per session, never a throw: a full quota must not break typing. */
|
|
3177
|
+
let quotaWarned = false;
|
|
3178
|
+
function warnQuota() {
|
|
3179
|
+
if (quotaWarned) return;
|
|
3180
|
+
quotaWarned = true;
|
|
3181
|
+
console.warn("dsh-select-quote: annotation persistence disabled (storage write failed)");
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Direct localStorage backend (the documented fallback). Writes are wrapped so
|
|
3185
|
+
* a quota failure degrades to memory-only instead of breaking the composer.
|
|
3186
|
+
*/
|
|
3187
|
+
function createStoragePersistence(storage) {
|
|
3188
|
+
let degraded = false;
|
|
3189
|
+
return {
|
|
3190
|
+
load: (sessionId) => decode(storage.getItem(keyForSession(sessionId))),
|
|
3191
|
+
save: (sessionId, items) => {
|
|
3192
|
+
if (degraded) return;
|
|
3193
|
+
try {
|
|
3194
|
+
storage.setItem(keyForSession(sessionId), encode(items, Date.now()));
|
|
3195
|
+
} catch {
|
|
3196
|
+
degraded = true;
|
|
3197
|
+
warnQuota();
|
|
3198
|
+
}
|
|
3199
|
+
},
|
|
3200
|
+
clear: (sessionId) => {
|
|
3201
|
+
try {
|
|
3202
|
+
storage.removeItem(keyForSession(sessionId));
|
|
3203
|
+
} catch {}
|
|
3204
|
+
},
|
|
3205
|
+
prune: (now) => pruneSessions(storage, now)
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Host store backend: the same `defineStore` the composer draft uses, so the
|
|
3210
|
+
* write path, per-session keying and failure semantics are the host's.
|
|
3211
|
+
*/
|
|
3212
|
+
function createHostStorePersistence(defineStore, storage) {
|
|
3213
|
+
const handle = defineStore({
|
|
3214
|
+
init: () => ({
|
|
3215
|
+
v: PAYLOAD_VERSION,
|
|
3216
|
+
updatedAt: 0,
|
|
3217
|
+
items: []
|
|
3218
|
+
}),
|
|
3219
|
+
persist: ANNOTATION_KEY_PREFIX,
|
|
3220
|
+
actions: { replace(draft, items, now) {
|
|
3221
|
+
draft.items = [...items];
|
|
3222
|
+
draft.updatedAt = now;
|
|
3223
|
+
} }
|
|
3224
|
+
});
|
|
3225
|
+
const instances = /* @__PURE__ */ new Map();
|
|
3226
|
+
const instanceOf = (sessionId) => {
|
|
3227
|
+
const cached = instances.get(sessionId);
|
|
3228
|
+
if (cached) return cached;
|
|
3229
|
+
const instance = handle.create(sessionId);
|
|
3230
|
+
instances.set(sessionId, instance);
|
|
3231
|
+
return instance;
|
|
3232
|
+
};
|
|
3233
|
+
return {
|
|
3234
|
+
load: (sessionId) => itemsOf(instanceOf(sessionId).getSnapshot()),
|
|
3235
|
+
save: (sessionId, items) => instanceOf(sessionId).actions.replace(items, Date.now()),
|
|
3236
|
+
clear: (sessionId) => {
|
|
3237
|
+
instanceOf(sessionId).clearPersisted();
|
|
3238
|
+
instances.delete(sessionId);
|
|
3239
|
+
},
|
|
3240
|
+
prune: (now) => {
|
|
3241
|
+
const removed = pruneSessions(storage, now);
|
|
3242
|
+
for (const sessionId of removed) instances.delete(sessionId);
|
|
3243
|
+
return removed;
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
/** Retention: newest `maxSessions` within `maxAgeMs`, by payload timestamp. */
|
|
3248
|
+
function pruneSessions(storage, now = Date.now(), options = {}) {
|
|
3249
|
+
const maxSessions = options.maxSessions ?? 20;
|
|
3250
|
+
const maxAgeMs = options.maxAgeMs ?? 6048e5;
|
|
3251
|
+
const rows = [];
|
|
3252
|
+
for (const key of storage.keys()) {
|
|
3253
|
+
const sessionId = sessionIdOfKey(key);
|
|
3254
|
+
if (sessionId === null) continue;
|
|
3255
|
+
const raw = storage.getItem(key);
|
|
3256
|
+
let updatedAt = 0;
|
|
3257
|
+
try {
|
|
3258
|
+
const parsed = JSON.parse(raw ?? "null");
|
|
3259
|
+
if (parsed && typeof parsed.updatedAt === "number") updatedAt = parsed.updatedAt;
|
|
3260
|
+
} catch {
|
|
3261
|
+
updatedAt = 0;
|
|
3262
|
+
}
|
|
3263
|
+
rows.push({
|
|
3264
|
+
sessionId,
|
|
3265
|
+
updatedAt
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3269
|
+
for (const row of rows) if (now - row.updatedAt > maxAgeMs) removed.add(row.sessionId);
|
|
3270
|
+
rows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3271
|
+
for (const row of rows.slice(maxSessions)) removed.add(row.sessionId);
|
|
3272
|
+
for (const sessionId of removed) storage.removeItem(keyForSession(sessionId));
|
|
3273
|
+
return [...removed];
|
|
3274
|
+
}
|
|
3275
|
+
function createNoopPersistence() {
|
|
3276
|
+
return {
|
|
3277
|
+
load: () => [],
|
|
3278
|
+
save: () => {},
|
|
3279
|
+
clear: () => {},
|
|
3280
|
+
prune: () => []
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
//#endregion
|
|
3284
|
+
//#region src/client/state/annotation-storage.ts
|
|
3285
|
+
/**
|
|
3286
|
+
* Backend selection for annotation persistence.
|
|
3287
|
+
*
|
|
3288
|
+
* Kept apart from `annotation-persistence.ts` because reaching the host's
|
|
3289
|
+
* client-store needs a guarded `require`, and that is the one thing the pure
|
|
3290
|
+
* module cannot do — verify-bundle imports the pure module straight into Node,
|
|
3291
|
+
* where a `declare` statement is not valid JavaScript.
|
|
3292
|
+
*
|
|
3293
|
+
* Preference order: the host store (same engine and keying as the composer
|
|
3294
|
+
* draft), plain localStorage, and a no-op when the environment has no storage
|
|
3295
|
+
* at all.
|
|
3296
|
+
*/
|
|
3297
|
+
/** The host package is a platform seed word; older hosts may not carry it. */
|
|
3298
|
+
const HOST_STORE_MODULE = "@deepseek-ai/dsh-client-store";
|
|
3299
|
+
function hostDefineStore() {
|
|
3300
|
+
try {
|
|
3301
|
+
const loaded = require(HOST_STORE_MODULE);
|
|
3302
|
+
return typeof loaded?.defineStore === "function" ? loaded.defineStore : null;
|
|
3303
|
+
} catch {
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function browserStorage() {
|
|
3308
|
+
try {
|
|
3309
|
+
const storage = globalThis.localStorage;
|
|
3310
|
+
if (!storage) return null;
|
|
3311
|
+
return {
|
|
3312
|
+
getItem: (key) => storage.getItem(key),
|
|
3313
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
3314
|
+
removeItem: (key) => storage.removeItem(key),
|
|
3315
|
+
keys: () => {
|
|
3316
|
+
const out = [];
|
|
3317
|
+
for (let i = 0; i < storage.length; i += 1) {
|
|
3318
|
+
const key = storage.key(i);
|
|
3319
|
+
if (key !== null) out.push(key);
|
|
3320
|
+
}
|
|
3321
|
+
return out;
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
} catch {
|
|
3325
|
+
return null;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function createAnnotationPersistence() {
|
|
3329
|
+
const storage = browserStorage();
|
|
3330
|
+
const defineStore = hostDefineStore();
|
|
3331
|
+
if (storage && defineStore) return createHostStorePersistence(defineStore, storage);
|
|
3332
|
+
if (storage) return createStoragePersistence(storage);
|
|
3333
|
+
return createNoopPersistence();
|
|
3334
|
+
}
|
|
3335
|
+
//#endregion
|
|
3100
3336
|
//#region src/client/index.tsx
|
|
3101
3337
|
/**
|
|
3102
3338
|
* Browser half of dsh-select-quote.
|
|
@@ -3141,6 +3377,12 @@ function apply(ctx) {
|
|
|
3141
3377
|
syncPendingAnnotationJobs(getAnnotations());
|
|
3142
3378
|
return subscribeAnnotations(() => syncPendingAnnotationJobs(getAnnotations()));
|
|
3143
3379
|
}, "dsh-select-quote: pending annotation marks");
|
|
3380
|
+
ctx.effect(() => {
|
|
3381
|
+
const port = createAnnotationPersistence();
|
|
3382
|
+
setAnnotationPersistence(port);
|
|
3383
|
+
port.prune();
|
|
3384
|
+
return () => setAnnotationPersistence(null);
|
|
3385
|
+
}, "dsh-select-quote: annotation persistence");
|
|
3144
3386
|
ctx.slots.inject("shell.overlay", () => {
|
|
3145
3387
|
ctx.slots.register({
|
|
3146
3388
|
name: "shell.overlay",
|
|
@@ -3172,7 +3414,7 @@ function apply(ctx) {
|
|
|
3172
3414
|
}
|
|
3173
3415
|
/** Invisible session-scoped bridge that publishes sessionId for the root toolbar. */
|
|
3174
3416
|
function SessionBridge({ sessionId }) {
|
|
3175
|
-
|
|
3417
|
+
bindAnnotationSession(sessionId);
|
|
3176
3418
|
return null;
|
|
3177
3419
|
}
|
|
3178
3420
|
//#endregion
|