feat(ui): 重做深色悬停与键盘焦点,补齐弹层出场动效
悬停:上一版靠「底色跳两档 + 四边边框一起提亮 + 外圈白光晕 + 白环」制造悬浮感, 五个变化同向叠加,结果是整张卡片均匀发亮、看着像换了张更浅的卡,光晕还把边缘糊住。 改用明暗方向表达厚度:新增受光层伪元素(顶部白 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:
@@ -236,7 +236,7 @@ function onDueChange(value: string | null): void {
|
||||
|
||||
<div
|
||||
v-if="dueOpen"
|
||||
class="absolute top-full left-0 z-30 mt-1.5 w-56 rounded-lg border border-slate-200 bg-white p-2 shadow-pop shadow-pop-dark dark:border-zinc-700 dark:bg-zinc-900"
|
||||
class="absolute top-full left-0 z-30 mt-1.5 w-56 animate-pop-in rounded-lg border border-slate-200 bg-white p-2 shadow-pop shadow-pop-dark motion-reduce:animate-none dark:border-zinc-700 dark:bg-zinc-900"
|
||||
>
|
||||
<DatePicker
|
||||
:model-value="batchDue"
|
||||
|
||||
@@ -102,13 +102,13 @@ onBeforeUnmount(stopListening)
|
||||
只把注意力让给抽屉;模糊负责把背景「推远」,避免和面板内容抢焦点。
|
||||
-->
|
||||
<div
|
||||
class="absolute inset-0 bg-slate-900/25 backdrop-blur-sm dark:bg-black/40"
|
||||
class="absolute inset-0 animate-scrim-in bg-slate-900/25 backdrop-blur-sm motion-reduce:animate-none dark:bg-black/40"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
|
||||
<aside
|
||||
ref="panelRef"
|
||||
class="absolute inset-y-0 right-0 flex w-full max-w-md flex-col bg-white shadow-panel shadow-panel-dark outline-none dark:border-l dark:border-zinc-800 dark:bg-zinc-900"
|
||||
class="absolute inset-y-0 right-0 flex w-full max-w-md animate-sheet-in flex-col bg-white shadow-panel shadow-panel-dark outline-none motion-reduce:animate-none dark:border-l dark:border-zinc-800 dark:bg-zinc-900"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="title"
|
||||
|
||||
@@ -86,7 +86,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div
|
||||
v-if="open"
|
||||
class="absolute top-full left-0 z-20 mt-1.5 w-56 rounded-lg border border-slate-200 bg-white p-1.5 shadow-pop shadow-pop-dark dark:border-zinc-700 dark:bg-zinc-900"
|
||||
class="absolute top-full left-0 z-20 mt-1.5 w-56 animate-pop-in rounded-lg border border-slate-200 bg-white p-1.5 shadow-pop shadow-pop-dark motion-reduce:animate-none dark:border-zinc-700 dark:bg-zinc-900"
|
||||
>
|
||||
<p v-if="tags.length === 0" class="px-2 py-3 text-center text-xs text-slate-400 dark:text-zinc-500">
|
||||
还没有任何标签
|
||||
|
||||
@@ -169,7 +169,7 @@ function onBlur(): void {
|
||||
id="tag-suggestions"
|
||||
role="listbox"
|
||||
aria-label="已有标签"
|
||||
class="absolute top-full left-0 z-20 mt-1 max-h-44 w-full overflow-y-auto rounded-lg border border-slate-200 bg-white p-1 shadow-pop shadow-pop-dark dark:border-zinc-700 dark:bg-zinc-900"
|
||||
class="absolute top-full left-0 z-20 mt-1 max-h-44 w-full animate-pop-in overflow-y-auto rounded-lg border border-slate-200 bg-white p-1 shadow-pop shadow-pop-dark motion-reduce:animate-none dark:border-zinc-700 dark:bg-zinc-900"
|
||||
>
|
||||
<li
|
||||
v-for="(tag, index) in candidates"
|
||||
|
||||
@@ -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">
|
||||
<!--
|
||||
多选态下用一枚纯装饰的勾选框:真正的可交互元素是整张卡片,
|
||||
|
||||
Reference in New Issue
Block a user