dsh-select-quote 0.3.5 → 0.3.6

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 CHANGED
@@ -48,10 +48,19 @@ dsh web --patch "$PWD/dev.patch.yml"
48
48
 
49
49
  界面语言由 dsh 决定,插件不再自己判断浏览器语言。在 **设置 → 通用 → 语言** 里切换。
50
50
 
51
+ ## 插件页展示
52
+
53
+ 插件管理页与设置项里的标题、描述、图标来自包自带的资源,宿主读取它们时**不会激活插件**:
54
+
55
+ - `locale/*.json` 的 `meta.title` / `meta.description`(`en.json` 是发现入口,其余语言文件与它同目录同名)
56
+ - `icon.svg`(SVG / PNG / JPEG / WebP,≤256 KiB,必须在包内)
57
+
58
+ 标题按当前语言取,取不到回退 `package.json` 的 `name` / `description`;图标缺失时用面板的默认插画。要加一种语言,往 `locale/` 放一个同名 JSON 即可,`pnpm run check` 会按宿主的规则逐条校验(语言 id 文件名、字段非空、图标类型与体积、`exports` 与 `files` 覆盖、`npm pack` 实际清单)。
59
+
51
60
 
52
61
  ## 已知限制
53
62
 
54
- - **待发批注按会话持久化**:刷新页面、重启客户端后回到原会话,未发送的批注会原样回来(和草稿同一套机制);保留最近 20 个会话、7 天。发送即清除,切换会话跟着切,hero 里换工作区等于换会话,批注不跟着搬。
63
+ - **待发批注按会话持久化**:刷新页面、重启客户端后回到原会话,未发送的批注会原样回来(与草稿同在 localStorage,键各自独立);保留最近 20 个会话、7 天。发送即清除,切换会话跟着切,hero 里换工作区等于换会话,批注不跟着搬。
55
64
  - **占用了用户气泡的渲染**:本插件以最低优先级替换内置 `user` 节点。其它插件若替换同一个键(例如 `dsh-easyrewrite` 用 `-1`),会被本插件屏蔽。
56
65
  - **卡片宽度有限**:输入框内每条摘要最宽约为输入框的 1/4,超长标题走省略号。
57
66
  - **划词只在对话区生效**:不介入编辑器、终端等面板。
@@ -80,7 +89,7 @@ pnpm run watch # 增量重建
80
89
 
81
90
  依赖用 pnpm 管理(锁文件 `pnpm-lock.yaml`);构建产物 `lib/` 随源码一起提交,运行时直接读它。
82
91
 
83
- `pnpm run verify` 无头执行产物,断言 8 项贡献都注册上,并检查三份词典的键与 `{placeholder}` 在每种语言里都齐备(复数消息必须有 `.other` 兜底)。
92
+ `pnpm run verify` 无头执行产物,断言 8 项贡献都注册上,并检查三份词典的键与 `{placeholder}` 在每种语言里都齐备(复数消息必须有 `.other` 兜底),外加插件页展示元信息(locale + icon)按宿主的读取规则解析、并核对 `npm pack` 的实际发货清单。
84
93
 
85
94
  ### 热更新到桌面客户端
86
95
 
package/icon.svg ADDED
@@ -0,0 +1,9 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M25 25H23C22.3705 25 21.7777 25.2964 21.4 25.8L20.5 27C20.5 27 19.2503 29 18 29C16.7497 29 15.5 27 15.5 27L14.6 25.8C14.2223 25.2964 13.6295 25 13 25H11C9.96813 24.9974 8.86434 24.4622 8.13596 23.7313C7.40758 23.0004 6.99902 22.0103 7 20.9784V11C6.99935 9.96834 7.40805 8.87567 8.13639 8.14504C8.86474 7.4144 9.96834 7.00259 11 7H25C26.0317 7.0026 27.1353 7.41441 27.8636 8.14504C28.592 8.87568 29.0007 9.96835 29 11V20.9784C29.001 22.0103 28.5924 23.0004 27.864 23.7313C27.1357 24.4622 26.0319 24.9974 25 25ZM21.3317 15.0265H19.7047C19.4286 15.0265 19.2047 14.8026 19.2047 14.5265V13.018C19.2047 12.3909 18.6681 11.8824 18.0043 11.8824C17.3405 11.8824 16.8026 12.3909 16.8026 13.018V14.5265C16.8026 14.8026 16.5788 15.0265 16.3026 15.0265H14.6756C14.0131 15.0265 13.474 15.5338 13.474 16.1609C13.474 16.788 14.0118 17.2965 14.6756 17.2965H16.3026C16.5788 17.2965 16.8026 17.5204 16.8026 17.7965V19.3049C16.8026 19.9308 17.3405 20.4394 18.0043 20.4394C18.6681 20.4394 19.2059 19.9308 19.2059 19.3049V17.7965C19.2059 17.5204 19.4298 17.2965 19.7059 17.2965H21.3329C21.9967 17.2965 22.5346 16.788 22.5346 16.1609C22.5346 15.5338 21.9967 15.0265 21.3317 15.0265Z" fill="url(#paint0_linear_7_8)"/>
3
+ <defs>
4
+ <linearGradient id="paint0_linear_7_8" x1="29" y1="3.5" x2="14" y2="32" gradientUnits="userSpaceOnUse">
5
+ <stop stop-color="#B2FFDA"/>
6
+ <stop offset="1" stop-color="#0099FF"/>
7
+ </linearGradient>
8
+ </defs>
9
+ </svg>
package/lib/client.js CHANGED
@@ -95,7 +95,6 @@ body {
95
95
  --dsq-font-size: var(--dsh-content-font-size, 14px);
96
96
  --dsq-font-delta: var(--dsh-content-font-delta, 0px);
97
97
  --dsq-bg-raised: var(--dsw-alias-bg-layer-1, #fff);
98
- --dsq-bg-panel: var(--dsw-alias-bg-layer-2, #fff);
99
98
  --dsq-bg-toast: var(--dsw-alias-bg-layer-3, rgba(0, 0, 0, 0.78));
100
99
  --dsq-line: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
101
100
  --dsq-line-divider: var(--dsw-alias-border-l4, rgba(0, 0, 0, 0.12));
@@ -106,10 +105,9 @@ body {
106
105
  --dsq-label-2: var(--dsw-alias-label-secondary, rgba(0, 0, 0, 0.65));
107
106
  --dsq-label-3: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, 0.45));
108
107
  --dsq-link: var(--dsw-alias-link, rgba(0, 0, 0, 0.65));
109
- --dsq-shadow-pop: var(--dsw-elevation-prominent, 0 3px 8px rgba(0, 0, 0, 0.04), 0 0 20px rgba(0, 0, 0, 0.05));
110
- --dsq-shadow-soft: var(--dsw-elevation-soft, 0 1px 2px rgba(0, 0, 0, 0.03), 0 6px 20px rgba(0, 0, 0, 0.06));
111
108
  --dsq-bubble: var(--dsw-specific-bubble, rgba(0, 0, 0, 0.05));
112
- --dsq-menu: var(--dsw-specific-menu, #fff);
109
+ --dsq-menu: var(--dsw-menu-surface-fill, rgba(248, 249, 250, .58));
110
+ --dsq-menu-backdrop: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));
113
111
  --dsq-accent-400: var(--dsw-static-blue-400, #1677ff);
114
112
  --dsq-accent: var(--dsw-static-blue-500, #3b82f6);
115
113
  --dsq-accent-tint: var(--dsw-static-blue-50p, #eaf3ff);
@@ -126,23 +124,91 @@ 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
+ /* 内容层的 rise:悬停面板的列表在用;工具条改用容器 scale 后这条在这里待命。 */
142
+ @keyframes dsq-rise-in {
143
+ from {
144
+ transform: translateY(4px);
145
+ }
146
+ to {
147
+ transform: translateY(0);
148
+ }
149
+ }
150
+
151
+ /* 宿主菜单材质(官方 material 图层同款):填充走 --dsw-menu-surface-fill,模糊走
152
+ --dsw-menu-backdrop-filter(恒为 blur(40px) saturate(150%)),亮暗自动翻。
153
+ 画在 z-index:-1 的独立图层上,内容在它之上:模糊只取身后的页面,不会把
154
+ 面板里的文字一起糊掉。工具条、评论气泡、悬停面板三个浮层共用这一条。
155
+ 注意:macOS 上宿主另有一个 --dsw-specific-menu(覆写成 94% 不透明)给它的
156
+ 菜单用;这里按需求用 surface-fill,所以我们的浮层会比宿主自己的菜单更透。 */
157
+ .dsq_toolbar::before,
158
+ .dsq_commentPop::before,
159
+ .dsq_summaryHoverInner::before {
160
+ content: "";
161
+ position: absolute;
162
+ inset: 0;
163
+ z-index: -1;
164
+ border-radius: inherit;
165
+ background: var(--dsq-menu);
166
+ backdrop-filter: var(--dsq-menu-backdrop);
167
+ -webkit-backdrop-filter: var(--dsq-menu-backdrop);
168
+ pointer-events: none;
169
+ }
170
+
171
+ /* 覆层不描边:elevation-prominent 自带的那圈 0 0 0 .5px(--dsw-elevation-stroke-color,
172
+ 默认 border-l4)对磨砂覆层是多余的——边缘自带材质分界,再勾一道反而抢视觉,覆写成
173
+ 更淡的 border-l1。
174
+ 两个坑,改之前先读:① 这两行必须写在三个浮层自己的选择器上。写在 body 上会随继承
175
+ 影响页面里其它依赖默认值的面板(宿主带 [data-menu-material] 的菜单有自己的覆写,
176
+ 不受影响)。② --dsq-shadow 也必须在这儿声明,不能留在 body:自定义属性是在**声明处**
177
+ 展开的,--dsq-shadow 留在 body 上时 var(--dsw-elevation-prominent) 会在 body 就被
178
+ 替换成 border-l4 那一版并继承下来,浮层上的描边覆写根本够不着;跟着声明挪到这里,
179
+ 整条链才在浮层元素上取值。 */
180
+ .dsq_toolbar,
181
+ .dsq_commentPop,
182
+ .dsq_summaryHoverInner {
183
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
184
+ --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));
185
+ }
186
+
129
187
  .dsq_toolbar {
130
188
  position: fixed;
131
189
  z-index: 10060;
132
190
  transform: translate(-50%, -100%);
133
191
  gap: 2px;
134
192
  padding: 4px;
135
- /* 无边框:--dsq-shadow-soft 自带 0 0 0 .5px 的描边环,由它勾勒轮廓,
136
- 所以这里不要再画 border,否则描边会和环叠成两条线。 */
193
+ /* 无边框:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的描边环,
194
+ 由它勾勒轮廓,所以这里不要再画 border,否则描边会和环叠成两条线。 */
137
195
  border: 0;
138
196
  border-radius: 12px;
139
- background: var(--dsq-bg-panel);
140
- box-shadow: var(--dsq-shadow-soft);
197
+ /* 底色与模糊都在上面的材质图层里,这里不再画背景 */
198
+ box-shadow: var(--dsq-shadow);
141
199
  pointer-events: auto;
142
- /* Mounts once per selection, after the drag ends: soft rise + fade. */
200
+ /* A/B:scale 入场放在容器上(材质图层归它所有)。关键帧自带 translate(-50%,-100%),
201
+ 所以缩放不会破坏居中。若真实客户端里磨砂在放大期间消失,改回内容层方案。 */
143
202
  animation: dsq-toolbar-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
144
203
  }
145
204
 
205
+ /* 按钮行:只负责布局;入场动画在容器上(见 .dsq_toolbar 的 A/B 说明)。 */
206
+ .dsq_toolbarInner {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ gap: 2px;
210
+ }
211
+
146
212
  .dsq_button {
147
213
  color: var(--dsq-label);
148
214
  font: inherit;
@@ -397,38 +463,44 @@ ${atomRules}
397
463
  }
398
464
 
399
465
  /* Must come after .dsq_summaryHover (same class weight otherwise loses to display:none). */
466
+ /* 容器上不做 opacity 过渡、也不做 scale 入场。opacity 那条是规范实锤:祖先一带上
467
+ opacity<1 就成了 backdrop root,::before 采样不到身后的页面,磨砂在整段淡入里
468
+ 消失、结束才糊上——所以 opacity 过渡删掉,只留 place() 里那次 0→1 瞬时翻转
469
+ (「定位前不可见」的守卫)。scale 一并去掉是保守起见:容器彻底静止,采样区域
470
+ 不会逐帧移动;入场感交给内层列表的 rise(内容不是材质的祖先)。 */
400
471
  .dsq_summaryHover.dsq_summaryHoverOpen {
401
472
  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
473
  }
409
474
 
410
- .dsq_summaryHover[data-side='top'] {
411
- transform-origin: bottom center;
475
+ .dsq_summaryHoverInner {
476
+ position: relative;
477
+ gap: 4px;
478
+ padding: 4px;
479
+ border-radius: 12px;
480
+ /* 不画 border:--dsq-shadow(宿主的 elevation-prominent)自带 0 0 0 .5px 的
481
+ 描边环,再画一遍会叠成两条线。底色与模糊都在材质图层里。 */
482
+ box-shadow: var(--dsq-shadow);
412
483
  }
413
484
 
414
- .dsq_summaryHoverInner {
485
+ /* 滚动只放在列表上:材质图层是绝对定位的,留在会滚动的容器里会跟着内容跑掉,
486
+ 滚到底部时面板顶部就露空。入场 rise 也在这层——内容不是材质图层的祖先,
487
+ 动它不会切断磨砂的采样源。 */
488
+ .dsq_summaryList {
415
489
  gap: 4px;
416
- max-height: min(420px, calc(100vh - 120px));
417
490
  overflow-y: auto;
418
491
  overscroll-behavior: contain;
419
- padding: 4px;
420
- border-radius: 12px;
421
- border: 0.5px solid var(--dsq-line);
422
- /* Frosted panel: the menu token at partial alpha over a blur. */
423
- background: color-mix(in srgb, var(--dsq-bg-raised) 50%, transparent);
424
- backdrop-filter: blur(14px) saturate(1.4);
425
- -webkit-backdrop-filter: blur(14px) saturate(1.4);
426
- box-shadow: var(--dsq-shadow-pop);
492
+ max-height: min(412px, calc(100vh - 128px));
493
+ animation: dsq-rise-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
427
494
  }
428
495
 
496
+ /* 老浏览器没有 backdrop-filter:图层退成不透明底色,否则半透明直接糊在文字上。 */
429
497
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
430
- .dsq_summaryHoverInner {
431
- background: var(--dsq-menu);
498
+ .dsq_toolbar::before,
499
+ .dsq_commentPop::before,
500
+ .dsq_summaryHoverInner::before {
501
+ background: var(--dsq-bg-raised);
502
+ backdrop-filter: none;
503
+ -webkit-backdrop-filter: none;
432
504
  }
433
505
  }
434
506
 
@@ -606,10 +678,8 @@ ${atomRules}
606
678
  gap: 8px;
607
679
  padding: 8px;
608
680
  border-radius: 16px;
609
- background: color-mix(in srgb, var(--dsq-bg-raised) 75%, transparent);
610
- backdrop-filter: blur(10px) saturate(1.4);
611
- -webkit-backdrop-filter: blur(14px) saturate(1.4);
612
- box-shadow: var(--dsq-shadow-pop);
681
+ /* 底色与模糊都在材质图层里 */
682
+ box-shadow: var(--dsq-shadow);
613
683
  }
614
684
 
615
685
  .dsq_commentInput {
@@ -821,26 +891,6 @@ ${atomRules}
821
891
  /* Layer 4 — host surface: keyframes, theme flips, and the two rules that reach
822
892
  into product-owned DOM. */
823
893
 
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
894
  @keyframes dsq-ann-flash {
845
895
  0% {
846
896
  background: var(--dsq-mark-color);
@@ -882,6 +932,7 @@ function ensureToolbarStyles() {
882
932
  }
883
933
  const styles = {
884
934
  toolbar: "dsq_toolbar",
935
+ toolbarInner: "dsq_toolbarInner",
885
936
  button: "dsq_button",
886
937
  buttonAddComment: "dsq_buttonAddComment",
887
938
  divider: "dsq_divider",
@@ -914,6 +965,7 @@ const styles = {
914
965
  annEditCancel: "dsq_annEditCancel",
915
966
  annEditSave: "dsq_annEditSave",
916
967
  summaryHoverInner: "dsq_summaryHoverInner",
968
+ summaryList: "dsq_summaryList",
917
969
  summaryItemButton: "dsq_summaryItemButton",
918
970
  summaryHoverOpen: "dsq_summaryHoverOpen",
919
971
  summaryItemActions: "dsq_summaryItemActions",
@@ -2052,7 +2104,7 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2052
2104
  const { top, left, side } = placePopover(root.getBoundingClientRect(), {
2053
2105
  width: pop.offsetWidth || 300,
2054
2106
  height: pop.offsetHeight || 220
2055
- }, "bottom", 0);
2107
+ }, "top", 0);
2056
2108
  pop.style.top = `${top}px`;
2057
2109
  pop.style.left = `${left}px`;
2058
2110
  pop.style.paddingTop = side === "bottom" ? "12px" : "12px";
@@ -2196,109 +2248,112 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
2196
2248
  onPointerLeave,
2197
2249
  children: (0, react_jsx_runtime.jsx)("div", {
2198
2250
  className: cx(A.col, styles.summaryHoverInner),
2199
- children: items.map((item, i) => {
2200
- const isEditing = editing === i;
2201
- return (0, react_jsx_runtime.jsxs)("div", {
2202
- className: styles.summaryItem,
2203
- children: [(0, react_jsx_runtime.jsxs)("div", {
2204
- className: cx(A.row, styles.summaryItemHead),
2205
- children: [
2206
- (0, react_jsx_runtime.jsx)("div", {
2207
- className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
2208
- "aria-hidden": true,
2209
- children: String(i + 1)
2210
- }),
2211
- (0, react_jsx_runtime.jsx)("div", {
2212
- className: styles.summaryItemLabel,
2213
- children: t("common.selectedText")
2214
- }),
2215
- !isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
2216
- className: cx(A.row, styles.summaryItemActions),
2217
- children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
2218
- type: "button",
2219
- className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2220
- "aria-label": t("summary.edit", { index: i + 1 }),
2221
- onClick: () => startEdit(i),
2222
- children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
2223
- }) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
2224
- type: "button",
2225
- className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2226
- "aria-label": t("summary.remove", { index: i + 1 }),
2227
- onClick: () => onItemRemove(i, item),
2228
- children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
2229
- }) : null]
2230
- }) : null
2231
- ]
2232
- }), isEditing ? (0, react_jsx_runtime.jsxs)("div", {
2233
- className: styles.summaryItemMain,
2234
- children: [
2235
- (0, react_jsx_runtime.jsx)("div", {
2236
- className: styles.annEditText,
2237
- children: item.text
2238
- }),
2239
- (0, react_jsx_runtime.jsx)("div", {
2240
- className: styles.summaryItemLabel,
2241
- children: t("summary.userComment")
2242
- }),
2243
- (0, react_jsx_runtime.jsx)("textarea", {
2244
- ref: editInputRef,
2245
- className: cx(A.box, styles.annEditInput),
2246
- placeholder: t("common.commentPlaceholder"),
2247
- value: editDraft,
2248
- onChange: (e) => setEditDraft(e.target.value),
2249
- onKeyDown: (e) => {
2250
- if (e.key === "Escape") {
2251
- e.preventDefault();
2252
- setEditing(null);
2253
- return;
2254
- }
2255
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2256
- e.preventDefault();
2257
- commitEdit();
2258
- }
2259
- }
2260
- }),
2261
- (0, react_jsx_runtime.jsxs)("div", {
2262
- className: cx(A.row, styles.annEditActions),
2263
- children: [(0, react_jsx_runtime.jsx)("button", {
2264
- type: "button",
2265
- className: cx(A.plain, styles.annEditCancel),
2266
- onClick: () => setEditing(null),
2267
- children: t("common.cancel")
2268
- }), (0, react_jsx_runtime.jsx)("button", {
2269
- type: "button",
2270
- className: styles.annEditSave,
2271
- onClick: commitEdit,
2272
- children: t("common.save")
2273
- })]
2274
- })
2275
- ]
2276
- }) : (0, react_jsx_runtime.jsx)("div", {
2277
- className: styles.summaryItemMain,
2278
- children: (0, react_jsx_runtime.jsxs)("button", {
2279
- type: "button",
2280
- className: cx(A.plain, styles.summaryItemButton),
2281
- onClick: () => {
2282
- onItemSelect?.(i, item);
2283
- closePop();
2284
- },
2251
+ children: (0, react_jsx_runtime.jsx)("div", {
2252
+ className: cx(A.col, styles.summaryList),
2253
+ children: items.map((item, i) => {
2254
+ const isEditing = editing === i;
2255
+ return (0, react_jsx_runtime.jsxs)("div", {
2256
+ className: styles.summaryItem,
2257
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2258
+ className: cx(A.row, styles.summaryItemHead),
2285
2259
  children: [
2286
2260
  (0, react_jsx_runtime.jsx)("div", {
2287
- className: styles.summaryItemText,
2288
- children: item.title
2261
+ className: cx(A.keep, A.gridMid, styles.summaryItemBadge),
2262
+ "aria-hidden": true,
2263
+ children: String(i + 1)
2289
2264
  }),
2290
2265
  (0, react_jsx_runtime.jsx)("div", {
2291
2266
  className: styles.summaryItemLabel,
2292
- children: t("summary.userComment")
2267
+ children: t("common.selectedText")
2268
+ }),
2269
+ !isEditing && (onItemRemove || onItemEdit) ? (0, react_jsx_runtime.jsxs)("div", {
2270
+ className: cx(A.row, styles.summaryItemActions),
2271
+ children: [onItemEdit ? (0, react_jsx_runtime.jsx)("button", {
2272
+ type: "button",
2273
+ className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2274
+ "aria-label": t("summary.edit", { index: i + 1 }),
2275
+ onClick: () => startEdit(i),
2276
+ children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "pencil" })
2277
+ }) : null, onItemRemove ? (0, react_jsx_runtime.jsx)("button", {
2278
+ type: "button",
2279
+ className: cx(A.plain, A.gridMid, styles.summaryItemAction),
2280
+ "aria-label": t("summary.remove", { index: i + 1 }),
2281
+ onClick: () => onItemRemove(i, item),
2282
+ children: (0, react_jsx_runtime.jsx)(LucideIcon, { name: "trash-2" })
2283
+ }) : null]
2284
+ }) : null
2285
+ ]
2286
+ }), isEditing ? (0, react_jsx_runtime.jsxs)("div", {
2287
+ className: styles.summaryItemMain,
2288
+ children: [
2289
+ (0, react_jsx_runtime.jsx)("div", {
2290
+ className: styles.annEditText,
2291
+ children: item.text
2293
2292
  }),
2294
2293
  (0, react_jsx_runtime.jsx)("div", {
2295
- className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
2296
- children: item.comment || t("summary.noComment")
2294
+ className: styles.summaryItemLabel,
2295
+ children: t("summary.userComment")
2296
+ }),
2297
+ (0, react_jsx_runtime.jsx)("textarea", {
2298
+ ref: editInputRef,
2299
+ className: cx(A.box, styles.annEditInput),
2300
+ placeholder: t("common.commentPlaceholder"),
2301
+ value: editDraft,
2302
+ onChange: (e) => setEditDraft(e.target.value),
2303
+ onKeyDown: (e) => {
2304
+ if (e.key === "Escape") {
2305
+ e.preventDefault();
2306
+ setEditing(null);
2307
+ return;
2308
+ }
2309
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
2310
+ e.preventDefault();
2311
+ commitEdit();
2312
+ }
2313
+ }
2314
+ }),
2315
+ (0, react_jsx_runtime.jsxs)("div", {
2316
+ className: cx(A.row, styles.annEditActions),
2317
+ children: [(0, react_jsx_runtime.jsx)("button", {
2318
+ type: "button",
2319
+ className: cx(A.plain, styles.annEditCancel),
2320
+ onClick: () => setEditing(null),
2321
+ children: t("common.cancel")
2322
+ }), (0, react_jsx_runtime.jsx)("button", {
2323
+ type: "button",
2324
+ className: styles.annEditSave,
2325
+ onClick: commitEdit,
2326
+ children: t("common.save")
2327
+ })]
2297
2328
  })
2298
2329
  ]
2299
- })
2300
- })]
2301
- }, item.id ?? `${i}`);
2330
+ }) : (0, react_jsx_runtime.jsx)("div", {
2331
+ className: styles.summaryItemMain,
2332
+ children: (0, react_jsx_runtime.jsxs)("button", {
2333
+ type: "button",
2334
+ className: cx(A.plain, styles.summaryItemButton),
2335
+ onClick: () => {
2336
+ onItemSelect?.(i, item);
2337
+ closePop();
2338
+ },
2339
+ children: [
2340
+ (0, react_jsx_runtime.jsx)("div", {
2341
+ className: styles.summaryItemText,
2342
+ children: item.title
2343
+ }),
2344
+ (0, react_jsx_runtime.jsx)("div", {
2345
+ className: styles.summaryItemLabel,
2346
+ children: t("summary.userComment")
2347
+ }),
2348
+ (0, react_jsx_runtime.jsx)("div", {
2349
+ className: item.comment ? styles.summaryItemText : `${styles.summaryItemText} ${styles.summaryItemTextEmpty}`,
2350
+ children: item.comment || t("summary.noComment")
2351
+ })
2352
+ ]
2353
+ })
2354
+ })]
2355
+ }, item.id ?? `${i}`);
2356
+ })
2302
2357
  })
2303
2358
  })
2304
2359
  }) : null;
@@ -2915,11 +2970,7 @@ function SelectionToolbar({ sessionId }) {
2915
2970
  "data-dsq-toolbar": "true",
2916
2971
  onPointerDown: handleToolbarPointerDown,
2917
2972
  children: phase === "comment" ? (0, react_jsx_runtime.jsx)("div", {
2918
- style: {
2919
- display: "inline-flex",
2920
- alignItems: "center",
2921
- gap: 2
2922
- },
2973
+ className: cx(A.inlineRow, styles.toolbarInner),
2923
2974
  children: (0, react_jsx_runtime.jsx)("span", {
2924
2975
  style: {
2925
2976
  padding: "0 8px",
@@ -2929,11 +2980,7 @@ function SelectionToolbar({ sessionId }) {
2929
2980
  children: t("toolbar.commentHint")
2930
2981
  })
2931
2982
  }) : (0, react_jsx_runtime.jsxs)("div", {
2932
- style: {
2933
- display: "inline-flex",
2934
- alignItems: "center",
2935
- gap: 2
2936
- },
2983
+ className: cx(A.inlineRow, styles.toolbarInner),
2937
2984
  children: [
2938
2985
  link ? (0, react_jsx_runtime.jsx)("button", {
2939
2986
  type: "button",
@@ -3281,8 +3328,8 @@ function warnQuota() {
3281
3328
  console.warn("dsh-select-quote: annotation persistence disabled (storage write failed)");
3282
3329
  }
3283
3330
  /**
3284
- * Direct localStorage backend (the documented fallback). Writes are wrapped so
3285
- * a quota failure degrades to memory-only instead of breaking the composer.
3331
+ * Direct localStorage backend. Writes are wrapped so a quota failure degrades
3332
+ * to memory-only instead of breaking the composer.
3286
3333
  */
3287
3334
  function createStoragePersistence(storage) {
3288
3335
  let degraded = false;
@@ -3305,45 +3352,6 @@ function createStoragePersistence(storage) {
3305
3352
  prune: (now) => pruneSessions(storage, now)
3306
3353
  };
3307
3354
  }
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
3355
  /** Retention: newest `maxSessions` within `maxAgeMs`, by payload timestamp. */
3348
3356
  function pruneSessions(storage, now = Date.now(), options = {}) {
3349
3357
  const maxSessions = options.maxSessions ?? 20;
@@ -3383,27 +3391,20 @@ function createNoopPersistence() {
3383
3391
  //#endregion
3384
3392
  //#region src/client/state/annotation-storage.ts
3385
3393
  /**
3386
- * Backend selection for annotation persistence.
3394
+ * Environment probe for annotation persistence.
3387
3395
  *
3388
- * Kept apart from `annotation-persistence.ts` because reaching the host's
3389
- * client-store needs a guarded `require`, and that is the one thing the pure
3390
- * module cannot do — verify-bundle imports the pure module straight into Node,
3391
- * where a `declare` statement is not valid JavaScript.
3396
+ * Kept apart from `annotation-persistence.ts` because this is the only module
3397
+ * that reads `globalThis`: the pure layer stays importable in Node, where
3398
+ * verify-bundle replays the whole cycle against a Map-backed storage.
3392
3399
  *
3393
- * Preference order: the host store (same engine and keying as the composer
3394
- * draft), plain localStorage, and a no-op when the environment has no storage
3395
- * at all.
3400
+ * The backend is plain localStorage — the same per-origin store the composer
3401
+ * draft lives in, under this plugin's own key prefix. Requiring the host's
3402
+ * `@deepseek-ai/dsh-client-store` instead would buy nothing: its persistence
3403
+ * backend is localStorage with the same key shape and the same whole-value
3404
+ * payload, while depending on a Harness Client package that the plugin spec
3405
+ * forbids loading as a module (it changes without notice, and a throwing
3406
+ * backend selection would take the whole slot entry down with it).
3396
3407
  */
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
3408
  function browserStorage() {
3408
3409
  try {
3409
3410
  const storage = globalThis.localStorage;
@@ -3427,10 +3428,7 @@ function browserStorage() {
3427
3428
  }
3428
3429
  function createAnnotationPersistence() {
3429
3430
  const storage = browserStorage();
3430
- const defineStore = hostDefineStore();
3431
- if (storage && defineStore) return createHostStorePersistence(defineStore, storage);
3432
- if (storage) return createStoragePersistence(storage);
3433
- return createNoopPersistence();
3431
+ return storage ? createStoragePersistence(storage) : createNoopPersistence();
3434
3432
  }
3435
3433
  //#endregion
3436
3434
  //#region src/client/index.tsx
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Selection Annotations",
4
+ "description": "Quote text from the conversation into your next message: the selection stays marked in the source and the model points back at it."
5
+ }
6
+ }
package/locale/ru.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Аннотации к выделенному",
4
+ "description": "Цитируйте выделенный текст из диалога в следующем сообщении: разметка остаётся в оригинале, модель отвечает по каждому фрагменту."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "划词批注",
4
+ "description": "把对话里选中的原文带进下一条消息:正文留下标记,模型可以逐条回应。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-select-quote",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "DeepSeek Harness web plugin: annotate text selected in the conversation — the selection rides along with your next message, is marked in the source text, and the model points back at it.",
5
5
  "keywords": [
6
6
  "deepseek",
@@ -26,12 +26,16 @@
26
26
  "exports": {
27
27
  ".": "./lib/index.js",
28
28
  "./client": "./lib/client.js",
29
- "./package.json": "./package.json"
29
+ "./package.json": "./package.json",
30
+ "./locale/*.json": "./locale/*.json",
31
+ "./icon": "./icon.svg"
30
32
  },
31
33
  "files": [
32
34
  "lib/index.js",
33
35
  "lib/client.js",
34
36
  "cordis.patch.yml",
37
+ "locale/*.json",
38
+ "icon.svg",
35
39
  "LICENSE"
36
40
  ],
37
41
  "scripts": {