mirror of https://github.com/langgenius/dify.git
refactor(dropdown-menu): improve primitive defaults and deduplicate account-dropdown
- Add overflow handling (max-h-[var(--available-height)]) to Popup - Add disabled styles (cursor-not-allowed, opacity-50) to Item and SubTrigger - Change hover token to bg-state-base-hover for consistency - Build arrow icon into SubTrigger so callers don't repeat it - Style DropdownMenuGroupLabel with default typography - Extract shared MenuItemContent and ExternalLinkIndicator into menu-item-content.tsx - Remove duplicated className constants and component definitions across account-dropdown files - Remove !important overrides from callers now that primitive defaults are correct - Remove manual max-h-[70vh] from SubContent (handled by primitive)
This commit is contained in:
parent
2dfd7f4c65
commit
a32ab27ce0
|
|
@ -11,13 +11,27 @@ export const DropdownMenuPortal = Menu.Portal
|
|||
export const DropdownMenuTrigger = Menu.Trigger
|
||||
export const DropdownMenuSub = Menu.SubmenuRoot
|
||||
export const DropdownMenuGroup = Menu.Group
|
||||
export const DropdownMenuGroupLabel = Menu.GroupLabel
|
||||
export const DropdownMenuRadioGroup = Menu.RadioGroup
|
||||
export const DropdownMenuRadioItem = Menu.RadioItem
|
||||
export const DropdownMenuRadioItemIndicator = Menu.RadioItemIndicator
|
||||
export const DropdownMenuCheckboxItem = Menu.CheckboxItem
|
||||
export const DropdownMenuCheckboxItemIndicator = Menu.CheckboxItemIndicator
|
||||
|
||||
export function DropdownMenuGroupLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Menu.GroupLabel>) {
|
||||
return (
|
||||
<Menu.GroupLabel
|
||||
className={cn(
|
||||
'px-3 py-1 text-text-tertiary system-2xs-medium-uppercase',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type DropdownMenuContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
|
|
@ -69,7 +83,7 @@ function renderDropdownMenuPopup({
|
|||
>
|
||||
<Menu.Popup
|
||||
className={cn(
|
||||
'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 py-1 text-sm text-text-secondary shadow-lg',
|
||||
'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,
|
||||
)}
|
||||
|
|
@ -111,18 +125,23 @@ type DropdownMenuSubTriggerProps = React.ComponentPropsWithoutRef<typeof Menu.Su
|
|||
export function DropdownMenuSubTrigger({
|
||||
className,
|
||||
destructive,
|
||||
children,
|
||||
...props
|
||||
}: DropdownMenuSubTriggerProps) {
|
||||
return (
|
||||
<Menu.SubmenuTrigger
|
||||
className={cn(
|
||||
'mx-1 flex h-8 cursor-pointer select-none items-center rounded-lg px-3 outline-none',
|
||||
'data-[highlighted]:bg-components-panel-on-panel-item-bg-hover',
|
||||
'data-[highlighted]:bg-state-base-hover',
|
||||
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
destructive && 'text-text-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
<span aria-hidden className="i-ri-arrow-right-s-line ml-auto size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Menu.SubmenuTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +191,8 @@ export function DropdownMenuItem({
|
|||
<Menu.Item
|
||||
className={cn(
|
||||
'mx-1 flex h-8 cursor-pointer select-none items-center rounded-lg px-3 outline-none',
|
||||
'data-[highlighted]:bg-components-panel-on-panel-item-bg-hover',
|
||||
'data-[highlighted]:bg-state-base-hover',
|
||||
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
destructive && 'text-text-destructive',
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,7 @@ import SparklesSoft from '../../base/icons/src/public/common/SparklesSoft'
|
|||
import PremiumBadge from '../../base/premium-badge'
|
||||
import Spinner from '../../base/spinner'
|
||||
import Toast from '../../base/toast'
|
||||
|
||||
const submenuTriggerClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
|
||||
const submenuItemClassName = '!mx-0 !h-10 !rounded-lg !py-1 !pl-1 !pr-2 data-[highlighted]:!bg-state-base-hover'
|
||||
const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
|
||||
const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
|
||||
const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
|
||||
import { MenuItemContent } from './menu-item-content'
|
||||
|
||||
enum DocName {
|
||||
SOC2_Type_I = 'SOC2_Type_I',
|
||||
|
|
@ -32,26 +27,6 @@ enum DocName {
|
|||
GDPR = 'GDPR',
|
||||
}
|
||||
|
||||
type ComplianceMenuItemContentProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function ComplianceMenuItemContent({
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: ComplianceMenuItemContentProps) {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
|
||||
<div className={menuLabelClassName}>{label}</div>
|
||||
{trailing}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ComplianceDocActionVisualProps = {
|
||||
isCurrentPlanCanDownload: boolean
|
||||
isPending: boolean
|
||||
|
|
@ -176,7 +151,7 @@ function ComplianceDocRowItem({
|
|||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={cn(submenuItemClassName, 'justify-between')}
|
||||
className="h-10 justify-between py-1 pl-1 pr-2"
|
||||
closeOnClick={!isCurrentPlanCanDownload}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
|
|
@ -199,15 +174,14 @@ export default function Compliance() {
|
|||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={cn(submenuTriggerClassName, 'justify-between')}>
|
||||
<ComplianceMenuItemContent
|
||||
<DropdownMenuSubTrigger>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-verified-badge-line"
|
||||
label={t('userProfile.compliance', { ns: 'common' })}
|
||||
trailing={<span aria-hidden className={cn('i-ri-arrow-right-s-line', menuTrailingIconClassName)} />}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
popupClassName="w-[337px] max-h-[70vh] overflow-y-auto divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
popupClassName="w-[337px] divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<ComplianceDocRowItem
|
||||
|
|
|
|||
|
|
@ -24,33 +24,10 @@ import AccountAbout from '../account-about'
|
|||
import GithubStar from '../github-star'
|
||||
import Indicator from '../indicator'
|
||||
import Compliance from './compliance'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
import Support from './support'
|
||||
|
||||
const menuItemClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
|
||||
const menuStaticRowClassName = 'flex h-8 w-full items-center rounded-lg px-3 text-text-secondary system-md-regular'
|
||||
const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
|
||||
const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
|
||||
const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
|
||||
|
||||
type AccountMenuItemContentProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function AccountMenuItemContent({
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: AccountMenuItemContentProps) {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
|
||||
<div className={menuLabelClassName}>{label}</div>
|
||||
{trailing}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type AccountMenuRouteItemProps = {
|
||||
href: string
|
||||
|
|
@ -67,10 +44,10 @@ function AccountMenuRouteItem({
|
|||
}: AccountMenuRouteItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={cn(menuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
render={<Link href={href} />}
|
||||
>
|
||||
<AccountMenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
|
@ -90,10 +67,10 @@ function AccountMenuExternalItem({
|
|||
}: AccountMenuExternalItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={cn(menuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
render={<a href={href} rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<AccountMenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
|
@ -113,18 +90,14 @@ function AccountMenuActionItem({
|
|||
}: AccountMenuActionItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={cn(menuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AccountMenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ExternalLinkIndicator() {
|
||||
return <span aria-hidden className={cn('i-ri-arrow-right-up-line', menuTrailingIconClassName)} />
|
||||
}
|
||||
|
||||
type AccountMenuSectionProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
|
@ -257,7 +230,7 @@ export default function AppSelector() {
|
|||
)}
|
||||
<AccountMenuSection>
|
||||
<div className={cn(menuStaticRowClassName, 'hover:bg-transparent')}>
|
||||
<AccountMenuItemContent
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-t-shirt-2-line"
|
||||
label={t('theme.theme', { ns: 'common' })}
|
||||
trailing={<ThemeSwitcher />}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const menuLabelClassName = 'min-w-0 grow truncate px-1 text-text-secondary system-md-regular'
|
||||
const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
|
||||
|
||||
export const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
|
||||
|
||||
type MenuItemContentProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
export function MenuItemContent({
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: MenuItemContentProps) {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
|
||||
<div className={menuLabelClassName}>{label}</div>
|
||||
{trailing}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExternalLinkIndicator() {
|
||||
return <span aria-hidden className={cn('i-ri-arrow-right-up-line', menuTrailingIconClassName)} />
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
|
||||
import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils'
|
||||
|
|
@ -6,43 +5,13 @@ import { Plan } from '@/app/components/billing/type'
|
|||
import { ZENDESK_WIDGET_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { mailToSupport } from '../utils/util'
|
||||
|
||||
const submenuTriggerClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
|
||||
const submenuItemClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
|
||||
const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
|
||||
const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
|
||||
const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
|
||||
type SupportProps = {
|
||||
closeAccountDropdown: () => void
|
||||
}
|
||||
|
||||
type SupportMenuItemContentProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function SupportMenuItemContent({
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: SupportMenuItemContentProps) {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
|
||||
<div className={menuLabelClassName}>{label}</div>
|
||||
{trailing}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SupportExternalLinkIndicator() {
|
||||
return <span aria-hidden className={cn('i-ri-arrow-right-up-line', menuTrailingIconClassName)} />
|
||||
}
|
||||
|
||||
// Submenu-only: this component must be rendered within an existing DropdownMenu root.
|
||||
export default function Support({ closeAccountDropdown }: SupportProps) {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -53,26 +22,25 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
|
|||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={cn(submenuTriggerClassName, 'justify-between')}>
|
||||
<SupportMenuItemContent
|
||||
<DropdownMenuSubTrigger>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-question-line"
|
||||
label={t('userProfile.support', { ns: 'common' })}
|
||||
trailing={<span aria-hidden className={cn('i-ri-arrow-right-s-line', menuTrailingIconClassName)} />}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
popupClassName="w-[216px] max-h-[70vh] overflow-y-auto divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
popupClassName="w-[216px] divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
{hasDedicatedChannel && hasZendeskWidget && (
|
||||
<DropdownMenuItem
|
||||
className={cn(submenuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
onClick={() => {
|
||||
toggleZendeskWindow(true)
|
||||
closeAccountDropdown()
|
||||
}}
|
||||
>
|
||||
<SupportMenuItemContent
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-chat-smile-2-line"
|
||||
label={t('userProfile.contactUs', { ns: 'common' })}
|
||||
/>
|
||||
|
|
@ -80,34 +48,34 @@ export default function Support({ closeAccountDropdown }: SupportProps) {
|
|||
)}
|
||||
{hasDedicatedChannel && !hasZendeskWidget && (
|
||||
<DropdownMenuItem
|
||||
className={cn(submenuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
render={<a href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version)} rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<SupportMenuItemContent
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-mail-send-line"
|
||||
label={t('userProfile.emailSupport', { ns: 'common' })}
|
||||
trailing={<SupportExternalLinkIndicator />}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className={cn(submenuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
render={<a href="https://forum.dify.ai/" rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<SupportMenuItemContent
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-discuss-line"
|
||||
label={t('userProfile.forum', { ns: 'common' })}
|
||||
trailing={<SupportExternalLinkIndicator />}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn(submenuItemClassName, 'justify-between')}
|
||||
className="justify-between"
|
||||
render={<a href="https://discord.gg/5AEfbxcd9k" rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<SupportMenuItemContent
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-discord-line"
|
||||
label={t('userProfile.community', { ns: 'common' })}
|
||||
trailing={<SupportExternalLinkIndicator />}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
|
|
|||
Loading…
Reference in New Issue