fix(admin): FormSelect 面板水平居中于窄触发器,修复双倍位移

紧凑触发器(如「手动」73px)下面板仅左对齐,宽出部分全堆在右侧,
视觉失衡。面板改为 left-1/2 + translateX(-50%) 以触发器中心居中,
宽出部分两侧对称探出。

两个 CSS 陷阱及其解法:
- select-enter 关键帧的 transform 会覆盖静态 transform,fill 结束时
  位置跳变 → translateX(-50%) 烘进关键帧首尾(同 popover-enter 先例)
- Tailwind v4 的 -translate-x-1/2 utility 走独立 translate 属性,与
  关键帧的 transform 属性叠加成双倍位移 → 面板改用任意属性类
  [transform:translateX(-50%)]

浏览器实测:紧凑面板中心偏差 -0.13px、胶囊面板 0.00px(动画播完后测量)。
This commit is contained in:
xfy 2026-07-28 14:53:59 +08:00
parent 60360f6b49
commit 1d1b32cdd2
2 changed files with 12 additions and 4 deletions

View File

@ -83,15 +83,19 @@
animation: popover-enter 150ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* 下拉选择面板入场(仅垂直微移,无水平居中变换;展开方向由 transform-origin 控制) */
/* 下拉选择面板入场面板的水平居中用 [transform:translateX(-50%)]不用
-translate-x-1/2 utilityTailwind v4 translate utility 走独立 translate
属性会与此处 transform 关键帧叠加成双倍位移关键帧 fill 值须与面板
静态 transform 完全一致否则动画结束时水平位置跳变垂直仅微移展开
方向由 transform-origin 控制 */
@keyframes select-enter {
0% {
opacity: 0;
transform: translateY(-4px) scale(0.98);
transform: translateX(-50%) translateY(-4px) scale(0.98);
}
100% {
opacity: 1;
transform: none;
transform: translateX(-50%);
}
}

View File

@ -110,10 +110,14 @@ pub fn FormSelect<T: Clone + PartialEq + 'static>(
) -> Element {
// 面板与 POPOVER_PANEL_CLASS 同源(卡片化圆角 + 阴影)。宽度取 max(触发器,
// 最长选项):紧凑触发器(如“手动”)下面板仍能完整展示长选项;上限防出屏。
// 水平以触发器中心居中:面板宽于触发器时两侧对称探出,窄触发器不失衡。
// 居中用 [transform:translateX(-50%)] 而非 -translate-x-1/2 utilityTailwind
// v4 的 translate utility 走独立 translate 属性,会与 select-enter 关键帧的
// transform 叠加造成双倍位移;关键帧的 fill 值与此处 transform 完全一致。
// 定义在函数体内:模块级私有常量若仅被 wasm 门控调用点引用,会在 server
// 构建下触发 dead_code。
const TRIGGER_CLASS: &str = "w-full block cursor-pointer truncate select-none text-left pl-4 pr-10 py-2 border border-paper-border rounded-2xl bg-paper-entry text-paper-primary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors duration-200";
const PANEL_CLASS: &str = "absolute left-0 z-50 w-max min-w-full max-w-[calc(100vw_-_2rem)] max-h-60 overflow-y-auto rounded-2xl border border-[var(--color-paper-border)] bg-[var(--color-paper-entry)] p-1.5 shadow-lg animate-select-enter";
const PANEL_CLASS: &str = "absolute left-1/2 z-50 w-max min-w-full max-w-[calc(100vw_-_2rem)] [transform:translateX(-50%)] max-h-60 overflow-y-auto rounded-2xl border border-[var(--color-paper-border)] bg-[var(--color-paper-entry)] p-1.5 shadow-lg animate-select-enter";
let trigger_cls = trigger_class.unwrap_or(TRIGGER_CLASS);