dsh-thick-scrollbar
DSH Web GUI 的小插件:滚动条默认保持原样(细条、低对比、该隐藏的地方照样隐藏),只有鼠标移到滚动槽附近时临时加粗到 10px,离开立刻恢复;加粗过程不改变滚动槽宽度,正文不会横向抖动或位移。

(左两张 1:1 —— 静止态绘制 6px、悬停态 10px,滚动槽与内容盒宽度完全一致;右两张 1.6× 放大。)
行为
- 基线(插件一加载就注入):
html body上把--dsh-scrollbar-width定为 10px、--dsh-scrollbar-thumb-border定为 2.5px。透明边框把「绘制的滑块」内缩回去,所以看起来和原来一样细,但滚动槽已永久预留 10px 的布局空间。 - 指针进入容器右边缘 13px 窄带(
ARM_DISTANCE):只给该容器加一个标记属性data-dsh-thick-scrollbar,由作用域规则html body [data-dsh-thick-scrollbar]::-webkit-scrollbar-thumb { border-width: 0 }把透明边框清零 —— 滑块原地从细条画成 10px。槽宽不变,所以周围内容一格都不移动。 - 指针离开窄带超过 21px(
DISARM_DISTANCE,留 8px 迟滞防抖动):删掉该属性。插件从不写内联样式,所以没有任何「恢复原值」的风险。 - 滚轮滚动时不改变状态:容器不会因为滚动而位移,鼠标还压在滚动槽上就应当保持粗条。
实测宽度(headless Chromium,DPR 1 与 2 逐像素数过)
| 状态 | 滚动槽 | 实际绘制滑块 |
|---|---|---|
| ui-theme 原始 | 5px | 5px |
| 插件静止 | 10px | 6px |
| 插件悬停 | 10px | 10px |
静止态是 6px 而不是 5px,因为 Chromium 在绘制滚动条伪元素时把 border-width 向下取整到整数 CSS 像素:2.5px → 2px,于是 10 − 2×2 = 6。想要静止态严格 5px 就需要「槽 9px + 边框 2px」,代价是悬停只能到 9px;这里选择保住用户要的 10px 悬停宽度,静止态多 1px。
为什么早期版本会抖
::-webkit-scrollbar 的 width 是参与布局的:Chromium 会从内容盒里扣掉这段宽度。第一版在悬停时把 --dsh-scrollbar-width 从 5px 改成 10px,槽宽变了 5px,旁边所有盒子跟着重排——表现就是画面整体向左缩一下。现在改成「槽宽恒定 10px + 悬停只去掉透明边框」,绘制变粗但布局零变化。headless Chromium 实测:restGutter=10 armedGutter=10 restContent=290 armedContent=290。
为什么用指针监听而不是纯 CSS :hover
纯 CSS container:hover::-webkit-scrollbar { width: 10px } 会在鼠标穿过正文时就加粗,滚动条会跟着乱跳。Chromium 还会在指针真正压上原生滚动条后停止派发指针事件,所以这里用「距离右边缘 ≤13px 就提前加粗」的策略:指针还没碰到滚动条,条已经变粗了;压上去之后即使收不到事件,状态也仍然保持。
可调参数
都在 lib/client.js 顶部:
| 常量 | 默认 | 含义 |
|---|---|---|
GUTTER_PX |
10 |
永久预留的滚动槽宽度,也是悬停时的绘制宽度(px) |
REST_BORDER_PX |
2.5 |
静止态透明边框,决定默认可见细条宽度 = GUTTER_PX − 2×REST_BORDER_PX |
ARM_DISTANCE |
GUTTER_PX + 3 |
距右边缘多近开始加粗 |
DISARM_DISTANCE |
GUTTER_PX + 11 |
距右边缘多远恢复 |
想让加粗更明显就抬 GUTTER_PX(同时抬 REST_BORDER_PX 保持默认细条宽度,否则静止态也会变粗)。改完刷新页面即可,不需要重新安装(本地 link: 安装,Host 直接读这个目录)。
安装 / 卸载
# 从本地目录安装(开发态,改源码立即生效)
plugin_manager install_bundle "link:<本目录绝对路径>"
# 从分发包安装
plugin_manager install_bundle "file:<dsh-thick-scrollbar-0.1.1.tgz 绝对路径>"
# 发布到 npm / GitHub 之后
plugin_manager install_bundle "dsh-thick-scrollbar"
plugin_manager install_bundle "github:<账号>/dsh-thick-scrollbar"
# 卸载
plugin_manager remove_bundle "dsh-thick-scrollbar"
装完刷新页面才生效(client bundle 惰性进图,不随 HMR 替换)。
测试
node --test test/*.test.mjs # 必须是 glob;在 Windows 上 node --test test/ 会报 Cannot find module
test/controller.test.mjs 用最小 DOM stub 直接跑真实的 lib/client.js,覆盖:基线样式注入、正文中部不加粗、窄带内加粗且只加标记属性、不写任何内联样式、离开后清除标记、标记不向下继承(子滚动容器不被带着变粗)、无溢出容器不处理、滚轮保持、失焦 / 移出窗口 / 卸载清理。
test/manifest.test.mjs 按 @deepseek-ai/dsh-client-modules 的严格校验逐条核对 package.json,避免「看起来对、装上不生效」。
两个几何 / 视觉探针(headless Chromium,--dump-dom 读数或 --screenshot 看图):
test/geometry.html:验证html body基线能压过主题的body规则(即使插入更早),以及加粗前后槽宽与内容宽度完全一致(零重排)。test/visual.html:四个面板并排对照静止 / 悬停(含 1.6× 放大),底部直接打印gutter / painted / content三组读数。
test/live.html 更进一步:它用 window.__ModuleLoader__ / window.__ctx 两个 shim 直接加载真实的 lib/client.js(不是复刻规则),再用 CDP 派发真实指针事件,验证 rest → armed → restored → dispose 全链路,以及嵌套容器不被遗传。
以下探针是评测「这项优化到底有没有用」时留下的证据页,可离线打开:
| 页面 | 回答的问题 |
|---|---|
test/hittest.html |
单个容器的拖动热区有多宽 |
test/hitscan.html |
主题原始 / 插件静止 / 插件悬停 三种配置的热区逐像素对比 |
test/nested.html |
外层加粗时内层滚动条会不会被连带加粗 |
test/impact.html |
各界面表面(普通容器 / 菜单 / 计划页 / 安静侧栏)的槽宽与绘制宽度代价 |
test/armrule.html |
属性式加粗 vs 内联变量式加粗的对照,以及每次 pointermove 的探测耗时 |
分享给别人
三条路径(tgz 包 / GitHub 仓库 / npm 发布),含各平台的安装命令、卸载与排错:见 SHARE.md。
边界
- 只处理纵向可滚动容器(
overflow-y: auto|scroll|overlay且scrollHeight > clientHeight)。 - 会永久预留 10px 滚动槽(相比主题默认 5px,正文窄 5px),这是「悬停零重排」的代价;
scrollbar-gutter: stable的面板同样按 10px 预留。 - 自己重绑了槽宽的表面不受影响:
ui-input-trigger的菜单把槽宽定成 6px、ui-schedule定成 9px(元素级 class 选择器,优先级高于插件基线),这些地方静止态保持原来的细条,悬停时也只到 6px / 9px,不是 10px。这是有意的:不覆盖各组件自己的几何决策。 - 加粗是瞬时的:Chromium 不为
::-webkit-scrollbar伪元素执行transition,没有渐变动画。 - 不处理 Firefox 的
@supports not selector(::-webkit-scrollbar)分支(桌面端是 Chromium)。 - 不改主题 token、不改 asar、不引入运行时依赖。
No comments yet. Be the first to write one.