feat(web): refine main nav onboarding UI

- Add a reusable dimm Badge variant for workspace plan labels

- Update MainNav workspace, web apps, account, and help menu styling to match Figma

- Add MainNav-specific account dropdown with appearance, language, timezone, and logout entries

- Keep account trigger compact without plan badge while preserving the badge in the popup header

- Prevent the common layout shell from creating a page-level scrollbar
This commit is contained in:
Jingyi-Dify 2026-05-04 15:00:33 -07:00
parent 0fa072f9fd
commit 6a3efe2e6e
7 changed files with 425 additions and 162 deletions

View File

@ -33,6 +33,13 @@ describe('Badge', () => {
expect(badge).toHaveClass('relative', 'inline-flex', 'h-5', 'items-center')
})
it('should render xs dimm badge variant', () => {
const { container } = render(<Badge text="beta" size="xs" variant="dimm" />)
const badge = container.firstChild as HTMLElement
expect(badge).toHaveClass('min-w-4', 'px-1', 'py-0.5', 'bg-components-badge-bg-dimm')
expect(badge).not.toHaveClass('h-5')
})
it('should apply uppercase class by default', () => {
const { container } = render(<Badge text="test" />)
const badge = container.firstChild as HTMLElement

View File

@ -6,6 +6,8 @@ type BadgeProps = {
className?: string
text?: ReactNode
children?: ReactNode
size?: 'm' | 'xs'
variant?: 'default' | 'dimm'
uppercase?: boolean
hasRedCornerMark?: boolean
}
@ -14,13 +16,17 @@ const Badge = ({
className,
text,
children,
size = 'm',
variant = 'default',
uppercase = true,
hasRedCornerMark,
}: BadgeProps) => {
return (
<div
className={cn(
'relative inline-flex h-5 items-center rounded-[5px] border border-divider-deep px-[5px] leading-3 whitespace-nowrap text-text-tertiary',
'relative inline-flex items-center rounded-[5px] border border-divider-deep leading-3 whitespace-nowrap text-text-tertiary',
size === 'xs' ? 'min-w-4 justify-center px-1 py-0.5' : 'h-5 px-[5px]',
variant === 'dimm' && 'bg-components-badge-bg-dimm',
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
className,
)}

View File

@ -1,10 +1,28 @@
'use client'
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
import type { Theme } from '@/app/components/base/theme-selector'
import type { Locale } from '@/i18n-config'
import { Avatar } from '@langgenius/dify-ui/avatar'
import { cn } from '@langgenius/dify-ui/cn'
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLinkItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuRadioItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTheme } from 'next-themes'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
@ -13,14 +31,18 @@ import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
import { useDocLink, useLocale } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { env } from '@/env'
import { setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import { updateUserProfile } from '@/service/common'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useLogout } from '@/service/use-common'
import { timezones } from '@/utils/timezone'
import AccountAbout from '../account-about'
import GithubStar from '../github-star'
import Indicator from '../indicator'
@ -112,10 +134,169 @@ type AccountDropdownProps = {
isOpen: boolean
ariaLabel: string
}) => ReactElement
mainNavBadge?: ReactNode
variant?: 'default' | 'mainNav'
}
const mainNavMenuPopupClassName = 'w-60 max-w-80 overflow-hidden bg-components-panel-bg-blur! p-0! backdrop-blur-[5px]'
const mainNavMenuGroupClassName = 'p-1'
const mainNavMenuItemClassName = 'mx-0 h-8 gap-1 px-3 py-1'
const mainNavMenuSubPopupClassName = 'w-60 max-h-[360px] bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]'
function MainNavRadioItemContent({
iconClassName,
label,
}: {
iconClassName?: string
label: ReactNode
}) {
return (
<>
{iconClassName && <span aria-hidden className={cn('size-4 shrink-0 text-text-tertiary', iconClassName)} />}
<span className="min-w-0 grow truncate px-1 system-md-regular text-text-secondary">{label}</span>
<DropdownMenuRadioItemIndicator />
</>
)
}
function AppearanceSubmenu() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
<MenuItemContent
iconClassName="i-ri-sun-line"
label={t('account.appearanceLabel', { ns: 'common' })}
/>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
placement="right-start"
sideOffset={6}
popupClassName={mainNavMenuSubPopupClassName}
>
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={value => setTheme(value as Theme)}>
<DropdownMenuRadioItem value="light" closeOnClick className={mainNavMenuItemClassName}>
<MainNavRadioItemContent iconClassName="i-ri-sun-line" label={t('account.appearanceLight', { ns: 'common' })} />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick className={mainNavMenuItemClassName}>
<MainNavRadioItemContent iconClassName="i-ri-moon-line" label={t('account.appearanceDark', { ns: 'common' })} />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick className={mainNavMenuItemClassName}>
<MainNavRadioItemContent iconClassName="i-ri-computer-line" label={t('account.appearanceSystem', { ns: 'common' })} />
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
function LanguageSubmenu() {
const locale = useLocale()
const router = useRouter()
const { t } = useTranslation()
const { userProfile, mutateUserProfile } = useAppContext()
const [editing, setEditing] = useState(false)
const languageOptions = languages.filter(item => item.supported)
const selectedLanguage = locale || userProfile.interface_language
const selectedTimezone = userProfile.timezone
const handleSelectLanguage = async (value: string) => {
setEditing(true)
try {
await updateUserProfile({ url: '/account/interface-language', body: { interface_language: value } })
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
await setLocaleOnClient(value as Locale, false)
router.refresh()
}
catch (error) {
toast.error((error as Error).message)
}
finally {
setEditing(false)
}
}
const handleSelectTimezone = async (value: string) => {
setEditing(true)
try {
await updateUserProfile({ url: '/account/timezone', body: { timezone: value } })
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
mutateUserProfile()
}
catch (error) {
toast.error((error as Error).message)
}
finally {
setEditing(false)
}
}
return (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
<MenuItemContent
iconClassName="i-ri-translate-2"
label={t('language.language', { ns: 'common' })}
/>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
placement="right-start"
sideOffset={6}
popupClassName={mainNavMenuSubPopupClassName}
>
<DropdownMenuRadioGroup
value={selectedLanguage}
onValueChange={(value) => {
if (!editing)
void handleSelectLanguage(value)
}}
>
{languageOptions.map(item => (
<DropdownMenuRadioItem key={item.value} value={item.value} closeOnClick className={mainNavMenuItemClassName}>
<MainNavRadioItemContent label={item.name} />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
<MenuItemContent
iconClassName="i-ri-global-line"
label={t('language.timezone', { ns: 'common' })}
/>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
placement="right-start"
sideOffset={6}
popupClassName={mainNavMenuSubPopupClassName}
>
<DropdownMenuRadioGroup
value={selectedTimezone}
onValueChange={(value) => {
if (!editing)
void handleSelectTimezone(value)
}}
>
{timezones.map(item => (
<DropdownMenuRadioItem key={item.value} value={String(item.value)} closeOnClick className={mainNavMenuItemClassName}>
<MainNavRadioItemContent label={item.name} />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
</>
)
}
export default function AppSelector({
mainNavBadge,
trigger,
variant = 'default',
}: AccountDropdownProps = {}) {
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
@ -164,113 +345,164 @@ export default function AppSelector({
</DropdownMenuTrigger>
)}
<DropdownMenuContent
placement={variant === 'mainNav' ? 'top-start' : 'bottom-end'}
sideOffset={6}
popupClassName="w-60 max-w-80 bg-components-panel-bg-blur! py-0! backdrop-blur-xs"
alignOffset={variant === 'mainNav' ? 4 : 0}
popupClassName={variant === 'mainNav' ? mainNavMenuPopupClassName : 'w-60 max-w-80 bg-components-panel-bg-blur! py-0! backdrop-blur-xs'}
>
<DropdownMenuGroup className="py-1">
<div className="mx-1 flex flex-nowrap items-center py-2 pr-2 pl-3">
<div className="grow">
<div className="system-md-medium break-all text-text-primary">
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
<span aria-hidden className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}
</div>
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
</div>
<AccountMenuRouteItem
href="/account"
iconClassName="i-ri-account-circle-line"
label={t('account.account', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
<AccountMenuActionItem
iconClassName="i-ri-settings-3-line"
label={t('userProfile.settings', { ns: 'common' })}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
{!systemFeatures.branding.enabled && (
<>
<AccountMenuSection>
<AccountMenuExternalItem
href={docLink('/use-dify/getting-started/introduction')}
iconClassName="i-ri-book-open-line"
label={t('userProfile.helpCenter', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
<AccountMenuSection>
<AccountMenuExternalItem
href="https://roadmap.dify.ai"
iconClassName="i-ri-map-2-line"
label={t('userProfile.roadmap', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
<AccountMenuExternalItem
href="https://github.com/langgenius/dify"
iconClassName="i-ri-github-line"
label={t('userProfile.github', { ns: 'common' })}
trailing={(
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
</div>
)}
/>
{
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
<AccountMenuActionItem
iconClassName="i-ri-information-2-line"
label={t('userProfile.about', { ns: 'common' })}
onClick={() => {
setAboutVisible(true)
setIsAccountMenuOpen(false)
}}
trailing={(
<div className="flex shrink-0 items-center">
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
{variant === 'mainNav'
? (
<>
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
<div className="flex items-center gap-1 rounded-xl bg-gradient-to-b from-background-section-burn to-background-section p-3">
<div className="flex min-w-0 grow flex-col gap-1">
<div className="flex min-w-0 items-center gap-1">
<div className="truncate body-md-medium text-text-primary">{userProfile.name}</div>
{mainNavBadge}
</div>
)}
<div className="truncate system-xs-regular text-text-tertiary">{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
</div>
</DropdownMenuGroup>
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
<DropdownMenuLinkItem
className={cn('justify-between', mainNavMenuItemClassName)}
render={<Link href="/account" />}
>
<MenuItemContent
iconClassName="i-ri-account-circle-line"
label={t('account.account', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
</DropdownMenuLinkItem>
<AppearanceSubmenu />
<LanguageSubmenu />
</DropdownMenuGroup>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
<DropdownMenuItem
className={mainNavMenuItemClassName}
onClick={() => {
void handleLogout()
}}
>
<MenuItemContent
iconClassName="i-ri-logout-box-r-line"
label={t('userProfile.logout', { ns: 'common' })}
/>
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)
: (
<>
<DropdownMenuGroup className="py-1">
<div className="mx-1 flex flex-nowrap items-center py-2 pr-2 pl-3">
<div className="grow">
<div className="system-md-medium break-all text-text-primary">
{userProfile.name}
{isEducationAccount && (
<PremiumBadge size="s" color="blue" className="ml-1 px-2!">
<span aria-hidden className="mr-1 i-ri-graduation-cap-fill h-3 w-3" />
<span className="system-2xs-medium">EDU</span>
</PremiumBadge>
)}
</div>
<div className="system-xs-regular break-all text-text-tertiary">{userProfile.email}</div>
</div>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" />
</div>
<AccountMenuRouteItem
href="/account"
iconClassName="i-ri-account-circle-line"
label={t('account.account', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
)
}
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
</>
)}
<AccountMenuSection>
<DropdownMenuItem
closeOnClick={false}
className="cursor-default data-highlighted:bg-transparent"
>
<MenuItemContent
iconClassName="i-ri-t-shirt-2-line"
label={t('theme.theme', { ns: 'common' })}
trailing={<ThemeSwitcher />}
/>
</DropdownMenuItem>
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
<AccountMenuSection>
<AccountMenuActionItem
iconClassName="i-ri-logout-box-r-line"
label={t('userProfile.logout', { ns: 'common' })}
onClick={() => {
void handleLogout()
}}
/>
</AccountMenuSection>
<AccountMenuActionItem
iconClassName="i-ri-settings-3-line"
label={t('userProfile.settings', { ns: 'common' })}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
{!systemFeatures.branding.enabled && (
<>
<AccountMenuSection>
<AccountMenuExternalItem
href={docLink('/use-dify/getting-started/introduction')}
iconClassName="i-ri-book-open-line"
label={t('userProfile.helpCenter', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
<AccountMenuSection>
<AccountMenuExternalItem
href="https://roadmap.dify.ai"
iconClassName="i-ri-map-2-line"
label={t('userProfile.roadmap', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
<AccountMenuExternalItem
href="https://github.com/langgenius/dify"
iconClassName="i-ri-github-line"
label={t('userProfile.github', { ns: 'common' })}
trailing={(
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
</div>
)}
/>
{
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
<AccountMenuActionItem
iconClassName="i-ri-information-2-line"
label={t('userProfile.about', { ns: 'common' })}
onClick={() => {
setAboutVisible(true)
setIsAccountMenuOpen(false)
}}
trailing={(
<div className="flex shrink-0 items-center">
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
)}
/>
)
}
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
</>
)}
<AccountMenuSection>
<DropdownMenuItem
closeOnClick={false}
className="cursor-default data-highlighted:bg-transparent"
>
<MenuItemContent
iconClassName="i-ri-t-shirt-2-line"
label={t('theme.theme', { ns: 'common' })}
trailing={<ThemeSwitcher />}
/>
</DropdownMenuItem>
</AccountMenuSection>
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
<AccountMenuSection>
<AccountMenuActionItem
iconClassName="i-ri-logout-box-r-line"
label={t('userProfile.logout', { ns: 'common' })}
onClick={() => {
void handleLogout()
}}
/>
</AccountMenuSection>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
{

View File

@ -191,7 +191,8 @@ describe('MainNav', () => {
it('renders primary navigation with the planned routes', () => {
renderMainNav()
expect(screen.getByText(Plan.team)).toBeInTheDocument()
expect(screen.getAllByText(Plan.team)).toHaveLength(1)
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/explore/apps')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')

View File

@ -27,6 +27,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { Plan } from '@/app/components/billing/type'
import ItemOperation from '@/app/components/explore/item-operation'
@ -34,11 +35,11 @@ import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
import AccountAbout from '@/app/components/header/account-about'
import AccountDropdown from '@/app/components/header/account-dropdown'
import Compliance from '@/app/components/header/account-dropdown/compliance'
import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
import Support from '@/app/components/header/account-dropdown/support'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import GithubStar from '@/app/components/header/github-star'
import Indicator from '@/app/components/header/indicator'
import PlanBadge from '@/app/components/header/plan-badge'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
@ -95,14 +96,6 @@ const WorkspaceIcon = ({
</div>
)
const MenuIcon = ({
className,
}: {
className?: string
}) => (
<span className={cn('flex h-4 w-4 shrink-0 items-center justify-center text-text-tertiary', className)} />
)
const NavIcon = ({
icon,
className,
@ -118,15 +111,10 @@ const WorkspacePlanBadge = ({
}: {
plan: Plan
}) => {
if (plan !== Plan.sandbox)
return <PlanBadge plan={plan} />
return (
<div className="flex min-w-4 shrink-0 items-center justify-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5">
<span className="min-w-0 text-center system-2xs-medium-uppercase text-text-tertiary">
{plan}
</span>
</div>
<Badge size="xs" variant="dimm" className="shrink-0">
{plan === Plan.professional ? 'pro' : plan}
</Badge>
)
}
@ -365,7 +353,7 @@ const WebAppItem = ({
return (
<div
className={cn(
'group flex h-6 cursor-pointer items-center justify-between rounded-lg pr-0.5 pl-2 system-sm-regular text-components-main-nav-text transition-colors',
'group flex cursor-pointer items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 text-components-main-nav-text transition-colors',
isSelected ? 'bg-state-base-hover text-components-main-nav-text' : 'hover:bg-state-base-hover hover:text-components-main-nav-text',
)}
onClick={() => router.push(url)}
@ -373,15 +361,16 @@ const WebAppItem = ({
onMouseLeave={() => setIsHovering(false)}
title={app.app.name}
>
<div className="flex min-w-0 grow items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<AppIcon
size="tiny"
className="size-5 rounded-md text-sm"
iconType={app.app.icon_type}
icon={app.app.icon}
background={app.app.icon_background}
imageUrl={app.app.icon_url}
/>
<span className="truncate">{app.app.name}</span>
<span className="min-w-0 flex-1 truncate py-1 pr-1 system-sm-regular">{app.app.name}</span>
</div>
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
<ItemOperation
@ -429,23 +418,25 @@ const WebAppsSection = () => {
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex items-center justify-between px-2 pb-1">
<div className="flex items-center justify-between py-1 pr-2.5 pl-2">
<button
type="button"
className="flex min-w-0 items-center gap-1 text-left system-xs-medium-uppercase text-text-quaternary hover:text-text-tertiary"
className="flex min-w-0 items-center rounded-md px-2 py-1 text-left system-xs-medium-uppercase text-text-tertiary hover:text-text-secondary"
onClick={() => setSearchVisible(value => !value)}
>
<span>{t('sidebar.webApps', { ns: 'explore' })}</span>
<span aria-hidden className="i-ri-arrow-down-s-line h-3.5 w-3.5 shrink-0" />
<span aria-hidden className="i-ri-arrow-down-s-fill h-4 w-4 shrink-0" />
</button>
<div className="flex items-center gap-1">
<div className="flex items-center gap-0.5">
<button
type="button"
aria-label={t('operation.search', { ns: 'common' })}
className={cn('flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', searchVisible && 'bg-state-base-hover text-text-secondary')}
className={cn('flex h-6 w-6 items-center justify-center rounded-md p-0.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', searchVisible && 'bg-state-base-hover text-text-secondary')}
onClick={() => setSearchVisible(value => !value)}
>
<span aria-hidden className="i-ri-search-line h-4 w-4" />
<span className="flex size-5 shrink-0 items-center justify-center">
<span aria-hidden className="i-ri-search-line size-3.5" />
</span>
</button>
</div>
</div>
@ -530,46 +521,60 @@ const HelpMenu = () => {
<DropdownMenuContent
placement="top-end"
sideOffset={8}
popupClassName="w-60 p-1"
popupClassName="w-60 overflow-hidden bg-components-panel-bg-blur! p-0! backdrop-blur-[5px]"
>
{!systemFeatures.branding.enabled && (
<>
<DropdownMenuGroup>
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
<MenuIcon className="i-ri-book-open-line" />
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.helpCenter', { ns: 'common' })}</span>
<DropdownMenuGroup className="p-1">
<DropdownMenuLinkItem href={docLink('/use-dify/getting-started/introduction')} target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1">
<MenuItemContent
iconClassName="i-ri-book-open-line"
label={t('mainNav.help.docs', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
</DropdownMenuLinkItem>
<Support closeAccountDropdown={() => setOpen(false)} />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
<MenuIcon className="i-ri-map-2-line" />
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.roadmap', { ns: 'common' })}</span>
<DropdownMenuSeparator className="my-0!" />
<DropdownMenuGroup className="p-1">
<DropdownMenuLinkItem href="https://roadmap.dify.ai" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
<MenuItemContent
iconClassName="i-ri-map-2-line"
label={t('userProfile.roadmap', { ns: 'common' })}
trailing={<ExternalLinkIndicator />}
/>
</DropdownMenuLinkItem>
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="gap-2 px-3">
<MenuIcon className="i-ri-github-line" />
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.github', { ns: 'common' })}</span>
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
</div>
<DropdownMenuLinkItem href="https://github.com/langgenius/dify" target="_blank" rel="noopener noreferrer" className="mx-0 h-8 gap-1 px-3 py-1.5">
<MenuItemContent
iconClassName="i-ri-github-line"
label={t('userProfile.github', { ns: 'common' })}
trailing={(
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
<GithubStar className="system-2xs-medium-uppercase text-text-tertiary" />
</div>
)}
/>
</DropdownMenuLinkItem>
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
<DropdownMenuItem
className="gap-2 px-3"
className="mx-0 h-8 gap-1 px-3 py-1.5"
onClick={() => {
setAboutVisible(true)
setOpen(false)
}}
>
<span aria-hidden className="i-ri-information-2-line h-4 w-4 shrink-0 text-text-tertiary" />
<span className="grow system-sm-regular text-text-secondary">{t('userProfile.about', { ns: 'common' })}</span>
<div className="flex shrink-0 items-center">
<div className="mr-2 system-xs-regular text-text-tertiary">{langGeniusVersionInfo.current_version}</div>
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
<MenuItemContent
iconClassName="i-ri-information-2-line"
label={t('userProfile.about', { ns: 'common' })}
trailing={(
<div className="flex shrink-0 items-center">
<div className="mr-2 system-xs-regular text-text-tertiary">{t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}</div>
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
)}
/>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
@ -587,8 +592,11 @@ const MainNav = ({
}: MainNavProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const { userProfile } = useAppContext()
const { currentWorkspace, userProfile } = useAppContext()
const { plan } = useProviderContext()
const { workspaces } = useWorkspacesContext()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan
const navItems = useMemo<MainNavItem[]>(() => [
{
href: '/explore/apps',
@ -663,14 +671,16 @@ const MainNav = ({
</div>
<div className="flex w-[240px] items-center justify-between bg-gradient-to-b from-background-body-transparent to-background-body to-50% py-3 pr-1 pl-3 backdrop-blur-[2px]">
<AccountDropdown
mainNavBadge={<WorkspacePlanBadge plan={workspacePlan} />}
variant="mainNav"
trigger={({ isOpen, ariaLabel }) => (
<button
type="button"
aria-label={ariaLabel}
className={cn('flex shrink-0 items-center gap-3 rounded-full py-1 pr-4 pl-1 text-left text-components-main-nav-text transition-colors hover:bg-state-base-hover', isOpen && 'bg-state-base-hover')}
className={cn('flex max-w-[188px] min-w-0 shrink items-center gap-3 rounded-full py-1 pr-4 pl-1 text-left text-components-main-nav-text transition-colors hover:bg-state-base-hover', isOpen && 'bg-state-base-hover')}
>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="md" className="size-7" />
<span className="system-md-medium whitespace-nowrap">{userProfile.name}</span>
<span className="min-w-0 flex-1 truncate system-md-medium">{userProfile.name}</span>
</button>
)}
/>

View File

@ -148,9 +148,9 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
isValidatingCurrentWorkspace,
}}
>
<div className="flex h-full flex-col overflow-y-auto">
<div className="flex h-full flex-col overflow-hidden">
{env.NEXT_PUBLIC_MAINTENANCE_NOTICE && <MaintenanceNotice />}
<div className="relative flex grow flex-col overflow-x-hidden overflow-y-auto bg-background-body">
<div className="relative flex h-0 min-h-0 grow flex-col overflow-hidden bg-background-body">
{children}
</div>
</div>

View File

@ -3,7 +3,12 @@
"about.latestAvailable": "Dify {{version}} is the latest version available.",
"about.nowAvailable": "Dify {{version}} is now available.",
"about.updateNow": "Update now",
"about.version": "version {{version}}",
"account.account": "Account",
"account.appearanceDark": "Dark",
"account.appearanceLabel": "Appearance",
"account.appearanceLight": "Light",
"account.appearanceSystem": "System",
"account.avatar": "Avatar",
"account.changeEmail.authTip": "Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.",
"account.changeEmail.changeTo": "Change to {{email}}",
@ -209,11 +214,13 @@
"integrations.googleAccount": "Login with Google account",
"label.optional": "(optional)",
"language.displayLanguage": "Display Language",
"language.language": "Language",
"language.timezone": "Time Zone",
"license.expiring": "Expiring in one day",
"license.expiring_plural": "Expiring in {{count}} days",
"license.unlimited": "Unlimited",
"loading": "Loading",
"mainNav.help.docs": "Docs",
"mainNav.help.openMenu": "Open help menu",
"mainNav.home": "Home",
"mainNav.integrations": "Integrations",