fix(web): restore starter card state after click
Staging Backend Quality Gate / validate (push) Successful in 15m14s
Staging Backend Quality Gate / publish (push) Successful in 10m19s

This commit is contained in:
Jesse
2026-08-07 11:32:05 +08:00
parent ae41b3aef5
commit e7482c49c6
3 changed files with 59 additions and 19 deletions
+16
View File
@@ -2384,3 +2384,19 @@
- 相关记录:BUG-134、BUG-137、ERR-097、ERR-100、ERR-101
- 复发自:无
- 修复版本:`6c1dcbe857006ec6ae7463b57b2b7d5947da4851`
## BUG-140 | 首页入口卡片点击后持续显示灰色交互态
- 状态:resolved
- 首次发现:2026-08-07
- 最近更新:2026-08-07
- 影响面:首页 `starter-theme-card``daily-starlanguage-card` 与共享 `product-entrypoint-card` 交互反馈
- 用户现象:用户点击首页主题或产品入口卡片后,卡片背景持续处于灰色/选中样式,没有在松开指针后恢复。
- 触发条件:在触摸设备点击卡片,或在桌面端点击后让指针继续停留在卡片上。
- 根因:卡片把铺满表面的背景反馈绑定到无设备能力边界的 `:hover`;移动浏览器可能在点击后保留模拟 hover,桌面指针停留也会让一次点击看起来像永久选中。`hover`、短暂按压和真实 disabled 三种状态因此在视觉上混在一起。
- 修复:将铺灰背景收敛到 `:active`,只在按压期间显示并于释放后恢复;桌面 `@media (hover: hover)` 只保留轻量边框、光泽和箭头位移,不再改变卡片底色;键盘继续使用现有 `focus-visible` 轮廓,真实 disabled 透明度规则保持不变。
- 验证:`frontend/tests/consultation-entrypoint.test.ts` 新增 sticky-hover 回归;与 `frontend/tests/starter-questions.test.ts` 精确运行 40/40 通过;目标 ESLint 与 `git diff --check` 通过。全量前端尝试中 1457 项通过,16 项因本机缺少 Docker 或 `python` 可执行命令而失败,与本次 CSS 修改无关。
- 防复发:首页可点击卡片的表面底色只能由短暂 `:active` 或真实业务 disabled 状态改变;hover 视觉必须限制在支持 hover 的设备,且回归测试禁止重新为这些卡片的 hover surface 添加背景色。
- 相关记录:无
- 复发自:无
- 修复版本:待提交
+27 -19
View File
@@ -550,9 +550,11 @@ button:disabled { cursor: default; opacity: .45; }
.product-entrypoint-card { position: relative; transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out; }
.product-entrypoint-hitarea { position: absolute; z-index: 2; inset: 0; width: 100%; min-height: 0; padding: 0; border: 0; border-radius: inherit; background: transparent; cursor: pointer; }
.product-entrypoint-card > :not(.product-entrypoint-hitarea) { position: relative; z-index: 1; pointer-events: none; }
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); transform: translateY(-1px); }
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):active) { transform: translateY(0); }
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):active) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); background: var(--color-action-soft); transform: translateY(0); }
.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 2px; }
@media (hover: hover) {
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { border-color: color-mix(in srgb, var(--color-action) 44%, var(--color-border)); transform: translateY(-1px); }
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { opacity: .65; }
.product-entrypoint-hitarea:disabled { cursor: not-allowed; }
.daily-starlanguage-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
@@ -1119,19 +1121,25 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
pointer-events: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) {
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):active) {
border-color: transparent;
background: color-mix(in srgb, var(--color-action-soft) 58%, var(--color-canvas));
transform: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after {
opacity: .65;
transform: translateX(48%);
}
@media (hover: hover) {
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) {
border-color: transparent;
transform: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action {
transform: translateX(3px);
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after {
opacity: .65;
transform: translateX(48%);
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action {
transform: translateX(3px);
}
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) {
@@ -1270,17 +1278,22 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
border-right: 0;
}
.starter-theme-card:not(:disabled):hover,
.starter-theme-card:focus-visible {
.starter-theme-card:not(:disabled):active {
background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas));
}
.starter-theme-card:not(:disabled):hover .starter-arrow,
.starter-theme-card:focus-visible .starter-arrow {
.starter-theme-card:not(:disabled):active .starter-arrow {
color: var(--color-action);
transform: translate(2px, -2px);
}
@media (hover: hover) {
.starter-theme-card:not(:disabled):hover .starter-arrow {
color: var(--color-action);
transform: translate(2px, -2px);
}
}
.starter-content {
min-width: 0;
display: grid;
@@ -1438,11 +1451,6 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
border-bottom: 0;
}
.starter-theme-card:not(:disabled):hover,
.starter-theme-card:focus-visible {
background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas));
}
.starter-content span {
max-width: 440px;
}
@@ -289,6 +289,22 @@ test("homepage entrypoints use two whole-card native actions", () => {
assert.doesNotMatch(source, /className="daily-starlanguage-heading">[\s\S]{0,180}<button/);
});
test("starter cards use transient pressed feedback instead of sticky hover shading", () => {
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const productHoverRules = [...styles.matchAll(
/\.product-entrypoint-card:has\(\.product-entrypoint-hitarea:not\(:disabled\):hover\)\s*\{([^}]*)\}/g,
)];
const starterHoverSurfaceRules = [...styles.matchAll(
/\.starter-theme-card:not\(:disabled\):hover\s*\{([^}]*)\}/g,
)];
assert.ok(productHoverRules.length > 0);
for (const rule of productHoverRules) assert.doesNotMatch(rule[1], /background\s*:/);
assert.equal(starterHoverSurfaceRules.length, 0);
assert.match(styles, /\.product-entrypoint-card:has\(\.product-entrypoint-hitarea:not\(:disabled\):active\)\s*\{[^}]*background\s*:/);
assert.match(styles, /\.starter-theme-card:not\(:disabled\):active\s*\{[^}]*background\s*:/);
});
test("starter homepage stays editorial and hides technical chart parameters", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");