fix(web): align dropdown-menu styles with Figma design (#32922)

This commit is contained in:
yyh 2026-03-04 09:53:15 +08:00 committed by GitHub
parent 6002fd09b4
commit 477bf6e075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 323 additions and 6 deletions

View File

@ -0,0 +1,317 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuCheckboxItemIndicator,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuGroupLabel,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuRadioItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '.'
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
<DropdownMenuTrigger
render={<button type="button" className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover" />}
>
{label}
</DropdownMenuTrigger>
)
const meta = {
title: 'Base/Navigation/DropdownMenu',
component: DropdownMenu,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Compound dropdown menu built on Base UI Menu. Supports items, separators, group labels, submenus, radio groups, checkbox items, destructive items, and disabled states.',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof DropdownMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
export const WithSeparator: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>Cut</DropdownMenuItem>
<DropdownMenuItem>Copy</DropdownMenuItem>
<DropdownMenuItem>Paste</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Select All</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Find and Replace</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
export const WithGroupLabel: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Actions</DropdownMenuGroupLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Export</DropdownMenuGroupLabel>
<DropdownMenuItem>Export as PDF</DropdownMenuItem>
<DropdownMenuItem>Export as CSV</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
),
}
export const WithDestructiveItem: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
export const WithSubmenu: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>New File</DropdownMenuItem>
<DropdownMenuItem>Open</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Slack</DropdownMenuItem>
<DropdownMenuItem>Copy Link</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>Download</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
const WithRadioItemsDemo = () => {
const [value, setValue] = useState('comfortable')
return (
<DropdownMenu>
<TriggerButton label={`Density: ${value}`} />
<DropdownMenuContent>
<DropdownMenuRadioGroup value={value} onValueChange={setValue}>
<DropdownMenuRadioItem value="compact">
Compact
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="comfortable">
Comfortable
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="spacious">
Spacious
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export const WithRadioItems: Story = {
render: () => <WithRadioItemsDemo />,
}
const WithCheckboxItemsDemo = () => {
const [showToolbar, setShowToolbar] = useState(true)
const [showSidebar, setShowSidebar] = useState(false)
const [showStatusBar, setShowStatusBar] = useState(true)
return (
<DropdownMenu>
<TriggerButton label="View Options" />
<DropdownMenuContent>
<DropdownMenuCheckboxItem checked={showToolbar} onCheckedChange={setShowToolbar}>
Toolbar
<DropdownMenuCheckboxItemIndicator />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={showSidebar} onCheckedChange={setShowSidebar}>
Sidebar
<DropdownMenuCheckboxItemIndicator />
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={showStatusBar} onCheckedChange={setShowStatusBar}>
Status Bar
<DropdownMenuCheckboxItemIndicator />
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
export const WithCheckboxItems: Story = {
render: () => <WithCheckboxItemsDemo />,
}
export const WithDisabledItems: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem disabled>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem disabled>Restore</DropdownMenuItem>
<DropdownMenuItem destructive>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
export const WithIcons: Story = {
render: () => (
<DropdownMenu>
<TriggerButton />
<DropdownMenuContent>
<DropdownMenuItem>
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
Edit
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
Archive
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}
const ComplexDemo = () => {
const [sortOrder, setSortOrder] = useState('newest')
const [showArchived, setShowArchived] = useState(false)
return (
<DropdownMenu>
<TriggerButton label="Actions" />
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Edit</DropdownMenuGroupLabel>
<DropdownMenuItem>
<span aria-hidden className="i-ri-pencil-line size-4 shrink-0 text-text-tertiary" />
Rename
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem disabled>
<span aria-hidden className="i-ri-lock-line size-4 shrink-0 text-text-tertiary" />
Move to Workspace
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<span aria-hidden className="i-ri-share-line size-4 shrink-0 text-text-tertiary" />
Share
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>
<span aria-hidden className="i-ri-mail-line size-4 shrink-0 text-text-tertiary" />
Email
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-chat-1-line size-4 shrink-0 text-text-tertiary" />
Slack
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden className="i-ri-link size-4 shrink-0 text-text-tertiary" />
Copy Link
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuGroupLabel>Sort by</DropdownMenuGroupLabel>
<DropdownMenuRadioGroup value={sortOrder} onValueChange={setSortOrder}>
<DropdownMenuRadioItem value="newest">
Newest first
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="oldest">
Oldest first
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name">
Name
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked={showArchived} onCheckedChange={setShowArchived}>
<span aria-hidden className="i-ri-archive-line size-4 shrink-0 text-text-tertiary" />
Show Archived
<DropdownMenuCheckboxItemIndicator />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem destructive>
<span aria-hidden className="i-ri-delete-bin-line size-4 shrink-0" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
export const Complex: Story = {
render: () => <ComplexDemo />,
}

View File

@ -13,8 +13,8 @@ export const DropdownMenuSub = Menu.SubmenuRoot
export const DropdownMenuGroup = Menu.Group
export const DropdownMenuRadioGroup = Menu.RadioGroup
const menuRowBaseClassName = 'mx-1 flex h-8 cursor-pointer select-none items-center rounded-lg px-2 outline-none'
const menuRowStateClassName = 'data-[highlighted]:bg-state-base-hover data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50'
const menuRowBaseClassName = 'mx-1 flex h-8 cursor-pointer select-none items-center gap-1 rounded-lg px-2 outline-none'
const menuRowStateClassName = 'data-[highlighted]:bg-state-base-hover data-[disabled]:cursor-not-allowed data-[disabled]:opacity-30'
export function DropdownMenuRadioItem({
className,
@ -89,7 +89,7 @@ export function DropdownMenuGroupLabel({
return (
<Menu.GroupLabel
className={cn(
'px-3 py-1 text-text-tertiary system-2xs-medium-uppercase',
'px-3 pb-0.5 pt-1 text-text-tertiary system-xs-medium-uppercase',
className,
)}
{...props}
@ -148,7 +148,7 @@ function renderDropdownMenuPopup({
>
<Menu.Popup
className={cn(
'max-h-[var(--available-height)] overflow-y-auto overflow-x-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg py-1 text-sm text-text-secondary shadow-lg',
'max-h-[var(--available-height)] overflow-y-auto overflow-x-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur py-1 text-sm text-text-secondary shadow-lg backdrop-blur-[5px]',
'origin-[var(--transform-origin)] transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
popupClassName,
)}
@ -204,7 +204,7 @@ export function DropdownMenuSubTrigger({
{...props}
>
{children}
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-[14px] shrink-0 text-text-tertiary" />
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-4 shrink-0 text-text-tertiary" />
</Menu.SubmenuTrigger>
)
}
@ -270,7 +270,7 @@ export function DropdownMenuSeparator({
}: React.ComponentPropsWithoutRef<typeof Menu.Separator>) {
return (
<Menu.Separator
className={cn('my-1 h-px bg-divider-regular', className)}
className={cn('my-1 h-px bg-divider-subtle', className)}
{...props}
/>
)