Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

## 🎨 面板

- **481–519px 是一段两不管的中间带:480 主断点之上、两栏又排不下,卡片横向溢出 2–11px 被外层 `overflow:hidden` 直接切掉(承接 #542,实测发现)**:#542 把 `minmax(252px,1fr)` 换成 `minmax(min(252px,100%),1fr)`,但那两条**只写在 `@container (max-width:480px)` 里**,而基础规则 `lib/client.js` 的 `.mneme-cards` 仍是裸 `minmax(252px,1fr)`、`.mneme-attrgrid` 仍是裸 `minmax(200px,1fr)`。于是 481px 一开始断点失效、左栏回到 216px,卡片被要求至少 252px——`Edge 156` 实测:记忆页 481/490/500/510 各有 1–2 条 `SCROLLX`(`.mneme-cards` 300 > 可用 265),实体页 detail 只剩 257–276px 而属性网格仍要 200px。**兜底必须落在基础规则上**:两处 `min()` 下限上移到基础规则(它们的正确作用域本来就是「所有宽度」,481–519px 只是其中最窄的一段),480 档里的重复声明一并删掉。修完 320–900px 全档 24 个页面 ovf=0,卡片宽 320→300 / 481→217 / 520→256 / 900→287,属性网格 481→209 / 900→628。**一处刻意的取舍**:我一度给基础规则加 `max-height:min(440px,52vh)` 想把 481–519px 那段 440px 高的图谱压下来,实测确实压到 353px——但 `52vh` 在常见窗口高度(≈680px)下对**桌面也生效**,等于顺手改了大屏观感,已回退。基础规则不带 `max-height`,限高仍只留在 480 档(`min(320px,52vh)`);**代价是 481–519px 中间带的图谱仍是 440px 高,这个中间带高度问题刻意不修**。**回归测试改锁位置**:原来那两条 `body.includes("minmax(min(252px,100%),1fr)")` 锚在 480 档 body 上,现在改成作用在「剥掉所有嵌套块之后的顶层规则」——新增一个 `topLevel` 切片器(遇到 `@` 就按括号配对跳过后整块丢弃),并断言切片长度 > 10000 防止切片器自己坏掉导致假绿;锁两件事:两处基础 `minmax(min(…,100%),1fr)`。变异自证(把基础 minmax 退回裸 252 → 红;恢复后 85/85)。全量 `npm test` 2609 / 2608 pass / 0 fail / 1 skipped。**已知未覆盖**:仍无真机(`adb devices` 为空,需要设备接上并重启宿主让插件重新探测 adb),宽度结论来自 Edge headless + 真实 CSS 的静态测量,交互(拖拽、滚动链、软键盘)未经真机验证。
- **面板此前根本没有移动端断点:320–430px 的手机上,记忆页与实体页的两栏骨架把主区挤成一条缝,导航项还会压到「⋯」按钮底下**:整份 `lib/client.js` 只有 8 条 `@container` 规则、0 条 `@media`,而且这 8 条全是「把数字改小」式收窄——记忆页左栏 216px + 卡片最小 252px + 沟槽 48px = 面板至少 517px 才放得下,实体页 rail 224px + 属性网格 200px + 48px = 至少 473px,而目标上沿只有 430px。**收窄数字永远追不上,必须动骨架**:新增 `@container (max-width: 480px)` 主断点,把 `.mneme-xmain` 与 `.mneme-ent` 从 `flex-direction:row` 翻成 `column`,左栏 / rail 改成满宽 + 限高可滚(`max-height:min(42vh,300px)` / `min(40vh,280px)` + `overscroll-behavior:contain`,滚到底不漏给页面),卡片与属性网格的 `minmax(252px,1fr)` 改成 `minmax(min(252px,100%),1fr)`(**`min()` 是防溢出的关键:320px 下 `minmax(252px,1fr)` 仍会把轨道撑到 252px 之外**),抽屉从 `width:min(432px,52%)` 改 `100%`(52% 相对的是 `.mneme-xmain`,320 下只剩 166px;抽屉自带 ✕,满宽不丢关闭入口),顶部导航改左对齐 + 右侧留 52px 给 `position:absolute` 的 `⋯`,并新增 `@container (max-width: 360px)` 兜底档(隐藏 14px 图标、导航横向可滚、卡片单列)。**三处刻意的取舍**:① **`@container` 里一个颜色都不写**——`test/contrast.test.js` 的提取器用 `/^([^{]+)\{([^}]*)\}$/` 只匹配单层花括号,`@container` / `@media` 的双层块整串被跳过,在里面写 `color` 就绕过了 #178 的 AA 锁;新增的回归测试会把这条纪律变成闸门(遍历所有双层块,出现 `color` / `fill` / `stroke` / `background` 即失败)。② **overlay 外框只能走 `@media`**:`.mneme-overlay` 不是 `.mneme-x` 的后代(`container-type:inline-size` 在 `.mneme-x` 上),容器查询够不到它,所以它的 `calc(100vw - 16px)` / `calc(100dvh - 16px)` 放在 `@media (max-width:640px)` 里。③ **触屏删除钮只加常显、不动配色**:`.mneme-set-del{opacity:0}` 靠 `:hover` / `:focus-within` 恢复,触屏永远没有 hover——改法是 `@media (hover:none){.mneme-set-del{opacity:1}}`,参照同文件 `.mneme-persona-del` 的常显范式;#457(内联 `opacity:1` 覆写)与 #459(连带点亮规则区真删除钮)两次回滚的教训就是**别顺手把配色也改了**。顺带给 `.mneme-set-del` / `.mneme-footbtn` 补 `min-height:24px`(WCAG 2.5.8 AA 的触控下限)。图谱另修三症:内联 `height:440` 被 `max-height:min(320px,52vh)` 压住(CSS 2.1 §10.7:`max-height` 优先于 `height`)、`touch-action:none` 改 `pan-y` 让页面能纵向滚、`.mneme-glabel` 从 11px 提到 14px 抵消 SVG 缩放(380 用户单位压到 288px 时缩放比 0.758,11px 视觉只剩 8.3px)。弹层固定宽同样越界:`.mneme-sharedialog{width:340px}` 在 320 屏上是硬矛盾,三条都改 `min(原宽,calc(100cqw - 32px))`。**回归测试 +1**(`test/client.test.js` 的 `mobile: the narrow-container breakpoints reflow the two-column skeletons instead of shrinking numbers`):锁两档断点存在、锁骨架翻转与 `minmax(min(...))`、锁抽屉满宽与弹层 `cqw` 上界、锁 overlay 走 `@media`、**锁双层块内不得出现颜色声明**、锁 `hover:none` 的常显。变异自证三例(把 `@container` 换成 `@media`、在 `.mneme-ent` 里插 `color:red`、把 `minmax(min(252px,100%),1fr)` 改回 `minmax(252px,1fr)`)逐条变红,恢复后 90/90 全绿。**已知未覆盖**:实际视觉未在真机或 Playwright 上验证过(本机无 playwright),全部结论是 CSS 级推演;`100vh` 未换成 `dvh` 的位置仍有 3 处(`2108` / `2493` / `2596`),`safe-area-inset-*` 全文件 0 处——这三项留作后续。

## 🐛 修复
Expand Down
21 changes: 17 additions & 4 deletions dsh-mneme/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2015,6 +2015,13 @@ window.__ModuleLoader__.load({
// --- graph sub-view (fills the content area under the tabs) ---
".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
// touch-action 保持 none:图谱的拖拽与缩放只挂了 mousedown/mousemove/wheel,
// 全仓 onPointerDown/onTouchStart 命中 0,所以在触屏上它本来就不是一个可交手势的
// 面。改成 pan-y 会让纵向拖拽退回页面滚动——而浏览器在 touch-action:none 下会派发
// 兼容鼠标事件,触屏纵向拖拽究竟走哪条路,没有真机无法定论;这一版不做无法复验的
// 行为变更,留给出指针事件重写的那条车道。高度也不在这里限:480 主断点已压到
// min(320px,52vh),把 52vh 写进基础规则会在常见窗口高度(≈680px)下把桌面也压成
// 353px,等于顺手改了大屏观感。
Comment thread
modusensus marked this conversation as resolved.
Resolved
".mneme-graphsvg{flex:1;min-height:180px;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-button-elevated-fill);cursor:grab;touch-action:none}",
".mneme-gnode{cursor:pointer}",
".mneme-gnode circle{stroke:var(--dsw-alias-bg-layer-2);stroke-width:2;transition:stroke-width .12s}",
Expand Down Expand Up @@ -2058,7 +2065,9 @@ window.__ModuleLoader__.load({
".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(--mneme-fg-dim)}",
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}",
// 属性网格同理:min(200px,100%) 的 100% 兜底压在基础规则上,实体页 detail 在
// 481–519px 只剩 257–276px(rail 仍占 224px 横排),裸 200px 下限会溢出。
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),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(--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}",
Expand Down Expand Up @@ -2173,8 +2182,12 @@ window.__ModuleLoader__.load({
".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,卡片悬停轻浮起
".mneme-cards{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(252px,1fr));gap:14px;padding:20px 24px 32px;align-content:start}",
// 卡片网格:响应式 auto-fill,卡片悬停轻浮起。
// 轨道下限用 min(252px,100%) 而不是裸 252px:容器(.mneme-xcontent)只有
// 265–294px 时裸下限仍要求 300px 轨道,网格横向溢出 2–11px 被外层 overflow:hidden
// 切掉。这个洞出现在 481–519px——480 主断点之外、两栏又放不下的中间带,所以
// 兜底必须落在基础规则上,只在断点里写等于给中间带留缺口。
".mneme-cards{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(252px,100%),1fr));gap:14px;padding:20px 24px 32px;align-content:start}",
".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)}",
Expand Down Expand Up @@ -2664,7 +2677,7 @@ window.__ModuleLoader__.load({
// sharedialog 自己不带 padding:它和 impdialog 同挂一条 div(见 JSX 里的
// `className:"mneme-impdialog mneme-sharedialog"`),盒模型由 impdialog 带过来,
// 所以它同样 +30px;宽度取哪一条由源码顺序决定,故两条都得是 border-box。
"@container (max-width: 480px){.mneme-xbar{justify-content:flex-start;padding:0 52px 0 10px}.mneme-vtab{padding:0 10px}.mneme-vtabs{min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.mneme-vtabs::-webkit-scrollbar{height:0}.mneme-xmain{flex-direction:column}.mneme-xside{width:100%;flex:0 1 auto;min-height:0;max-height:min(42vh,300px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2);overscroll-behavior:contain}.mneme-xbrowsebar{padding:8px 12px}.mneme-filterbar{padding:8px 12px}.mneme-drawer{width:100%}.mneme-drawerbody{padding:16px 14px 24px}.mneme-cards{grid-template-columns:repeat(auto-fill,minmax(min(252px,100%),1fr));padding:14px 12px 24px;overscroll-behavior:contain}.mneme-ent{flex-direction:column}.mneme-entrail{width:100%;flex:0 1 auto;min-height:0;max-height:min(40vh,280px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2)}.mneme-entrailbody{overscroll-behavior:contain}.mneme-entdetail{padding:16px 14px 24px}.mneme-attrgrid{grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr))}.mneme-set{padding:8px 14px 40px}.mneme-status{padding:16px 14px 32px}.mneme-menu{box-sizing:border-box;min-width:0;width:min(200px,calc(100cqw - 32px))}.mneme-impdialog{box-sizing:border-box;width:min(300px,calc(100cqw - 32px))}.mneme-sharedialog{box-sizing:border-box;width:min(340px,calc(100cqw - 32px))}.mneme-graph{padding:10px 12px 12px}.mneme-graphsvg{max-height:min(320px,52vh);touch-action:pan-y}.mneme-glabel{font-size:14px}.mneme-graphside{position:static;width:auto;max-width:100%;max-height:min(200px,30vh);margin-top:8px}}",
"@container (max-width: 480px){.mneme-xbar{justify-content:flex-start;padding:0 52px 0 10px}.mneme-vtab{padding:0 10px}.mneme-vtabs{min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.mneme-vtabs::-webkit-scrollbar{height:0}.mneme-xmain{flex-direction:column}.mneme-xside{width:100%;flex:0 1 auto;min-height:0;max-height:min(42vh,300px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2);overscroll-behavior:contain}.mneme-xbrowsebar{padding:8px 12px}.mneme-filterbar{padding:8px 12px}.mneme-drawer{width:100%}.mneme-drawerbody{padding:16px 14px 24px}.mneme-cards{padding:14px 12px 24px;overscroll-behavior:contain}.mneme-ent{flex-direction:column}.mneme-entrail{width:100%;flex:0 1 auto;min-height:0;max-height:min(40vh,280px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2)}.mneme-entrailbody{overscroll-behavior:contain}.mneme-entdetail{padding:16px 14px 24px}.mneme-set{padding:8px 14px 40px}.mneme-status{padding:16px 14px 32px}.mneme-menu{box-sizing:border-box;min-width:0;width:min(200px,calc(100cqw - 32px))}.mneme-impdialog{box-sizing:border-box;width:min(300px,calc(100cqw - 32px))}.mneme-sharedialog{box-sizing:border-box;width:min(340px,calc(100cqw - 32px))}.mneme-graph{padding:10px 12px 12px}.mneme-graphsvg{max-height:min(320px,52vh);touch-action:pan-y}.mneme-glabel{font-size:14px}.mneme-graphside{position:static;width:auto;max-width:100%;max-height:min(200px,30vh);margin-top:8px}}",
// 360 档只做两件事:隐掉 14px 的 tab 图标(.mneme-vtab 带图标时约 109px/个、
// 4 个约 436px,320px 容器原地就横向溢出),再把正文 padding 收到 12–14px。
// 页签横向滚动不在这一档、属于 480 主断点,理由见 480 档那条算式。
Expand Down
46 changes: 43 additions & 3 deletions dsh-mneme/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2454,9 +2454,9 @@ test("mobile: the narrow-container breakpoints reflow the two-column skeletons i
".mneme-entrail 是 overflow:hidden 的非滚动容器,overscroll-behavior 写在它身上是死代码");
// 抽屉是覆盖层,窄容器里 52% 只剩 166px;满宽不丢关闭入口(✕ 在 .mneme-drawerbar)。
assert.ok(body.includes(".mneme-drawer{width:100%}"), "详情抽屉在窄容器里必须满宽");
// 网格轨道必须有 100% 下限,否则容器比轨道窄时横向溢出被外层 overflow:hidden 切掉。
assert.ok(body.includes("minmax(min(252px,100%),1fr)"), "卡片网格的最小轨道必须夹到 100%");
assert.ok(body.includes("minmax(min(200px,100%),1fr)"), "属性网格的最小轨道必须夹到 100%");
// 网格轨道的 100% 下限不再锚定在本档:它的作用域是「所有宽度」,而 481–519px
// (480 主断点之上、两栏又排不下的中间带)恰恰落回基础规则。锁在基础规则上,
// 见本用例末尾的 topLevel 那组。本档只保留窄屏特有的 padding 与滚动链。
// 固定宽弹层在 320px 会顶边:340px 的分享弹层比视口还宽。
assert.ok(/\.mneme-sharedialog\{[^}]*calc\(100cqw - 32px\)/.test(body), "分享弹层宽度必须跟着容器收");
assert.ok(/\.mneme-impdialog\{[^}]*calc\(100cqw - 32px\)/.test(body), "导入弹层宽度必须跟着容器收");
Expand Down Expand Up @@ -2501,4 +2501,44 @@ test("mobile: the narrow-container breakpoints reflow the two-column skeletons i
// 这里只许加 opacity:1,不许在同一条里再动 color)。
assert.match(cssBlock, /@media \(hover:none\)\{\.mneme-set-del\{opacity:1/,
"触屏设备上 .mneme-set-del 必须常显——它的 base 是 opacity:0,只靠 :hover 恢复,手机上等于永久不可见");
// 断点之外还有一段中间带:481–519px 容器宽(480 主断点之上、两栏又排不下)。
// 这一段落回基础规则,所以 min(…,100%) 的轨道下限必须写在基础规则上,只在断点里
// 写等于给中间带留缺口。本组锁都作用在「剥掉嵌套块之后的顶层规则」上——否则
// 480 档里的同名声明会让断言假绿。
const topLevel = (() => {
let out = "";
let i = 0;
while (i < cssBlock.length) {
const at = cssBlock.indexOf("@", i);
out += cssBlock.slice(i, at === -1 ? cssBlock.length : at);
if (at === -1) break;
const open = cssBlock.indexOf("{", at);
if (open === -1) { out += cssBlock.slice(at); break; }
let depth = 0, j = open;
while (j < cssBlock.length) {
if (cssBlock[j] === "{") depth++;
else if (cssBlock[j] === "}") { depth--; if (depth === 0) { j++; break; } }
j++;
}
i = j;
}
return out;
})();
assert.ok(topLevel.length > 10000, "顶层规则切片异常(" + topLevel.length + " 字符)——切片实现坏了,下面的锁会假绿");
assert.match(topLevel, /\.mneme-cards\{[^}]*minmax\(min\(252px,100%\),1fr\)/,
"卡片网格的 100% 轨道下限必须写在基础规则上:481–519px 是 480 主断点之外的中间带," +
"只在断点里兜底等于让这一段横向溢出 2–11px(外层 overflow:hidden 直接切掉)");
assert.match(topLevel, /\.mneme-attrgrid\{[^}]*minmax\(min\(200px,100%\),1fr\)/,
"属性网格同理:481–519px 时实体页 detail 只剩 257–276px,裸 200px 下限会溢出");
// 图谱的 touch-action 保持 none(这里是从 pan-y 回退后补的锁)。pan-y 那一版想解决
// 「手指在图上竖划时页面滚不动」,但它同时把纵向手势交给浏览器做页面平移,而图谱的
// 拖拽只挂了 mousedown/mousemove(全仓 onPointerDown/onTouchStart 命中 0)——触屏上
// 纵向拖拽到底走哪条路,浏览器是否派发兼容鼠标事件,没有真机无法定论。宁可保留现状
// 也不做无法复验的行为变更;真要两步都能用,得改成 Pointer Events,另开车道。
assert.match(topLevel, /\.mneme-graphsvg\{[^}]*touch-action:none/,
"图谱的 touch-action 必须写在基础规则上:481–519px 这段中间带落回基础规则," +
"只在断点里声明等于给中间带留缺口");
assert.doesNotMatch(topLevel, /\.mneme-graphsvg\{[^}]*touch-action:pan-y/,
".mneme-graphsvg 不得改回 touch-action:pan-y —— 纵向手势会被浏览器拿去滚页面," +
"而图谱的拖拽只有鼠标事件实现,触屏拖拽行为无真机无法验证(见上方注释)");
});
Loading