From 7046e48590e9a33c15f77261140d27e41f483e7b Mon Sep 17 00:00:00 2001 From: Jingyi-Dify Date: Mon, 4 May 2026 12:44:54 -0700 Subject: [PATCH] feat: refine desktop main nav visuals --- .../vender/main-nav/workspace-settings.svg | 5 + .../custom-vender/icons.json | 5 + .../custom-vender/info.json | 2 +- .../main-nav/__tests__/index.spec.tsx | 27 ++- web/app/components/main-nav/index.css | 41 ++++ web/app/components/main-nav/index.tsx | 203 +++++++++++------- web/i18n/en-US/common.json | 1 + 7 files changed, 202 insertions(+), 82 deletions(-) create mode 100644 packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg create mode 100644 web/app/components/main-nav/index.css diff --git a/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg b/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg new file mode 100644 index 0000000000..0eba74b874 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/workspace-settings.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index a6dcd536b9..bf6b4bb8a7 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1158,6 +1158,11 @@ "body": "", "width": 20, "height": 20 + }, + "main-nav-workspace-settings": { + "body": "", + "width": 16, + "height": 16 } } } diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index fd25449385..27ee20eb64 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 290, + "total": 291, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index e908b474ed..8c84a6cf0d 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -161,6 +161,7 @@ describe('MainNav', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, isEducationAccount: false, + isFetchedPlan: true, plan: { type: Plan.sandbox }, } as ProviderContextState) ;(useModalContext as Mock).mockReturnValue({ @@ -169,8 +170,8 @@ describe('MainNav', () => { } as unknown as ModalContextState) ;(useWorkspacesContext as Mock).mockReturnValue({ workspaces: [ - { id: 'workspace-1', name: 'Solar Studio', current: true }, - { id: 'workspace-2', name: 'Evan Workspace', current: false }, + { id: 'workspace-1', name: 'Solar Studio', plan: Plan.team, status: 'normal', created_at: 0, current: true }, + { id: 'workspace-2', name: 'Evan Workspace', plan: Plan.sandbox, status: 'normal', created_at: 0, current: false }, ], }) ;(useGetInstalledApps as Mock).mockImplementation(() => ({ @@ -190,6 +191,7 @@ describe('MainNav', () => { it('renders primary navigation with the planned routes', () => { renderMainNav() + expect(screen.getByText(Plan.team)).toBeInTheDocument() 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') @@ -202,7 +204,24 @@ describe('MainNav', () => { renderMainNav() - expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveClass('bg-components-main-nav-nav-button-bg-active') + const datasetsLink = screen.getByRole('link', { name: /common.menus.datasets/ }) + expect(datasetsLink.className).toContain('bg-[linear-gradient(98.077deg') + expect(datasetsLink).toHaveClass('main-nav-active-edge') + }) + + it('applies the Figma glass active state to the Home route', () => { + mockPathname = '/explore/apps' + + renderMainNav() + + const homeLink = screen.getByRole('link', { name: /common.mainNav.home/ }) + + expect(homeLink).toHaveClass( + 'border-transparent', + 'backdrop-blur-[5px]', + 'main-nav-active-edge', + ) + expect(homeLink.className).toContain('bg-[linear-gradient(98.077deg') }) it('dispatches the goto anything open event from the search button', () => { @@ -219,7 +238,7 @@ describe('MainNav', () => { it('opens workspace settings, members, provider credits, upgrade, and workspace switching actions', async () => { renderMainNav() - fireEvent.click(screen.getByText(/common\.mainNav\.workspace\.credits|7,500 credits/)) + fireEvent.click(screen.getByRole('button', { name: /common\.mainNav\.workspace\.credits|7,500 credits/ })) expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PROVIDER }) fireEvent.click(screen.getByText('billing.upgradeBtn.encourageShort')) diff --git a/web/app/components/main-nav/index.css b/web/app/components/main-nav/index.css new file mode 100644 index 0000000000..599791c7a1 --- /dev/null +++ b/web/app/components/main-nav/index.css @@ -0,0 +1,41 @@ +.main-nav-active-edge::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + pointer-events: none; + background: + 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; + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; +} + +.main-nav-active-edge::after { + content: ""; + position: absolute; + inset: -1px; + border: 1px solid var(--color-components-main-nav-glass-edge-highlight-first); + border-radius: inherit; + pointer-events: none; + box-shadow: inset 0 0 8px 0 var(--color-components-main-nav-glass-inner-glow); +} diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index c3d07521af..3a8efcb9ea 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -68,11 +68,11 @@ type MainNavItem = { const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl p-2 transition-colors' const activeNavItemClassName = [ - 'overflow-hidden border border-components-main-nav-glass-edge-reflection-first bg-components-main-nav-nav-button-bg-active', - 'bg-[linear-gradient(98deg,var(--color-components-main-nav-glass-surface-first)_0%,var(--color-components-main-nav-glass-surface-middle-1)_18%,var(--color-components-main-nav-glass-surface-middle-2)_59%,var(--color-components-main-nav-glass-surface-end)_100%)]', + 'overflow-hidden border border-transparent', + '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-components-main-nav-text-active 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-5),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-[-1px] before:rounded-xl before:border before:border-components-main-nav-glass-edge-highlight-first before:shadow-[inset_0px_0px_8px_0px_var(--color-components-main-nav-glass-inner-glow)] before:content-[""]', + 'main-nav-active-edge', ].join(' ') const inactiveNavItemClassName = 'system-md-medium bg-components-main-nav-nav-button-bg text-components-main-nav-text hover:bg-state-base-hover hover:text-components-main-nav-text' @@ -113,6 +113,23 @@ const NavIcon = ({ ) +const WorkspacePlanBadge = ({ + plan, +}: { + plan: Plan +}) => { + if (plan !== Plan.sandbox) + return + + return ( +
+ + {plan} + +
+ ) +} + const WorkspaceMenuItemContent = ({ icon, label, @@ -124,7 +141,7 @@ const WorkspaceMenuItemContent = ({ }) => ( <> {icon} - {label} + {label} {trailing} ) @@ -135,7 +152,10 @@ const WorkspaceCard = () => { const { workspaces } = useWorkspacesContext() const { enableBilling, plan } = useProviderContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() + const [open, setOpen] = useState(false) const credits = getRemainingCredits(currentWorkspace.trial_credits, currentWorkspace.trial_credits_used) + const formattedCredits = formatCredits(credits) + const workspacePlan = (workspaces.find(workspace => workspace.current)?.plan || currentWorkspace.plan || plan.type) as Plan const isFreePlan = plan.type === Plan.sandbox const handlePlanClick = () => { @@ -160,43 +180,55 @@ const WorkspaceCard = () => { } return ( - - +
+
-
- +
-
+ + +
{enableBilling && ( )}
- - - -
- -
-
{currentWorkspace.name}
- -
+
+ {open && ( +
+
+ + +
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })} - > - } label={t('mainNav.workspace.settings', { ns: 'common' })} /> - - setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })} - > - } label={t('mainNav.workspace.inviteMembers', { ns: 'common' })} /> - - - {workspaces.length > 0 && ( - <> - - -
+ {workspaces.length > 0 && ( +
+
{t('mainNav.workspace.switchWorkspace', { ns: 'common' })}
{workspaces.map(workspace => ( - { + setOpen(false) void handleSwitchWorkspace(workspace.id) }} > @@ -253,13 +299,13 @@ const WorkspaceCard = () => { label={workspace.name} trailing={workspace.current ? : undefined} /> - + ))} - - - )} - - +
+ )} +
+ )} +
) } @@ -583,7 +629,7 @@ const MainNav = ({ const renderLogo = () => (

- + {systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'} {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? ( @@ -599,20 +645,23 @@ const MainNav = ({ ) return ( -