# 前端设计说明(dsh-vaultwarden 浏览器半边) > 对应代码:`lib/client.js`(条目浏览面板)、`lib/gateway.js`(Host 侧 `vw` 网关)、`test/client-card.test.mjs`(252 项,含 localStorage 快照、`vw/boot` 打桩、旧宿主拒绝分页参数回退整表并被记住、满额截断诚实提示、列表分页与分页游标校准、层级分区导航与分区缓存零 RPC、服务端图标与降级(列表行与详情头部同源)、验证码分区行、写操作二次确认与只读档、五类条目详情)。 > 设计基线:与宿主设置页同水准;明暗双主题;仅 `--dsw-alias-*` 令牌;可访问性达标。 ## 1. 一个界面,一个槽位 | 界面 | 槽位 | 内容 | |---|---|---| | 条目浏览面板 | `settings.section`(id = `vaultwarden`,order 30) | 层级分区菜单(类型 / 文件夹 / 收藏 / 验证码 / 归档 / 回收站)+ 同步后的凭据列表 + 详情 + 复制 + TOTP 圆环倒计时 | | 配置表单 | 宿主从插件 `Config` schema 派生 | 服务器/账号/密钥/同步控制/权限档,密钥字段只写 | 选型依据(`cordis_inspect_query` → Slots.listSubTree): - `settings.section` 是 list 槽、`replaceRisk: none`、**有分配空间**,插件获得自己的设置页,不与任何第三方插件耦合(不依赖 dsh-better-sidebar 之类)。 - 0.2.0 起配置表单由宿主按 `Config` schema 渲染,插件不再自绘卡片;旧的 `settingsScope` / `settings.plugin.item` 已被移除,继续用它们会导致前端整块不注册(曾实测踩到)。 - 会话内表面(composer dock / 右栏 tab)都要求 session 作用域或依赖其他插件,v1 不做。 ## 1b. 图标规范 | 位置 | 形态 | 来源 | |---|---|---| | 插件列表 / 插件市场 | 双层盾牌(紫 #6C4DF6 后盾 + 蓝 #2E6BE6 前盾)配白色镂空钥匙孔 | 包根 `icon.svg`(1024 画布,扁平色块 + 镂空),`package.json` `icon` 字段指向它,`files` 收录 | | 设置页导航行「凭证据库」 | 单色盾牌线稿(`currentColor` 跟随行文字色,明暗双主题自适应) | `lib/client.js` `VaultGlyph` | 宿主 artwork 规范(实证自 dsh-context / dsh-im):包根带 `icon.svg` + `package.json` 顶层 `"icon": "icon.svg"` + `files` 收录; 插件管理器渲染 `pkg.meta.icon`(`@deepseek-ai/dsh-client-ui-plugin-manager`,卡片 36px / 列表行 30px,`PluginArtworkDefault` 兜底), 官方风 = 鲜艳扁平的纯色形状组合(dsh-im = 交叠对话气泡 + 渐变,dsh-context = 六色拼贴),不是「线稿 + 彩色底框」。 设置导航行没有插件图标槽(宿主只给五个内置 section 自己的图标,其余一律发默认齿轮),所以那一行的图标由标签自己画: `VaultNavLabel` 把盾牌线稿并进标签自身的 flex 流(`.vw-nav-label` 行向布局,桌面 gap 8px / 移动 tab 条 6px 由样式表断点给, 不实测、不覆盖宿主槽位),同时把宿主齿轮 `display: none`——图标占的就是齿轮原来的位置,行文字与其余行对齐。 样式表由 `VaultNavLabel` 挂载时 `ensureStyles()` 保证存在(设置 nav 是宿主弹层,可能早于面板首次渲染)。 ## 2. 数据通道 浏览器只走 `/api` connection RPC(`vw/*` 命名空间,`connection.rpc.call('/api','vw/',{args})`), **承载操作者已认证会话**——与 dsh-vault 相同的受信通道;插件刻意不自建 HTTP 路由(那会绕过鉴权,实测裸请求能拿到 200)。 ``` vw/status 配置/解锁/同步状态 vw/overview 分区计数(全部/收藏/验证码/归档/回收站)+ 类型与文件夹分组(侧栏菜单用) vw/list 条目摘要(永不含密码;空查询=全量) vw/reveal 单条字段;field 默认 all(窄字段需点名 card / identity / secureNote / sshKey / totp);reprompt 条目只有在 authorizeReprompt 开过时间窗后才返回明文 vw/authorizeReprompt 面板内输入主密码 → 比对会话主密钥 → 开一个纯内存时间窗(默认 5 分钟),并把刚解锁的条目一并回传 vw/repromptGrant 轮询剩余解锁时间(仅当门正在屏幕上时) vw/totp 当前 TOTP + 倒计时 vw/sync 强制同步 vw/session · vw/reset · vw/discardChallenge 会话 vw/boot 一次取回配置 + 会话 + 是否恢复(开面板用) vw/config · vw/configure · vw/connect · vw/twoFactor · vw/submitTwoFactor 配置与登录 vw/list section / sectionValue 选分区池(all/totp/type/folder/unfiled/favorites/archive/trash),宿主侧收窄后才计数与分页;includeArchived / includeTrashed 决定归档与回收站是否入列 vw/reveal includeTrashed 供回收站条目读取(否则报 trashed) vw/folders 文件夹 id + 名字 + 条目数 + 未归类条数 vw/create · vw/update · vw/remove · vw/restore 写回(受 Config.accessMode 约束) ``` Host 侧由 `lib/gateway.js`(`TypertRemoteService` 子类,命名空间 `vw`)实现。纯 JS 无法使用装饰器语法 (Node 22 不解析),标记由 `markRemote()` 按协议描述符格式在运行时打上,`remoteMethods()` 可回读验证 (host-entry 测试即断言这 20 个线面方法)。配置表单不自绘:0.2.0 起宿主从插件 `Config` schema 派生 (旧版的 `settingsScope` / `settings.plugin.item` 已移除,用它会导致前端整块不注册)。 ## 3. 设计体系 **间距**:4 / 8 / 12 / 16 / 24 五档;卡片内边距 16/20,节间距 16。 **字号**:正文 13、次级 12、小节标题 12(semibold)、标题 15/14。 **控件**:输入框高 30、圆角 6;主按钮(accent)+ 次按钮(ghost);布尔用自建 `role="switch"` + `aria-checked`(抄宿主开关行为:36×20 轨道 + 14 圆形 knob + 120ms 位移)。 **焦点**:2px focus ring 用 `border-strong`/accent 令牌表达(输入框 outline 由 token 控制)。 **令牌层**(`--vw-*` 是插件本地语义别名,套在宿主令牌上,字面量只作 fallback——与 dsh-vault 的 `--v-*` 同构): | 本地令牌 | 宿主令牌 | 用途 | |---|---|---| | `--vw-text` / `--vw-text-2` | label-primary / label-secondary | 正文 / 次级文字(**不用 opacity 压暗**) | | `--vw-border` / `--vw-border-strong` | border-l1 / border-l2 | 分隔线 / 输入框描边 | | `--vw-bg` / `--vw-bg-2` | bg-layer-1 / bg-layer-2 | 卡片 / 嵌套面 | | `--vw-accent` / `--vw-on-accent` | brand-primary / 白 | 主按钮、进度条 | | `--vw-ok/warn/err/idle` | state-success/warn/error/idle-primary | 状态圆点与边框(**不当正文色**) | 明暗双主题:所有令牌随 `body[data-ds-dark-theme]` 自动翻转,无私立第二套样式。 ## 4. 条目面板 - **标题行**:`Bitwarden 凭据库` + 小字 `(支持 Bitwarden / Vaultwarden)`;标题本身保持短,兼容说明不喧宾夺主。 - **工具行**:搜索框(占位符只说明搜什么;`/` 聚焦、Esc 清空、↑↓ 移动选中放在 tooltip——原先塞在占位符里,窄屏先被截断)、同步状态 chip(实时/轮询/关闭 + 连接圆点 + 相对同步时间)、条目总数,以及**三个图标按钮**:新建条目(`+`)、设置、刷新(v0.8.0 起由文字改 16×16 线稿;文字版在手机上占掉整第三行)。 - **布局**:左「侧栏」+ 右「内容列」两栏;内容列在列表与条目详情之间切换(打开条目时列表让位给详情,列表上方留「‹ 返回列表」一行)。侧栏始终可见,不用手机那种「点进去就换屏」。 滚动区是带高度预算的 flex 列(`max-height: min(58vh, 560px)` + `overflow-y: auto`,标题与工具栏因此常驻、内容自己滚)。**它的直接子项一律 `flex: none`(`S.scrollItem`)**:flex 子项默认会缩,卡片内容高过预算时盒子被压扁、字段却继续画到卡片底衬之外——API 密钥模式比主密码模式多两行,829px 高的窗口下 58vh 只有 480px,client_secret、它的说明与按钮整块浮在弹窗底色上,看着就像字段错位。 左栏 `position: sticky; top: 0`,列表滚动时钉在滚动区顶部(`align-items: start` 的网格给它位移空间;侧栏父级按内容自然高度排布才有处可粘,即上一条)。**解钉只对粗指针(手指)生效**:`@media (pointer: coarse)` 里再嵌 `@container vw-panel (max-width: 480px)`(无容器查询时用 `@media (max-width: 760px) and (pointer: coarse)`)各写一条 `position: static !important`——手机上左栏搬到顶部、成为普通内容,跟着列表一起滑动;鼠标在**任意宽度都保持钉住**,窄窗口不会让分区菜单(收藏 / 验证码 / 未归类…)跟着列表滚走。左栏比滚动口高时(文件夹多)sticky 元素会随列表滚到自身末尾,因此**不加 `max-height`**(加了会把最后一行截在半个高度上,看着像坏盒子)。 工具栏在窄面板下改两行:搜索框独占一行,下面一行左是同步胶囊、右是**三个动作按钮**(`@container vw-panel (max-width: 520px)`,容器查询够不到内联样式故用 `!important`)。此前它把操作区整行铺满成第三行,手机上出现半行空白、两个按钮飘在下面。 面板窄到 **300px 以下**(宿主弹窗减去自己的导航列后只剩 ~110px)时每个控件各占一行:三个 32px 按钮加间距要 112px,比内容列宽一个像素,第三个会被挤到新的一行——收到 28px 正好一排。这个宽度下计数所在的可伸缩轨道还会塌成 0、文字直接画到按钮上面(`elementsFromPoint` 命中的是计数而不是按钮),所以必须先让每个控件各占一行。 **齿轮图标是一条闭合轮廓 + 中心毂**(八齿、32 个点),不是「圆 + 八根辐条」:辐条与轮圈同粗,14px 下只读得出太阳。 断点走 **CSS 容器查询**(`@container vw-panel`)而不是窗口宽度:宿主设置弹窗固定 800px 宽,其中约 280px 是设置导航,面板自己只剩 ~560px,用窗口宽度判断会永远判成「桌面」。 `<480px` 才把侧栏搬到顶部、钻取时藏侧栏;`<620px` 把字段行的标签列从 92px 收到 72px。不支持容器查询的浏览器回退到 `@supports not (container-type)` + 760px 窗口断点。 - **分区菜单**(侧栏,计数来自 `vw/overview`):全部条目 / 收藏 / 验证码 三行,然后是两组带标题的层级:类型 (n)(登录、安全笔记、银行卡、身份信息、SSH 密钥,按数量降序)与 文件夹 (n)(各文件夹 + 未归类),最后是 归档 与 回收站。点一行即把列表切成该分区的池子(`vw/list` 带 `section`/`sectionValue`,宿主侧收窄后才计数、才分页,因此计数与列表永远一致);当前分区带 `aria-current` 高亮。 每行的图标是**画出来的 SVG 线稿**(`NAV_ICON_SHAPES` → `navGlyph`,16×16、1.3px 描边、`currentColor`),不再用 `▦ ★ ⏱ ◍ ▸ · ⌸ ⌫` 这些字体字符——它们的视觉大小与基线随宿主字体而变(半个高度的三角挨着满高的方框,未归类的中点在文字基线上),换成同一套线稿后每行读起来一致;条目类型的图标按 `TYPE_ICON_NAMES` 投影,未知类型退到通用记号。图标是装饰(`aria-hidden`),颜色比标签浅一档,当前行加深。 计数是**统一的扁平药丸**(`min-width 22` / 高 20 / `border-radius: 999px` / tabular-nums),一位数的回收站与三位数的全部条目等重、数字对齐成列;当前行的药丸反色成面板底色,避免 bg-2 叠在 bg-2 上看不见。计数元素里始终只有裸数字,样式只是它外面的壳。 - **分区缓存**:切回刚离开的分区时,若该分区快照在 5 分钟内(`SECTION_CACHE_MS`)就**直接重绘、零 RPC**;搜索、刷新按钮与写操作不走缓存(一律重查)。首次进入某分区仍是「先画后拉」。 - **列表**:行 = 服务端缓存的站点图标(`` 指向 `/icons//icon.png`,主题无关、随服务器缓存;加载失败或像素小于 `ICON_MIN_PX = 24` 的行退回中性首字母圆枕,纯数字名如 IP 不会退化成同一个「1」)+ 名称 + 用户名·URI 截断 + 收藏星 + TOTP 徽标;归档条目带「已归档」徽标,回收站条目名称划掉、带「回收站」徽标(warn 色);`role="listbox"`/`role="option"` + `aria-selected`;选中行用 bg-2 高亮。 列表行的盒子模型是 **border-box**:`listRow` 是 `width:100%` 加自身内边距,`