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 @@
-
+
@@ -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 @@
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
[](LICENSE)
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
-[](https://github.com/slow-stack/mneme)
+[](https://github.com/slow-stack/mneme)
[](https://github.com/slow-stack/mneme/actions)
[](https://nodejs.org)
[](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 @@
-
+
@@ -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 @@
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
[](LICENSE)
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
-[](https://github.com/slow-stack/mneme)
+[](https://github.com/slow-stack/mneme)
[](https://github.com/slow-stack/mneme/actions)
[](https://nodejs.org)
[](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