feat(ui): 重做深色悬停与键盘焦点,补齐弹层出场动效
CI / verify (push) Successful in 1m42s
CI / 部署到服务器 (push) Successful in 29s

悬停:上一版靠「底色跳两档 + 四边边框一起提亮 + 外圈白光晕 + 白环」制造悬浮感,
五个变化同向叠加,结果是整张卡片均匀发亮、看着像换了张更浅的卡,光晕还把边缘糊住。
改用明暗方向表达厚度:新增受光层伪元素(顶部白 7% → 底部黑 14%,悬停淡入),
配一道锐利的接触阴影压实边界、一层远处柔化投影作高度暗示;底色与边框各只提一档。
过渡 150ms → 200ms ease-out。新增按下态:位移归零、阴影收紧、受光减弱,卡片「落回纸面」。

焦点:全局描边在深色下掺透明度柔化(sky-400 78%);卡片被 :focus-visible 时与悬停
一致地抬起并受光,焦点环退为最后一道提示,不再是一圈生硬方框。顺带删掉
:focus-visible 里的 border-radius: 4px —— 对带 rounded-* 的元素无效(layer 优先级),
对没圆角的元素反而会让聚焦瞬间跳一下。

动效:新增 pop-in(160ms) / sheet-in(260ms) / scrim-in(200ms) 三组关键帧,
下拉从锚点上方落入、抽屉自右侧滑入、遮罩淡入,组件上一律配 motion-reduce:animate-none。
只做入场不做退场:关闭是用户主动取消,瞬时消失更利落;做退场要引入 Vue Transition,
会牵扯 SideDrawer 的焦点与卸载时序,收益不抵风险。

实现要点:
- Tailwind v4 会把 @theme 的阴影值内联进 --tw-shadow,覆盖 CSS 变量对已生成的工具类
  无效,深色阴影因此直接重写 box-shadow,且规则不放进 @layer 以压过原子类;
  选择器要按 hover: 变体转义后的类名书写(.hover\:shadow-card-hover),否则静默失效
- 焦点态位移不能只改 --tw-translate-y:translate 属性声明只存在于 :hover 那条原子类
  规则里,焦点态不经过 :hover,必须直接写 translate
- 受光层要画在内容之下,因此 TaskCard 的内容容器补了 relative(同为定位元素时按 DOM
  顺序绘制,伪元素在前)

验证:vue-tsc -b 与 eslint src 干净,738 例全量单测通过;Playwright 读计算样式确认
动画时序(下拉 @30ms opacity .909、抽屉 translateX 214.77px、遮罩 opacity .419),
并用垂直亮度剖面确认卡片上亮下暗(内部亮度 33 → 20)。
This commit is contained in:
chenshoulin
2026-10-24 00:11:51 +08:00
parent 69a868a7b1
commit 9ee1261b90
6 changed files with 156 additions and 28 deletions
+3 -3
View File
@@ -98,7 +98,7 @@ const cardClass = computed(() => {
}
return isDone.value
? 'border-slate-200 bg-white opacity-60 dark:border-zinc-800 dark:bg-zinc-900'
: 'border-slate-200 bg-white hover:-translate-y-px hover:border-slate-300 hover:shadow-card-hover dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
: 'border-slate-200 bg-white hover:-translate-y-px hover:border-slate-300 hover:shadow-card-hover dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/25'
})
/**
@@ -130,7 +130,7 @@ const priorityTextClass = computed(() => {
<template>
<article
class="relative shrink-0 cursor-pointer overflow-hidden rounded-lg border shadow-card transition duration-150"
class="relative shrink-0 cursor-pointer overflow-hidden rounded-lg border shadow-card transition duration-200 ease-out"
:class="cardClass"
:data-task-id="task.id"
:role="selectable ? 'checkbox' : undefined"
@@ -143,7 +143,7 @@ const priorityTextClass = computed(() => {
>
<span class="absolute inset-y-0 left-0 w-[3px]" :class="priorityBarClass" aria-hidden="true" />
<div class="py-2 pr-2.5 pl-3.5">
<div class="relative py-2 pr-2.5 pl-3.5">
<div class="flex items-start gap-2">
<!--
多选态下用一枚纯装饰的勾选框:真正的可交互元素是整张卡片,