fix(web): polish onboarding main nav and preferences tab (#37844)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Jingyi 2026-06-23 22:46:53 -07:00 committed by GitHub
parent 67a5eacf2d
commit 4573aaa717
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 225 additions and 61 deletions

View File

@ -3668,11 +3668,6 @@
"count": 1
}
},
"web/app/components/header/account-setting/language-page/__tests__/index.spec.tsx": {
"jsx-a11y/role-has-required-aria-props": {
"count": 1
}
},
"web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1

View File

@ -162,6 +162,7 @@ html[data-theme="dark"] {
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
--color-components-main-nav-glass-surface-end: #0033ff14;
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
--color-components-main-nav-glass-edge-highlight-middle: #ffffff00;
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;

View File

@ -162,6 +162,7 @@ html[data-theme="light"] {
--color-components-main-nav-glass-surface-middle-2: #0033ff1a;
--color-components-main-nav-glass-surface-end: #0033ff14;
--color-components-main-nav-glass-edge-highlight-first: #fffffffa;
--color-components-main-nav-glass-edge-highlight-middle: #ffffff00;
--color-components-main-nav-glass-edge-highlight-end: #ffffff6b;
--color-components-main-nav-glass-edge-reflection-first: #0033ff00;
--color-components-main-nav-glass-edge-reflection-middle: #0033ff99;

View File

@ -169,6 +169,7 @@
--color-components-main-nav-glass-surface-middle-2: var(--color-components-main-nav-glass-surface-middle-2);
--color-components-main-nav-glass-surface-end: var(--color-components-main-nav-glass-surface-end);
--color-components-main-nav-glass-edge-highlight-first: var(--color-components-main-nav-glass-edge-highlight-first);
--color-components-main-nav-glass-edge-highlight-middle: var(--color-components-main-nav-glass-edge-highlight-middle);
--color-components-main-nav-glass-edge-highlight-end: var(--color-components-main-nav-glass-edge-highlight-end);
--color-components-main-nav-glass-edge-reflection-first: var(--color-components-main-nav-glass-edge-reflection-first);
--color-components-main-nav-glass-edge-reflection-middle: var(--color-components-main-nav-glass-edge-reflection-middle);

View File

@ -248,7 +248,7 @@ describe('AccountDropdown', () => {
fireEvent.click(screen.getByText('common.settings.preferences'))
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
})
it('should show Appearance after Preferences in the main nav account dropdown', () => {

View File

@ -127,7 +127,7 @@ export function MainNavMenuContent({
</DropdownMenuLinkItem>
<DropdownMenuItem
className={mainNavMenuItemClassName}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
>
<MenuItemContent
iconClassName="i-ri-equalizer-2-line"

View File

@ -26,6 +26,7 @@ describe('AccountSetting Constants', () => {
expect(ACCOUNT_SETTING_TAB.DATA_SOURCE).toBe('data-source')
expect(ACCOUNT_SETTING_TAB.API_BASED_EXTENSION).toBe('custom-endpoint')
expect(ACCOUNT_SETTING_TAB.CUSTOM).toBe('custom')
expect(ACCOUNT_SETTING_TAB.PREFERENCES).toBe('preferences')
expect(ACCOUNT_SETTING_TAB.LANGUAGE).toBe('language')
})
@ -42,6 +43,7 @@ describe('AccountSetting Constants', () => {
expect(isValidAccountSettingTab('data-source')).toBe(true)
expect(isValidAccountSettingTab('custom-endpoint')).toBe(true)
expect(isValidAccountSettingTab('custom')).toBe(true)
expect(isValidAccountSettingTab('preferences')).toBe(true)
expect(isValidAccountSettingTab('language')).toBe(true)
})
@ -55,6 +57,7 @@ describe('AccountSetting Constants', () => {
expect(isValidSettingsTab('permissions')).toBe(true)
expect(isValidSettingsTab('access-rules')).toBe(true)
expect(isValidSettingsTab('billing')).toBe(true)
expect(isValidSettingsTab('preferences')).toBe(true)
expect(isValidSettingsTab('language')).toBe(true)
expect(isValidSettingsTab('provider')).toBe(true)
expect(isValidSettingsTab('mcp')).toBe(true)

View File

@ -257,6 +257,17 @@ describe('AccountSetting', () => {
expect(screen.getByText('common.settings.dataSource'))!.toBeInTheDocument()
})
it('should normalize legacy language tab entries to preferences', () => {
// Act
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.LANGUAGE })
// Assert
const preferencesButton = screen.getByRole('button', { name: 'common.settings.preferences' })
expect(preferencesButton.querySelector('.i-ri-equalizer-2-fill')).toBeInTheDocument()
expect(screen.getByText('common.account.general')).toBeInTheDocument()
expect(screen.getByText('common.account.appearanceLabel')).toBeInTheDocument()
})
it('should hide sidebar labels on mobile', () => {
// Arrange
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)

View File

@ -0,0 +1,88 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { ACCOUNT_SETTING_TAB } from '../constants'
import UpdateSettingDialogForm from '../update-setting-dialog-form'
const mockSetShowAccountSettingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: (selector: (s: { setShowAccountSettingModal: typeof mockSetShowAccountSettingModal }) => typeof mockSetShowAccountSettingModal) => {
return selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal })
},
}))
vi.mock('react-i18next', () => ({
useTranslation: (defaultNs?: string) => ({
t: (key: string, options?: Record<string, unknown>) => {
const ns = (options?.ns as string | undefined) ?? defaultNs
return `${ns ? `${ns}.` : ''}${key}`
},
i18n: {
language: 'en',
changeLanguage: vi.fn(),
},
}),
Trans: ({ i18nKey, components }: {
i18nKey: string
components?: Record<string, React.ReactElement>
}) => {
const setTimezone = components?.setTimezone
if (setTimezone)
return React.cloneElement(setTimezone, undefined, i18nKey)
return <span>{i18nKey}</span>
},
}))
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
default: () => <div data-testid="time-picker" />,
}))
vi.mock('@/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker', () => ({
default: () => <div data-testid="plugins-picker" />,
}))
describe('UpdateSettingDialogForm', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should open preferences after closing the update setting dialog when timezone link is clicked', () => {
const onRequestClose = vi.fn()
render(
<UpdateSettingDialogForm
autoUpgrade={{
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
upgrade_time_of_day: 0,
upgrade_mode: AUTO_UPDATE_MODE.update_all,
exclude_plugins: [],
include_plugins: [],
}}
category={PluginCategoryEnum.tool}
plugins={[]}
scopeOptions={[
{ value: AUTO_UPDATE_MODE.update_all, label: 'All' },
]}
strategyOptions={[
{ value: AUTO_UPDATE_STRATEGY.fixOnly, label: 'Fix only' },
]}
timezone="UTC"
updateTimeValue="00:00"
minuteFilter={minutes => minutes}
onAutoUpgradeChange={vi.fn()}
onPluginsChange={vi.fn()}
onRequestClose={onRequestClose}
onUpdateTimeChange={vi.fn()}
renderTimePickerTrigger={() => <button type="button">Pick time</button>}
/>,
)
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
expect(onRequestClose).toHaveBeenCalledTimes(1)
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
})
})

View File

@ -12,6 +12,7 @@ export const ACCOUNT_SETTING_TAB = {
DATA_SOURCE: 'data-source',
API_BASED_EXTENSION: 'custom-endpoint',
CUSTOM: 'custom',
PREFERENCES: 'preferences',
LANGUAGE: 'language',
} as const
@ -30,6 +31,7 @@ const WORKSPACE_SETTING_TAB_VALUES = [
export type WorkspaceSettingTab = typeof WORKSPACE_SETTING_TAB_VALUES[number]
const USER_SETTING_TAB_VALUES = [
ACCOUNT_SETTING_TAB.PREFERENCES,
ACCOUNT_SETTING_TAB.LANGUAGE,
] as const

View File

@ -20,11 +20,11 @@ import { BillingPermission, hasPermission } from '@/utils/permission'
import AccessRulesPage from './access-rules-page'
import { ApiBasedExtensionPage } from './api-based-extension-page'
import DataSourcePage from './data-source-page-new'
import LanguagePage from './language-page'
import MembersPage from './members-page'
import ModelProviderPage from './model-provider-page'
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
import PermissionsPage from './permissions-page'
import PreferencePage from './preference-page'
const iconClassName = `
w-4 h-4 mr-2
@ -58,12 +58,14 @@ export default function AccountSetting({
const isRbacEnabled = systemFeatures.rbac_enabled
const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage')
const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View)
// Keep legacy `language` deep links opening Preferences during the tab rename migration.
const normalizedActiveTab = activeTab === ACCOUNT_SETTING_TAB.LANGUAGE ? ACCOUNT_SETTING_TAB.PREFERENCES : activeTab
const activeMenu = (() => {
if (activeTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
return ACCOUNT_SETTING_TAB.LANGUAGE
if ((activeTab === ACCOUNT_SETTING_TAB.PERMISSIONS || activeTab === ACCOUNT_SETTING_TAB.ACCESS_RULES) && !canManageWorkspaceRoles)
if (normalizedActiveTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
return ACCOUNT_SETTING_TAB.PREFERENCES
if ((normalizedActiveTab === ACCOUNT_SETTING_TAB.PERMISSIONS || normalizedActiveTab === ACCOUNT_SETTING_TAB.ACCESS_RULES) && !canManageWorkspaceRoles)
return ACCOUNT_SETTING_TAB.MEMBERS
return activeTab
return normalizedActiveTab
})()
const scrollContainerRef = useRef<HTMLDivElement>(null)
@ -119,7 +121,7 @@ export default function AccountSetting({
activeIcon: <span className={cn('i-ri-color-filter-fill', iconClassName)} />,
},
{
key: ACCOUNT_SETTING_TAB.LANGUAGE,
key: ACCOUNT_SETTING_TAB.PREFERENCES,
name: t('settings.preferences', { ns: 'common' }),
title: t('account.general', { ns: 'common' }),
icon: <span className={cn('i-ri-equalizer-2-line', iconClassName)} />,
@ -151,7 +153,7 @@ export default function AccountSetting({
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const languageItem = settingItems.find(item => item.key === ACCOUNT_SETTING_TAB.LANGUAGE)
const preferenceItem = settingItems.find(item => item.key === ACCOUNT_SETTING_TAB.PREFERENCES)
const menuItems = [
{
@ -161,7 +163,7 @@ export default function AccountSetting({
},
{
key: 'user-group',
items: languageItem ? [languageItem] : [],
items: preferenceItem ? [preferenceItem] : [],
},
]
@ -266,7 +268,7 @@ export default function AccountSetting({
{activeMenu === ACCOUNT_SETTING_TAB.DATA_SOURCE && <DataSourcePage />}
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.LANGUAGE && <LanguagePage />}
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
</div>
</ScrollArea>
</div>

View File

@ -4,7 +4,7 @@ import { act, fireEvent, render, screen, waitFor, within } from '@testing-librar
import { languages } from '@/i18n-config/language'
import { updateUserProfile } from '@/service/common'
import { timezones } from '@/utils/timezone'
import LanguagePage from '../index'
import PreferencePage from '../index'
const mockRefresh = vi.fn()
const mockMutateUserProfile = vi.fn()
@ -54,7 +54,7 @@ vi.mock('@langgenius/dify-ui/select', async () => {
SelectItem: ({ children, value }: { children: React.ReactNode, value: string }) => {
const context = React.useContext(SelectContext)
return (
<button type="button" role="option" onClick={() => context.onValueChange?.(value)}>
<button type="button" role="option" aria-selected={false} onClick={() => context.onValueChange?.(value)}>
{children}
</button>
)
@ -104,7 +104,7 @@ const createUserProfile = (overrides: Partial<GetAccountProfileResponse> = {}):
const renderPage = () => {
render(
<>
<LanguagePage />
<PreferencePage />
<ToastHost />
</>,
)
@ -150,7 +150,7 @@ beforeEach(() => {
})
// Rendering
describe('LanguagePage - Rendering', () => {
describe('PreferencePage - Rendering', () => {
it('should render default language and timezone labels', () => {
const english = getLanguageOption('en-US')
const niueTimezone = getTimezoneOption('Pacific/Niue')
@ -182,7 +182,7 @@ describe('LanguagePage - Rendering', () => {
})
// Interactions
describe('LanguagePage - Interactions', () => {
describe('PreferencePage - Interactions', () => {
it('should show success toast when language updates', async () => {
const chinese = getLanguageOption('zh-Hans')
mockUserProfile = createUserProfile({ interface_language: 'en-US' })

View File

@ -33,7 +33,7 @@ const isThemeOption = (value: string): value is ThemeOption => {
return (themes as readonly string[]).includes(value)
}
export default function LanguagePage() {
export default function PreferencePage() {
const locale = useLocale()
const { userProfile, mutateUserProfile } = useAppContext()
const [editing, setEditing] = useState(false)

View File

@ -53,7 +53,7 @@ function SettingTimeZone({
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={() => {
onRequestClose()
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
}}
>
{children}

View File

@ -23,7 +23,8 @@ import { AppModeEnum } from '@/types/app'
import MainNav from '../index'
import { DETAIL_SIDEBAR_STORAGE_KEY } from '../storage'
const activeEdgeClassName = 'before:pointer-events-none'
const activeGradientMaskClassName = 'aria-[current=page]:main-nav-active-glass'
const activeStackingClassName = 'aria-[current=page]:z-1'
const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess, hotkeyRegistrations } = vi.hoisted(() => ({
mockSwitchWorkspace: vi.fn(),
@ -428,7 +429,7 @@ describe('MainNav', () => {
expect(logoLink.parentElement).toHaveClass('pt-3', 'pr-2', 'pb-2', 'pl-4')
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
expect(homeLink.closest('nav')).toHaveClass('flex', 'flex-col', 'gap-px', 'p-2')
expect(homeLink.closest('nav')).toHaveClass('isolate', 'flex', 'flex-col', 'gap-px', 'p-2')
expect(homeLink).toHaveClass('h-8', 'w-full', 'rounded-[10px]', 'px-2', 'py-1.5')
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
@ -566,8 +567,7 @@ describe('MainNav', () => {
renderMainNav()
const datasetsLink = screen.getByRole('link', { name: /common.menus.datasets/ })
expect(datasetsLink.className).toContain('bg-[linear-gradient(98.077deg')
expect(datasetsLink).toHaveClass(activeEdgeClassName)
expect(datasetsLink).toHaveClass(activeGradientMaskClassName)
expect(datasetsLink).toHaveAttribute('aria-current', 'page')
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
})
@ -578,7 +578,7 @@ describe('MainNav', () => {
renderMainNav()
const studioLink = screen.getByRole('link', { name: /common.menus.apps/ })
expect(studioLink).toHaveClass(activeEdgeClassName)
expect(studioLink).toHaveClass(activeGradientMaskClassName)
expect(studioLink).toHaveAttribute('aria-current', 'page')
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
})
@ -854,7 +854,7 @@ describe('MainNav', () => {
renderMainNav()
const marketplaceLink = screen.getByRole('link', { name: /common.mainNav.marketplace/ })
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
expect(marketplaceLink).toHaveClass(activeGradientMaskClassName)
})
it('marks roster active on roster routes', () => {
@ -863,7 +863,7 @@ describe('MainNav', () => {
renderMainNav()
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
expect(rosterLink).toHaveClass(activeEdgeClassName)
expect(rosterLink).toHaveClass(activeGradientMaskClassName)
expect(rosterLink).toHaveAttribute('aria-current', 'page')
})
@ -874,13 +874,8 @@ describe('MainNav', () => {
const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ })
expect(homeLink).toHaveClass(
'backdrop-blur-[5px]',
'text-saas-dify-blue-inverted',
activeEdgeClassName,
'after:border-components-main-nav-glass-edge-highlight-first',
)
expect(homeLink.className).toContain('var(--color-components-main-nav-glass-surface-first)')
expect(homeLink).toHaveClass(activeGradientMaskClassName)
expect(homeLink).toHaveClass(activeStackingClassName)
})
it('keeps Home active on the legacy explore apps route only', () => {

View File

@ -0,0 +1,48 @@
@utility main-nav-active-glass {
@apply overflow-hidden system-md-semibold text-saas-dify-blue-inverted backdrop-blur-[5px];
background-image: linear-gradient(
91.46deg,
var(--color-components-main-nav-glass-surface-first, rgb(0 51 255 / 0.08)) 0%,
var(--color-components-main-nav-glass-surface-middle-1, rgb(0 51 255 / 0.12)) 17.98%,
var(--color-components-main-nav-glass-surface-middle-2, rgb(0 51 255 / 0.1)) 58.75%,
var(--color-components-main-nav-glass-surface-end, rgb(0 51 255 / 0.08)) 101.09%
);
box-shadow:
0 4px 8px 0 var(--color-components-main-nav-glass-shadow-reflection-glow),
0 10px 12px -4px var(--color-shadow-shadow-4),
0 3px 5px -2px var(--color-shadow-shadow-1),
0 8px 16px -4px var(--color-components-main-nav-glass-shadow-reflection);
&::before {
content: "";
pointer-events: none;
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid transparent;
background: linear-gradient(
0deg,
var(--color-components-main-nav-glass-edge-reflection-first, rgb(0 51 255 / 0)) 0%,
var(--color-components-main-nav-glass-edge-reflection-middle, rgb(0 51 255 / 0.6)) 50%,
var(--color-components-main-nav-glass-edge-reflection-end, rgb(0 51 255 / 0)) 100%
) border-box;
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: destination-out;
mask-composite: exclude;
}
&::after {
content: "";
pointer-events: none;
position: absolute;
inset: 0;
border-radius: inherit;
border: 1px solid transparent;
background: linear-gradient(180deg, var(--color-components-main-nav-glass-edge-highlight-first, rgb(255 255 255 / 0.98)) 0%, var(--color-components-main-nav-glass-edge-highlight-middle, rgb(255 255 255 / 0)) 18%, var(--color-components-main-nav-glass-edge-highlight-end, rgb(255 255 255 / 0.42)) 100%) border-box;
-webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: destination-out;
mask-composite: exclude;
box-shadow: inset 0 0 8px 0 var(--color-components-main-nav-glass-inner-glow);
}
}

View File

@ -4,21 +4,6 @@ import type { MainNavItem } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import Link from '@/next/link'
const navItemClassName = 'group relative flex h-8 w-full items-center gap-2 rounded-[10px] px-2 py-1.5 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-state-accent-solid'
const activeNavItemClassName = cn(
'overflow-hidden',
'bg-[linear-gradient(98.077deg,var(--color-components-main-nav-glass-surface-first)_0%,var(--color-components-main-nav-glass-surface-middle-1)_17.98%,var(--color-components-main-nav-glass-surface-middle-2)_58.75%,var(--color-components-main-nav-glass-surface-end)_101.09%)]',
'system-md-semibold text-saas-dify-blue-inverted backdrop-blur-[5px]',
'shadow-[0px_4px_8px_0px_var(--color-components-main-nav-glass-shadow-reflection-glow),0px_12px_16px_-4px_var(--color-shadow-shadow-4),0px_4px_6px_-2px_var(--color-shadow-shadow-1),0px_10px_16px_-4px_var(--color-components-main-nav-glass-shadow-reflection)]',
'before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:content-[\'\']',
'before:bg-[linear-gradient(var(--color-components-main-nav-glass-edge-highlight-first),var(--color-components-main-nav-glass-edge-highlight-first))_top/100%_1px_no-repeat,linear-gradient(var(--color-components-main-nav-glass-edge-highlight-end),var(--color-components-main-nav-glass-edge-highlight-end))_bottom/100%_1px_no-repeat,linear-gradient(180deg,var(--color-components-main-nav-glass-edge-reflection-first)_0%,var(--color-components-main-nav-glass-edge-reflection-middle)_50%,var(--color-components-main-nav-glass-edge-reflection-end)_100%)_left/1px_100%_no-repeat,linear-gradient(180deg,var(--color-components-main-nav-glass-edge-reflection-first)_0%,var(--color-components-main-nav-glass-edge-reflection-middle)_50%,var(--color-components-main-nav-glass-edge-reflection-end)_100%)_right/1px_100%_no-repeat]',
'before:[mask-composite:exclude] before:[-webkit-mask-composite:xor] before:[-webkit-mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)] before:[mask:linear-gradient(#000_0_0)_content-box,linear-gradient(#000_0_0)]',
'after:pointer-events-none after:absolute after:inset-[-1px] after:rounded-[inherit] after:border after:border-components-main-nav-glass-edge-highlight-first after:shadow-[inset_0_0_8px_0_var(--color-components-main-nav-glass-inner-glow)] after:content-[\'\']',
)
const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover hover:text-components-main-nav-nav-button-text'
const NavIcon = ({
icon,
className,
@ -46,12 +31,14 @@ const MainNavLink = ({
aria-current={activated ? 'page' : undefined}
title={item.label}
className={cn(
navItemClassName,
activated ? activeNavItemClassName : inactiveNavItemClassName,
'group relative flex h-8 w-full items-center gap-2 rounded-[10px] px-2 py-1.5 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset',
'not-aria-[current=page]:bg-components-main-nav-nav-button-bg not-aria-[current=page]:system-md-medium not-aria-[current=page]:text-components-main-nav-nav-button-text not-aria-[current=page]:hover:bg-components-main-nav-nav-button-bg-hover not-aria-[current=page]:hover:text-components-main-nav-nav-button-text',
'aria-[current=page]:main-nav-active-glass aria-[current=page]:z-1',
)}
>
<NavIcon icon={activated ? item.activeIcon : item.icon} />
<span className={cn('truncate', activated && 'text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]')}>{item.label}</span>
<NavIcon icon={item.icon} className="group-aria-[current=page]:hidden" />
<NavIcon icon={item.activeIcon} className="hidden group-aria-[current=page]:block" />
<span className="truncate group-aria-[current=page]:text-shadow-[0px_0px_8px_var(--color-components-main-nav-glass-text-glow)]">{item.label}</span>
</Link>
)
}

View File

@ -297,7 +297,7 @@ const MainNav = ({
? null
: (
<>
<nav className="flex flex-col gap-px p-2">
<nav className="isolate flex flex-col gap-px p-2">
{navItems.map(item => (
<MainNavLink key={item.href} item={item} pathname={pathname} />
))}

View File

@ -8,6 +8,7 @@ import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { createAccountProfileQueryClient } from '@/test/account-profile-query'
import { PluginCategoryEnum, PluginSource } from '../../../types'
import AutoUpdateSetting from '../index'
@ -53,6 +54,32 @@ vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))
vi.mock('react-i18next', () => ({
useTranslation: (defaultNs?: string) => ({
t: (key: string, options?: Record<string, unknown>) => {
const ns = (options?.ns as string | undefined) ?? defaultNs
const params = { ...options }
delete params.ns
const suffix = Object.keys(params).length > 0 ? `:${JSON.stringify(params)}` : ''
return `${ns ? `${ns}.` : ''}${key}${suffix}`
},
i18n: {
language: 'en',
changeLanguage: vi.fn(),
},
}),
Trans: ({ i18nKey, components }: {
i18nKey: string
components?: Record<string, React.ReactElement>
}) => {
const setTimezone = components?.setTimezone
if (setTimezone)
return React.cloneElement(setTimezone, undefined, i18nKey)
return <span>{i18nKey}</span>
},
}))
// Mock plugins service
const mockPluginsData: { plugins: PluginDetail[] } = { plugins: [] }
vi.mock('@/service/use-plugins', () => ({
@ -1330,8 +1357,10 @@ describe('auto-update-setting', () => {
// Act
render(<AutoUpdateSetting {...defaultProps} payload={payload} />)
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
expect(screen.getByText('autoUpdate.changeTimezone')).toBeInTheDocument()
// Assert
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
})
})

View File

@ -37,7 +37,7 @@ const SettingTimeZone: FC<{
<button
type="button"
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
>
{children}
</button>

View File

@ -28,6 +28,7 @@
@import '../components/base/action-button/index.css';
@import '../components/base/badge/index.css';
@import '../components/base/premium-badge/index.css';
@import '../components/main-nav/components/nav-link.css';
/* ---------- JS plugins ------------------------------------------------ */
@plugin './plugins/icons.ts';

View File

@ -106,7 +106,7 @@ const PreferencesOpener = () => {
return (
<button
type="button"
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.LANGUAGE })}
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
>
open preferences
</button>
@ -191,7 +191,7 @@ describe('ModalContextProvider trigger events limit modal', () => {
await user.click(screen.getByRole('button', { name: 'open preferences' }))
expect(await screen.findByTestId('account-setting-active-tab')).toHaveTextContent(ACCOUNT_SETTING_TAB.LANGUAGE)
expect(await screen.findByTestId('account-setting-active-tab')).toHaveTextContent(ACCOUNT_SETTING_TAB.PREFERENCES)
})
it('relies on the in-memory guard when localStorage reads throw', async () => {