@lynn123411/dsh-model-submenu 0.3.10 → 0.3.12

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/DETAILS.md CHANGED
@@ -72,13 +72,15 @@ dsh plugin --profile web add @lynn123411/dsh-model-submenu
72
72
 
73
73
  | 状态 | 轨道 | 旋钮 |
74
74
  | --- | --- | --- |
75
- | Off | 灰色,只填到旋钮 | 实心白 |
76
- | 中间档 | 该档的纯色,只填到旋钮 | 实心白 |
77
- | 最高档 | 整条蓝→紫渐变 | 实心白 |
75
+ | Off | 空——填充只有旋钮那么大,完全藏在旋钮底下 | 实心白 |
76
+ | 中间档 | 该档的纯色,右端圆弧落在旋钮底下 | 实心白 |
77
+ | 最高档 | 整条蓝→紫渐变,铺到轨道尽头 | 实心白 |
78
78
  | provider default | 空 | 空心环,停在模型声明的默认档(没声明就停最左) |
79
- | 拖动中 | 跟着指针吸附 | 实心白,松手才提交 |
79
+ | 拖动中 | 跟着指针连续移动 | 实心白,松手吸附到最近一档并提交 |
80
80
 
81
- 轨道高度取这一行的文字行高(20px),旋钮与轨道同高。provider default 不是档位:`effortChoices` 只返回模型声明的真实等级,默认态由 `state.current.reasoningEffort === undefined` 判定,点标签提交 `reasoningEffort: undefined` 切回去。颜色由 `level-color.ts` 的 `levelColor(index, count)` 给出:`0` 是中性灰,其余档位把「Off 以上」均匀铺在蓝→紫的斜坡上,最高档固定取紫端并用整条渐变填充。
81
+ 轨道高 16px、旋钮直径 20px:旋钮比轨道大一圈,两端因此露出轨道自己的圆角,旋钮不会被误读成轨道的端帽。填充是「轨道形状裁到旋钮」——右端圆弧取轨道的半径、圆心落在旋钮底下,所以色块是钻进旋钮而不是在它旁边直切一刀;最高档的填充宽度正好铺满整条轨道。
82
+
83
+ 拖动时旋钮跟的是指针的连续位置(`fractionAt`),不是一档一格:档位取离指针最近的那个刻度(`detentAt`),所以旋钮在两档之间也能停在任意位置,只有跨过中点才换档。松手时旋钮用 160ms 的缓出滑到那个刻度上再提交;拖动期间这条过渡关掉(`data-dragging`),否则旋钮会落后于指针。`prefers-reduced-motion` 下过渡一律关掉。provider default 不是档位:`effortChoices` 只返回模型声明的真实等级,默认态由 `state.current.reasoningEffort === undefined` 判定,点标签提交 `reasoningEffort: undefined` 切回去。颜色由 `level-color.ts` 的 `levelColor(index, count)` 给出:`0` 是中性灰,其余档位把「Off 以上」均匀铺在蓝→紫的斜坡上,最高档固定取紫端并用整条渐变填充。
82
84
 
83
85
  ## 开发副本
84
86
 
@@ -102,7 +104,7 @@ cd plugins/dsh-model-submenu && pnpm check
102
104
  `check` = `typecheck` + `build`(tsdown)+ `smoke` + `test` + `assert:lib-untracked`。
103
105
 
104
106
  - `scripts/smoke-client.mjs` 按 Web shell 的方式装载构建产物 `lib/client.js`(`window.__ModuleLoader__.load` 桩件),喂假 cordis 服务,断言导出身份、`inject` 表逐字相等、三处贡献各自的 list `id`(`model-menu-bridge`、`model-picker`、`model-effort-tint`)、前两处共用同一个 store、着色那条读同一个目录、产物里走的是共享的 `Modal`、产物里带着认宿主菜单面板(`data-menu-material`)、两格(`:scope > button[role=`)、档位文字(`span[class*='cellValue']`)与触发器档位文字(`span[class*="triggerEffort"]`)的四段选择器、滑块以 `role="slider"` 暴露给辅助技术、字典键集中英对齐且带着滑块那三条文案、元信息取数确实问过设置镜像与供应商目录,以及「设置没答案时答未知而不猜」。末尾 7 条是 store 自身的契约:起始关闭、open 发布目标、重复 open 不通知、第二次 open 替换目标、close 清空、退订后静默。
105
- - `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs`:滑块几何与配色——档位到轨道比例、指针到最近档、两端夹取、单档与零宽轨道的退路、Off 的中性灰、蓝到紫的两端、只有最高档用渐变。`scripts/test-render.mjs`:用真 react 把菜单桥、触发器着色与弹窗画进 jsdom,40 条用例锁住「点宿主第一格 → 关菜单 → store → 弹窗」这条链、宿主那两格的下钻确实没执行、点标签切回默认、点滑块以外的位置把焦点交给滑块、刻度数与旋钮位置、数值与箭头隐藏、Off 灰/中间纯色/最高档渐变、触发器档位文字跟着变色且默认态交还宿主颜色、默认态空轨道与空心旋钮、拖动松手才提交一次、按下与松手同任务也不丢提交、末档与进行中不提交、被拒走宿主提示、单档与无档位不挂滑块、卸载后样式写回,以及弹窗侧的收藏置顶、星标独立、提交与拒绝、思考等级记忆、加载与失败、会话不可选时整块不渲染。
107
+ - `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs`:滑块几何与配色——档位到轨道比例、拖动位置是连续比例而不是逐档跳、比例换回最近一档且跨过中点才变、指针到最近档、两端夹取、单档与零宽轨道的退路、填充圆弧始终藏在旋钮底下且末档铺满轨道、Off 的中性灰、蓝到紫的两端、只有最高档用渐变。`scripts/test-render.mjs`:用真 react 把菜单桥、触发器着色与弹窗画进 jsdom,40 条用例锁住「点宿主第一格 → 关菜单 → store → 弹窗」这条链、宿主那两格的下钻确实没执行、点标签切回默认、点滑块以外的位置把焦点交给滑块、刻度数与旋钮位置、数值与箭头隐藏、Off 灰/中间纯色/最高档渐变、触发器档位文字跟着变色且默认态交还宿主颜色、默认态空轨道与空心旋钮、拖动松手才提交一次、按下与松手同任务也不丢提交、末档与进行中不提交、被拒走宿主提示、单档与无档位不挂滑块、卸载后样式写回,以及弹窗侧的收藏置顶、星标独立、提交与拒绝、思考等级记忆、加载与失败、会话不可选时整块不渲染。
106
108
  - `lib/` 是构建镜像,`assert:lib-untracked` 判它没被 git 跟踪。
107
109
 
108
110
  要肉眼确认页面上的形态,用仓库的 `node tools/verify-client.mjs dsh-model-submenu --needle '<你新写的类名或文案>'`。
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ## 功能
6
6
 
7
7
  - **第一行就是入口**:菜单里的「模型」点下去直接打开弹窗,不再只列当前供应商;顶部搜索框同时搜模型名与供应商名,每个有模型的供应商一个标签页。
8
- - **第二行是滑块**:推理等级不再下钻到档位列表,拖动轨道松手即切换,键盘左右键逐档调整;轨道上的刻度点数就是该模型可选的档位数,高度与这一行的文字齐平。
8
+ - **第二行是滑块**:推理等级不再下钻到档位列表,拖动时旋钮跟着指针走、松手吸附到最近一档并切换,键盘左右键逐档调整;轨道上的刻度点数就是该模型可选的档位数,旋钮比轨道大一圈。
9
9
  - **档位有颜色**:Off 是灰色,往上从蓝走到紫,只有最高档的填充是渐变;输入框触发器上的档位文字跟着同一套颜色走。
10
10
  - **默认档位是一个状态**:点最左边的「推理等级」四个字切回 provider default——轨道空着、旋钮是空心环,不会被误认成最低档;再拖一下滑块就回到明确档位。
11
11
  - **收藏靠前**:每行右侧一个星标,点它只切换收藏、不切换模型;收藏表存在浏览器本地,整台浏览器共用一张,收藏的模型排在列表前面。
package/lib/client.js CHANGED
@@ -198,6 +198,8 @@ window.__ModuleLoader__.load({
198
198
  hidden: true
199
199
  });
200
200
  }
201
+ /** The knob's centre keeps its own radius away from either end of the track. */
202
+ const KNOB_RADIUS = 10;
201
203
  /**
202
204
  * @param index - detent index.
203
205
  * @param count - number of detents.
@@ -208,17 +210,26 @@ window.__ModuleLoader__.load({
208
210
  return Math.min(Math.max(index, 0), count - 1) / (count - 1);
209
211
  }
210
212
  /**
213
+ * Where a pointer sits along the knob's travel. The drag reads this rather than a
214
+ * detent, so the knob follows the pointer instead of jumping a level at a time.
211
215
  * @param clientX - pointer position in viewport coordinates.
212
216
  * @param left - the track's left edge in viewport coordinates.
213
217
  * @param width - the track's width in pixels.
214
- * @param count - number of detents.
215
- * @returns the detent nearest the pointer, clamped to the two ends.
218
+ * @returns the position as a fraction of the travel, clamped to 0 and 1.
216
219
  */
217
- function stepAt(clientX, left, width, count) {
218
- if (count <= 1) return 0;
220
+ function fractionAt(clientX, left, width) {
219
221
  const usable = width - 20;
220
222
  if (usable <= 0) return 0;
221
- const fraction = (clientX - left - 10) / usable;
223
+ const fraction = (clientX - left - KNOB_RADIUS) / usable;
224
+ return Math.min(Math.max(fraction, 0), 1);
225
+ }
226
+ /**
227
+ * @param fraction - a knob position between 0 and 1.
228
+ * @param count - number of detents.
229
+ * @returns the detent nearest that position.
230
+ */
231
+ function detentAt(fraction, count) {
232
+ if (count <= 1) return 0;
222
233
  return Math.round(Math.min(Math.max(fraction, 0), 1) * (count - 1));
223
234
  }
224
235
  /**
@@ -226,11 +237,24 @@ window.__ModuleLoader__.load({
226
237
  * @returns the CSS length that puts the knob centre there, inset at both ends.
227
238
  */
228
239
  function knobOffset(fraction) {
229
- return `calc(10px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
240
+ return `calc(${KNOB_RADIUS}px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
241
+ }
242
+ /**
243
+ * Width of the colored fill for a knob position.
244
+ *
245
+ * The fill is the track's own shape cut at the knob: its right cap carries the
246
+ * track's radius and ends under the knob, so the color runs into the knob
247
+ * instead of stopping at a straight edge, and the last detent reaches the track's
248
+ * end rather than the knob's centre.
249
+ * @param fraction - a knob position between 0 and 1.
250
+ * @returns the CSS width of the fill.
251
+ */
252
+ function fillWidth(fraction) {
253
+ return `calc(16px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 16px))`;
230
254
  }
231
255
  //#endregion
232
256
  //#region \0dsh-css:/home/runner/work/my-dsh/my-dsh/plugins/dsh-model-submenu/src/client/ModelOptions.module.css.mjs
233
- const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:20px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;position:absolute;top:0;bottom:0;left:0;overflow:hidden}.TcCbqa_sliderTick{corner-shape:round;background:var(--dsw-alias-label-tertiary);pointer-events:none;border-radius:50%;width:4px;height:4px;margin:-2px 0 0 -2px;position:absolute;top:50%}.TcCbqa_sliderTick[data-on]{background:#00000047}.TcCbqa_sliderKnob{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);width:20px;height:20px;box-shadow:var(--dsw-elevation-soft);pointer-events:none;border-radius:50%;margin:-10px 0 0 -10px;position:absolute;top:50%}.TcCbqa_slider[data-default] .TcCbqa_sliderKnob{background:var(--dsw-alias-bg-layer-1);border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none}.TcCbqa_slider[data-busy] .TcCbqa_sliderKnob{opacity:.6}.TcCbqa_slider:focus-visible .TcCbqa_sliderTrack{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.TcCbqa_rowBadge{flex:none;margin-inline-start:6px}.TcCbqa_row{align-items:center;gap:2px;display:flex}.TcCbqa_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-md);padding-inline:6px;background:0 0;border:none;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.TcCbqa_rowPick:hover:not(:disabled),.TcCbqa_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.TcCbqa_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.TcCbqa_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.TcCbqa_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.TcCbqa_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.TcCbqa_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:var(--dsw-radius-sm);opacity:0;background:0 0;border:none;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.TcCbqa_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_row:hover .TcCbqa_starButton,.TcCbqa_row:focus-within .TcCbqa_starButton{opacity:1}.TcCbqa_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.TcCbqa_starButton{opacity:1}}.TcCbqa_star{flex:none}.TcCbqa_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.TcCbqa_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.TcCbqa_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.TcCbqa_pickerContent,.TcCbqa_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.TcCbqa_pickerSearch{padding:0 20px 12px}.TcCbqa_pickerSearchInput{width:100%}.TcCbqa_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.TcCbqa_pickerTabs::-webkit-scrollbar{display:none}.TcCbqa_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:var(--dsw-radius-sm);white-space:nowrap;background:0 0;border:none;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.TcCbqa_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.TcCbqa_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.TcCbqa_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.TcCbqa_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.TcCbqa_footerDot{color:var(--dsw-alias-label-dimmed)}";
257
+ const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:16px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;transition:width .16s cubic-bezier(.2,0,0,1);position:absolute;top:0;bottom:0;left:0}.TcCbqa_sliderTick{corner-shape:round;background:var(--dsw-alias-label-tertiary);pointer-events:none;border-radius:50%;width:4px;height:4px;margin:-2px 0 0 -2px;position:absolute;top:50%}.TcCbqa_sliderTick[data-on]{background:#00000047}.TcCbqa_sliderKnob{box-sizing:border-box;corner-shape:round;background:var(--dsw-alias-label-primary-foreground);width:20px;height:20px;box-shadow:var(--dsw-elevation-soft);pointer-events:none;border-radius:50%;margin:-10px 0 0 -10px;transition:left .16s cubic-bezier(.2,0,0,1);position:absolute;top:50%}.TcCbqa_slider[data-dragging] .TcCbqa_sliderKnob,.TcCbqa_slider[data-dragging] .TcCbqa_sliderFill{transition:none}@media (prefers-reduced-motion:reduce){.TcCbqa_sliderKnob,.TcCbqa_sliderFill{transition:none}}.TcCbqa_slider[data-default] .TcCbqa_sliderKnob{background:var(--dsw-alias-bg-layer-1);border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none}.TcCbqa_slider[data-busy] .TcCbqa_sliderKnob{opacity:.6}.TcCbqa_slider:focus-visible .TcCbqa_sliderTrack{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.TcCbqa_rowBadge{flex:none;margin-inline-start:6px}.TcCbqa_row{align-items:center;gap:2px;display:flex}.TcCbqa_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-md);padding-inline:6px;background:0 0;border:none;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.TcCbqa_rowPick:hover:not(:disabled),.TcCbqa_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.TcCbqa_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.TcCbqa_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.TcCbqa_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.TcCbqa_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.TcCbqa_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:var(--dsw-radius-sm);opacity:0;background:0 0;border:none;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.TcCbqa_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_row:hover .TcCbqa_starButton,.TcCbqa_row:focus-within .TcCbqa_starButton{opacity:1}.TcCbqa_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.TcCbqa_starButton{opacity:1}}.TcCbqa_star{flex:none}.TcCbqa_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.TcCbqa_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.TcCbqa_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.TcCbqa_pickerContent,.TcCbqa_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.TcCbqa_pickerSearch{padding:0 20px 12px}.TcCbqa_pickerSearchInput{width:100%}.TcCbqa_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.TcCbqa_pickerTabs::-webkit-scrollbar{display:none}.TcCbqa_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:var(--dsw-radius-sm);white-space:nowrap;background:0 0;border:none;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.TcCbqa_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.TcCbqa_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.TcCbqa_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.TcCbqa_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}";
234
258
  const tagId = "@lynn123411/dsh-model-submenu/ModelOptions.module.css";
235
259
  if (typeof document !== "undefined") {
236
260
  let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
@@ -244,7 +268,6 @@ window.__ModuleLoader__.load({
244
268
  }
245
269
  var ModelOptions_module_css_default = {
246
270
  "check": "TcCbqa_check",
247
- "footerDot": "TcCbqa_footerDot",
248
271
  "footerHint": "TcCbqa_footerHint",
249
272
  "notice": "TcCbqa_notice",
250
273
  "pickerContent": "TcCbqa_pickerContent",
@@ -279,9 +302,10 @@ window.__ModuleLoader__.load({
279
302
  *
280
303
  * The Host's second menu cell is a button that drills into a list of levels.
281
304
  * This slider sits inside that same cell, between the Host's label and value, so
282
- * the levels are reachable without leaving the menu. Levels are discrete: the
283
- * knob snaps to one detent per level, the drag only moves the knob, and the
284
- * release commits once. Keyboard arrows step and commit immediately.
305
+ * the levels are reachable without leaving the menu. The levels are discrete but
306
+ * the drag is not: the knob follows the pointer continuously, the level it
307
+ * reports is the detent nearest that position, and letting go eases the knob onto
308
+ * that detent and commits once. Keyboard arrows step and commit immediately.
285
309
  *
286
310
  * The fill takes the level's color, so the track and the two captions read as
287
311
  * one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
@@ -299,22 +323,34 @@ window.__ModuleLoader__.load({
299
323
  const trackRef = (0, react.useRef)(null);
300
324
  const dragRef = (0, react.useRef)(null);
301
325
  const [drag, setDrag] = (0, react.useState)(null);
302
- const active = drag ?? (defaultActive ? defaultIndex : current);
303
- const beginDrag = (index) => {
304
- dragRef.current = index;
305
- setDrag(index);
326
+ const [hold, setHold] = (0, react.useState)(null);
327
+ const dragging = drag !== null;
328
+ const resting = defaultActive ? defaultIndex : current;
329
+ const active = dragging ? detentAt(drag, steps.length) : resting;
330
+ const knobFraction = drag ?? hold ?? offsetOf(resting, steps.length);
331
+ const shown = dragging || hold !== null ? detentAt(knobFraction, steps.length) : resting;
332
+ const beginDrag = (fraction) => {
333
+ dragRef.current = fraction;
334
+ setDrag(fraction);
306
335
  };
307
- const indexAt = (clientX) => {
336
+ (0, react.useEffect)(() => {
337
+ if (!busy) setHold(null);
338
+ }, [busy]);
339
+ (0, react.useEffect)(() => {
340
+ setHold(null);
341
+ }, [current, defaultActive]);
342
+ const fractionAtPointer = (clientX) => {
308
343
  const track = trackRef.current;
309
- if (track === null) return active;
344
+ if (track === null) return dragRef.current ?? knobFraction;
310
345
  const rect = track.getBoundingClientRect();
311
- return stepAt(clientX, rect.left, rect.width, steps.length);
346
+ return fractionAt(clientX, rect.left, rect.width);
312
347
  };
313
348
  const onPointerDown = (event) => {
314
349
  if (busy) return;
315
350
  event.preventDefault();
316
351
  event.stopPropagation();
317
- beginDrag(indexAt(event.clientX));
352
+ setHold(null);
353
+ beginDrag(fractionAtPointer(event.clientX));
318
354
  try {
319
355
  event.currentTarget.setPointerCapture(event.pointerId);
320
356
  } catch {}
@@ -322,15 +358,17 @@ window.__ModuleLoader__.load({
322
358
  };
323
359
  const onPointerMove = (event) => {
324
360
  if (dragRef.current === null || busy) return;
325
- const next = indexAt(event.clientX);
326
- if (next !== dragRef.current) beginDrag(next);
361
+ beginDrag(fractionAtPointer(event.clientX));
327
362
  };
328
363
  const settle = (event, commit) => {
329
364
  if (dragRef.current === null) return;
330
- const next = indexAt(event.clientX);
365
+ const next = detentAt(fractionAtPointer(event.clientX), steps.length);
331
366
  beginDrag(null);
332
367
  if (!commit) return;
333
- if (next !== current || defaultActive) onCommit(next);
368
+ if (next !== current || defaultActive) {
369
+ setHold(offsetOf(next, steps.length));
370
+ onCommit(next);
371
+ }
334
372
  };
335
373
  const onKeyDown = (event) => {
336
374
  if (busy) return;
@@ -341,11 +379,10 @@ window.__ModuleLoader__.load({
341
379
  event.stopPropagation();
342
380
  if (next !== current || defaultActive) onCommit(next);
343
381
  };
344
- const knobFraction = offsetOf(active, steps.length);
345
382
  const offset = knobOffset(knobFraction);
346
- const idle = defaultActive && drag === null;
383
+ const idle = defaultActive && !dragging && hold === null;
347
384
  const filled = idle ? 0 : knobFraction;
348
- const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);
385
+ const fill = isTopLevel(shown, steps.length) ? LEVEL_GRADIENT : levelColor(shown, steps.length);
349
386
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
350
387
  ref: trackRef,
351
388
  className: ModelOptions_module_css_default.slider,
@@ -355,11 +392,11 @@ window.__ModuleLoader__.load({
355
392
  "aria-valuemin": 0,
356
393
  "aria-valuemax": Math.max(0, steps.length - 1),
357
394
  "aria-valuenow": active,
358
- "aria-valuetext": defaultActive && drag === null ? defaultLabel : steps[active]?.label,
395
+ "aria-valuetext": defaultActive && !dragging ? defaultLabel : steps[active]?.label,
359
396
  "aria-disabled": busy || void 0,
360
397
  "data-busy": busy ? "" : void 0,
361
- "data-dragging": drag === null ? void 0 : "",
362
- "data-default": defaultActive && drag === null ? "" : void 0,
398
+ "data-dragging": dragging ? "" : void 0,
399
+ "data-default": idle ? "" : void 0,
363
400
  onPointerDown,
364
401
  onPointerMove,
365
402
  onPointerUp: (event) => {
@@ -379,13 +416,13 @@ window.__ModuleLoader__.load({
379
416
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
380
417
  className: ModelOptions_module_css_default.sliderFill,
381
418
  style: {
382
- width: knobOffset(filled),
419
+ width: fillWidth(filled),
383
420
  background: fill
384
421
  }
385
422
  }),
386
423
  steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
387
424
  className: ModelOptions_module_css_default.sliderTick,
388
- "data-on": index <= active && !idle ? "" : void 0,
425
+ "data-on": index <= shown && !idle ? "" : void 0,
389
426
  style: { left: knobOffset(offsetOf(index, steps.length)) }
390
427
  }, step.key)),
391
428
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -605,7 +642,7 @@ window.__ModuleLoader__.load({
605
642
  * @param props - the row's content and behaviour.
606
643
  * @returns the row element.
607
644
  */
608
- function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
645
+ function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel, onPick, onToggleFavorite, children }) {
609
646
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
610
647
  className: ModelOptions_module_css_default.row,
611
648
  "data-selected": selected ? "" : void 0,
@@ -613,14 +650,11 @@ window.__ModuleLoader__.load({
613
650
  children: [
614
651
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
615
652
  type: "button",
616
- role: "menuitemradio",
617
- "aria-checked": selected,
618
- tabIndex: rowTabIndex,
653
+ "aria-current": selected ? "true" : void 0,
619
654
  className: ModelOptions_module_css_default.rowPick,
620
655
  title: name,
621
656
  disabled: busy,
622
657
  onClick: onPick,
623
- onFocus: onFocusRow,
624
658
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
625
659
  className: ModelOptions_module_css_default.rowCopy,
626
660
  children: [
@@ -641,7 +675,6 @@ window.__ModuleLoader__.load({
641
675
  className: ModelOptions_module_css_default.starButton,
642
676
  "aria-pressed": favorited,
643
677
  "aria-label": starLabel,
644
- tabIndex: starTabStop ? 0 : -1,
645
678
  disabled: busy,
646
679
  onClick: onToggleFavorite,
647
680
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
@@ -940,8 +973,6 @@ window.__ModuleLoader__.load({
940
973
  const listRef = (0, react.useRef)(null);
941
974
  const [query, setQuery] = (0, react.useState)("");
942
975
  const [tab, setTab] = (0, react.useState)("*");
943
- const [highlight, setHighlight] = (0, react.useState)(0);
944
- const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
945
976
  const tabs = (0, react.useMemo)(() => pickerTabs(state.groups, t("picker.all")), [state.groups, t]);
946
977
  const reason = (0, react.useMemo)(() => reasoningOf(state.groups), [state.groups]);
947
978
  const rows = (0, react.useMemo)(() => orderWithFavorites(rowsForTab(flattenGroups(state.groups), tab), (row) => isFavorite(favorites, row.provider, row.model)), [
@@ -958,7 +989,6 @@ window.__ModuleLoader__.load({
958
989
  if (!open) return;
959
990
  setQuery("");
960
991
  setTab("*");
961
- setHighlight(0);
962
992
  if (listRef.current !== null) listRef.current.scrollTop = 0;
963
993
  const frame = requestAnimationFrame(() => {
964
994
  if (!(0, _deepseek_ai_dsh_client_ui_primitives.isMobileDevice)()) searchRef.current?.focus();
@@ -967,29 +997,7 @@ window.__ModuleLoader__.load({
967
997
  cancelAnimationFrame(frame);
968
998
  };
969
999
  }, [open]);
970
- (0, react.useEffect)(() => {
971
- setHighlight((current) => visible.length === 0 ? 0 : Math.min(current, visible.length - 1));
972
- }, [visible.length]);
973
- (0, react.useEffect)(() => {
974
- (listRef.current?.querySelector(`[data-row-index="${highlight}"]`))?.scrollIntoView({ block: "nearest" });
975
- }, [highlight]);
976
1000
  if (!open) return null;
977
- const activeRow = visible[highlight];
978
- const activeId = activeRow === void 0 ? void 0 : `${id}-row-${highlight}`;
979
- const onSearchKeyDown = (event) => {
980
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
981
- event.preventDefault();
982
- if (visible.length === 0) return;
983
- setHighlight((current) => {
984
- return (current + (event.key === "ArrowDown" ? 1 : -1) + visible.length) % visible.length;
985
- });
986
- return;
987
- }
988
- if (event.key === "Enter" && activeRow !== void 0 && !busy) {
989
- event.preventDefault();
990
- pick(activeRow);
991
- }
992
- };
993
1001
  const pick = (row) => {
994
1002
  const selection = planModelCommit(live, row.provider, row.model, reason, rememberedOf?.(row.provider, row.model));
995
1003
  if (selection === null) {
@@ -998,26 +1006,6 @@ window.__ModuleLoader__.load({
998
1006
  }
999
1007
  onPick(selection);
1000
1008
  };
1001
- /** Move to another tab and hand it the keyboard, in one step. */
1002
- const goTab = (next) => {
1003
- if (next === void 0 || next.id === tab) return;
1004
- setTab(next.id);
1005
- setHighlight(0);
1006
- tabRefs.current.get(next.id)?.focus();
1007
- };
1008
- const onTabKeyDown = (event) => {
1009
- const index = tabs.findIndex((candidate) => candidate.id === tab);
1010
- if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
1011
- event.preventDefault();
1012
- const step = event.key === "ArrowRight" ? 1 : -1;
1013
- goTab(tabs[((index === -1 ? 0 : index) + step + tabs.length) % tabs.length]);
1014
- return;
1015
- }
1016
- if (event.key === "Home" || event.key === "End") {
1017
- event.preventDefault();
1018
- goTab(tabs[event.key === "Home" ? 0 : tabs.length - 1]);
1019
- }
1020
- };
1021
1009
  const failures = state.failures.filter((failure) => tab === "*" || failure.id === tab);
1022
1010
  const failed = state.error !== null && state.status === "error";
1023
1011
  const loading = state.status === "loading";
@@ -1038,7 +1026,6 @@ window.__ModuleLoader__.load({
1038
1026
  role: "searchbox",
1039
1027
  "aria-label": t("picker.search"),
1040
1028
  "aria-controls": `${id}-list`,
1041
- "aria-activedescendant": activeId,
1042
1029
  placeholder: t("picker.search"),
1043
1030
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }),
1044
1031
  value: query,
@@ -1046,7 +1033,6 @@ window.__ModuleLoader__.load({
1046
1033
  onChange: (event) => {
1047
1034
  setQuery(event.target.value);
1048
1035
  },
1049
- onKeyDown: onSearchKeyDown,
1050
1036
  "data-modal-autofocus": ""
1051
1037
  })
1052
1038
  }),
@@ -1055,10 +1041,6 @@ window.__ModuleLoader__.load({
1055
1041
  role: "tablist",
1056
1042
  "aria-label": t("picker.title"),
1057
1043
  children: tabs.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1058
- ref: (node) => {
1059
- if (node === null) tabRefs.current.delete(entry.id);
1060
- else tabRefs.current.set(entry.id, node);
1061
- },
1062
1044
  type: "button",
1063
1045
  role: "tab",
1064
1046
  id: `${id}-tab-${entry.id}`,
@@ -1068,14 +1050,6 @@ window.__ModuleLoader__.load({
1068
1050
  className: ModelOptions_module_css_default.pickerTab,
1069
1051
  onClick: () => {
1070
1052
  setTab(entry.id);
1071
- setHighlight(0);
1072
- },
1073
- onKeyDown: onTabKeyDown,
1074
- onFocus: () => {
1075
- if (entry.id !== tab) {
1076
- setTab(entry.id);
1077
- setHighlight(0);
1078
- }
1079
1053
  },
1080
1054
  children: entry.label
1081
1055
  }, entry.id))
@@ -1084,8 +1058,6 @@ window.__ModuleLoader__.load({
1084
1058
  ref: listRef,
1085
1059
  id: `${id}-list`,
1086
1060
  className: ModelOptions_module_css_default.pickerList,
1087
- role: "menu",
1088
- "aria-label": t("picker.title"),
1089
1061
  children: [
1090
1062
  loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1091
1063
  className: ModelOptions_module_css_default.notice,
@@ -1116,63 +1088,39 @@ window.__ModuleLoader__.load({
1116
1088
  className: ModelOptions_module_css_default.notice,
1117
1089
  children: state.groups.length === 0 ? t("status.empty") : t("picker.searchEmpty")
1118
1090
  }),
1119
- visible.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1120
- "data-row-index": index,
1121
- id: `${id}-row-${index}`,
1122
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1123
- name: row.entry.name,
1124
- badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1125
- tone: row.entry.badge.tone,
1126
- className: ModelOptions_module_css_default.rowBadge,
1127
- children: resolveText(row.entry.badge.text)
1128
- }),
1129
- selected: live?.provider === row.provider && live.model === row.model,
1130
- busy,
1131
- favorited: isFavorite(favorites, row.provider, row.model),
1132
- meta: metaLine({
1133
- row,
1134
- facts: facts(row.provider, row.model),
1135
- providerName: tab === "*" ? row.providerName : void 0,
1136
- thinks: row.entry.reasoning !== void 0,
1137
- effortLabel: t("meta.effort"),
1138
- imageLabel: t("meta.images")
1139
- }),
1140
- starLabel: isFavorite(favorites, row.provider, row.model) ? t("picker.unfavorite", { model: row.entry.name }) : t("picker.favorite", { model: row.entry.name }),
1141
- starTabStop: index === highlight,
1142
- rowTabIndex: -1,
1143
- onPick: () => {
1144
- pick(row);
1145
- },
1146
- onFocusRow: () => {
1147
- setHighlight(index);
1148
- },
1149
- onToggleFavorite: () => {
1150
- onToggleFavorite(row.provider, row.model);
1151
- }
1152
- })
1153
- }, `${row.provider}/${row.model}`))
1091
+ visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1092
+ name: row.entry.name,
1093
+ badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1094
+ tone: row.entry.badge.tone,
1095
+ className: ModelOptions_module_css_default.rowBadge,
1096
+ children: resolveText(row.entry.badge.text)
1097
+ }),
1098
+ selected: live?.provider === row.provider && live.model === row.model,
1099
+ busy,
1100
+ favorited: isFavorite(favorites, row.provider, row.model),
1101
+ meta: metaLine({
1102
+ row,
1103
+ facts: facts(row.provider, row.model),
1104
+ providerName: tab === "*" ? row.providerName : void 0,
1105
+ thinks: row.entry.reasoning !== void 0,
1106
+ effortLabel: t("meta.effort"),
1107
+ imageLabel: t("meta.images")
1108
+ }),
1109
+ starLabel: isFavorite(favorites, row.provider, row.model) ? t("picker.unfavorite", { model: row.entry.name }) : t("picker.favorite", { model: row.entry.name }),
1110
+ onPick: () => {
1111
+ pick(row);
1112
+ },
1113
+ onToggleFavorite: () => {
1114
+ onToggleFavorite(row.provider, row.model);
1115
+ }
1116
+ }) }, `${row.provider}/${row.model}`))
1154
1117
  ]
1155
1118
  }),
1156
1119
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1157
1120
  className: ModelOptions_module_css_default.pickerFooter,
1158
1121
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1159
1122
  className: ModelOptions_module_css_default.footerHint,
1160
- children: [
1161
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["↑", "↓"] }),
1162
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintNavigate") }),
1163
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1164
- className: ModelOptions_module_css_default.footerDot,
1165
- children: "·"
1166
- }),
1167
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Enter"] }),
1168
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintSelect") }),
1169
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1170
- className: ModelOptions_module_css_default.footerDot,
1171
- children: "·"
1172
- }),
1173
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }),
1174
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintClose") })
1175
- ]
1123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintClose") })]
1176
1124
  })
1177
1125
  })
1178
1126
  ]
@@ -1437,8 +1385,6 @@ window.__ModuleLoader__.load({
1437
1385
  "picker.all": "全部",
1438
1386
  "picker.favorite": "收藏 {model}",
1439
1387
  "picker.unfavorite": "取消收藏 {model}",
1440
- "picker.hintNavigate": "导航",
1441
- "picker.hintSelect": "选择",
1442
1388
  "picker.hintClose": "关闭",
1443
1389
  "meta.effort": "思考",
1444
1390
  "meta.images": "图片输入",
@@ -1462,8 +1408,6 @@ window.__ModuleLoader__.load({
1462
1408
  "picker.all": "All",
1463
1409
  "picker.favorite": "Favorite {model}",
1464
1410
  "picker.unfavorite": "Remove {model} from favorites",
1465
- "picker.hintNavigate": "navigate",
1466
- "picker.hintSelect": "select",
1467
1411
  "picker.hintClose": "close",
1468
1412
  "meta.effort": "Effort",
1469
1413
  "meta.images": "Images",