From b8b7c7052624b6fe6ccef63c1c6d56da1290dab4 Mon Sep 17 00:00:00 2001 From: modusensus Date: Mon, 12 Oct 2026 02:47:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(a11y):=20=E9=9D=A2=E6=9D=BF=E5=AF=B9?= =?UTF-8?q?=E6=AF=94=E5=BA=A6=E8=BE=BE=E6=A0=87=20WCAG=20AA=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E9=9D=A2=E6=9D=BF=E8=87=AA=E5=B7=B1=E7=9A=84?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E8=89=B2=E5=B1=82=EF=BC=88#178=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #178 第四小项(对比度审计)收口。面板 270 条 color 声明里,70 条在浅色或 深色主题下达不到 AA(浅 65 / 深 5,最差 .mneme-statfoot 只有 1.26 / 1.64)。 根因不是个别颜色写错,而是面板把宿主的标签层 token 当正文色用: label-tertiary 50 处、label-dimmed 4 处——这两个是给分隔线/占位符用的低对比 层,不是正文层。三处图形同病:热度条与热度点的冷档压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,三档热度画成同一个看不见的灰。 修法:在四个渲染根上定义面板自己的语义色层(六条 --mneme-*),每条都用 color-mix() 从宿主 token 混出,宿主换配色时面板自动跟。四个根一个都不能少: .mneme-overlay 与 .mneme-topentry 是 createPortal 出来的独立子树、 .mneme-triggerwrap 挂在宿主 sidebar footer 插槽——只挂 .mneme-x 的话这三个 根拿不到变量,静默退回旧色。 顺带修掉 6 个幻影 token(宿主从未定义,一直靠 CSS 兜底字面量在跑,宿主改 配色时面板不会跟)与三处 knockout 配色(其中 .mneme-btndangerconfirm 写死的 #fff 旧底色下侥幸过关,底色一变浅就会在深色主题掉到 2.67:1)。 回归锁 test/contrast.test.js 四条(宿主 token 快照硬编码进测试——CI 上没有 宿主包,读不到 asar): - 逐条重算全部 color 声明,浅深两主题都必须过 AA - 面板引用的每个 --dsw-* 必须在快照表里存在(幻影名回归锁) - 语义色层选择器必须恰好是那四个根(少挂一个主锁照样全绿) - 图形色(background / stroke)单独按 3:1 复核——热度条、热度点、打卡点、 关系图连线都不在文字口径的视野里 变异检验五条:改淡 fg-dim、一处正文用回宿主标签色、塞回幻影名、渲染根少挂 一个、热度冷档改回宿主 label-dimmed,五条锁分别转红(已实测)。 npm test:1619 tests / 1618 pass / 0 fail / 1 skipped(Windows 符号链接用例, 仓库原生);src↔lib 一致(57 个文件)。 --- AGENTS.md | 1 + README.md | 6 +- dsh-mneme/CHANGELOG.md | 4 + dsh-mneme/README.md | 6 +- dsh-mneme/lib/client.js | 208 ++++++++++++------------ dsh-mneme/test/client.test.js | 7 +- dsh-mneme/test/contrast.test.js | 270 ++++++++++++++++++++++++++++++++ 7 files changed, 398 insertions(+), 104 deletions(-) create mode 100644 dsh-mneme/test/contrast.test.js diff --git a/AGENTS.md b/AGENTS.md index c8843e34..7e4a7fd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ dsh-mneme 是 DSH 宿主的记忆插件(蒸馏 / 注入 / 检索 / 巩固 / sc 6. 提交署名可归属与 AI 内容核验义务见 [CONTRIBUTING](CONTRIBUTING.md)(硬性要求); 7. **发布/打包只在 `dsh-mneme/` 包目录内执行**——根目录历史上发出过坏包(0.6.9、0.7.0–0.7.10),此条为防复发红线; 8. **注释写「为什么」,不写「是什么」**:非常规实现、防御段、行为开关的动机(issue 编号、消融数据、基准数字)必须落注释,代码含义靠命名与测试名表达;测试同样适用——锁形状/锁字面量的用例要注释写清它在防哪类回归(例:`test/recall-evals.test.js` 的 signals 形状锁)。 +9. **面板配色走 `--mneme-*` 语义色层**:宿主 `label-tertiary` / `label-dimmed` 是分隔线/占位符用的低对比层,**不是正文色**,直接拿来当 `color` 会在浅色或深色主题下掉到 AA 以下(#178 实测 70 条)。正文与图形一律用 `.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry` 上定义的语义变量;新增或改动颜色后 `test/contrast.test.js` 会自动重算全部 `color` 声明的对比度,并锁死「引用的 `--dsw-*` 必须真实存在」与「四个渲染根一个不少」。 ## 闸门清单 diff --git a/README.md b/README.md index 5c26a64f..3e0f1247 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790 ```bash cd dsh-mneme && npm install -npm test # 1615 个测试 +npm test # 1619 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo ```bash cd dsh-mneme && npm install -npm test # 1615 tests +npm test # 1619 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index 0a644cd0..f48d35ab 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## 🐛 修复 + +- **面板文字对比度全面达标 WCAG AA(issue #178)**:面板有 270 条 `color` 声明,此前 **70 条**在浅色或深色主题下达不到 AA(正文 4.5:1、大字号 3:1;浅色 65 条、深色 5 条,最差的两条 `.mneme-statfoot` 只有 1.26 / 1.64)。根因是面板直接拿宿主的**标签层** token 当正文色(`label-tertiary` 50 处、`label-dimmed` 4 处):这些是给分隔线/占位符用的低对比层,不是正文层。另有三处**图形**同病(不在文字口径内,按 3:1 复核):热度条与热度点的冷档用 `label-dimmed` 压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,等于把三档热度画成同一个看不见的灰。修法不是逐条换颜色,而是在四个渲染根上定义**面板自己的语义色层**:`.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{…}` 六条 `--mneme-*`(`fg-dim` / `graphic-dim` / `accent` / `danger` / `warn` / `ok`),每条都用 `color-mix()` 从宿主 token 混出——`fg-dim` = `label-primary` 60% + 透明(浅 4.94 / 深 6.30),其余同法。**四个根一个都不能少**:`.mneme-overlay` 与 `.mneme-topentry` 是 `createPortal` 出来的独立子树,`.mneme-triggerwrap` 挂在宿主 sidebar footer 插槽,只挂 `.mneme-x` 的话这三个根拿不到变量、静默退回旧色。顺带修掉 **6 个幻影 token**(`--dsw-alias-state-error` 19 处、`state-warning` 14 处、`label-dimmed` 4 处、`state-success` 3 处、`border` 2 处、`state-warn` 1 处):这些名字宿主从未定义过,一直靠 CSS 兜底字面量在跑,宿主改配色时面板不会跟——现全部换成真名(`state-error-primary` / `static-amber-600` / `border-l1` 等)。三处 knockout 配色一并修:`.mneme-conflict-primary` 浅色主题下白字压品牌蓝 `#4176e6` 只有 4.23:1,底色换成 `--mneme-accent`(浅 5.32 / 深 7.67);`.mneme-entrybadge` 深色主题下 `bg-layer-1` 压 `#c33` 只有 3.06:1,底色换成 `--mneme-danger`;`.mneme-btndangerconfirm` 底色同步换成 `--mneme-danger`,并把写死的 `#fff` 换成 `var(--dsw-alias-bg-layer-1)`——它旧的写死白字只在旧底色上侥幸过关,底色一变浅就会在深色主题掉到 2.67:1。回归锁 `test/contrast.test.js` 四条:①把宿主 token 的浅/深取值快照硬编码进测试(CI 上没有宿主包,读不到 asar),逐条重算全部 `color` 声明的对比度,两主题都必须过;②面板引用的每个 `--dsw-*` 必须在快照表里存在(幻影名回归锁);③语义色层的选择器必须恰好是那四个根(少了根主锁照样全绿——主锁只算颜色,不知道变量有没有继承到);④图形色(`background` / `stroke`,非 `color`)单独按 3:1 复核——热度条、热度点、打卡点、关系图连线都不在主锁的视野里。变异检验五条:改淡 `fg-dim`、一处正文用回宿主标签色、塞回幻影名、渲染根少挂一个、热度冷档改回宿主 `label-dimmed`,五条锁分别转红。 + ## [0.8.15] - 2026-10-10 ## 🆕 新增 diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index e48aa1ff..27e101c6 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1615%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1619%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -613,7 +613,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1615 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1619 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1615 个测试 +npm test # 运行 1619 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 76fb6892..1bcc1664 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -1269,18 +1269,34 @@ window.__ModuleLoader__.load({ // --- shared controls --- ".mneme-search{box-sizing:border-box;height:30px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;transition:border-color .12s,box-shadow .12s}", ".mneme-search:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}", - ".mneme-search::placeholder{color:var(--dsw-alias-label-tertiary)}", + ".mneme-search::placeholder{color:var(--mneme-fg-dim)}", ".mneme-chip{height:26px;padding:0 10px;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}", ".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}", - ".mneme-chip.mneme-active{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}", + ".mneme-chip.mneme-active{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}", ".mneme-select{box-sizing:border-box;height:30px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none}", ".mneme-routeselect{width:240px;max-width:60%}", ".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}", ".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}", - ".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}", - ".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--dsw-alias-state-business-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}", + ".mneme-hint{color:var(--mneme-fg-dim);padding:24px 0;text-align:center;font-size:13px}", + ".mneme-entitychip{flex:none;height:26px;padding:0 10px;border-radius:8px;border:none;background:none;color:var(--mneme-accent);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center}", ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}", // --- main-area memory library page --- + // 面板语义色层(#178):宿主 token 在浅色下给正文/次要文字只有 1.26–4.49 + // 的对比度,且面板还一直在引用 6 个根本不存在的宿主名(--dsw-alias-state-error + // / -warning / -success / -warn / --dsw-alias-label-dimmed / --dsw-alias-border), + // 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组 + // 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。 + // 必须挂四个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body + // (.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口 + // portal 到宿主侧边栏(.mneme-topentry)。 + ".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{" + + "--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" + + "--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" + + "--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" + + "--mneme-danger:color-mix(in srgb,var(--dsw-alias-state-error-primary) 80%,var(--dsw-alias-label-primary));" + + "--mneme-warn:color-mix(in srgb,var(--dsw-static-amber-600,#dd8629) 65%,var(--dsw-alias-label-primary));" + + "--mneme-ok:color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 65%,var(--dsw-alias-label-primary))" + + "}", ".mneme-x{flex:1;min-height:0;height:100%;width:100%;box-sizing:border-box;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);container-type:inline-size}", // 窄容器(better-sidebar 工作台 ~500px)下中央 vtabs 会与右缘悬浮的 // 卡片/时间线切换器重叠:容器查询收窄时隐藏切换器,视图模式沿用 @@ -1289,14 +1305,14 @@ window.__ModuleLoader__.load({ ".mneme-xbar{position:relative;flex:none;display:flex;align-items:center;justify-content:center;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding:0 16px;min-height:52px}", ".mneme-filterbar{flex:none;display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}", ".mneme-vtabs{display:flex;align-items:stretch;height:46px}", - ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:14px;font-weight:500;line-height:20px;display:inline-flex;align-items:center;gap:7px}", + ".mneme-vtab{position:relative;border:0;background:none;cursor:pointer;padding:0 16px;color:var(--mneme-fg-dim);font-family:inherit;font-size:14px;font-weight:500;line-height:20px;display:inline-flex;align-items:center;gap:7px}", ".mneme-vtab:hover{color:var(--dsw-alias-label-primary)}", - ".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}", + ".mneme-vtab.mneme-active{color:var(--mneme-accent)}", ".mneme-vtab.mneme-active::after{content:\"\";position:absolute;left:10px;right:10px;bottom:-1px;height:2.5px;border-radius:2px;background:var(--dsw-alias-state-business-primary)}", // transform 会自建层叠上下文:层级必须给在容器上——3 高于吸顶月份头(2)、 // 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。 ".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}", - ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}", + ".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}", // --- three-column browse layout: hairline separators, no outer box --- ".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}", ".mneme-xside{flex:none;width:236px;min-width:0;min-height:0;overflow-y:auto;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}", @@ -1305,23 +1321,23 @@ window.__ModuleLoader__.load({ ".mneme-xrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}", ".mneme-xsearch{width:100%}", ".mneme-xselect{flex:1;min-width:0}", - ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--dsw-alias-label-tertiary);padding:2px 8px 8px}", + ".mneme-xcolhead{flex:none;font-size:12px;font-weight:500;margin:0;color:var(--mneme-fg-dim);padding:2px 8px 8px}", ".mneme-xtype{display:flex;justify-content:flex-start;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}", ".mneme-xtype:hover{background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-xtype.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}", - ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-xcount2{flex:none;margin-left:auto;font-size:12px;color:var(--mneme-fg-dim)}", ".mneme-xmonth{display:flex;align-items:center;gap:4px;width:100%;padding:6px 8px 4px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}", ".mneme-xmonth:first-child{margin-top:0}", ".mneme-xmonth:hover{background:var(--dsw-alias-interactive-bg-hover)}", - ".mneme-xcaret{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary);width:10px}", - ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);cursor:pointer;text-align:left}", + ".mneme-xcaret{flex:none;font-size:10px;color:var(--mneme-fg-dim);width:10px}", + ".mneme-xday{display:flex;align-items:center;gap:4px;margin:6px 0 2px 22px;padding:2px 6px;border:none;border-radius:6px;background:none;font-family:inherit;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);cursor:pointer;text-align:left}", ".mneme-xday:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}", ".mneme-xitem{display:flex;gap:8px;align-items:baseline;width:calc(100% - 22px);margin-left:22px;padding:4px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}", ".mneme-xitem:hover{background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-xitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}", - ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}", + ".mneme-xtime{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}", ".mneme-xname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", - ".mneme-xempty{padding:18px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px}", + ".mneme-xempty{padding:18px 16px;text-align:center;color:var(--mneme-fg-dim);font-size:13px}", // --- memory-type color dots (mirrored across filter / timeline / detail) --- ".mneme-xdot{flex:none;display:inline-block;width:8px;height:8px;border-radius:50%;background:currentColor}", ".mneme-xdot--all{background:transparent;border:1.5px solid currentColor;opacity:.55;box-sizing:border-box}", @@ -1333,7 +1349,7 @@ window.__ModuleLoader__.load({ ".mneme-xtree{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 28px}", ".mneme-xdinner{max-width:720px}", ".mneme-xdtitle{font-size:16px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);margin-bottom:10px;word-break:break-word}", - ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-xdmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-bottom:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}", ".mneme-xdsrc{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}", ".mneme-xdcontent{margin-top:14px;font-size:14px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}", ".mneme-xdactions{display:flex;gap:8px;margin-top:18px}", @@ -1346,42 +1362,42 @@ window.__ModuleLoader__.load({ ".mneme-gnode:hover circle{stroke-width:4}", ".mneme-gnode.mneme-groot circle{stroke:var(--dsw-alias-state-business-primary);stroke-width:3}", ".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}", - ".mneme-gedge{stroke:var(--dsw-alias-label-dimmed);stroke-width:1.5;cursor:pointer}", + ".mneme-gedge{stroke:var(--mneme-graphic-dim);stroke-width:1.5;cursor:pointer}", ".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}", ".mneme-gedge-dashed{stroke-dasharray:5 4;opacity:.7}", - ".mneme-graphhint{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px;text-align:center;padding:6px 0 2px}", + ".mneme-graphhint{flex:none;color:var(--mneme-fg-dim);font-size:13px;text-align:center;padding:6px 0 2px}", ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}", ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}", - ".mneme-gs-meta{font-size:13px;color:var(--dsw-alias-label-tertiary);margin-bottom:6px}", + ".mneme-gs-meta{font-size:13px;color:var(--mneme-fg-dim);margin-bottom:6px}", ".mneme-gs-attr{display:flex;gap:6px;font-size:13px;padding:2px 0}", - ".mneme-gs-attrkey{flex:none;color:var(--dsw-alias-label-tertiary);font-size:13px}", + ".mneme-gs-attrkey{flex:none;color:var(--mneme-fg-dim);font-size:13px}", ".mneme-gs-attrval{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:13px}", ".mneme-gs-link{display:block;width:100%;text-align:left;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;padding:3px 6px;border-radius:6px;word-break:break-word}", ".mneme-gs-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}", // --- entity explorer: rail + layered detail (entities → attrs → relations → graph) --- ".mneme-ent{flex:1;min-height:0;display:flex;overflow:hidden}", ".mneme-entrail{flex:none;width:224px;min-height:0;overflow-y:auto;padding:12px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:4px}", - ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}", + ".mneme-enttype{display:flex;align-items:center;gap:6px;width:100%;padding:8px 8px 2px;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:11px;font-weight:600;line-height:16px;text-align:left;letter-spacing:.02em}", ".mneme-entitem{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:18px;text-align:left}", ".mneme-entitem:hover{background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-entitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}", ".mneme-entname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", - ".mneme-entmentions{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}", + ".mneme-entmentions{flex:none;font-size:11px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}", ".mneme-entdetail{flex:1;min-width:0;min-height:0;overflow-y:auto;padding:20px 24px 32px;box-sizing:border-box}", ".mneme-entinner{max-width:720px}", ".mneme-enttitle{display:flex;align-items:center;gap:10px;font-size:17px;font-weight:600;line-height:24px;color:var(--dsw-alias-label-primary);word-break:break-word}", ".mneme-enttitle .mneme-xdot{width:10px;height:10px}", - ".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-entmeta{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:6px;font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}", ".mneme-layer{margin-top:24px}", ".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}", - ".mneme-layercount{font-size:12px;font-weight:400;color:var(--dsw-alias-label-tertiary)}", + ".mneme-layercount{font-size:12px;font-weight:400;color:var(--mneme-fg-dim)}", ".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}", ".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}", - ".mneme-attrkey{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", + ".mneme-attrkey{font-size:11px;color:var(--mneme-fg-dim);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", ".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}", ".mneme-relrow{display:flex;align-items:center;gap:10px;width:100%;padding:7px 10px;border:none;border-radius:10px;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:13px;line-height:20px;text-align:left}", ".mneme-relrow:hover{background:var(--dsw-alias-interactive-bg-hover)}", - ".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--dsw-alias-state-business-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}", + ".mneme-reltype{flex:none;font-size:11px;line-height:16px;color:var(--mneme-accent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);border-radius:6px;padding:0 6px}", ".mneme-relname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", // --- settings: quiet stacked sections, one concern per section --- ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}", @@ -1389,25 +1405,25 @@ window.__ModuleLoader__.load({ // 设置页各节一律用 boxed 卡(.mneme-set-card)+ 分组标题(.mneme-set-grouphead): // 旧的「无框 set-sec + 有框 set-card」混排是版面杂乱的一半来源 ".mneme-set-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px}", - ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);margin-bottom:14px}", + ".mneme-set-desc{font-size:13px;line-height:19px;color:var(--mneme-fg-dim);margin-bottom:14px}", ".mneme-set-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none;margin-bottom:10px;transition:border-color .12s,box-shadow .12s}", ".mneme-set-input:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}", - ".mneme-set-input::placeholder{color:var(--dsw-alias-label-tertiary)}", + ".mneme-set-input::placeholder{color:var(--mneme-fg-dim)}", ".mneme-set-input--area{height:auto;min-height:76px;padding:8px 12px;resize:vertical;line-height:1.6}", ".mneme-btn{flex:none;height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;display:inline-flex;align-items:center;gap:6px}", ".mneme-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-btn:disabled{opacity:.5;cursor:default}", - ".mneme-saved{font-size:12px;color:var(--dsw-alias-state-success,#2a7)}", + ".mneme-saved{font-size:12px;color:var(--mneme-ok)}", // rule rows: numbered chips + hover-revealed delete ".mneme-set-row{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:8px;background:var(--dsw-alias-bg-base,transparent)}", - ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}", + ".mneme-set-idx{flex:none;width:20px;height:20px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--mneme-fg-dim);font-size:11px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}", ".mneme-set-ruletext{flex:1;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);word-break:break-word}", - ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}", + ".mneme-set-del{flex:none;opacity:0;transition:opacity .12s;border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-size:14px;line-height:20px;padding:0 4px;border-radius:6px}", ".mneme-set-row:hover .mneme-set-del,.mneme-set-row:focus-within .mneme-set-del{opacity:1}", - ".mneme-set-del:hover{color:var(--dsw-alias-state-error,#c33);background:var(--dsw-alias-interactive-bg-hover)}", + ".mneme-set-del:hover{color:var(--mneme-danger);background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-set-cmd{flex:1;min-width:0}", - ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--dsw-alias-state-business-primary)}", - ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px;word-break:break-word}", + ".mneme-set-cmdname{font-size:13px;font-weight:600;color:var(--mneme-accent)}", + ".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}", // --- settings sub-view --- ".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}", // --- 记忆库页面:居中 sheet,非全屏 --- @@ -1427,9 +1443,9 @@ window.__ModuleLoader__.load({ ".mneme-stars{display:inline-flex;align-items:center;gap:2px}", // 热度徽章三档:热(橙)/温(次级文字)/冷(弱化文字),冷档藏百分比只留图标。 ".mneme-heat{display:inline-flex;align-items:center;gap:2px;font-size:11px;line-height:14px;font-variant-numeric:tabular-nums}", - ".mneme-heat--hot{color:var(--dsw-alias-state-warning,#d97706)}", + ".mneme-heat--hot{color:var(--mneme-warn)}", ".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}", - ".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}", + ".mneme-heat--cold{color:var(--mneme-fg-dim)}", ".mneme-heat--cold .mneme-heatpct{display:none}", // 侧边栏入口两种形态的样式(挂载与搬移逻辑见 SidebarTopEntry 的头注释)。 // 展开态兜底:逐条镜像宿主 panelRow(36px 行高 / 透明底 / 2px 外边距 / @@ -1452,12 +1468,12 @@ window.__ModuleLoader__.load({ // 菜单让位只在「收起 + 入口确实挂在标题栏里」时生效;展开态完全不碰宿主变量。 "html[data-windows-titlebar]:has([data-sidebar-collapsed=true]):has([data-plugin-entry=\"@modusensus/dsh-mneme\"]){--dsh-windows-menu-start:120px}", // --- 功能开关:一行一开关,Claude 式安静排版 --- - ".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:16px 0 2px}", + ".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:16px 0 2px}", ".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}", ".mneme-featrow:last-child{border-bottom:none}", ".mneme-featmain{flex:1;min-width:0}", ".mneme-featname{font-size:13.5px;font-weight:500;line-height:20px;color:var(--dsw-alias-label-primary)}", - ".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-tertiary);margin-top:2px}", + ".mneme-feathint{font-size:12.5px;line-height:18px;color:var(--mneme-fg-dim);margin-top:2px}", ".mneme-switch{position:relative;flex:none;width:36px;height:22px;border-radius:11px;border:none;cursor:pointer;background:var(--dsw-alias-interactive-bg-active);transition:background .15s}", ".mneme-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 3px color-mix(in srgb,var(--dsw-alias-label-primary) 25%,transparent);transition:transform .15s}", ".mneme-switch.mneme-on{background:var(--dsw-alias-state-business-primary)}", @@ -1473,14 +1489,14 @@ window.__ModuleLoader__.load({ ".mneme-strinput:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}", ".mneme-featsub{margin:2px 0 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;display:flex;flex-direction:column;gap:6px}", ".mneme-featsub .mneme-featnum{padding:6px 0;border-bottom:none}", - ".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}", // 运行时那一块的说明文字:比常规 hint 再小一档,它是背景信息而不是要读的正文。 - ".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}", // --- 交互式记忆库:工具栏 / 卡片网格 / 详情抽屉 / 更多菜单 --- // sheet 边距放宽:与窗口边缘保持呼吸距离,居中不顶满。 // 视图切换(卡片/时间线)分段控件挂在子页栏右侧。 ".mneme-seg{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}", - ".mneme-seg button{border:none;background:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}", + ".mneme-seg button{border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}", ".mneme-seg button:hover{color:var(--dsw-alias-label-primary)}", ".mneme-seg button.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}", // 卡片网格:响应式 auto-fill,卡片悬停轻浮起 @@ -1488,34 +1504,34 @@ window.__ModuleLoader__.load({ ".mneme-card{display:flex;flex-direction:column;gap:7px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 13px;background:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .12s,box-shadow .12s,transform .12s}", ".mneme-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,var(--dsw-alias-border-l2));box-shadow:0 6px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);transform:translateY(-1px)}", ".mneme-card.mneme-active{border-color:var(--dsw-alias-state-business-primary)}", - ".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-cardhead{display:flex;align-items:center;gap:6px;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}", ".mneme-cardtitle{font-size:13.5px;font-weight:600;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}", ".mneme-cardexcerpt{font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary);word-break:break-word;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}", - ".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-cardfoot{display:flex;align-items:center;gap:8px;margin-top:auto;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim)}", ".mneme-cardsrc{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", // 徽章:冲突待确认 / 已归档 ".mneme-badge{flex:none;display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 7px;border-radius:6px;font-size:11px;line-height:14px}", - ".mneme-badge--conflict{color:var(--dsw-alias-state-error,#c33);background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error,#c33) 30%,transparent)}", - ".mneme-badge--archived{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}", - ".mneme-badge--scope{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}", + ".mneme-badge--conflict{color:var(--mneme-danger);background:color-mix(in srgb,var(--mneme-danger) 10%,transparent);border:1px solid color-mix(in srgb,var(--mneme-danger) 30%,transparent)}", + ".mneme-badge--archived{color:var(--mneme-fg-dim);background:var(--dsw-alias-interactive-bg-hover)}", + ".mneme-badge--scope{color:var(--mneme-accent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}", // 冲突集中处理队列(v0.8.0):状态页的待确认冲突列表 ".mneme-conflictq{display:flex;flex-direction:column;gap:10px}", ".mneme-conflictq-head{display:flex;align-items:center;gap:8px}", - ".mneme-conflict-item{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}", + ".mneme-conflict-item{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:10px;padding:10px;display:flex;flex-direction:column;gap:8px}", ".mneme-conflict-reason{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}", ".mneme-conflict-pair{display:grid;grid-template-columns:1fr 1fr;gap:8px}", - ".mneme-conflict-side{border:1px solid var(--dsw-alias-border,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}", - ".mneme-conflict-sidelabel{font-size:11px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-conflict-side{border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-interactive-bg-hover));border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:0}", + ".mneme-conflict-sidelabel{font-size:11px;color:var(--mneme-fg-dim)}", ".mneme-conflict-sidetitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}", ".mneme-conflict-snippet{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}", - ".mneme-conflict-missing{font-size:12px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-conflict-missing{font-size:12px;color:var(--mneme-fg-dim)}", ".mneme-conflict-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}", - ".mneme-conflict-hint{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-right:auto}", + ".mneme-conflict-hint{font-size:11px;color:var(--mneme-fg-dim);margin-right:auto}", // #177 冲突队列视觉批次:A/B 侧色、相似度条、词级 diff、主色按钮、空态 ".mneme-conflict-side--a{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 4%,transparent)}", - ".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--dsw-alias-state-business-primary);font-weight:600}", - ".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 5%,transparent)}", - ".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--dsw-alias-state-warning,#e6a23c);font-weight:600}", + ".mneme-conflict-side--a .mneme-conflict-sidelabel{color:var(--mneme-accent);font-weight:600}", + ".mneme-conflict-side--b{border-color:color-mix(in srgb,var(--mneme-warn) 45%,transparent);background:color-mix(in srgb,var(--mneme-warn) 5%,transparent)}", + ".mneme-conflict-side--b .mneme-conflict-sidelabel{color:var(--mneme-warn);font-weight:600}", ".mneme-conflict-simrow{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary)}", ".mneme-conflict-simbar{position:relative;flex:1;height:5px;border-radius:3px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}", ".mneme-conflict-simfill{position:absolute;top:0;bottom:0;left:0;border-radius:3px;background:var(--dsw-alias-state-business-primary)}", @@ -1526,25 +1542,25 @@ window.__ModuleLoader__.load({ // 页),那时 46vh 会算成 0px,展开态直接看不见字——实测踩过。 ".mneme-conflict-diff--full{display:block;-webkit-line-clamp:unset;max-height:28em;overflow-y:auto}", ".mneme-conflict-text{font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-height:28em;overflow-y:auto}", - ".mneme-conflict-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-conflict-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}", ".mneme-conflict-metaval{color:var(--dsw-alias-label-secondary)}", // 窄面板下并排会把每侧挤到无法阅读,退成单列(容器查询按面板实际宽度判) "@container (max-width:560px){.mneme-conflict-pair{grid-template-columns:1fr}}", ".mneme-conflict-mark{text-decoration:none;border-radius:3px;padding:0 1px}", - ".mneme-conflict-mark--del{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 16%,transparent);text-decoration:line-through}", - ".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--dsw-alias-state-success,#3c9) 18%,transparent)}", - ".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}", + ".mneme-conflict-mark--del{background:color-mix(in srgb,var(--mneme-danger) 16%,transparent);text-decoration:line-through}", + ".mneme-conflict-mark--ins{background:color-mix(in srgb,var(--mneme-ok) 18%,transparent)}", + ".mneme-conflict-primary{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 10px;border-radius:8px;border:none;background:var(--mneme-accent);color:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px}", ".mneme-conflict-primary:disabled{opacity:.55;cursor:default}", - ".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,black)}", - ".mneme-badge--frozen{color:var(--dsw-alias-state-warning,#e6a23c);background:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 12%,transparent)}", + ".mneme-conflict-primary:hover:not(:disabled){background:color-mix(in srgb,var(--mneme-accent) 85%,var(--dsw-alias-label-primary))}", + ".mneme-badge--frozen{color:var(--mneme-warn);background:color-mix(in srgb,var(--mneme-warn) 12%,transparent)}", ".mneme-conflict-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:4px}", ".mneme-conflict-empty-title{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary)}", - ".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}", - ".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 35%,transparent)}", - ".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--dsw-alias-state-error,#c33);color:var(--dsw-alias-bg-layer-1,#fff);font-size:10.5px;line-height:16px;font-weight:600;text-align:center;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1)}", + ".mneme-conflict-empty-body{font-size:12px;line-height:18px;color:var(--mneme-fg-dim)}", + ".mneme-statuscard--actionable{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}", + ".mneme-entrybadge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1,#fff);font-size:10.5px;line-height:16px;font-weight:600;text-align:center;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-1)}", ".mneme-triggerwrap{position:relative;pointer-events:none}", ".mneme-triggerwrap .mneme-trigger{pointer-events:auto}", - ".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--dsw-alias-state-warning,#e6a23c) 35%,transparent)}", + ".mneme-conflict-jump .mneme-statuscard{border-color:color-mix(in srgb,var(--mneme-warn) 55%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--mneme-warn) 35%,transparent)}", ".mneme-conflict-jump:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:12px}", // 详情抽屉:sheet 内右侧滑出,覆盖在浏览区之上 ".mneme-xmain{position:relative}", @@ -1556,7 +1572,7 @@ window.__ModuleLoader__.load({ ".mneme-drawertitle{width:100%;font-size:16px;font-weight:600;line-height:23px;color:var(--dsw-alias-label-primary);word-break:break-word}", ".mneme-drawertitle-input{width:100%;box-sizing:border-box;font-size:15px;font-weight:600;line-height:22px;padding:7px 10px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}", ".mneme-dmeta{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin-top:12px;font-size:12.5px;line-height:19px}", - ".mneme-dmetakey{color:var(--dsw-alias-label-tertiary)}", + ".mneme-dmetakey{color:var(--mneme-fg-dim)}", ".mneme-dmetaval{color:var(--dsw-alias-label-secondary);min-width:0;word-break:break-word}", ".mneme-dcontent{margin-top:14px;font-size:13.5px;line-height:1.75;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word}", ".mneme-dcontent-input{width:100%;box-sizing:border-box;min-height:180px;font-size:13.5px;line-height:1.7;padding:9px 11px;border-radius:8px;border:1px solid var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;outline:none;resize:vertical;box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}", @@ -1576,12 +1592,12 @@ window.__ModuleLoader__.load({ // --- explorer refresh: paged month tree, sticky headers, inline icons --- ".mneme-vtabico{flex:none;opacity:.85}", ".mneme-xsearchwrap{position:relative;flex:none}", - ".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--dsw-alias-label-tertiary);pointer-events:none}", + ".mneme-xsearchico{position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--mneme-fg-dim);pointer-events:none}", ".mneme-xsearchwrap .mneme-xsearch{padding-left:27px}", ".mneme-footbtn{display:inline-flex;align-items:center;gap:4px}", ".mneme-xmonth{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;gap:6px}", - ".mneme-xmonthcount{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-weight:400;font-variant-numeric:tabular-nums}", - ".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary)}", + ".mneme-xmonthcount{margin-left:auto;color:var(--mneme-fg-dim);font-weight:400;font-variant-numeric:tabular-nums}", + ".mneme-xcaret{display:inline-flex;align-items:center;justify-content:center;color:var(--mneme-fg-dim)}", ".mneme-xmore{display:flex;justify-content:center;align-items:center;padding:10px 0 24px;min-height:20px}", ".mneme-xemptyico{display:block;margin:0 auto 6px;opacity:.7}", // --- status sub-view: responsive stat-card grid (auto-fill, ~220px min) --- @@ -1595,45 +1611,45 @@ window.__ModuleLoader__.load({ "@container (min-width:452px) and (max-width:683px){.mneme-statuscard--vectorwide{grid-column:span 2}}", "@container (min-width:452px){.mneme-statuscard--wide{grid-column:span 2}}", // 状态页分组标题(库内一览 / 后台运转):只做扫读锚点,压得比卡片标题低 - ".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary)}", + ".mneme-statushead{max-width:1000px;margin:0 auto 10px;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim)}", ".mneme-statusgrid+.mneme-statushead{margin-top:26px}", ".mneme-statuscard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 16px 14px}", // 指标行:一行一指标(label 左 / 数值右),取代「·」串联的说明长句—— // 拥挤感的根源是把多指标压进一段会换行的散文 ".mneme-statrows{margin-top:10px;display:flex;flex-direction:column;gap:4px}", - ".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-statrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}", ".mneme-statrow>span:first-child{flex:none;white-space:nowrap}", ".mneme-statrow .mneme-heatdot{align-self:center}", ".mneme-statrowval{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}", - ".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-dimmed)}", + ".mneme-statfoot{margin-top:8px;font-size:11px;line-height:16px;color:var(--mneme-fg-dim)}", // 热度分布条:三档占比一图看清,行标只补数字(类名带双横线,与 heatdot 同款) ".mneme-heatbar{display:flex;height:6px;border-radius:3px;overflow:hidden;margin-top:12px;background:var(--dsw-alias-interactive-bg-hover)}", ".mneme-heatbar-seg{min-width:2px}", - ".mneme-heatbar--hot{background:var(--dsw-alias-state-warning,#d97706)}", - ".mneme-heatbar--warm{background:var(--dsw-alias-label-tertiary)}", - ".mneme-heatbar--cold{background:var(--dsw-alias-label-dimmed)}", + ".mneme-heatbar--hot{background:var(--mneme-warn)}", + ".mneme-heatbar--warm{background:var(--mneme-fg-dim)}", + ".mneme-heatbar--cold{background:var(--mneme-graphic-dim)}", ".mneme-heatdot{flex:none;width:8px;height:8px;border-radius:50%}", - ".mneme-heatdot--hot{background:var(--dsw-alias-state-warning,#d97706)}", - ".mneme-heatdot--warm{background:var(--dsw-alias-label-tertiary)}", - ".mneme-heatdot--cold{background:var(--dsw-alias-label-dimmed)}", + ".mneme-heatdot--hot{background:var(--mneme-warn)}", + ".mneme-heatdot--warm{background:var(--mneme-fg-dim)}", + ".mneme-heatdot--cold{background:var(--mneme-graphic-dim)}", // --- 状态页工作台:让用户看见插件在干活(动态/沉淀/归档) --- ".mneme-wbhead{margin:28px auto 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);max-width:1000px;display:flex;align-items:baseline;justify-content:space-between;gap:8px}", ".mneme-wblist{max-width:1000px;display:flex;flex-direction:column;gap:6px}", ".mneme-wbrow{display:flex;gap:10px;align-items:flex-start;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}", - ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--dsw-alias-state-success,#2a9d6a)}", - ".mneme-wbdot--err{background:var(--dsw-alias-state-error,#c33)}", + ".mneme-wbdot{flex:none;width:8px;height:8px;border-radius:50%;margin-top:6px;background:var(--mneme-ok)}", + ".mneme-wbdot--err{background:var(--mneme-danger)}", ".mneme-wbmain{min-width:0;flex:1}", ".mneme-wbtitle{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}", - ".mneme-wbsub{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:1px}", + ".mneme-wbsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px}", ".mneme-wbmemo{display:flex;gap:10px;align-items:center;padding:10px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px}", ".mneme-wbmemo .mneme-xdot{flex:none}", ".mneme-wbmemo-main{min-width:0;flex:1}", ".mneme-statusnum{font-size:24px;font-weight:600;line-height:32px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}", - ".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--dsw-alias-label-tertiary);word-break:break-word}", + ".mneme-statuscap{margin-top:4px;font-size:13px;line-height:19px;color:var(--mneme-fg-dim);word-break:break-word}", // --- destructive actions: red outline = delete, solid red = confirm --- - ".mneme-btndanger{color:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33)}", - ".mneme-btndanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error,#c33) 12%,transparent)}", - ".mneme-btndangerconfirm{background:var(--dsw-alias-state-error,#c33);border-color:var(--dsw-alias-state-error,#c33);color:#fff}", + ".mneme-btndanger{color:var(--mneme-danger);border-color:var(--mneme-danger)}", + ".mneme-btndanger:hover{background:color-mix(in srgb,var(--mneme-danger) 12%,transparent)}", + ".mneme-btndangerconfirm{background:var(--mneme-danger);border-color:var(--mneme-danger);color:var(--dsw-alias-bg-layer-1)}", ".mneme-btndangerconfirm:hover{filter:brightness(.9)}", // --- settings cards: boxed cards for the runtime-mode and external-API // sections — each card owns its fetch/PUT state, so it renders as a @@ -1641,14 +1657,14 @@ window.__ModuleLoader__.load({ ".mneme-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;margin-bottom:10px}", // 设置页分组标题(个性化 / 记忆引擎 / 搜索 / 连接与安全 / 帮助): // 先给「这一段在管什么」,卡片才是具体选项 - ".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:24px 0 10px}", + ".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:24px 0 10px}", ".mneme-set-grouphead:first-child{margin-top:8px}", ".mneme-set-token{font-family:monospace;font-size:12px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);word-break:break-all;user-select:all}", - ".mneme-set-hint{font-size:12px;color:var(--dsw-alias-label-tertiary)}", + ".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}", // --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) --- ".mneme-staredit{display:inline-flex;align-items:center;gap:1px;justify-self:start}", ".mneme-starbtn{border:none;background:none;cursor:pointer;padding:2px 1px;display:inline-flex;color:var(--dsw-alias-label-secondary);transition:transform .12s}", - ".mneme-starbtn--on{color:var(--dsw-alias-state-warning,#d97706)}", + ".mneme-starbtn--on{color:var(--mneme-warn)}", ".mneme-starbtn:hover{transform:scale(1.12)}", ".mneme-starbtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-radius:4px}", // --- 意见与反馈:图标行 + 版本 footer,收在设置页末尾当「关于」用 --- @@ -1659,8 +1675,8 @@ window.__ModuleLoader__.load({ ".mneme-feedrow:hover .mneme-feedico{background:var(--dsw-alias-interactive-bg-active)}", ".mneme-feedtext{display:flex;flex-direction:column;gap:1px;min-width:0}", ".mneme-feedname{font-size:13px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-primary)}", - ".mneme-feedsub{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}", - ".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--dsw-alias-label-tertiary)}" + ".mneme-feedsub{font-size:12px;line-height:17px;color:var(--mneme-fg-dim)}", + ".mneme-set-foot{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;color:var(--mneme-fg-dim)}" ].join("\n"); if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) { const tag = document.createElement("style"); @@ -3008,7 +3024,7 @@ window.__ModuleLoader__.load({ h("textarea", { className: "mneme-set-input mneme-set-input--area", style: { minHeight: 56 }, value: newCmd.instruction, placeholder: t("memory.settings.cmdInstruction"), onChange: (e) => setNewCmd({ ...newCmd, instruction: e.target.value }) }), h("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, h("button", { className: "mneme-btn", onClick: addCommand }, t("memory.settings.cmdAdd")), - cmdError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error, #c33)" } }, cmdError) + cmdError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, cmdError) ) ) ), @@ -3019,7 +3035,7 @@ window.__ModuleLoader__.load({ h("section", { className: "mneme-set-card" }, h("div", { className: "mneme-set-title" }, t("memory.settings.mode.title")), h("div", { className: "mneme-set-desc" }, t("memory.settings.mode.desc")), - modeError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, modeError), + modeError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, modeError), mode === null && !modeError ? h("div", { className: "mneme-set-hint" }, "…") : h(react.Fragment, null, @@ -3070,7 +3086,7 @@ window.__ModuleLoader__.load({ h("section", { className: "mneme-set-card" }, h("div", { className: "mneme-set-title" }, t("memory.settings.extapi.title")), h("div", { className: "mneme-set-desc" }, t("memory.settings.extapi.desc")), - extapiError && h("div", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)", marginBottom: 8 } }, extapiError), + extapiError && h("div", { style: { fontSize: 12, color: "var(--mneme-danger)", marginBottom: 8 } }, extapiError), extapi === null && !extapiError ? h("div", { className: "mneme-set-hint" }, "…") : h(react.Fragment, null, @@ -3314,7 +3330,7 @@ window.__ModuleLoader__.load({ loading ? h("div", { className: "mneme-statusnum" }, "…") : error - ? h("div", { className: "mneme-statuscap", style: { color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.status.error")) + ? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error")) : h(react.Fragment, null, h("div", { className: "mneme-statusnum" }, num), rows && rows.length @@ -3708,7 +3724,7 @@ window.__ModuleLoader__.load({ return () => { cancelled = true; }; }, []); if (state.loading || !state.suppressed) return null; - return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--dsw-alias-state-warning,#e6a23c)" } }, + return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--mneme-warn)" } }, h("div", { className: "mneme-statusnum", style: { fontSize: "16px", lineHeight: "24px" } }, t("memory.status.injectSuppressed")), h("div", { className: "mneme-statuscap" }, t("memory.status.injectSuppressedHint")) ); @@ -4442,7 +4458,7 @@ window.__ModuleLoader__.load({ ? h(react.Fragment, null, confirmWiden ? h(react.Fragment, null, - h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-warn,#b8860b)" } }, + h("span", { style: { fontSize: 12, color: "var(--mneme-warn)" } }, t("memory.explorer.detail.scopeWidenHint")), h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: busy, onClick: save }, t("memory.explorer.detail.scopeWidenConfirm")), @@ -4465,7 +4481,7 @@ window.__ModuleLoader__.load({ } }, t("memory.explorer.detail.cancel")), savedTick && h("span", { className: "mneme-saved" }, t("memory.explorer.detail.saved")), - saveError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } }, t("memory.explorer.detail.archiveFailed")) + saveError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, t("memory.explorer.detail.archiveFailed")) ) : h(react.Fragment, null, h("button", { type: "button", className: "mneme-btn", onClick: () => setEditing(true) }, t("memory.explorer.detail.edit")), @@ -4493,7 +4509,7 @@ window.__ModuleLoader__.load({ ) : h("button", { type: "button", className: "mneme-btn mneme-btndanger", onClick: () => setConfirmDelete(true) }, t("memory.explorer.delete")), - deleteError && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-error,#c33)" } }, + deleteError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } }, t("memory.explorer.deleteFailed")) ) ) @@ -4548,7 +4564,7 @@ window.__ModuleLoader__.load({ onChange: (e) => { setFile((e.target.files && e.target.files[0]) || null); setErr(""); } }) ), - err && h("div", { className: "mneme-featsubhint", style: { color: "var(--dsw-alias-state-error,#c33)", marginTop: 8 } }, err), + err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err), msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg), h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } }, h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")), diff --git a/dsh-mneme/test/client.test.js b/dsh-mneme/test/client.test.js index 57b00fc2..9066b22a 100644 --- a/dsh-mneme/test/client.test.js +++ b/dsh-mneme/test/client.test.js @@ -600,9 +600,12 @@ test("explorer chrome aligns with the host design system", () => { /\.mneme-vtab\.mneme-active::after/.test(clientSource), "active sub-tabs use the host underline treatment" ); + // #178:这里原本直接断言宿主品牌蓝。宿主蓝在浅色画布上只有 4.23:1,对 13px + // 正文不足 AA,所以面板改用自己的语义色层 --mneme-accent(同一条宿主 token + // 往 label-primary 方向混 15%),换肤仍跟随宿主。意图不变:激活态是品牌蓝。 assert.ok( - clientSource.includes(".mneme-vtab.mneme-active{color:var(--dsw-alias-state-business-primary)}"), - "active sub-tab text must turn the host brand blue" + clientSource.includes(".mneme-vtab.mneme-active{color:var(--mneme-accent)}"), + "active sub-tab text must turn the host brand blue (via the panel accent layer)" ); assert.equal( /\.mneme-xmain\{[^}]*border:1px/.test(clientSource), diff --git a/dsh-mneme/test/contrast.test.js b/dsh-mneme/test/contrast.test.js new file mode 100644 index 00000000..65c0761a --- /dev/null +++ b/dsh-mneme/test/contrast.test.js @@ -0,0 +1,270 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// #178 的回归锁:面板正文色的对比度。 +// +// 为什么是硬编码快照而不是现场读宿主:CI runner 上没有 DSH 安装(没有 +// app.asar),宿主 token 表根本读不到;而这条锁要在 CI 上拦住「面板又用回了 +// 一个低对比度的宿主 token」。所以把面板实际引用到的宿主 token 浅/深取值抄成 +// 快照(2026-10 取自 DSH app.asar 的 token 表:body = 浅色、 +// body[data-ds-dark-theme] = 深色、:root = 两主题同值),锁的是**面板自己的 +// 用法**,宿主改色不会让这条锁变红。 +// +// 面板的坑有两层,这条锁两层都咬: +// ① 宿主 token 在浅色下给正文只有 1.26–4.49 的对比度(label-tertiary 50 处、 +// label-dimmed 4 处),照抄宿主名就等于照抄不合规; +// ② 面板还长期引用 6 个**根本不存在的**宿主名(--dsw-alias-state-error / +// -warning / -success / -warn / --dsw-alias-label-dimmed / --dsw-alias-border), +// 于是吃的一直是写在 fallback 里的硬编码色。所以除了比值,还要锁「引用到的 +// 宿主名必须真实存在」。 +// +// 修法是面板自有的语义色层(--mneme-*,定义在 .mneme-x 等四个渲染根上), +// 混色基准仍是宿主 token,宿主换肤时跟着走。见 lib/client.js 里那段注释。 + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const clientSource = readFileSync(join(root, "lib/client.js"), "utf8"); + +const HOST_TOKENS = { + "--dsw-alias-label-primary": { light: "#0f1115", dark: "#f9fafb" }, + "--dsw-alias-label-secondary": { light: "#61666b", dark: "#cfd3d6" }, + "--dsw-alias-label-tertiary": { light: "#81858c", dark: "#adb2b8" }, + "--dsw-alias-label-dimmed": { light: "#e1e5ee", dark: "#43454a" }, + "--dsw-alias-state-business-primary": { light: "#4176e6", dark: "#7aaaff" }, + "--dsw-alias-state-error-primary": { light: "#ec1313", dark: "#f25a5a" }, + "--dsw-alias-bg-layer-1": { light: "#ffffff", dark: "#232324" }, + "--dsw-alias-bg-layer-2": { light: "#ffffff", dark: "#2c2c2e" }, + "--dsw-alias-bg-base": { light: "#ffffff", dark: "#151517" }, + "--dsw-alias-button-elevated-fill": { light: "#ffffff", dark: "#43454a" }, + "--dsw-alias-border-l1": { light: "#0000000a", dark: "#ffffff0f" }, + "--dsw-alias-border-l2": { light: "#0000001a", dark: "#ffffff1f" }, + "--dsw-alias-interactive-bg-hover": { light: "#2631480f", dark: "#ffffff14" }, + "--dsw-alias-interactive-bg-active": { light: "#2631481a", dark: "#ffffff24" }, + "--dsw-alias-scrollbar-bg-l2": { light: "#e5e5e5", dark: "#545557" }, + "--dsw-alias-scrollbar-hover-l2": { light: "#d4d4d4", dark: "#65676b" }, + "--dsw-static-amber-600": { light: "#dd8629", dark: "#dd8629" }, + "--dsw-static-green-500": { light: "#22c55e", dark: "#22c55e" }, + // 真实存在但**不是颜色**的宿主 token:只参与上面的「名字必须存在」检查。 + "--dsw-radius-md": { light: null, dark: null }, + "--dsw-focus-ring-width": { light: null, dark: null }, + "--dsw-focus-ring-color": { light: "transparent", dark: "transparent" } +}; + +/* ---------- 从面板产物里切出 CSS 块 ---------- */ +const cssStart = clientSource.indexOf("const css = ["); +const cssEnd = clientSource.indexOf('].join("\\n");', cssStart); +assert.ok(cssStart > 0 && cssEnd > cssStart, "lib/client.js 里找不到 CSS 数组(切片锚点变了?)"); +const cssBlock = clientSource.slice(cssStart, cssEnd); + +const rules = []; +for (const m of cssBlock.matchAll(/"((?:[^"\\]|\\.)*)"/g)) { + const s = m[1].replace(/\\"/g, '"'); + if (!s.includes("{")) continue; + const rm = /^([^{]+)\{([^}]*)\}$/.exec(s.trim()); + if (rm) rules.push({ sel: rm[1].trim(), body: rm[2] }); +} + +/* ---------- 颜色数学(alpha 合成,不是「和白底混」) ---------- */ +function parseColor(c) { + let m; + if ((m = /^#([0-9a-fA-F]{3,8})$/.exec(String(c).trim()))) { + let s = m[1]; + if (s.length === 3 || s.length === 4) s = s.split("").map((x) => x + x).join(""); + return [ + parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16), + s.length === 8 ? parseInt(s.slice(6, 8), 16) / 255 : 1 + ]; + } + if ((m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)$/.exec(String(c).trim()))) { + const a = m[4] === undefined ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]); + return [+m[1], +m[2], +m[3], a]; + } + return null; +} +// 半透明前景必须真的叠到底色上再算比值:早期审计脚本拿「和白底混」量深色主题, +// 把 --dsw-alias-label-primary 60% 算成 15.36 的假高分(真实值 6.30)。 +function composite(top, bottom) { + const a = top[3] + bottom[3] * (1 - top[3]); + const f = (i) => (top[i] * top[3] + bottom[i] * bottom[3] * (1 - top[3])) / (a || 1); + return [f(0), f(1), f(2), a]; +} +function luminance([r, g, b]) { + const [x, y, z] = [r, g, b].map((v) => { + const s = v / 255; + return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * x + 0.7152 * y + 0.0722 * z; +} +function contrast(fg, bg) { + const a = luminance(composite(fg, bg)); + const b = luminance(bg); + const [hi, lo] = [a, b].sort((p, q) => q - p); + return (hi + 0.05) / (lo + 0.05); +} + +/* ---------- 解析:宿主快照 + 面板自己的语义色层 ---------- */ +const TOKENS = new Map(); +for (const [name, v] of Object.entries(HOST_TOKENS)) TOKENS.set(name, v); + +// 面板语义色层是一条由多个字符串字面量拼起来的规则,不出现在上面的 rules 里, +// 所以按拼好的文本再读一次。 +const joined = [...cssBlock.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\"/g, '"')).join(""); +const layerM = /([^{}]*)\{[^{}]*--mneme-fg-dim\s*:[^{}]*\}/.exec(joined); +assert.ok(layerM, "找不到面板语义色层(--mneme-* 定义),四个渲染根那条规则被删了?"); +const layer = { sel: layerM[1].split("}").pop().trim(), body: layerM[0].slice(layerM[0].indexOf("{")) }; +for (const g of layer.body.matchAll(/(--mneme-[a-z0-9-]+)\s*:\s*([^;}]+)/g)) { + TOKENS.set(g[1], { light: g[2].trim(), dark: g[2].trim() }); +} + +// 先把 var()/color-mix() 逐层折成字符串,再交给 colorOf 解析成 RGBA。 +// 之所以不用「一个递归函数同时返回数组和字符串」:字符串替换里塞数组会被打散。 +function resolveValue(raw, theme) { + let v = String(raw).trim().replace(/!important$/, "").trim(); + let changed = true; + for (let guard = 0; changed && guard < 20; guard++) { + changed = false; + v = v.replace(/var\(\s*(--[a-z0-9-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/g, (_, tok, fb) => { + const e = TOKENS.get(tok); + const inner = e ? resolveValue(e[theme], theme) : null; + if (inner) { changed = true; return inner; } + if (fb) { changed = true; return fb.trim(); } + return "\u00acMISSING\u00ac"; + }); + } + return v.includes("\u00acMISSING\u00ac") ? null : v.trim(); +} +function colorOf(raw, theme) { + const v = resolveValue(raw, theme); + if (v === null) return null; + let m = /^color-mix\(\s*in\s+srgb\s*,\s*(.+?)\s+([\d.]+)%\s*,\s*transparent\s*\)$/.exec(v); + if (m) { + const base = colorOf(m[1], theme); + if (!base) return null; + return [...base.slice(0, 3), base[3] * (parseFloat(m[2]) / 100)]; + } + m = /^color-mix\(\s*in\s+srgb\s*,\s*(.+?)\s+([\d.]+)%\s*,\s*(.+?)\s*\)$/.exec(v); + if (m) { + const a = colorOf(m[1], theme); + const b = colorOf(m[3], theme); + if (!a || !b) return null; + const p = parseFloat(m[2]) / 100; + return [...a.slice(0, 3).map((x, i) => x * p + b[i] * (1 - p)), a[3] * p + b[3] * (1 - p)]; + } + return parseColor(v); +} + +/* ---------- 底色:按类名找它自己的 background ---------- */ +const BG_BY_CLASS = new Map(); +for (const r of rules) { + const bg = /(?:^|;)\s*background(?:-color)?\s*:\s*([^;]+)/.exec(r.body); + if (!bg) continue; + for (const c of r.sel.matchAll(/\.([a-z0-9-]+)/gi)) { + const k = c[1].toLowerCase(); + if (!BG_BY_CLASS.has(k)) BG_BY_CLASS.set(k, []); + BG_BY_CLASS.get(k).push({ val: bg[1].trim(), sel: r.sel }); + } +} +const THEME_BG = { light: "#ffffff", dark: "#232324" }; // --dsw-alias-bg-layer-1 +function backgroundOf(sel, theme) { + const base = parseColor(THEME_BG[theme]); + const own = []; + for (const c of sel.matchAll(/\.([a-z0-9-]+)/gi)) { + for (const e of BG_BY_CLASS.get(c[1].toLowerCase()) ?? []) { + if (!/hover|active|focus|disabled/.test(e.sel)) own.push(e); + } + } + for (const e of own) { + const v = colorOf(e.val, theme); + if (v) return v[3] < 1 ? composite(v, base) : v; + } + return base; +} + +/* ---------- 逐条算 ---------- */ +const failures = []; +let audited = 0; +for (const r of rules) { + const colM = /(?:^|;)\s*color\s*:\s*([^;]+)/.exec(r.body); + if (!colM) continue; + if (/::(before|after)/.test(r.sel)) continue; + const fsM = /font-size\s*:\s*([\d.]+)px/.exec(r.body); + const fwM = /font-weight\s*:\s*([\d]+|bold|normal)/.exec(r.body); + const fs = fsM ? +fsM[1] : 13; + const fw = fwM ? (/bold/.test(fwM[1]) ? 700 : fwM[1] === "normal" ? 400 : +fwM[1]) : 400; + const need = fs >= 24 || (fs >= 18.66 && fw >= 700) ? 3 : 4.5; + for (const theme of ["light", "dark"]) { + const fg = colorOf(colM[1], theme); + if (!fg) continue; + audited++; + const cr = contrast(fg, backgroundOf(r.sel, theme)); + if (cr < need) { + failures.push(`${theme} ${r.sel} ${fs}px/${fw} = ${cr.toFixed(2)} < ${need}(${colM[1].trim()})`); + } + } +} + +test("面板每个 color 声明在浅/深两主题下都达到 WCAG AA(#178 主锁)", () => { + assert.ok(audited >= 200, `审计到的 color 声明太少(${audited}),CSS 切片或解析器可能失效了`); + assert.deepEqual(failures, [], "以下声明对比度不足:\n" + failures.join("\n")); +}); + +// 幻影 token 的回归锁:面板曾经引用 6 个宿主里不存在的名字,于是长年吃 fallback +// 里的硬编码色(换肤不跟随、深色主题翻车)。这条锁不看比值、只看名字是否存在, +// 因此将来任何「顺手写个听起来合理的 --dsw-* 名」都会被拦下。 +test("面板引用的宿主 token 必须真实存在(幻影 token 回归锁)", () => { + // 只扫真实声明:CSS 数组里夹着 JS 注释,注释里会提到被修掉的旧名。 + const declared = cssBlock.split("\n").filter((l) => !/^\s*\/\//.test(l)).join("\n"); + const referenced = new Set([...declared.matchAll(/--dsw-[a-z0-9-]+/g)].map((m) => m[0])); + const unknown = [...referenced].filter((n) => !(n in HOST_TOKENS)); + assert.deepEqual(unknown, [], "面板引用了宿主快照里没有的 token(幻影名或快照过期):" + unknown.join(", ")); +}); + +// 变量层必须挂满四个渲染根:面板不只长在 .mneme-x 下——记忆库 sheet portal 到 +// body(.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口 +// portal 到宿主侧边栏(.mneme-topentry)。少挂一个,那个根下的文字就退回宿主的 +// 低对比度 token,而且上面的主锁**照样全绿**(主锁只算颜色,不知道变量有没有继承到)。 +test("面板语义色层覆盖四个渲染根", () => { + const sel = layer.sel; + assert.equal(sel, ".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry"); +}); + +// 图形色的对比度(WCAG 1.4.11,非文字 3:1)。上面那条主锁只看 `color:`, +// 而「颜色就是信息」的地方用的是 `background:` / `stroke:`——热度条、热度点、 +// 打卡点、关系图连线。它们此前用宿主的 label-dimmed(浅 1.26 / 深 1.45), +// 等于把三档热度画成同一个看不见的灰。谁把它们改回去,主锁不会响,这条会。 +const GRAPHICS = [ + { sel: ".mneme-heatbar--hot", prop: "background" }, + { sel: ".mneme-heatbar--warm", prop: "background" }, + { sel: ".mneme-heatbar--cold", prop: "background" }, + { sel: ".mneme-heatdot--hot", prop: "background" }, + { sel: ".mneme-heatdot--warm", prop: "background" }, + { sel: ".mneme-heatdot--cold", prop: "background" }, + { sel: ".mneme-wbdot", prop: "background" }, + { sel: ".mneme-wbdot--err", prop: "background" }, + { sel: ".mneme-gedge", prop: "stroke" } +]; +test("图形色(热度/打卡/连线)在浅深两主题下都达到 3:1", () => { + const bySel = new Map(rules.map((r) => [r.sel, r])); + const problems = []; + for (const g of GRAPHICS) { + const r = bySel.get(g.sel); + if (!r) { problems.push(`${g.sel} 规则没了`); continue; } + const m = new RegExp(`(?:^|;)\\s*${g.prop}\\s*:\\s*([^;]+)`).exec(r.body); + if (!m) { problems.push(`${g.sel} 没有 ${g.prop}`); continue; } + for (const theme of ["light", "dark"]) { + const fg = colorOf(m[1], theme); + if (!fg) { problems.push(`${g.sel} 解析不出颜色:${m[1]}`); continue; } + // 热度条画在它自己的轨道(interactive-bg-hover)上,其余直接压在画布上。 + let bg = parseColor(THEME_BG[theme]); + if (/heatbar/.test(g.sel)) { + const track = colorOf("var(--dsw-alias-interactive-bg-hover)", theme); + if (track) bg = composite(track, bg); + } + const cr = contrast(fg, bg); + if (cr < 3) problems.push(`${theme} ${g.sel} ${cr.toFixed(2)} < 3(${m[1].trim()})`); + } + } + assert.deepEqual(problems, [], "图形色对比度不足:\n" + problems.join("\n")); +}); \ No newline at end of file From 84c9b9f87ecfe6b054cd611a0d1725ba8f31c729 Mon Sep 17 00:00:00 2001 From: modusensus Date: Mon, 12 Oct 2026 03:17:55 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(a11y):=20=E6=94=B6=E5=8F=A3=20CodeRabbi?= =?UTF-8?q?t=20=E4=B8=89=E6=9D=A1=E8=AF=84=E5=AE=A1=E6=84=8F=E8=A7=81?= =?UTF-8?q?=EF=BC=88#178=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交 b8b7c70 把面板正文色改成自有的语义色层后,CodeRabbit 提了三条意见, 逐条核实后全部属实,本提交一并处置。 1) 虚线关系边实际对比度不足(dsh-mneme/lib/client.js) 无 memory_id 的边同时命中 .mneme-gedge(50% 描边)与 .mneme-gedge-dashed (opacity:.7),叠加后浅色画布只剩 2.28,低于图形要求的 3:1。虚线态本来就靠 dash 图案表达语义,不再叠透明度即可;现在浅 3.55 / 深 4.82。 2) 幻影 token 的计数与归属写错了(注释与 CHANGELOG) --dsw-alias-label-dimmed 是真实存在的宿主 token(指向 static-neutral-bluish-200 / -750),我在上一提交里把它错列为幻影名。幻影名是 5 个不是 6 个;label-dimmed 的问题不在名字而在用法——宿主把它定位成分隔线/占位符的低对比层,面板却拿来当 正文与图形色,这一条由比值锁负责。已改正 lib/client.js 与 CHANGELOG 的措辞, 并在 test/contrast.test.js 的快照表里就地注明它为什么留在表里。 3) 解析不出的颜色被静默跳过(test/contrast.test.js) 主锁里 colorOf() 返回 null 时原本 continue,于是把一条声明改成 color:var(--mneme-unknown) 就能溜过,其余声明仍够满足 audited >= 200。 现在解析失败记为失败。面板 CSS 块里的 color 全是 hex/rgb/var/color-mix, 没有 inherit/currentColor 之类的关键字,所以 null 只可能意味着引用了不存在的 token 或写了不认识的值——正是要拦的东西。 新增两条回归锁(test/contrast.test.js): - 虚线关系边不得靠降低不透明度区分(图形锁只读单条规则自己的 stroke,看不见 「同一条边命中两条规则、其中一条在降透明度」这种叠加,故单列一条) - 解析不出的 color 声明记失败(并入主锁) 变异检验在原有五条基础上补两条:一条 color 改成 var(--mneme-unknown) → 主锁 转红;虚线边加回 opacity:.7 → 虚线锁转红。七条锁分别转红,均已实测。 npm test:1620 tests / 1619 pass / 0 fail / 1 skipped(Windows 符号链接用例, 仓库原生);src↔lib 一致(57 个文件);双 README 测试数已同步到 1620。 --- AGENTS.md | 2 +- README.md | 6 +++--- dsh-mneme/CHANGELOG.md | 2 +- dsh-mneme/README.md | 6 +++--- dsh-mneme/lib/client.js | 8 +++++--- dsh-mneme/test/contrast.test.js | 34 ++++++++++++++++++++++++++++----- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7e4a7fd1..f8568a40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ dsh-mneme 是 DSH 宿主的记忆插件(蒸馏 / 注入 / 检索 / 巩固 / sc 6. 提交署名可归属与 AI 内容核验义务见 [CONTRIBUTING](CONTRIBUTING.md)(硬性要求); 7. **发布/打包只在 `dsh-mneme/` 包目录内执行**——根目录历史上发出过坏包(0.6.9、0.7.0–0.7.10),此条为防复发红线; 8. **注释写「为什么」,不写「是什么」**:非常规实现、防御段、行为开关的动机(issue 编号、消融数据、基准数字)必须落注释,代码含义靠命名与测试名表达;测试同样适用——锁形状/锁字面量的用例要注释写清它在防哪类回归(例:`test/recall-evals.test.js` 的 signals 形状锁)。 -9. **面板配色走 `--mneme-*` 语义色层**:宿主 `label-tertiary` / `label-dimmed` 是分隔线/占位符用的低对比层,**不是正文色**,直接拿来当 `color` 会在浅色或深色主题下掉到 AA 以下(#178 实测 70 条)。正文与图形一律用 `.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry` 上定义的语义变量;新增或改动颜色后 `test/contrast.test.js` 会自动重算全部 `color` 声明的对比度,并锁死「引用的 `--dsw-*` 必须真实存在」与「四个渲染根一个不少」。 +9. **面板配色走 `--mneme-*` 语义色层**:宿主 `label-tertiary` / `label-dimmed` 是分隔线/占位符用的低对比层,**不是正文色**,直接拿来当 `color` 会在浅色或深色主题下掉到 AA 以下(#178 实测 70 条)。正文与图形一律用 `.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry` 上定义的语义变量;新增或改动颜色后 `test/contrast.test.js` 会自动重算全部 `color` 声明的对比度(解析不出的颜色记失败,不静默跳过),并锁死「引用的 `--dsw-*` 必须真实存在」「四个渲染根一个不少」「图形色按 3:1」「虚线边不得靠 `opacity` 区分」。 ## 闸门清单 diff --git a/README.md b/README.md index 3e0f1247..0e5c9356 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790 ```bash cd dsh-mneme && npm install -npm test # 1619 个测试 +npm test # 1620 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo ```bash cd dsh-mneme && npm install -npm test # 1619 tests +npm test # 1620 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index f48d35ab..1ad9cad3 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -4,7 +4,7 @@ ## 🐛 修复 -- **面板文字对比度全面达标 WCAG AA(issue #178)**:面板有 270 条 `color` 声明,此前 **70 条**在浅色或深色主题下达不到 AA(正文 4.5:1、大字号 3:1;浅色 65 条、深色 5 条,最差的两条 `.mneme-statfoot` 只有 1.26 / 1.64)。根因是面板直接拿宿主的**标签层** token 当正文色(`label-tertiary` 50 处、`label-dimmed` 4 处):这些是给分隔线/占位符用的低对比层,不是正文层。另有三处**图形**同病(不在文字口径内,按 3:1 复核):热度条与热度点的冷档用 `label-dimmed` 压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,等于把三档热度画成同一个看不见的灰。修法不是逐条换颜色,而是在四个渲染根上定义**面板自己的语义色层**:`.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{…}` 六条 `--mneme-*`(`fg-dim` / `graphic-dim` / `accent` / `danger` / `warn` / `ok`),每条都用 `color-mix()` 从宿主 token 混出——`fg-dim` = `label-primary` 60% + 透明(浅 4.94 / 深 6.30),其余同法。**四个根一个都不能少**:`.mneme-overlay` 与 `.mneme-topentry` 是 `createPortal` 出来的独立子树,`.mneme-triggerwrap` 挂在宿主 sidebar footer 插槽,只挂 `.mneme-x` 的话这三个根拿不到变量、静默退回旧色。顺带修掉 **6 个幻影 token**(`--dsw-alias-state-error` 19 处、`state-warning` 14 处、`label-dimmed` 4 处、`state-success` 3 处、`border` 2 处、`state-warn` 1 处):这些名字宿主从未定义过,一直靠 CSS 兜底字面量在跑,宿主改配色时面板不会跟——现全部换成真名(`state-error-primary` / `static-amber-600` / `border-l1` 等)。三处 knockout 配色一并修:`.mneme-conflict-primary` 浅色主题下白字压品牌蓝 `#4176e6` 只有 4.23:1,底色换成 `--mneme-accent`(浅 5.32 / 深 7.67);`.mneme-entrybadge` 深色主题下 `bg-layer-1` 压 `#c33` 只有 3.06:1,底色换成 `--mneme-danger`;`.mneme-btndangerconfirm` 底色同步换成 `--mneme-danger`,并把写死的 `#fff` 换成 `var(--dsw-alias-bg-layer-1)`——它旧的写死白字只在旧底色上侥幸过关,底色一变浅就会在深色主题掉到 2.67:1。回归锁 `test/contrast.test.js` 四条:①把宿主 token 的浅/深取值快照硬编码进测试(CI 上没有宿主包,读不到 asar),逐条重算全部 `color` 声明的对比度,两主题都必须过;②面板引用的每个 `--dsw-*` 必须在快照表里存在(幻影名回归锁);③语义色层的选择器必须恰好是那四个根(少了根主锁照样全绿——主锁只算颜色,不知道变量有没有继承到);④图形色(`background` / `stroke`,非 `color`)单独按 3:1 复核——热度条、热度点、打卡点、关系图连线都不在主锁的视野里。变异检验五条:改淡 `fg-dim`、一处正文用回宿主标签色、塞回幻影名、渲染根少挂一个、热度冷档改回宿主 `label-dimmed`,五条锁分别转红。 +- **面板文字对比度全面达标 WCAG AA(issue #178)**:面板有 270 条 `color` 声明,此前 **70 条**在浅色或深色主题下达不到 AA(正文 4.5:1、大字号 3:1;浅色 65 条、深色 5 条,最差的两条 `.mneme-statfoot` 只有 1.26 / 1.64)。根因是面板直接拿宿主的**标签层** token 当正文色(`label-tertiary` 50 处、`label-dimmed` 4 处):这些是给分隔线/占位符用的低对比层,不是正文层。另有三处**图形**同病(不在文字口径内,按 3:1 复核):热度条与热度点的冷档用 `label-dimmed` 压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,等于把三档热度画成同一个看不见的灰。修法不是逐条换颜色,而是在四个渲染根上定义**面板自己的语义色层**:`.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{…}` 六条 `--mneme-*`(`fg-dim` / `graphic-dim` / `accent` / `danger` / `warn` / `ok`),每条都用 `color-mix()` 从宿主 token 混出——`fg-dim` = `label-primary` 60% + 透明(浅 4.94 / 深 6.30),其余同法。**四个根一个都不能少**:`.mneme-overlay` 与 `.mneme-topentry` 是 `createPortal` 出来的独立子树,`.mneme-triggerwrap` 挂在宿主 sidebar footer 插槽,只挂 `.mneme-x` 的话这三个根拿不到变量、静默退回旧色。顺带修掉 **5 个幻影 token**(`--dsw-alias-state-error` 19 处、`state-warning` 14 处、`state-success` 3 处、`border` 2 处、`state-warn` 1 处):这些名字宿主从未定义过,一直靠 CSS 兜底字面量在跑,宿主改配色时面板不会跟——现全部换成真名(`state-error-primary` / `static-amber-600` / `border-l1` 等)。另需分清:`label-dimmed`(4 处)与 `label-tertiary`(50 处)**都是真实存在的宿主 token**,问题不在名字而在用法——宿主把它们定位成分隔线/占位符的低对比层,面板却拿来当正文与图形色。三处 knockout 配色一并修:`.mneme-conflict-primary` 浅色主题下白字压品牌蓝 `#4176e6` 只有 4.23:1,底色换成 `--mneme-accent`(浅 5.32 / 深 7.67);`.mneme-entrybadge` 深色主题下 `bg-layer-1` 压 `#c33` 只有 3.06:1,底色换成 `--mneme-danger`;`.mneme-btndangerconfirm` 底色同步换成 `--mneme-danger`,并把写死的 `#fff` 换成 `var(--dsw-alias-bg-layer-1)`——它旧的写死白字只在旧底色上侥幸过关,底色一变浅就会在深色主题掉到 2.67:1。回归锁 `test/contrast.test.js` 五条:①把宿主 token 的浅/深取值快照硬编码进测试(CI 上没有宿主包,读不到 asar),逐条重算全部 `color` 声明的对比度,两主题都必须过,**解析不出的颜色记失败而不是跳过**(否则改成 `var(--mneme-unknown)` 就能静默溜过);②面板引用的每个 `--dsw-*` 必须在快照表里存在(幻影名回归锁);③语义色层的选择器必须恰好是那四个根(少了根主锁照样全绿——主锁只算颜色,不知道变量有没有继承到);④图形色(`background` / `stroke`,非 `color`)单独按 3:1 复核——热度条、热度点、打卡点、关系图连线都不在主锁的视野里;⑤虚线关系边不得靠 `opacity` 区分(无 `memory_id` 的边同时命中 `.mneme-gedge` 与 `.mneme-gedge-dashed`,后者的 `opacity:.7` 把 50% 描边压到浅色 2.28,而图形锁只读单条规则、看不见这种叠加)。变异检验七条:改淡 `fg-dim`、一处正文用回宿主标签色、塞回幻影名、渲染根少挂一个、热度冷档改回宿主 `label-dimmed`、一条 `color` 改成解析不出的 `var(--mneme-unknown)`、虚线边加回 `opacity:.7`,七条锁分别转红。 ## [0.8.15] - 2026-10-10 diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 27e101c6..07425533 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1619%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1620%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -613,7 +613,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1619 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1620 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1619 个测试 +npm test # 运行 1620 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/client.js b/dsh-mneme/lib/client.js index 1bcc1664..7988e1f4 100644 --- a/dsh-mneme/lib/client.js +++ b/dsh-mneme/lib/client.js @@ -1282,8 +1282,8 @@ window.__ModuleLoader__.load({ ".mneme-entitychip:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}", // --- main-area memory library page --- // 面板语义色层(#178):宿主 token 在浅色下给正文/次要文字只有 1.26–4.49 - // 的对比度,且面板还一直在引用 6 个根本不存在的宿主名(--dsw-alias-state-error - // / -warning / -success / -warn / --dsw-alias-label-dimmed / --dsw-alias-border), + // 的对比度,且面板还一直在引用 5 个根本不存在的宿主名(--dsw-alias-state-error + // / -warning / -success / -warn / --dsw-alias-border), // 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组 // 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。 // 必须挂四个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body @@ -1364,7 +1364,9 @@ window.__ModuleLoader__.load({ ".mneme-glabel{fill:var(--dsw-alias-label-secondary);font-size:11px;text-anchor:middle;pointer-events:none;user-select:none}", ".mneme-gedge{stroke:var(--mneme-graphic-dim);stroke-width:1.5;cursor:pointer}", ".mneme-gedge:hover{stroke:var(--dsw-alias-state-business-primary)}", - ".mneme-gedge-dashed{stroke-dasharray:5 4;opacity:.7}", + // 虚线态靠 dash 图案区分,不再叠 opacity:50% 的描边再乘 .7 后浅色画布上 + // 只剩 2.28,低于图形要求的 3:1(图案已经承担了「没有 memory_id」这个语义)。 + ".mneme-gedge-dashed{stroke-dasharray:5 4}", ".mneme-graphhint{flex:none;color:var(--mneme-fg-dim);font-size:13px;text-align:center;padding:6px 0 2px}", ".mneme-graphside{flex:none;max-height:220px;overflow-y:auto;border-top:1px solid var(--dsw-alias-border-l1);margin-top:10px;padding-top:10px}", ".mneme-gs-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline;gap:8px}", diff --git a/dsh-mneme/test/contrast.test.js b/dsh-mneme/test/contrast.test.js index 65c0761a..1e918bd5 100644 --- a/dsh-mneme/test/contrast.test.js +++ b/dsh-mneme/test/contrast.test.js @@ -15,9 +15,10 @@ import { fileURLToPath } from "node:url"; // // 面板的坑有两层,这条锁两层都咬: // ① 宿主 token 在浅色下给正文只有 1.26–4.49 的对比度(label-tertiary 50 处、 -// label-dimmed 4 处),照抄宿主名就等于照抄不合规; -// ② 面板还长期引用 6 个**根本不存在的**宿主名(--dsw-alias-state-error / -// -warning / -success / -warn / --dsw-alias-label-dimmed / --dsw-alias-border), +// label-dimmed 4 处),照抄宿主名就等于照抄不合规——注意这两个名字**都是 +// 真实存在的**宿主 token,只是宿主把它们定位成分隔线/占位符的低对比层; +// ② 面板还长期引用 5 个**根本不存在的**宿主名(--dsw-alias-state-error / +// -warning / -success / -warn / --dsw-alias-border), // 于是吃的一直是写在 fallback 里的硬编码色。所以除了比值,还要锁「引用到的 // 宿主名必须真实存在」。 // @@ -31,6 +32,9 @@ const HOST_TOKENS = { "--dsw-alias-label-primary": { light: "#0f1115", dark: "#f9fafb" }, "--dsw-alias-label-secondary": { light: "#61666b", dark: "#cfd3d6" }, "--dsw-alias-label-tertiary": { light: "#81858c", dark: "#adb2b8" }, + // 真名,不是幻影:宿主确有 --dsw-alias-label-dimmed(指向 static-neutral-bluish-200 + // / -750)。它留在快照里是为了让存在性锁**不**把它当幻影报错——该拦的是「拿它当 + // 正文/图形色」的用法,那由上面的比值锁负责。 "--dsw-alias-label-dimmed": { light: "#e1e5ee", dark: "#43454a" }, "--dsw-alias-state-business-primary": { light: "#4176e6", dark: "#7aaaff" }, "--dsw-alias-state-error-primary": { light: "#ec1313", dark: "#f25a5a" }, @@ -196,7 +200,15 @@ for (const r of rules) { const need = fs >= 24 || (fs >= 18.66 && fw >= 700) ? 3 : 4.5; for (const theme of ["light", "dark"]) { const fg = colorOf(colM[1], theme); - if (!fg) continue; + // 解析不出来**不算通过**:面板 CSS 块里的 color 全是 hex/rgb/var/color-mix + // (没有 inherit/currentColor 之类的关键字),所以 null 只可能意味着引用了 + // 不存在的 token 或写了个解析器不认识的值——那正是要拦的东西。若在这里 + // `continue`,把一条声明改成 `color:var(--mneme-unknown)` 就能静默溜过, + // 剩下的声明仍然够 audited >= 200。 + if (!fg) { + failures.push(`${theme} ${r.sel} 颜色解析失败:${colM[1].trim()}`); + continue; + } audited++; const cr = contrast(fg, backgroundOf(r.sel, theme)); if (cr < need) { @@ -210,7 +222,7 @@ test("面板每个 color 声明在浅/深两主题下都达到 WCAG AA(#178 assert.deepEqual(failures, [], "以下声明对比度不足:\n" + failures.join("\n")); }); -// 幻影 token 的回归锁:面板曾经引用 6 个宿主里不存在的名字,于是长年吃 fallback +// 幻影 token 的回归锁:面板曾经引用 5 个宿主里不存在的名字,于是长年吃 fallback // 里的硬编码色(换肤不跟随、深色主题翻车)。这条锁不看比值、只看名字是否存在, // 因此将来任何「顺手写个听起来合理的 --dsw-* 名」都会被拦下。 test("面板引用的宿主 token 必须真实存在(幻影 token 回归锁)", () => { @@ -267,4 +279,16 @@ test("图形色(热度/打卡/连线)在浅深两主题下都达到 3:1", () } } assert.deepEqual(problems, [], "图形色对比度不足:\n" + problems.join("\n")); +}); + +// 虚线关系边曾经在 .mneme-gedge-dashed 上再叠一层 opacity:.7,把已经只有 50% 的 +// 描边压到浅色 2.28(低于图形 3:1)。上面那条图形锁只读单条规则自己的 stroke, +// 看不到「同一条边同时命中两条规则、其中一条在降透明度」这种叠加,所以单独锁一层: +// 虚线态只允许用 dash 图案区分,不许再动透明度。 +test("虚线关系边不得靠降低不透明度区分", () => { + const dashed = rules.find((r) => r.sel === ".mneme-gedge-dashed"); + assert.ok(dashed, ".mneme-gedge-dashed 规则没了(虚线态怎么区分无 memory_id 的边?)"); + const op = /(?:^|;)\s*opacity\s*:\s*([^;]+)/.exec(dashed.body); + assert.equal(op, null, `虚线边又叠了 opacity(${op?.[1].trim()}):50% 描边乘上它会在浅色画布跌破 3:1`); + assert.match(dashed.body, /stroke-dasharray/, "虚线态丢了 dash 图案,就和实线边分不出来了"); }); \ No newline at end of file