style: fix user menu alignment and spacing, and codify UI standards in skill

This commit is contained in:
Haitao Pan 2026-02-02 13:06:09 +08:00
parent d4c5dbfc57
commit 0ec2588735
3 changed files with 87 additions and 27 deletions

View File

@ -0,0 +1,40 @@
---
name: UI Design Standards
description: Rules and guidelines for consistent UI components including dropdowns, popovers, and list items.
---
# UI Design Standards
This skill provides guidelines for building consistent, accessible, and high-quality UI components within the codebase.
## 1. Popovers and Dropdowns (弹窗与下拉规范)
### Alignment (定位逻辑)
- **Right Edge Alignment**: For triggers located on the right side of the screen or navigation bar, the associated dropdown/popover MUST be right-aligned with the trigger.
- In Radix UI / shadcn: Use `align="end"`.
- In CSS/Tailwind: Use `absolute right-0`.
- **Vertical Spacing**: Maintain a consistent vertical offset from the trigger, typically `8px` (`mt-2` or `sideOffset={8}`).
### Layering and Visual Hierarchy (层级管理)
- **Z-Index**: Always explicitly declare a `z-index` of `50` or higher for floating UI elements to ensure they appear above all other content.
- **Background Integrity**: Drodown containers should use opaque background colors (e.g., `bg-white` or theme-defined `bg-surface`) to prevent "see-through" visual noise from underlying content. Avoid excessive transparency/blur if it compromises readability.
- **Shadows**: Use distinct shadows (e.g., `shadow-md` or `shadow-xl`) to provide depth.
### Clipping Prevention (溢出处理)
- Use Portals (e.g., `DropdownMenu.Portal`) to render floating content into the `document.body`. This prevents the menu from being clipped by parents with `overflow: hidden`.
## 2. List Items and Components (列表项规范)
### Layout and Alignment (对齐规范)
- **Flexbox**: Use `flex items-center` for all list items that include both an icon and a text label.
- **Icon Spacing**: Maintain a standard horizontal gap between icons and labels (recommended: `gap-3` or `12px`).
### Component Integrity (防压缩与自适应)
- **Icon Shrinking**: Icons within flex containers MUST have `flex-shrink: 0` (Tailwind: `shrink-0`) to prevent them from distorting when the container is narrow.
- **Minimum Width**: Containers displaying variable-length text (like user emails) should have a reasonable `min-width` (e.g., `min-w-[200px]`) to ensure comfortable display.
## 3. Interaction and Accessibility
- **Keyboard Support**: Ensure dropdowns support `Esc` to close and allow keyboard navigation (Tab/Arrows).
- **Outside Clicks**: Implement "click outside to close" logic for all popovers.
- **Motion**: Use subtle entry/exit animations (e.g., 120ms fade and scale). Respect `prefers-reduced-motion`.

12
.cursorrules Normal file
View File

@ -0,0 +1,12 @@
# UI Development Rules
## 1. 弹窗与下拉规范 (Popovers & Dropdowns)
- **定位逻辑**:除非特殊说明,右上角的触发器对应的弹出层必须强制 `right-0` (或 `align="end"`) 对齐。
- **层级管理**:所有 Floating UI 必须显式声明 `z-[50+]` 以防被 Content Card 遮挡或透视。
- **背景显示**:下拉容器背景应使用不透明色(如 `bg-white` 或主题定义的 `bg-surface`),避免透明度导致的视觉干扰。
## 2. 列表项规范 (List Items)
- **对齐**:所有带有图标的列表项必须使用 `flex items-center`。
- **防止重叠**:图标与文字之间应保持足够的间距(推荐 `gap-3` 或 `12px`)。
- **防压缩**:图标必须带有 `flex-shrink: 0` (或 Tailwind 的 `shrink-0`),防止在容器宽度不足时图标变形。
- **最小宽度**:涉及用户信息的容器应设置合理的 `min-width` (如 `min-w-[200px]`) 以保证长文字内容正常显示。

View File

@ -275,7 +275,7 @@ export default function UnifiedNavigation() {
<div className="hidden flex-1 items-center justify-end gap-3 lg:flex"> <div className="hidden flex-1 items-center justify-end gap-3 lg:flex">
{user ? ( {user ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-3 relative">
<DropdownMenu.Root open={accountMenuOpen} onOpenChange={setAccountMenuOpen}> <DropdownMenu.Root open={accountMenuOpen} onOpenChange={setAccountMenuOpen}>
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button <button
@ -291,7 +291,7 @@ export default function UnifiedNavigation() {
<DropdownMenu.Content <DropdownMenu.Content
align="end" align="end"
sideOffset={8} sideOffset={8}
className="z-50 min-w-[220px] overflow-hidden rounded-[12px] border border-surface-border bg-surface/95 p-1 shadow-shadow-md backdrop-blur-sm animate-in fade-in zoom-in-95 duration-[120ms] data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95 motion-reduce:animate-none" className="z-50 min-w-[220px] overflow-hidden rounded-[12px] border border-surface-border bg-surface p-1 shadow-shadow-md animate-in fade-in zoom-in-95 duration-[120ms] data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95 motion-reduce:animate-none"
> >
<div className="px-4 py-3 border-b border-surface-border/50 mb-1"> <div className="px-4 py-3 border-b border-surface-border/50 mb-1">
<p className="text-sm font-semibold text-text leading-none mb-1.5"> <p className="text-sm font-semibold text-text leading-none mb-1.5">
@ -311,14 +311,14 @@ export default function UnifiedNavigation() {
> >
<Link <Link
href={item.href} href={item.href}
className={`flex h-[38px] items-center gap-2.5 px-3 rounded-lg text-[13px] font-medium transition-all group select-none ${item.key === 'logout' className={`flex h-[38px] items-center gap-3 px-3 rounded-lg text-[13px] font-medium transition-all group select-none ${item.key === 'logout'
? "text-rose-500 hover:bg-rose-500/10 hover:text-rose-600 focus:bg-rose-500/10 focus:text-rose-600" ? "text-rose-500 hover:bg-rose-500/10 hover:text-rose-600 focus:bg-rose-500/10 focus:text-rose-600"
: "text-text-muted hover:bg-primary/10 hover:text-primary focus:bg-primary/10 focus:text-primary" : "text-text-muted hover:bg-primary/10 hover:text-primary focus:bg-primary/10 focus:text-primary"
}`} }`}
onClick={() => setAccountMenuOpen(false)} onClick={() => setAccountMenuOpen(false)}
> >
{item.icon && ( {item.icon && (
<item.icon className={`w-4 h-4 opacity-70 group-hover:opacity-100 transition-opacity ${item.key === 'logout' ? 'text-rose-500' : 'text-current'}`} /> <item.icon className={`w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity ${item.key === 'logout' ? 'text-rose-500' : 'text-current'}`} />
)} )}
<span> <span>
{typeof item.label === "function" {typeof item.label === "function"
@ -332,33 +332,41 @@ export default function UnifiedNavigation() {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu.Root> </DropdownMenu.Root>
<LanguageToggle />
<ReleaseChannelSelector
selected={selectedChannels}
onToggle={toggleChannel}
variant="icon"
/>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-3 text-sm font-medium text-text-muted"> <>
<Link <div className="flex items-center gap-3 text-sm font-medium text-text-muted">
href="/login" <Link
className="text-sm opacity-80 transition hover:text-primary hover:opacity-100" href="/login"
> className="text-sm opacity-80 transition hover:text-primary hover:opacity-100"
{nav.account.login} >
</Link> {nav.account.login}
<span </Link>
className="h-3 w-px bg-surface-border" <span
aria-hidden="true" className="h-3 w-px bg-surface-border"
aria-hidden="true"
/>
<Link
href="/register"
className="rounded-md border border-surface-border px-3 py-1 text-primary transition hover:border-primary/40 hover:bg-surface-muted"
>
{nav.account.register}
</Link>
</div>
<LanguageToggle />
<ReleaseChannelSelector
selected={selectedChannels}
onToggle={toggleChannel}
variant="icon"
/> />
<Link </>
href="/register"
className="rounded-md border border-surface-border px-3 py-1 text-primary transition hover:border-primary/40 hover:bg-surface-muted"
>
{nav.account.register}
</Link>
</div>
)} )}
<LanguageToggle />
<ReleaseChannelSelector
selected={selectedChannels}
onToggle={toggleChannel}
variant="icon"
/>
</div> </div>
</div> </div>
</div> </div>