From 00c4df4e98875c609d8e8b51bb969a02ae5fbc16 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 15:44:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E6=BB=9A=E5=8A=A8=E8=BE=B9?= =?UTF-8?q?=E7=BC=98=E6=B7=A1=E5=87=BA=E6=8C=89=E5=AE=B9=E5=99=A8=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E5=B0=81=E9=A1=B6=EF=BC=8C=E5=BC=B9=E5=B1=82=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=B7=A1=EF=BC=88=E3=80=8C=E6=B8=90=E5=8F=98=E8=BF=87?= =?UTF-8?q?=E7=8C=9B=E3=80=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户(2026-09-14):「你有的地方渐变用的过猛了,比如收件人候选那里」。 上一轮加的「边缘淡出」写的是上下各固定 12px —— 那是拿**长列表**的内边距 (10px)当尺子量的,可它作用在**所有** `overflow-y-auto` 上。 实测收件人候选菜单整块只有 58px 高(提示行 22px + 一条候选 34px), 上下各淡 12px 共 24px ⇒ 小半个菜单是渐变的,那条唯一的候选底部被洗白。 根因不是「12px 太大」,而是**固定像素用在了高度不固定的东西上**。所以: 1. `.overflow-y-auto` 的淡出宽度改成按可见高度封顶 `min(12px, 10%)`; 2. 弹层(`.overflow-y-auto.glass-control`)**不淡**:它是圆角+边框的独立 表面,内容被边框截住已经「有交代」,而它高度小到淡出只剩负作用。 实测(Chromium 渲染后读像素,不是读声明的字面): 60px 容器:新规则淡出 6px(10.0%),旧规则 12px(20.0%) 700px 容器:新规则淡出 12px(1.7%)—— 高列表观感不变 真实候选菜单:`getComputedStyle(...).maskImage` 从 12px 渐变变为 `none` 判据:narrow-layout 新增 4 条,钉的是**结构**而不是那个数字 —— 淡出宽度必须带上限、两个上限都必须 > 0、弹层必须豁免、两套前缀都要在。 (第一版只取了 `min(` 里的 px 就断言 > 0,把 10% 改成 0% 时照样绿, 被变异测试抓到后重写。) --- client/electron/src/index.css | 27 ++++++++++++-- client/electron/test/narrow-layout.test.mjs | 41 +++++++++++++++++++++ client/electron/test/run-all.mjs | 2 +- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/client/electron/src/index.css b/client/electron/src/index.css index 1170bf1..57c9845 100644 --- a/client/electron/src/index.css +++ b/client/electron/src/index.css @@ -1365,20 +1365,39 @@ html[data-bg='on'] .glass-card:hover { * 只作用在**纵向**滚动容器(`.overflow-y-auto`):周视图那种**横向**滚动 * 有自己的横向滚动条,加纵向遮罩会跟它打架(用户刚提过手势冲突那件事)。 * 上下各 12px 与列表内边距(10px)接近,静止时几乎看不出,滚动时才起作用。 + * + * ★ 2026-09-14 追加(用户:「你有的地方渐变用的过猛了,比如收件人候选那里」): + * 那个「12px」是拿**长列表**的内边距(10px)当尺子量的,可它作用在**所有** + * `overflow-y-auto` 上。收件人候选菜单整块才 58px 高(提示行 22px + 一条候选 34px), + * 上下各淡 12px 共 24px —— 小半个菜单是渐变的,那条唯一的候选底部直接被洗白。 + * 「过猛」的根因不是 12px 太大,而是**固定像素用在了一个高度不固定的东西上**。 + * 所以改成按容器可见高度封顶:min(12px, 10%)。 + * - 高列表(≥120px)拿到的还是 12px,原观感不变; + * - 矮容器按比例收敛(58px 的菜单 → 5.8px)。 */ .overflow-y-auto { -webkit-mask-image: linear-gradient( to bottom, transparent 0, - #000 12px, - #000 calc(100% - 12px), + #000 min(12px, 10%), + #000 calc(100% - min(12px, 10%)), transparent 100% ); mask-image: linear-gradient( to bottom, transparent 0, - #000 12px, - #000 calc(100% - 12px), + #000 min(12px, 10%), + #000 calc(100% - min(12px, 10%)), transparent 100% ); } + +/* + * 弹层(控件档的滚动区)**不淡**:它是圆角 + 边框的独立表面,内容被边框截住 + * 已经「有交代」了 —— 那个淡出本来是为了不让长列表的卡片显得被硬切断的。 + * 而弹层高度本来就小,淡出只会把首尾项洗白(实测:候选菜单 58px,淡掉 24px)。 + */ +.overflow-y-auto.glass-control { + -webkit-mask-image: none; + mask-image: none; +} diff --git a/client/electron/test/narrow-layout.test.mjs b/client/electron/test/narrow-layout.test.mjs index c5b82d6..83550eb 100644 --- a/client/electron/test/narrow-layout.test.mjs +++ b/client/electron/test/narrow-layout.test.mjs @@ -172,6 +172,47 @@ check( compose.includes('shrink-0 px-4 md:px-6 pb-20 lg:pb-3') && compose.includes('sticky bottom-0') ); + +// 5.9) 滚动容器的边缘淡出不能按**固定像素**给。 +// +// 用户(2026-09-14):「你有的地方渐变用的过猛了,比如收件人候选那里」。 +// 原来的写法是上下各固定 12px(拿长列表 10px 内边距当尺子量的),可它作用在所有 +// `overflow-y-auto` 上 —— 收件人候选菜单整块才 58px(提示行 22 + 候选 34), +// 上下各淡 12px 共 24px,等于把小半个菜单洗掉。 +// 根因不是 12px 太大,而是**固定像素用在了一个高度不固定的东西上**。 +// +// 所以这里的判据不是「等于某个值」,而是三条**结构**约束: +// 1. 淡出宽度必须带一个上限(`min(…)`)—— 否则矮容器一定被洗掉; +// 2. 上限不能为 0(那等于没有淡出,用户上一轮的「硬截断」又回来了); +// 3. 弹层(控件档的滚动区)必须豁免 —— 它自己是圆角+边框的独立表面, +// 被边框截住已经「有交代」,而它高度小到淡出只剩负作用。 +const maskRule = (css.match(/\.overflow-y-auto\s*\{[\s\S]*?\n\}/) || [''])[0]; +check( + '滚动边缘淡出的宽度有上限(不是写死的固定像素)', + /min\(\s*\d+px\s*,/.test(maskRule), + '淡出宽度写成了固定 px —— 矮容器(候选菜单只有 58px)会被洗掉大半' +); +/* + * 上限本身也要验。第一版只取了 `min(` 里**第一个**数字(12px)就断言 > 0 —— + * 把 10% 改成 0% 时它照样绿(变异 B 抓到的):两个上限都生效才是真的收敛。 + */ +const cap = maskRule.match(/min\(\s*(\d+)px\s*,\s*(\d+)%\s*\)/); +const [capPx, capPct] = cap ? [Number(cap[1]), Number(cap[2])] : [0, 0]; +check( + '上限是「像素与百分比取小」(高列表不变、矮容器收敛),且两个上限都 > 0', + capPx > 0 && capPct > 0, + `上限取到了 px=${capPx} / %=${capPct} —— 0 等于没收敛(矮容器仍被洗)或没淡出(硬截断回来了)` +); +check( + '弹层(控件档滚动区)不做边缘淡出', + /\.overflow-y-auto\.glass-control\s*\{[\s\S]*?mask-image:\s*none/.test(css), + '候选菜单这类弹层还在淡 —— 它高度只有几十像素,首尾项会被洗白' +); +check( + '淡出规则同时带 -webkit- 前缀(Electron 里以哪个为准不由我们定)', + (maskRule.match(/-webkit-mask-image:/g) || []).length === 1 && + (maskRule.match(/[^-]mask-image:/g) || []).length === 1 +); check('根视口使用 100dvh 跟随软键盘', css.includes('@supports (height: 100dvh)')); // 6) 横向内边距在窄屏收窄(px-6 在 375px 屏上白吃 48px) diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 2312973..773745b 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -58,7 +58,7 @@ for (const must of ['配对/解析', 'allow-list', '变异验证', '剥掉注释 const SUITE = [ ['test/markdown-xss.test.mjs', [], 9], - ['test/narrow-layout.test.mjs', [], 54], + ['test/narrow-layout.test.mjs', [], 58], ['test/nav-merge.test.mjs', [], 8], ['test/theme.test.mjs', [], 30], ['test/background.test.mjs', [], 42],