Timeline 时间轴圆点对齐时间戳而非正文:翻转渲染顺序而非像素推移
给后台管理系统做「近期动态」feed 时,Timeline 组件评审反馈说"padding 有点问题"——仔细看截图才发现:彩色圆点对齐的是灰色时间戳那一行,而不是有意义的事件文本那一行。第一直觉是加个 mt-7 把圆点往下推。加完一看,圆点确实下移了,但连接线在圆点上方出现了一截断口——整条时间轴看上去每一项之间都被割开了。
现象
每个 Timeline 条目渲染两行:主内容(事件描述)和元信息(时间戳/作者)。组件结构是这样:
// 原来的渲染顺序:meta 在上,content 在下
{label && <div className="text-xs text-muted-foreground mb-0.5">{label}</div>}
{children && <div className="text-sm">{children}</div>}节点列用 flex-col 布局:圆点在顶部,连接线(border-l flex-1)紧跟其下。结果圆点天然对齐到条目的第一行——也就是 label(时间戳)。
同时还有两个附带问题:第一个条目紧贴卡片头部没有呼吸感,时间戳和正文之间 2px 间距(mb-0.5)太小,反而条目间 24px 太大,整体节奏割裂。
根因
节点列圆点是顶部锚点(mt-0.5 + 下方连接线),它对齐的是该条目 flex 容器的第一个子元素。只要渲染顺序是 meta(时间戳)先、content(正文)后,圆点就指向 meta——无论 meta 字号多小、颜色多淡。
"圆点对齐正文" 和 "时间戳在正文之上" 在顶部锚点架构下是互斥的,不能同时成立。
有人会想用 margin-top 把圆点往下推到正文位置。这个方向有两个致命问题:
- 字体尺寸敏感:
mt-7(28px)是 label 行高 + gap 的硬编码魔法数字。一旦 label 换行或 font-size 变化,这个数字立刻失效。 - 连接线断裂:连接线锚定在圆点下方。把圆点往下推,圆点上方就出现空白,时间轴连接线视觉上在每一项都断开。
正解
把 children(主内容)放在前,label(元信息)放在后。圆点架构和连接线完全不动,零魔法数字,对任意 label 长度都健壮。
完整的五处改动:
// 1. 翻转顺序:content 先,meta 后
{children && <div className="text-sm">{children}</div>}
{label && <div className="mt-1 text-xs text-muted-foreground">{label}</div>}
// 2. 圆点微调:text-sm 行高 20px,圆点 12px,mt-1(4px) 使其垂直居中
// <dot className="mt-1 ..."> ← 原来 mt-0.5
// 3. 条目间距节奏:条目内(content→meta) mt-1(4px),条目间 pb-5(20px)
// 原来 mb-0.5 intra / pb-6 inter,Gestalt 感知失调
// 4. 列表容器加 py-1,首尾条目不再紧贴 CardBody header
// 5. 更新 label prop 注释
/** 渲染在主内容下方的淡化元信息;圆点对齐主内容第一行 */这是组件级修复,不是每页 CSS 补丁——所有使用这个组件的地方(仪表盘、项目详情、侧边抽屉)一次全部受益。
验证
不要靠眼睛估,用 Playwright 量盒模型:
// 圆点中心 y 应约等于正文行中心 y
const dot = await page.$('.timeline-dot');
const content = await page.$('.timeline-content');
const dotRect = await dot.boundingBox();
const contentRect = await content.boundingBox();
const dotCenterY = dotRect.y + dotRect.height / 2;
const contentCenterY = contentRect.y + contentRect.height / 2;
console.assert(Math.abs(dotCenterY - contentCenterY) < 4, '圆点未对齐正文');同时确认:intra-item gap < inter-item gap(Gestalt:时间戳属于上方正文,不是下一条的前缀)。
一句话外卖
顶部锚点时间轴的圆点对齐问题,根因是渲染顺序,解法是翻转顺序——而不是用魔法 margin 和布局结构对抗。