From 646fd90716bacb26420685414a0849db4c33c02d Mon Sep 17 00:00:00 2001 From: Jingyi-Dify Date: Mon, 4 May 2026 15:43:32 -0700 Subject: [PATCH] refactor(web): split main nav components - Move MainNav sections into focused components under main-nav/components - Reuse Explore AppNavItem for MainNav web app rows via a mainNav variant - Keep WorkspaceCard expanded panel behavior and styling aligned with the pre-refactor UI --- .../explore/sidebar/app-nav-item/index.tsx | 20 +- .../main-nav/components/account-section.tsx | 37 ++ .../main-nav/components/help-menu.tsx | 117 ++++ .../main-nav/components/nav-link.tsx | 54 ++ .../main-nav/components/search-button.tsx | 22 + .../main-nav/components/web-apps-section.tsx | 142 ++++ .../main-nav/components/workspace-card.tsx | 210 ++++++ .../components/workspace-plan-badge.tsx | 18 + web/app/components/main-nav/index.tsx | 604 +----------------- web/app/components/main-nav/types.ts | 11 + web/app/components/main-nav/utils.ts | 5 + 11 files changed, 644 insertions(+), 596 deletions(-) create mode 100644 web/app/components/main-nav/components/account-section.tsx create mode 100644 web/app/components/main-nav/components/help-menu.tsx create mode 100644 web/app/components/main-nav/components/nav-link.tsx create mode 100644 web/app/components/main-nav/components/search-button.tsx create mode 100644 web/app/components/main-nav/components/web-apps-section.tsx create mode 100644 web/app/components/main-nav/components/workspace-card.tsx create mode 100644 web/app/components/main-nav/components/workspace-plan-badge.tsx create mode 100644 web/app/components/main-nav/types.ts create mode 100644 web/app/components/main-nav/utils.ts diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx index c2156d9dec..6c1e96a393 100644 --- a/web/app/components/explore/sidebar/app-nav-item/index.tsx +++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx @@ -11,6 +11,7 @@ import { useRouter } from '@/next/navigation' type IAppNavItemProps = { isMobile: boolean + variant?: 'default' | 'mainNav' name: string id: string icon_type: AppIconType | null @@ -26,6 +27,7 @@ type IAppNavItemProps = { export default function AppNavItem({ isMobile, + variant = 'default', name, id, icon_type, @@ -42,11 +44,21 @@ export default function AppNavItem({ const url = `/explore/installed/${id}` const ref = useRef(null) const isHovering = useHover(ref) + const isMainNav = variant === 'mainNav' + return (
{ router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation(). }} @@ -54,9 +66,9 @@ export default function AppNavItem({ {isMobile && } {!isMobile && ( <> -
- -
{name}
+
+ +
{name}
e.stopPropagation()}> { + const { userProfile } = useAppContext() + + return ( + } + variant="mainNav" + trigger={({ isOpen, ariaLabel }) => ( + + )} + /> + ) +} + +export default AccountSection diff --git a/web/app/components/main-nav/components/help-menu.tsx b/web/app/components/main-nav/components/help-menu.tsx new file mode 100644 index 0000000000..b9e54c2ce7 --- /dev/null +++ b/web/app/components/main-nav/components/help-menu.tsx @@ -0,0 +1,117 @@ +'use client' + +import { cn } from '@langgenius/dify-ui/cn' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLinkItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@langgenius/dify-ui/dropdown-menu' +import { useSuspenseQuery } from '@tanstack/react-query' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import AccountAbout from '@/app/components/header/account-about' +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 GithubStar from '@/app/components/header/github-star' +import Indicator from '@/app/components/header/indicator' +import { IS_CLOUD_EDITION } from '@/config' +import { useAppContext } from '@/context/app-context' +import { useDocLink } from '@/context/i18n' +import { env } from '@/env' +import { systemFeaturesQueryOptions } from '@/service/system-features' + +const HelpMenu = () => { + const { t } = useTranslation() + const docLink = useDocLink() + const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) + const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() + const [aboutVisible, setAboutVisible] = useState(false) + const [open, setOpen] = useState(false) + + return ( + <> + + + + + + {!systemFeatures.branding.enabled && ( + <> + + + } + /> + + setOpen(false)} /> + {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } + + + + + } + /> + + + + + +
+ )} + /> + + {env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && ( + { + setAboutVisible(true) + setOpen(false) + }} + > + +
{t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}
+ +
+ )} + /> + + )} + + + )} + + + {aboutVisible && setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />} + + ) +} + +export default HelpMenu diff --git a/web/app/components/main-nav/components/nav-link.tsx b/web/app/components/main-nav/components/nav-link.tsx new file mode 100644 index 0000000000..4ae1757573 --- /dev/null +++ b/web/app/components/main-nav/components/nav-link.tsx @@ -0,0 +1,54 @@ +'use client' + +import type { MainNavItem } from '../types' +import { cn } from '@langgenius/dify-ui/cn' +import Link from '@/next/link' + +const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl p-2 transition-colors' + +const activeNavItemClassName = [ + '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)]', + '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' + +const NavIcon = ({ + icon, + className, +}: { + icon: string + className?: string +}) => ( + +) + +type MainNavLinkProps = { + item: MainNavItem + pathname: string +} + +const MainNavLink = ({ + item, + pathname, +}: MainNavLinkProps) => { + const activated = item.active(pathname) + + return ( + + + {item.label} + + ) +} + +export default MainNavLink diff --git a/web/app/components/main-nav/components/search-button.tsx b/web/app/components/main-nav/components/search-button.tsx new file mode 100644 index 0000000000..caebb553d6 --- /dev/null +++ b/web/app/components/main-nav/components/search-button.tsx @@ -0,0 +1,22 @@ +'use client' + +import { useTranslation } from 'react-i18next' +import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks' + +const MainNavSearchButton = () => { + const { t } = useTranslation() + + return ( + + ) +} + +export default MainNavSearchButton diff --git a/web/app/components/main-nav/components/web-apps-section.tsx b/web/app/components/main-nav/components/web-apps-section.tsx new file mode 100644 index 0000000000..8aa897e3be --- /dev/null +++ b/web/app/components/main-nav/components/web-apps-section.tsx @@ -0,0 +1,142 @@ +'use client' + +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@langgenius/dify-ui/alert-dialog' +import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import AppNavItem from '@/app/components/explore/sidebar/app-nav-item' +import { usePathname } from '@/next/navigation' +import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' + +const WebAppsSection = () => { + const { t } = useTranslation() + const pathname = usePathname() + const { data, isPending } = useGetInstalledApps() + const installedApps = useMemo(() => data?.installed_apps ?? [], [data?.installed_apps]) + const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp() + const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() + const [searchVisible, setSearchVisible] = useState(false) + const [searchText, setSearchText] = useState('') + const [showConfirm, setShowConfirm] = useState(false) + const [currentId, setCurrentId] = useState('') + + const filteredApps = useMemo(() => { + const normalizedSearch = searchText.trim().toLowerCase() + if (!normalizedSearch) + return installedApps + + return installedApps.filter(item => item.app.name.toLowerCase().includes(normalizedSearch)) + }, [installedApps, searchText]) + + const handleDelete = async () => { + await uninstallApp(currentId) + setShowConfirm(false) + toast.success(t('api.remove', { ns: 'common' })) + } + + const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { + await updatePinStatus({ appId: id, isPinned }) + toast.success(t('api.success', { ns: 'common' })) + } + + return ( +
+
+ +
+ +
+
+ {searchVisible && ( +
+ setSearchText(e.target.value)} + placeholder={t('mainNav.webApps.searchPlaceholder', { ns: 'common' })} + className="h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 system-sm-regular text-text-secondary outline-none placeholder:text-text-quaternary hover:border-components-input-border-hover focus:border-components-input-border-active" + /> +
+ )} +
+ {isPending && ( +
{t('loading', { ns: 'common' })}
+ )} + {!isPending && filteredApps.length === 0 && ( +
+ {searchText ? t('mainNav.webApps.noResults', { ns: 'common' }) : t('sidebar.noApps.title', { ns: 'explore' })} +
+ )} + {filteredApps.map(({ id, is_pinned, uninstallable, app }) => ( + { + void handleUpdatePinStatus(id, !is_pinned) + }} + uninstallable={uninstallable} + onDelete={(id) => { + setCurrentId(id) + setShowConfirm(true) + }} + /> + ))} +
+ + +
+ + {t('sidebar.delete.title', { ns: 'explore' })} + + + {t('sidebar.delete.content', { ns: 'explore' })} + +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t('operation.confirm', { ns: 'common' })} + + +
+
+
+ ) +} + +export default WebAppsSection diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx new file mode 100644 index 0000000000..78f617a62c --- /dev/null +++ b/web/app/components/main-nav/components/workspace-card.tsx @@ -0,0 +1,210 @@ +'use client' + +import type { ReactNode } from 'react' +import { cn } from '@langgenius/dify-ui/cn' +import { toast } from '@langgenius/dify-ui/toast' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Plan } from '@/app/components/billing/type' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' +import { useAppContext } from '@/context/app-context' +import { useModalContext } from '@/context/modal-context' +import { useProviderContext } from '@/context/provider-context' +import { useWorkspacesContext } from '@/context/workspace-context' +import { switchWorkspace } from '@/service/common' +import { basePath } from '@/utils/var' +import { formatCredits, getRemainingCredits, getWorkspaceInitial } from '../utils' +import WorkspacePlanBadge from './workspace-plan-badge' + +const WorkspaceIcon = ({ + name, + className, +}: { + name?: string + className?: string +}) => ( +
+ {getWorkspaceInitial(name)} +
+) + +const WorkspaceMenuItemContent = ({ + icon, + label, + trailing, +}: { + icon: ReactNode + label: ReactNode + trailing?: ReactNode +}) => ( + <> + {icon} + {label} + {trailing} + +) + +const WorkspaceCard = () => { + const { t } = useTranslation() + const { currentWorkspace } = useAppContext() + 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 = () => { + if (isFreePlan) + setShowPricingModal() + else + setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING }) + } + + const handleSwitchWorkspace = async (tenant_id: string) => { + try { + if (currentWorkspace.id === tenant_id) + return + + await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) + location.assign(`${location.origin}${basePath}`) + } + catch { + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) + } + } + + return ( +
+
+ +
+ + {enableBilling && ( + + )} +
+
+ {open && ( +
+
+ + + +
+ {workspaces.length > 0 && ( +
+
+ {t('mainNav.workspace.switchWorkspace', { ns: 'common' })} +
+ {workspaces.map(workspace => ( + + ))} +
+ )} +
+ )} +
+ ) +} + +export default WorkspaceCard diff --git a/web/app/components/main-nav/components/workspace-plan-badge.tsx b/web/app/components/main-nav/components/workspace-plan-badge.tsx new file mode 100644 index 0000000000..35b9044578 --- /dev/null +++ b/web/app/components/main-nav/components/workspace-plan-badge.tsx @@ -0,0 +1,18 @@ +import Badge from '@/app/components/base/badge' +import { Plan } from '@/app/components/billing/type' + +type WorkspacePlanBadgeProps = { + plan: Plan +} + +const WorkspacePlanBadge = ({ + plan, +}: WorkspacePlanBadgeProps) => { + return ( + + {plan === Plan.professional ? 'pro' : plan} + + ) +} + +export default WorkspacePlanBadge diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index da9aed1684..9f9c3214d8 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -1,598 +1,31 @@ 'use client' -import type { ReactNode } from 'react' -import type { InstalledApp } from '@/models/explore' -import { - AlertDialog, - AlertDialogActions, - AlertDialogCancelButton, - AlertDialogConfirmButton, - AlertDialogContent, - AlertDialogDescription, - AlertDialogTitle, -} from '@langgenius/dify-ui/alert-dialog' -import { Avatar } from '@langgenius/dify-ui/avatar' +import type { MainNavItem, MainNavProps } from './types' +import type { Plan } from '@/app/components/billing/type' import { cn } from '@langgenius/dify-ui/cn' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLinkItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@langgenius/dify-ui/dropdown-menu' -import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' -import { useMemo, useState } from 'react' +import { useMemo } 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' -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 { IS_CLOUD_EDITION } from '@/config' import { useAppContext } from '@/context/app-context' -import { useDocLink } from '@/context/i18n' -import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { useWorkspacesContext } from '@/context/workspace-context' -import { env } from '@/env' import Link from '@/next/link' -import { usePathname, useRouter } from '@/next/navigation' -import { switchWorkspace } from '@/service/common' +import { usePathname } from '@/next/navigation' import { systemFeaturesQueryOptions } from '@/service/system-features' -import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' -import { basePath } from '@/utils/var' - -type MainNavProps = { - className?: string -} - -type MainNavItem = { - href: string - label: string - active: (pathname: string) => boolean - icon: string - activeIcon: string -} - -const navItemClassName = 'group relative flex h-9 items-center gap-2 rounded-xl p-2 transition-colors' - -const activeNavItemClassName = [ - '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)]', - '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' - -const getWorkspaceInitial = (name?: string) => name?.[0]?.toLocaleUpperCase() || '?' - -const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0) - -const formatCredits = (value: number) => new Intl.NumberFormat().format(value) - -const WorkspaceIcon = ({ - name, - className, -}: { - name?: string - className?: string -}) => ( -
- {getWorkspaceInitial(name)} -
-) - -const NavIcon = ({ - icon, - className, -}: { - icon: string - className?: string -}) => ( - -) - -const WorkspacePlanBadge = ({ - plan, -}: { - plan: Plan -}) => { - return ( - - {plan === Plan.professional ? 'pro' : plan} - - ) -} - -const WorkspaceMenuItemContent = ({ - icon, - label, - trailing, -}: { - icon: ReactNode - label: ReactNode - trailing?: ReactNode -}) => ( - <> - {icon} - {label} - {trailing} - -) - -const WorkspaceCard = () => { - const { t } = useTranslation() - const { currentWorkspace } = useAppContext() - 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 = () => { - if (isFreePlan) - setShowPricingModal() - else - setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING }) - } - - const handleSwitchWorkspace = async (tenant_id: string) => { - try { - if (currentWorkspace.id === tenant_id) - return - - await switchWorkspace({ url: '/workspaces/switch', body: { tenant_id } }) - toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) - location.assign(`${location.origin}${basePath}`) - } - catch { - toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) - } - } - - return ( -
-
- -
- - {enableBilling && ( - - )} -
-
- {open && ( -
-
- - - -
- {workspaces.length > 0 && ( -
-
- {t('mainNav.workspace.switchWorkspace', { ns: 'common' })} -
- {workspaces.map(workspace => ( - - ))} -
- )} -
- )} -
- ) -} - -const MainNavLink = ({ - item, - pathname, -}: { - item: MainNavItem - pathname: string -}) => { - const activated = item.active(pathname) - return ( - - - {item.label} - - ) -} - -const MainNavSearchButton = () => { - const { t } = useTranslation() - - return ( - - ) -} - -const WebAppItem = ({ - app, - isSelected, - onDelete, - onTogglePin, -}: { - app: InstalledApp - isSelected: boolean - onDelete: (id: string) => void - onTogglePin: () => void -}) => { - const router = useRouter() - const url = `/explore/installed/${app.id}` - const [isHovering, setIsHovering] = useState(false) - - return ( -
router.push(url)} - onMouseEnter={() => setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - title={app.app.name} - > -
- - {app.app.name} -
-
e.stopPropagation()}> - onDelete(app.id)} - /> -
-
- ) -} - -const WebAppsSection = () => { - const { t } = useTranslation() - const pathname = usePathname() - const { data, isPending } = useGetInstalledApps() - const installedApps = useMemo(() => data?.installed_apps ?? [], [data?.installed_apps]) - const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp() - const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() - const [searchVisible, setSearchVisible] = useState(false) - const [searchText, setSearchText] = useState('') - const [showConfirm, setShowConfirm] = useState(false) - const [currentId, setCurrentId] = useState('') - - const filteredApps = useMemo(() => { - const normalizedSearch = searchText.trim().toLowerCase() - if (!normalizedSearch) - return installedApps - - return installedApps.filter(item => item.app.name.toLowerCase().includes(normalizedSearch)) - }, [installedApps, searchText]) - - const handleDelete = async () => { - await uninstallApp(currentId) - setShowConfirm(false) - toast.success(t('api.remove', { ns: 'common' })) - } - - const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { - await updatePinStatus({ appId: id, isPinned }) - toast.success(t('api.success', { ns: 'common' })) - } - - return ( -
-
- -
- -
-
- {searchVisible && ( -
- setSearchText(e.target.value)} - placeholder={t('mainNav.webApps.searchPlaceholder', { ns: 'common' })} - className="h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 system-sm-regular text-text-secondary outline-none placeholder:text-text-quaternary hover:border-components-input-border-hover focus:border-components-input-border-active" - /> -
- )} -
- {isPending && ( -
{t('loading', { ns: 'common' })}
- )} - {!isPending && filteredApps.length === 0 && ( -
- {searchText ? t('mainNav.webApps.noResults', { ns: 'common' }) : t('sidebar.noApps.title', { ns: 'explore' })} -
- )} - {filteredApps.map(app => ( - { - setCurrentId(id) - setShowConfirm(true) - }} - onTogglePin={() => { - void handleUpdatePinStatus(app.id, !app.is_pinned) - }} - /> - ))} -
- - -
- - {t('sidebar.delete.title', { ns: 'explore' })} - - - {t('sidebar.delete.content', { ns: 'explore' })} - -
- - - {t('operation.cancel', { ns: 'common' })} - - - {t('operation.confirm', { ns: 'common' })} - - -
-
-
- ) -} - -const HelpMenu = () => { - const { t } = useTranslation() - const docLink = useDocLink() - const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() - const [aboutVisible, setAboutVisible] = useState(false) - const [open, setOpen] = useState(false) - - return ( - <> - - - - - - {!systemFeatures.branding.enabled && ( - <> - - - } - /> - - setOpen(false)} /> - {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } - - - - - } - /> - - - - - -
- )} - /> - - {env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && ( - { - setAboutVisible(true) - setOpen(false) - }} - > - -
{t('about.version', { ns: 'common', version: langGeniusVersionInfo.current_version })}
- - - )} - /> -
- )} - - - )} - - - {aboutVisible && setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />} - - ) -} +import AccountSection from './components/account-section' +import HelpMenu from './components/help-menu' +import MainNavLink from './components/nav-link' +import MainNavSearchButton from './components/search-button' +import WebAppsSection from './components/web-apps-section' +import WorkspaceCard from './components/workspace-card' const MainNav = ({ className, }: MainNavProps) => { const { t } = useTranslation() const pathname = usePathname() - const { currentWorkspace, userProfile } = useAppContext() + const { currentWorkspace } = useAppContext() const { plan } = useProviderContext() const { workspaces } = useWorkspacesContext() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) @@ -670,20 +103,7 @@ const MainNav = ({
- } - variant="mainNav" - trigger={({ isOpen, ariaLabel }) => ( - - )} - /> +
diff --git a/web/app/components/main-nav/types.ts b/web/app/components/main-nav/types.ts new file mode 100644 index 0000000000..4ac9937d32 --- /dev/null +++ b/web/app/components/main-nav/types.ts @@ -0,0 +1,11 @@ +export type MainNavProps = { + className?: string +} + +export type MainNavItem = { + href: string + label: string + active: (pathname: string) => boolean + icon: string + activeIcon: string +} diff --git a/web/app/components/main-nav/utils.ts b/web/app/components/main-nav/utils.ts new file mode 100644 index 0000000000..619393fd30 --- /dev/null +++ b/web/app/components/main-nav/utils.ts @@ -0,0 +1,5 @@ +export const getWorkspaceInitial = (name?: string) => name?.[0]?.toLocaleUpperCase() || '?' + +export const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0) + +export const formatCredits = (value: number) => new Intl.NumberFormat().format(value)